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

This commit is contained in:
root
2026-07-07 07:58:55 -05:00
parent e060ff0c63
commit f8a095f783
2 changed files with 181 additions and 8 deletions
+176 -4
View File
@@ -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 = `
<div style="font-family:var(--mono);font-size:0.72rem;line-height:1.8">
<div id="wl-history" style="background:#0a0f14;border:1px solid var(--border);border-radius:3px;padding:8px 12px;margin-bottom:10px;font-size:0.6rem;color:var(--text-dim)">Loading history...</div>
<div id="wl-status" style="color:var(--cyan);margin-bottom:8px;font-size:0.65rem;letter-spacing:1px">LAUNCHING...</div>
<div id="wl-log" style="background:#060a0e;border:1px solid var(--border);border-radius:3px;padding:10px 12px;min-height:160px;max-height:320px;overflow-y:auto;white-space:pre-wrap;font-size:0.62rem;line-height:1.7;color:var(--text-dim)">Waiting for first output...</div>
</div>`;
openModal(`&#9654; ${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 += `<span style="color:var(--green)">&#10003; Last success: ${h.last_success}</span>`;
else html += '<span style="color:var(--text-dim)">No recorded successes in log</span>';
if (h?.failures?.length) {
html += `<br><span style="color:var(--red)">&#9888; ${h.failures.length} failure line(s) in last 7 days:</span>`;
h.failures.slice(-3).forEach(f => {
html += `<div style="color:var(--red);opacity:0.7;margin-left:8px">${f.replace(/</g,'&lt;')}</div>`;
});
}
historyEl.innerHTML = html || '<span>No history found</span>';
}).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?`<button onclick="workerAction('cron','${c.id}','run')" style="${wBtn('cyan')}">&#9654; RUN</button>`:'<span class="ts">&mdash;</span>';
let runBtn;
if (c.norun) {
runBtn = '<span class="ts">&mdash;</span>';
} else if (c.id === 'kb_intent_generator') {
runBtn = `<button onclick="runIntentGenerator()" style="${wBtn('cyan')}">&#9654; RUN</button>`;
} else if (LIVE_LOG_WORKERS[c.id]) {
runBtn = `<button onclick="runWorkerWithLog('${c.id}','${c.label}',${LIVE_LOG_WORKERS[c.id]})" style="${wBtn('cyan')}">&#9654; RUN</button>`;
} else {
runBtn = `<button onclick="workerAction('cron','${c.id}','run')" style="${wBtn('cyan')}">&#9654; RUN</button>`;
}
return `<tr>
<td><strong>${c.label}</strong></td>
<td class="ts">${c.schedule}</td>