qa: hourly in-process heartbeat + run pruning

- runner: QA_HEARTBEAT_MIN (default 60) in-process timer runs a scheduled QA
  pass; one pulse ~90s after boot. Triage (LLM) skipped on clean scheduled runs
  to save tokens; only manual runs or runs with errors get a written summary.
- qa-finish.php prunes to the 50 most recent runs.
- systemd unit exposes QA_HEARTBEAT_MIN for tuning.
This commit is contained in:
Carlos Arias 2026-07-24 09:47:29 +00:00
parent 56c1926b81
commit d548d8a8a8
2 changed files with 25 additions and 1 deletions

View file

@ -796,7 +796,12 @@ async function runQaFlow(trigger) {
runId = JSON.parse(start.out || "{}").run_id || 0;
if (!runId) throw new Error("could not open a QA run");
const { findings, counts } = await runQa();
const summary = await qaTriage(findings, counts);
// Triage (an LLM call) only when it's worth it: a manual run, or a
// scheduled heartbeat that actually found errors. Clean heartbeats get a
// cheap templated summary — no tokens spent when the site is fine.
const summary = (trigger === "manual" || counts.error > 0)
? await qaTriage(findings, counts)
: `${counts.error} error(s) and ${counts.warning} warning(s) across ${counts.pages} pages.`;
const tmp = `/tmp/qa-${runId}.json`;
writeFileSync(tmp, JSON.stringify(findings));
await phpCli(["api/cli/qa-finish.php", `--run=${runId}`, "--status=done", `--summary=${summary}`, `--counts=${JSON.stringify(counts)}`, `--findings-file=${tmp}`]);
@ -993,3 +998,15 @@ if (!existsSync(PREVIEW_DIR)) mkdirSync(PREVIEW_DIR, { recursive: true });
server.listen(PORT, "127.0.0.1", () => {
console.log(`[console] runner on 127.0.0.1:${PORT}, repo ${REPO}`);
});
// ---- QA heartbeat ------------------------------------------------------------
// The runner is long-lived, so the QA heartbeat is just an in-process timer —
// no cron, no extra units. Runs a scheduled QA pass every QA_HEARTBEAT_MIN
// minutes (0 disables), plus one pulse shortly after boot. runQaFlow self-guards
// against overlap and is read-only against the live site.
const QA_HEARTBEAT_MIN = Number(process.env.QA_HEARTBEAT_MIN || 60);
if (QA_HEARTBEAT_MIN > 0) {
setTimeout(() => runQaFlow("scheduled"), 90_000); // first pulse ~90s after boot
setInterval(() => runQaFlow("scheduled"), QA_HEARTBEAT_MIN * 60_000);
console.log(`[qa] heartbeat every ${QA_HEARTBEAT_MIN} min`);
}

View file

@ -61,4 +61,11 @@ foreach ($findings as $f) {
$upd = $pdo->prepare('UPDATE cja_qa_runs SET status = ?, summary = ?, counts = ?, finished_at = NOW() WHERE run_id = ?');
$upd->execute([$status, $summary, $counts, $runId]);
// Prune to the 50 most recent runs (the heartbeat runs often).
$cut = (int) ($pdo->query('SELECT run_id FROM cja_qa_runs ORDER BY run_id DESC LIMIT 1 OFFSET 50')->fetchColumn() ?: 0);
if ($cut > 0) {
$pdo->prepare('DELETE FROM cja_qa_findings WHERE run_id < ?')->execute([$cut]);
$pdo->prepare('DELETE FROM cja_qa_runs WHERE run_id < ?')->execute([$cut]);
}
echo json_encode(['ok' => true, 'run_id' => $runId, 'findings' => $n]);