mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 08:43:00 -05:00
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:
+176
-4
@@ -491,9 +491,9 @@ if ($action) {
|
|||||||
if (file_exists($cronLog)) {
|
if (file_exists($cronLog)) {
|
||||||
$lines = array_filter(explode("\n", shell_exec("grep -a 'facts\\|stats\\|calendar\\|intent' " . escapeshellarg($cronLog) . " | tail -60")));
|
$lines = array_filter(explode("\n", shell_exec("grep -a 'facts\\|stats\\|calendar\\|intent' " . escapeshellarg($cronLog) . " | tail -60")));
|
||||||
foreach ($lines as $line) {
|
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})\\]?.*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})\\]?.*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})\\]?.*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];
|
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]);
|
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 ─────────────────────────────────────────────
|
// ── KB GENERATOR TOPICS ─────────────────────────────────────────────
|
||||||
case 'kb_topics':
|
case 'kb_topics':
|
||||||
$where = ['1=1']; $params = [];
|
$where = ['1=1']; $params = [];
|
||||||
@@ -2461,6 +2511,114 @@ async function runIntentGenerator() {
|
|||||||
setTimeout(pollLog, 2000);
|
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(`▶ ${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)">✓ 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)">⚠ ${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,'<')}</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 _topicsData = [];
|
||||||
let _topicsCats = [];
|
let _topicsCats = [];
|
||||||
|
|
||||||
@@ -2870,8 +3028,22 @@ async function loadWorkers() {
|
|||||||
}
|
}
|
||||||
// Cron Workers
|
// Cron Workers
|
||||||
const cl=d.cron_last||{};
|
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=>{
|
document.getElementById('workers-crons').innerHTML=CRON_DEFS.map(c=>{
|
||||||
const runBtn=!c.norun?`<button onclick="workerAction('cron','${c.id}','run')" style="${wBtn('cyan')}">▶ RUN</button>`:'<span class="ts">—</span>';
|
let runBtn;
|
||||||
|
if (c.norun) {
|
||||||
|
runBtn = '<span class="ts">—</span>';
|
||||||
|
} else if (c.id === 'kb_intent_generator') {
|
||||||
|
runBtn = `<button onclick="runIntentGenerator()" style="${wBtn('cyan')}">▶ 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')}">▶ RUN</button>`;
|
||||||
|
} else {
|
||||||
|
runBtn = `<button onclick="workerAction('cron','${c.id}','run')" style="${wBtn('cyan')}">▶ RUN</button>`;
|
||||||
|
}
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><strong>${c.label}</strong></td>
|
<td><strong>${c.label}</strong></td>
|
||||||
<td class="ts">${c.schedule}</td>
|
<td class="ts">${c.schedule}</td>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ if (!defined('WEBHOOK_SECRET')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
|
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');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
@@ -33,9 +33,10 @@ $repo = $data['repository']['name'] ?? '';
|
|||||||
$ref = $data['ref'] ?? '';
|
$ref = $data['ref'] ?? '';
|
||||||
$pusher = $data['pusher']['name'] ?? 'unknown';
|
$pusher = $data['pusher']['name'] ?? 'unknown';
|
||||||
|
|
||||||
// Only deploy on pushes to main
|
// Only deploy on pushes to the repo's actual default branch (master, not main —
|
||||||
if ($ref !== 'refs/heads/main') {
|
// this was checking 'main' for a while even though the jarvis repo has always used 'master')
|
||||||
echo json_encode(['ok' => true, 'skipped' => "ref $ref is not main"]);
|
if ($ref !== 'refs/heads/master') {
|
||||||
|
echo json_encode(['ok' => true, 'skipped' => "ref $ref is not master"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user