mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 00:34:35 -05:00
feat(admin): KB intent generator — force run, background on close, history panel
- Always runs when triggered from admin (JARVIS_FORCE_RUN=1 bypasses 20h guard) - Closing popup stops UI polling but server job continues; toast confirms background run - History panel at top of popup shows last success time/count + failures from last 7 days - New intent_gen_history backend case parses cron.log for status summary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
This commit is contained in:
+77
-24
@@ -543,8 +543,9 @@ if ($action) {
|
||||
];
|
||||
if (isset($scripts[$wId])) {
|
||||
[$isPhp,$path] = $scripts[$wId];
|
||||
$env = ($wId === 'kb_intent_generator') ? 'JARVIS_FORCE_RUN=1 ' : '';
|
||||
$cmd = $isPhp
|
||||
? '/usr/bin/php8.3 '.escapeshellarg($path).' >> /var/log/jarvis/cron.log 2>&1 &'
|
||||
? $env.'/usr/bin/php8.3 '.escapeshellarg($path).' >> /var/log/jarvis/cron.log 2>&1 &'
|
||||
: escapeshellcmd($path).' >> /var/log/jarvis/deploy.log 2>&1 &';
|
||||
shell_exec($cmd);
|
||||
j(['ok'=>true,'msg'=>ucwords(str_replace('_',' ',$wId)).' triggered']);
|
||||
@@ -572,6 +573,32 @@ if ($action) {
|
||||
j(['ok'=>true,'version'=>$ag['version']??null,'status'=>$ag['status']??'unknown']);
|
||||
} else { bad('Invalid worker action'); }
|
||||
break;
|
||||
case 'intent_gen_history':
|
||||
$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 (stripos($line, 'KB Intent Generator') === false) continue;
|
||||
// parse timestamp
|
||||
preg_match('/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/', $line, $tm);
|
||||
$ts = isset($tm[1]) ? strtotime($tm[1]) : 0;
|
||||
// last success (done/complete)
|
||||
if (!$result['last_success'] && preg_match('/done|complete/i', $line)) {
|
||||
$result['last_success'] = $tm[1] ?? null;
|
||||
preg_match('/inserted[:\s]+(\d+)/i', $line, $cnt);
|
||||
$result['last_success_count'] = isset($cnt[1]) ? (int)$cnt[1] : null;
|
||||
}
|
||||
// failures in last 7 days
|
||||
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);
|
||||
case 'intent_gen_log':
|
||||
$logFile = '/var/log/jarvis/cron.log';
|
||||
$since = max(0, (int)($_GET['since'] ?? 0));
|
||||
@@ -2233,31 +2260,61 @@ function wToast(msg,err=false) {
|
||||
setTimeout(()=>{t.style.opacity='0';},3000);
|
||||
}
|
||||
async function runIntentGenerator() {
|
||||
// Open the working popup
|
||||
const modalHtml = `
|
||||
<div style="font-family:var(--mono);font-size:0.72rem;line-height:1.8">
|
||||
<div id="ig-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="ig-status" style="color:var(--cyan);margin-bottom:8px;font-size:0.65rem;letter-spacing:1px">LAUNCHING...</div>
|
||||
<div id="ig-log" style="background:#060a0e;border:1px solid var(--border);border-radius:3px;padding:10px 12px;min-height:160px;max-height:340px;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 id="ig-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 id="ig-stats" style="margin-top:10px;display:flex;gap:20px;font-size:0.6rem;color:var(--text-dim)"></div>
|
||||
</div>`;
|
||||
|
||||
openModal('▶ KB INTENT GENERATOR', modalHtml, null, null);
|
||||
document.getElementById('modalSave').textContent = 'CLOSE';
|
||||
document.getElementById('modalSave').onclick = () => { _igStop = true; closeModal(); };
|
||||
|
||||
const logEl = document.getElementById('ig-log');
|
||||
const statEl = document.getElementById('ig-status');
|
||||
const statsEl = document.getElementById('ig-stats');
|
||||
const logEl = document.getElementById('ig-log');
|
||||
const statEl = document.getElementById('ig-status');
|
||||
const statsEl = document.getElementById('ig-stats');
|
||||
const historyEl = document.getElementById('ig-history');
|
||||
|
||||
let _igStop = false;
|
||||
let lastLine = 0;
|
||||
let dotted = 0;
|
||||
let _igStop = false;
|
||||
let _igDone = false;
|
||||
let lastLine = 0;
|
||||
let dotted = 0;
|
||||
|
||||
// Snapshot current log position BEFORE triggering so we only see new lines
|
||||
// Close just stops polling — server job keeps running
|
||||
const stopAndClose = () => {
|
||||
_igStop = true;
|
||||
closeModal();
|
||||
if (!_igDone) toast('KB generator running in background', 'ok');
|
||||
};
|
||||
document.getElementById('modalSave').onclick = stopAndClose;
|
||||
document.getElementById('modalClose').onclick = stopAndClose;
|
||||
|
||||
// Load history panel (async, non-blocking)
|
||||
api('intent_gen_history').then(h => {
|
||||
if (!historyEl) return;
|
||||
let html = '';
|
||||
if (h?.last_success) {
|
||||
html += `<span style="color:var(--green)">✓ Last success: ${h.last_success}`;
|
||||
if (h.last_success_count != null) html += ` (+${h.last_success_count} intents)`;
|
||||
html += '</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'; });
|
||||
|
||||
// Snapshot log position BEFORE triggering
|
||||
const snap = await api('intent_gen_log', {snapshot:1});
|
||||
lastLine = snap?.next_line ?? 0;
|
||||
|
||||
// Kick off the generator (fire-and-forget in background on server)
|
||||
// Kick off the generator (JARVIS_FORCE_RUN=1 set server-side — bypasses 20h guard)
|
||||
const kick = await api('worker_action', {worker_type:'cron', worker_id:'kb_intent_generator', waction:'run'});
|
||||
if (!kick || !kick.ok) {
|
||||
statEl.style.color = 'var(--red)';
|
||||
@@ -2267,7 +2324,6 @@ async function runIntentGenerator() {
|
||||
}
|
||||
statEl.textContent = 'RUNNING — polling log every 3s...';
|
||||
|
||||
// Poll the cron log for new output
|
||||
async function pollLog() {
|
||||
if (_igStop) return;
|
||||
try {
|
||||
@@ -2283,7 +2339,7 @@ async function runIntentGenerator() {
|
||||
div.style.color = 'var(--red)';
|
||||
else if (lower.includes('skip') || lower.includes('warn'))
|
||||
div.style.color = 'var(--yellow)';
|
||||
else if (lower.includes('insert') || lower.includes('added') || lower.includes('done') || lower.includes('complete') || lower.includes('cleanup'))
|
||||
else if (lower.includes('insert') || lower.includes('added') || lower.includes('done') || lower.includes('complete') || lower.includes('cleanup') || lower.includes('force-run'))
|
||||
div.style.color = 'var(--green)';
|
||||
else
|
||||
div.style.color = 'var(--text-dim)';
|
||||
@@ -2292,25 +2348,23 @@ async function runIntentGenerator() {
|
||||
});
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
|
||||
// Detect completion
|
||||
const lastLines = res.lines.join(' ').toLowerCase();
|
||||
if (lastLines.includes('kb intent generator: done') || lastLines.includes('total inserted') || lastLines.includes('cleanup complete')) {
|
||||
_igDone = true;
|
||||
statEl.style.color = 'var(--green)';
|
||||
statEl.textContent = 'COMPLETE';
|
||||
// Extract stats from log
|
||||
const total = (res.lines.join('\n').match(/inserted[:\s]+(\d+)/i) || [])[1];
|
||||
if (total) statsEl.innerHTML = `<span style="color:var(--green)">+${total} intents added</span>`;
|
||||
return; // stop polling
|
||||
document.getElementById('modalSave').textContent = 'CLOSE';
|
||||
return;
|
||||
}
|
||||
dotted = 0;
|
||||
} else {
|
||||
// No new lines yet — show a waiting dot
|
||||
dotted++;
|
||||
if (dotted < 30) { // Stop waiting after ~90s with no output
|
||||
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);
|
||||
// Replace last waiting line instead of appending
|
||||
const last = logEl.lastChild;
|
||||
if (last && last.textContent && last.textContent.startsWith('· waiting')) {
|
||||
logEl.replaceChild(waitDiv, last);
|
||||
@@ -2325,15 +2379,14 @@ async function runIntentGenerator() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
// network error — keep trying
|
||||
}
|
||||
} catch(e) {}
|
||||
if (!_igStop) setTimeout(pollLog, 3000);
|
||||
}
|
||||
|
||||
setTimeout(pollLog, 2000); // give server 2s head-start
|
||||
setTimeout(pollLog, 2000);
|
||||
}
|
||||
|
||||
|
||||
async function arcSetup() {
|
||||
const modalHtml = `
|
||||
<div style="font-family:var(--mono);font-size:0.72rem;line-height:1.8">
|
||||
|
||||
Reference in New Issue
Block a user