From f8a095f7834f5f0486da2e1b17cf779970c8e305 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 7 Jul 2026 07:58:55 -0500 Subject: [PATCH] Fix Works-tab last-run detection bugs, add generic live-log popup for Facts/Stats/Calendar workers, fix webhook branch check (master not main) and log path --- public_html/admin/index.php | 180 +++++++++++++++++++++++++++++++++++- public_html/webhook.php | 9 +- 2 files changed, 181 insertions(+), 8 deletions(-) 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?``:''; + let runBtn; + if (c.norun) { + runBtn = ''; + } else if (c.id === 'kb_intent_generator') { + runBtn = ``; + } else if (LIVE_LOG_WORKERS[c.id]) { + runBtn = ``; + } else { + runBtn = ``; + } 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; }