diff --git a/public_html/admin/index.php b/public_html/admin/index.php
index 44b4842..48ae9f7 100644
--- a/public_html/admin/index.php
+++ b/public_html/admin/index.php
@@ -491,9 +491,9 @@ if ($action) {
if (file_exists($cronLog)) {
$lines = array_filter(explode("\n", shell_exec("grep -a 'facts\\|stats\\|calendar\\|intent' " . escapeshellarg($cronLog) . " | tail -60")));
foreach ($lines as $line) {
- if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*facts/i', $line, $m)) $cronLast['facts_collector'] = $m[1];
- if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*stats/i', $line, $m)) $cronLast['stats_cache'] = $m[1];
- if (preg_match('/^\\[(\\d{2}{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*calendar/i', $line, $m)) $cronLast['calendar_sync'] = $m[1];
+ if (preg_match('/^\\[?(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]?.*facts/i', $line, $m)) $cronLast['facts_collector'] = $m[1];
+ if (preg_match('/^\\[?(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]?.*stats/i', $line, $m)) $cronLast['stats_cache'] = $m[1];
+ if (preg_match('/^\\[?(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]?.*calendar/i', $line, $m)) $cronLast['calendar_sync'] = $m[1];
if (preg_match('/^\\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\\].*intent/i', $line, $m)) $cronLast['kb_intent_generator'] = $m[1];
}
}
@@ -616,6 +616,56 @@ if ($action) {
}
j(['lines'=>$out, 'next_line'=>$total]);
+ // ── GENERIC WORKER LOG (live-popup support for Facts/Stats/Calendar) ────────
+ case 'worker_log':
+ $wKeywords = [
+ 'facts_collector' => '/facts|system:|network:|proxmox:|ha:|do_server:|ollama:|sites:|nmap_scan:/i',
+ 'stats_cache' => '/\[cache\]/i',
+ 'calendar_sync' => '/calendar/i',
+ ];
+ $wid = $_REQUEST['worker_id'] ?? '';
+ if (!isset($wKeywords[$wid])) { bad('Unknown worker'); }
+ $logFile = '/var/log/jarvis/cron.log';
+ $since = max(0, (int)($_GET['since'] ?? 0));
+ if (!file_exists($logFile)) { j(['lines'=>[],'next_line'=>0]); }
+ $allLines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ $total = count($allLines);
+ if (!empty($_GET['snapshot'])) { j(['next_line'=>$total]); }
+ $out = [];
+ for ($i = $since; $i < $total; $i++) {
+ if (preg_match($wKeywords[$wid], $allLines[$i])) $out[] = $allLines[$i];
+ }
+ j(['lines'=>$out, 'next_line'=>$total]);
+
+ case 'worker_history':
+ $wKeywords = [
+ 'facts_collector' => '/facts|system:|network:|proxmox:|ha:|do_server:|ollama:|sites:|nmap_scan:/i',
+ 'stats_cache' => '/\[cache\]/i',
+ 'calendar_sync' => '/calendar/i',
+ ];
+ $wid = $_REQUEST['worker_id'] ?? '';
+ if (!isset($wKeywords[$wid])) { bad('Unknown worker'); }
+ $logFile = '/var/log/jarvis/cron.log';
+ $result = ['last_success'=>null,'last_success_count'=>null,'failures'=>[]];
+ if (file_exists($logFile)) {
+ $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ $cutoff = time() - 7 * 86400;
+ foreach (array_reverse($lines) as $line) {
+ if (!preg_match($wKeywords[$wid], $line)) continue;
+ preg_match('/^\[?(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]?/', $line, $tm);
+ $ts = isset($tm[1]) ? strtotime($tm[1]) : 0;
+ if (!$result['last_success'] && preg_match('/done|complete|collected|synced/i', $line)) {
+ $result['last_success'] = $tm[1] ?? null;
+ }
+ if ($ts >= $cutoff && preg_match('/error|fail/i', $line)) {
+ $result['failures'][] = $line;
+ if (count($result['failures']) >= 20) break;
+ }
+ }
+ $result['failures'] = array_reverse($result['failures']);
+ }
+ j($result);
+
// ── KB GENERATOR TOPICS ─────────────────────────────────────────────
case 'kb_topics':
$where = ['1=1']; $params = [];
@@ -2461,6 +2511,114 @@ async function runIntentGenerator() {
setTimeout(pollLog, 2000);
}
+// Generic live-log popup for simple cron workers (Facts Collector, Stats Cache, Calendar Sync).
+// Same pattern as runIntentGenerator() above, parameterized instead of duplicated per worker.
+async function runWorkerWithLog(workerId, label, doneRegex) {
+ const modalHtml = `
+
+
Loading history...
+
LAUNCHING...
+
Waiting for first output...
+
`;
+
+ openModal(`▶ ${label.toUpperCase()}`, modalHtml, null, null);
+ document.getElementById('modalSave').textContent = 'CLOSE';
+
+ const logEl = document.getElementById('wl-log');
+ const statEl = document.getElementById('wl-status');
+ const historyEl = document.getElementById('wl-history');
+
+ let _stop = false, _done = false, lastLine = 0, dotted = 0;
+
+ const stopAndClose = () => {
+ _stop = true;
+ closeModal();
+ if (!_done) wToast(`${label} running in background`);
+ };
+ document.getElementById('modalSave').onclick = stopAndClose;
+ document.getElementById('modalClose').onclick = stopAndClose;
+
+ api('worker_history', {worker_id: workerId}).then(h => {
+ if (!historyEl) return;
+ let html = '';
+ if (h?.last_success) html += `✓ Last success: ${h.last_success} `;
+ else html += 'No recorded successes in log ';
+ if (h?.failures?.length) {
+ html += `⚠ ${h.failures.length} failure line(s) in last 7 days: `;
+ h.failures.slice(-3).forEach(f => {
+ html += `${f.replace(/`;
+ });
+ }
+ historyEl.innerHTML = html || 'No history found ';
+ }).catch(() => { if (historyEl) historyEl.textContent = 'History unavailable'; });
+
+ const snap = await api('worker_log', {worker_id: workerId, snapshot:1});
+ lastLine = snap?.next_line ?? 0;
+
+ const kick = await api('worker_action', {worker_type:'cron', worker_id: workerId, waction:'run'});
+ if (!kick || !kick.ok) {
+ statEl.style.color = 'var(--red)';
+ statEl.textContent = 'LAUNCH FAILED: ' + (kick?.error || 'unknown error');
+ document.getElementById('modalSave').textContent = 'CLOSE';
+ return;
+ }
+ statEl.textContent = 'RUNNING — polling log every 3s...';
+
+ async function pollLog() {
+ if (_stop) return;
+ try {
+ const res = await api('worker_log', {worker_id: workerId, since: lastLine});
+ if (res && Array.isArray(res.lines) && res.lines.length) {
+ lastLine = res.next_line;
+ if (logEl.textContent === 'Waiting for first output...') logEl.textContent = '';
+ res.lines.forEach(line => {
+ const div = document.createElement('div');
+ const lower = line.toLowerCase();
+ if (lower.includes('error') || lower.includes('fail')) div.style.color = 'var(--red)';
+ else if (lower.includes('skip') || lower.includes('warn')) div.style.color = 'var(--yellow)';
+ else if (lower.includes('done') || lower.includes('complete') || lower.includes('ok')) div.style.color = 'var(--green)';
+ else div.style.color = 'var(--text-dim)';
+ div.textContent = line;
+ logEl.appendChild(div);
+ });
+ logEl.scrollTop = logEl.scrollHeight;
+
+ const joined = res.lines.join(' ').toLowerCase();
+ if (doneRegex.test(joined)) {
+ _done = true;
+ statEl.style.color = 'var(--green)';
+ statEl.textContent = 'COMPLETE';
+ document.getElementById('modalSave').textContent = 'CLOSE';
+ return;
+ }
+ dotted = 0;
+ } else {
+ dotted++;
+ if (dotted < 30) {
+ const waitDiv = document.createElement('div');
+ waitDiv.style.color = 'rgba(0,212,255,0.3)';
+ waitDiv.textContent = '· waiting for output' + '.'.repeat(dotted % 4);
+ const last = logEl.lastChild;
+ if (last && last.textContent && last.textContent.startsWith('· waiting')) {
+ logEl.replaceChild(waitDiv, last);
+ } else {
+ if (logEl.textContent === 'Waiting for first output...') logEl.textContent = '';
+ logEl.appendChild(waitDiv);
+ }
+ logEl.scrollTop = logEl.scrollHeight;
+ } else {
+ statEl.style.color = 'var(--yellow)';
+ statEl.textContent = 'RUNNING IN BACKGROUND (check cron log for results)';
+ return;
+ }
+ }
+ } catch(e) {}
+ if (!_stop) setTimeout(pollLog, 3000);
+ }
+
+ setTimeout(pollLog, 2000);
+}
+
let _topicsData = [];
let _topicsCats = [];
@@ -2870,8 +3028,22 @@ async function loadWorkers() {
}
// Cron Workers
const cl=d.cron_last||{};
+ const LIVE_LOG_WORKERS = {
+ facts_collector: /done|collected/,
+ stats_cache: /done/,
+ calendar_sync: /done/,
+ };
document.getElementById('workers-crons').innerHTML=CRON_DEFS.map(c=>{
- const runBtn=!c.norun?`▶ RUN `:'— ';
+ let runBtn;
+ if (c.norun) {
+ runBtn = '— ';
+ } else if (c.id === 'kb_intent_generator') {
+ runBtn = `▶ RUN `;
+ } else if (LIVE_LOG_WORKERS[c.id]) {
+ runBtn = `▶ RUN `;
+ } else {
+ runBtn = `▶ RUN `;
+ }
return `
${c.label}
${c.schedule}
diff --git a/public_html/webhook.php b/public_html/webhook.php
index 258dc6e..9cceee1 100644
--- a/public_html/webhook.php
+++ b/public_html/webhook.php
@@ -14,7 +14,7 @@ if (!defined('WEBHOOK_SECRET')) {
exit;
}
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
-define('DEPLOY_LOG', '/var/www/jarvis/logs/deploy.log');
+define('DEPLOY_LOG', '/var/log/jarvis/deploy.log');
header('Content-Type: application/json');
@@ -33,9 +33,10 @@ $repo = $data['repository']['name'] ?? '';
$ref = $data['ref'] ?? '';
$pusher = $data['pusher']['name'] ?? 'unknown';
-// Only deploy on pushes to main
-if ($ref !== 'refs/heads/main') {
- echo json_encode(['ok' => true, 'skipped' => "ref $ref is not main"]);
+// Only deploy on pushes to the repo's actual default branch (master, not main —
+// this was checking 'main' for a while even though the jarvis repo has always used 'master')
+if ($ref !== 'refs/heads/master') {
+ echo json_encode(['ok' => true, 'skipped' => "ref $ref is not master"]);
exit;
}