File AI-detected Varaverk defects as deduped reports, surfaced on the AI tab and counted in the coffee report
This commit is contained in:
@@ -184,6 +184,19 @@ echo "━━━━━ ☕ COFFEE REPORT SUMMARY ━━━━━"
|
|||||||
echo "🖥️ Host: $MY_ID"
|
echo "🖥️ Host: $MY_ID"
|
||||||
echo "⏱️ Done: $(date '+%Y-%m-%d %H:%M:%S')"
|
echo "⏱️ Done: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
|
||||||
|
# Anything the Scheduler troubleshooter concluded was a Varaverk defect rather than a setting.
|
||||||
|
# Counted here because a finding filed on a Tuesday and read on a Tuesday is a finding nobody
|
||||||
|
# acts on — this report is the weekly moment the operator is actually looking. Count only: the
|
||||||
|
# detail lives on the AI tab, and a report that reprints every open bug stops being skimmable.
|
||||||
|
# Silent when there are none, and silent when AI is off, so a host without it reads the same as
|
||||||
|
# it always has.
|
||||||
|
if [[ "${AI_ENABLED:-false}" == true && -d "$DATA_DIR/ai_bugs" ]]; then
|
||||||
|
_ai_bugs_open=$(grep -l '"open": true' "$DATA_DIR"/ai_bugs/*.json 2>/dev/null | wc -l)
|
||||||
|
if [[ "${_ai_bugs_open:-0}" -gt 0 ]]; then
|
||||||
|
echo "🐞 AI bugs: $_ai_bugs_open open — see the AI tab"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ${#JOB_PASS[@]} -gt 0 ]]; then
|
if [[ ${#JOB_PASS[@]} -gt 0 ]]; then
|
||||||
echo "✅ Passed: ${JOB_PASS[*]}"
|
echo "✅ Passed: ${JOB_PASS[*]}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -278,7 +278,19 @@ if ($profile === 'chat') {
|
|||||||
. "sends someone hunting through code they did not write and cannot fix, which is "
|
. "sends someone hunting through code they did not write and cannot fix, which is "
|
||||||
. "the least useful place you can send them.\n\n"
|
. "the least useful place you can send them.\n\n"
|
||||||
. "Do not suggest editing conf files by hand. Settings on this page have controls, "
|
. "Do not suggest editing conf files by hand. Settings on this page have controls, "
|
||||||
. "and the operator is reading this inside the WebGUI. Name the control.\n\n";
|
. "and the operator is reading this inside the WebGUI. Name the control.\n\n"
|
||||||
|
. "IF IT REALLY IS A DEFECT, FILE IT\n"
|
||||||
|
. "When — and only when — the evidence shows Varaverk itself misbehaving rather than "
|
||||||
|
. "a setting, finish your reply with exactly this block so it gets recorded:\n\n"
|
||||||
|
. "[VARAVERK-BUG]\n"
|
||||||
|
. "component: <the script or file at fault, e.g. Arrs_Stack/arr_sync.sh>\n"
|
||||||
|
. "summary: <one line, what is wrong>\n"
|
||||||
|
. "evidence: <the log line or lines that show it, quoted verbatim>\n"
|
||||||
|
. "[/VARAVERK-BUG]\n\n"
|
||||||
|
. "Leave the block out entirely for anything explained by configuration, by a host "
|
||||||
|
. "being offline, or by a job simply not having run. The evidence field is not "
|
||||||
|
. "optional and must be a line you actually saw — a report nobody can check is worse "
|
||||||
|
. "than no report, because it costs someone an investigation to disprove.\n\n";
|
||||||
|
|
||||||
} elseif ($profile === 'code') {
|
} elseif ($profile === 'code') {
|
||||||
$system = "You are drafting a short shell script for the operator of an Unraid server, to be "
|
$system = "You are drafting a short shell script for the operator of an Unraid server, to be "
|
||||||
@@ -461,6 +473,35 @@ if ($profile === 'code' && preg_match_all('/```(?:\w+)?\n(.*?)```/s', $answer, $
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pull the bug block out of the answer and file it. Stripped from what the operator sees — the
|
||||||
|
// block is a machine contract, not prose — and replaced with a one-line confirmation so the
|
||||||
|
// filing is never silent. A malformed or evidence-free block is dropped rather than filed:
|
||||||
|
// the guard lives here, in code, not in the instruction that asked for it. A prompt is a
|
||||||
|
// request; this is the part that decides.
|
||||||
|
$bugFiled = null;
|
||||||
|
if ($profile === 'troubleshoot'
|
||||||
|
&& preg_match('/\[VARAVERK-BUG\](.*?)\[\/VARAVERK-BUG\]/s', $answer, $bm)) {
|
||||||
|
$answer = trim(preg_replace('/\[VARAVERK-BUG\].*?\[\/VARAVERK-BUG\]/s', '', $answer));
|
||||||
|
|
||||||
|
$field = function (string $k) use ($bm): string {
|
||||||
|
return preg_match('/^\s*' . $k . '\s*:\s*(.+?)\s*$/mi', $bm[1], $m) ? trim($m[1]) : '';
|
||||||
|
};
|
||||||
|
// evidence may run to several lines; take everything after the label.
|
||||||
|
$ev = preg_match('/^\s*evidence\s*:\s*(.*)$/mis', $bm[1], $em) ? trim($em[1]) : '';
|
||||||
|
|
||||||
|
$res = vv_ai_bug_write($field('component'), $field('summary'), $ev, [
|
||||||
|
'asked' => mb_substr($question, 0, 300),
|
||||||
|
'scope' => $scope,
|
||||||
|
'log' => $scopedLog['path'] ?? null,
|
||||||
|
'profile' => $profile,
|
||||||
|
]);
|
||||||
|
if ($res['ok']) {
|
||||||
|
$bugFiled = $res;
|
||||||
|
$answer .= "\n\n_Filed as bug " . $res['id']
|
||||||
|
. ($res['seen'] > 1 ? ' — seen ' . $res['seen'] . ' times' : '') . "._";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$evalCount = (int)($d['eval_count'] ?? 0);
|
$evalCount = (int)($d['eval_count'] ?? 0);
|
||||||
$evalNs = (int)($d['eval_duration'] ?? 0);
|
$evalNs = (int)($d['eval_duration'] ?? 0);
|
||||||
$tokS = $evalNs > 0 ? round($evalCount / ($evalNs / 1e9), 1) : null;
|
$tokS = $evalNs > 0 ? round($evalCount / ($evalNs / 1e9), 1) : null;
|
||||||
|
|||||||
@@ -187,6 +187,20 @@ if ($action === 'clear') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── bugs / bug_close ──────────────────────────────────────────────────────────
|
||||||
|
// Reports the troubleshooter filed. Listing is a GET because it changes nothing; dismissing is
|
||||||
|
// a POST, like every other mutation in this plugin.
|
||||||
|
if ($action === 'bugs') {
|
||||||
|
echo json_encode(['ok' => true, 'bugs' => vv_ai_bugs_list(($_GET['all'] ?? '') !== '1')]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($action === 'bug_close') {
|
||||||
|
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
|
||||||
|
$ok = vv_ai_bug_set_open(trim($_POST['id'] ?? ''), ($_POST['open'] ?? '0') === '1');
|
||||||
|
echo json_encode(['ok' => $ok]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// ── incident_add ──────────────────────────────────────────────────────────────
|
// ── incident_add ──────────────────────────────────────────────────────────────
|
||||||
// Appends one operator-written "this was the fix" note against a scope. POST only, and the
|
// Appends one operator-written "this was the fix" note against a scope. POST only, and the
|
||||||
// scope is whitelisted the same way ask's is — it is written to a file that later rides in a
|
// scope is whitelisted the same way ask's is — it is written to a file that later rides in a
|
||||||
|
|||||||
@@ -792,6 +792,83 @@ function vv_ai_scope_ok(string $scope): bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── AI-filed bug reports ─────────────────────────────────────────────────────────────────────
|
||||||
|
// When the troubleshooter concludes the evidence shows a defect in Varaverk itself — not a
|
||||||
|
// setting — it files a report here rather than only saying so in a chat window that closes.
|
||||||
|
//
|
||||||
|
// Filed automatically, with the care put into the guard rather than into a review queue: a
|
||||||
|
// report requires a component and a quoted line of evidence, and identical findings collapse
|
||||||
|
// onto one record with a seen count. Without that, asking the same question three times files
|
||||||
|
// three bugs and the pile becomes noise within a week.
|
||||||
|
//
|
||||||
|
// Under data/ and therefore gitignored: these quote this installation's logs.
|
||||||
|
function vv_ai_bugs_dir(): string {
|
||||||
|
$d = DATA_DIR . '/ai_bugs';
|
||||||
|
if (!is_dir($d)) @mkdir($d, 0755, true);
|
||||||
|
return $d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same component + same summary is the same finding. Hashing them means a recurring fault
|
||||||
|
// increments a counter instead of breeding files, and the count is itself the signal: seen 40
|
||||||
|
// times is a different problem from seen once.
|
||||||
|
function vv_ai_bug_file(array $r): string {
|
||||||
|
return vv_ai_bugs_dir() . '/' . substr(sha1(strtolower($r['component'] . '|' . $r['summary'])), 0, 12) . '.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
function vv_ai_bug_write(string $component, string $summary, string $evidence, array $ctx = []): array {
|
||||||
|
$component = trim($component);
|
||||||
|
$summary = trim($summary);
|
||||||
|
$evidence = trim($evidence);
|
||||||
|
// Evidence is mandatory. A report that cannot quote the line it is based on is an opinion,
|
||||||
|
// and this file exists to hold findings that can be checked.
|
||||||
|
if ($component === '' || $summary === '' || $evidence === '') return ['ok' => false];
|
||||||
|
if (!vv_ai_scope_ok($component)) return ['ok' => false];
|
||||||
|
|
||||||
|
$rec = [
|
||||||
|
'component' => mb_substr($component, 0, 120),
|
||||||
|
'summary' => mb_substr($summary, 0, 300),
|
||||||
|
'evidence' => mb_substr($evidence, 0, 2000),
|
||||||
|
'host' => vv_detect_host(),
|
||||||
|
'first' => time(),
|
||||||
|
'last' => time(),
|
||||||
|
'seen' => 1,
|
||||||
|
'open' => true,
|
||||||
|
'context' => array_slice($ctx, 0, 12),
|
||||||
|
];
|
||||||
|
$p = vv_ai_bug_file($rec);
|
||||||
|
if (is_file($p)) {
|
||||||
|
$old = json_decode((string)@file_get_contents($p), true);
|
||||||
|
if (is_array($old)) {
|
||||||
|
$rec['first'] = $old['first'] ?? $rec['first'];
|
||||||
|
$rec['seen'] = (int)($old['seen'] ?? 0) + 1;
|
||||||
|
$rec['open'] = $old['open'] ?? true; // dismissing it stays dismissed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$rec['id'] = basename($p, '.json');
|
||||||
|
if (@file_put_contents($p, json_encode($rec, JSON_PRETTY_PRINT)) === false) return ['ok' => false];
|
||||||
|
return ['ok' => true, 'id' => $rec['id'], 'seen' => $rec['seen']];
|
||||||
|
}
|
||||||
|
|
||||||
|
function vv_ai_bugs_list(bool $openOnly = true): array {
|
||||||
|
$out = [];
|
||||||
|
foreach ((array)@glob(vv_ai_bugs_dir() . '/*.json') as $f) {
|
||||||
|
$r = json_decode((string)@file_get_contents($f), true);
|
||||||
|
if (!is_array($r) || ($openOnly && empty($r['open']))) continue;
|
||||||
|
$out[] = $r;
|
||||||
|
}
|
||||||
|
usort($out, fn($a, $b) => ($b['last'] ?? 0) <=> ($a['last'] ?? 0));
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vv_ai_bug_set_open(string $id, bool $open): bool {
|
||||||
|
if (!preg_match('/^[0-9a-f]{12}$/', $id)) return false;
|
||||||
|
$p = vv_ai_bugs_dir() . '/' . $id . '.json';
|
||||||
|
$r = json_decode((string)@file_get_contents($p), true);
|
||||||
|
if (!is_array($r)) return false;
|
||||||
|
$r['open'] = $open;
|
||||||
|
return @file_put_contents($p, json_encode($r, JSON_PRETTY_PRINT)) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Incident journal ─────────────────────────────────────────────────────────────────────────
|
// ── Incident journal ─────────────────────────────────────────────────────────────────────────
|
||||||
// "We have seen this before, and here is what it was." Appended as you work through logs, and
|
// "We have seen this before, and here is what it was." Appended as you work through logs, and
|
||||||
// fed back the next time the same script is being diagnosed.
|
// fed back the next time the same script is being diagnosed.
|
||||||
|
|||||||
@@ -187,6 +187,18 @@ if (is_dir('/var/log/varaverk')) {
|
|||||||
.vv-ai-tok-foot { margin-top:9px; padding-top:7px; border-top:1px solid #1a1a1a; font-size:10px;
|
.vv-ai-tok-foot { margin-top:9px; padding-top:7px; border-top:1px solid #1a1a1a; font-size:10px;
|
||||||
color:#4a4a4a; display:flex; gap:14px; flex-wrap:wrap; }
|
color:#4a4a4a; display:flex; gap:14px; flex-wrap:wrap; }
|
||||||
|
|
||||||
|
/* ── Assistant-filed bug reports ─────────────────────────────────────────── */
|
||||||
|
.vv-ai-bug { border-left:2px solid #5a3a2a; background:#140f0c; border-radius:0 3px 3px 0;
|
||||||
|
padding:8px 10px; margin-bottom:8px; }
|
||||||
|
.vv-ai-bug-h { display:flex; align-items:center; gap:9px; margin-bottom:4px; }
|
||||||
|
.vv-ai-bug-c { font-family:monospace; font-size:11px; color:#c8a87a; }
|
||||||
|
.vv-ai-bug-m { font-size:10px; color:#4a4a4a; font-family:monospace; margin-left:auto; }
|
||||||
|
.vv-ai-bug-s { font-size:12px; color:#b8b8b8; line-height:1.5; margin-bottom:5px; }
|
||||||
|
.vv-ai-bug-e { margin:0; padding:6px 8px; background:#0b0b0b; border:1px solid #1e1e1e;
|
||||||
|
border-radius:3px; font-size:10px; line-height:1.5; color:#8a8a8a;
|
||||||
|
white-space:pre-wrap; overflow-x:auto; max-height:110px; overflow-y:auto; }
|
||||||
|
.vv-ai-bug-q { font-size:10px; color:#4a4a4a; margin-top:5px; font-style:italic; }
|
||||||
|
|
||||||
.vv-ai-pending { font-size:12px; color:#5a5a5a; display:flex; align-items:center; gap:8px; }
|
.vv-ai-pending { font-size:12px; color:#5a5a5a; display:flex; align-items:center; gap:8px; }
|
||||||
.vv-ai-dot { width:6px; height:6px; border-radius:50%; background:#6fcf97; animation:vvAiPulse 1.1s infinite; }
|
.vv-ai-dot { width:6px; height:6px; border-radius:50%; background:#6fcf97; animation:vvAiPulse 1.1s infinite; }
|
||||||
@keyframes vvAiPulse { 0%,100%{opacity:.25;} 50%{opacity:1;} }
|
@keyframes vvAiPulse { 0%,100%{opacity:.25;} 50%{opacity:1;} }
|
||||||
@@ -256,6 +268,19 @@ if (is_dir('/var/log/varaverk')) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Bug reports the scheduler's troubleshooter filed. Hidden entirely when there are none:
|
||||||
|
an empty card here would be a permanent reminder of nothing, and this row already
|
||||||
|
competes for the space above the transcript. -->
|
||||||
|
<div class="vv-ai-tok" id="vv-ai-bugs-wrap" style="display:none">
|
||||||
|
<div class="vv-ai-diag-col" style="grid-column:1/-1">
|
||||||
|
<div class="vv-ai-diag-h">
|
||||||
|
<span id="vv-ai-bugs-sum"></span> Reported by the assistant
|
||||||
|
<span class="vv-ai-set-sum" style="margin-left:auto">filed from the Scheduler troubleshooter</span>
|
||||||
|
</div>
|
||||||
|
<div id="vv-ai-bugs"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="vv-ai-diag">
|
<div class="vv-ai-diag">
|
||||||
<div class="vv-ai-diag-col">
|
<div class="vv-ai-diag-col">
|
||||||
<div class="vv-ai-diag-h"><span id="vv-ai-health-sum"></span> System checks</div>
|
<div class="vv-ai-diag-h"><span id="vv-ai-health-sum"></span> System checks</div>
|
||||||
@@ -499,6 +524,47 @@ if (is_dir('/var/log/varaverk')) {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bug reports ─────────────────────────────────────────────────────────
|
||||||
|
// Read-only here plus dismissal. These are filed by the troubleshooter on the Scheduler tab;
|
||||||
|
// this card is where they are actually seen, since a chat window closes and a finding that
|
||||||
|
// only ever existed in one is a finding you do not have.
|
||||||
|
function loadBugs() {
|
||||||
|
fetch(API + '?action=bugs').then(r => r.json())
|
||||||
|
.then(d => { if (d.ok) renderBugs(d.bugs || []); })
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBugs(bugs) {
|
||||||
|
const wrap = $('vv-ai-bugs-wrap');
|
||||||
|
if (!bugs.length) { wrap.style.display = 'none'; return; }
|
||||||
|
wrap.style.display = '';
|
||||||
|
$('vv-ai-bugs-sum').innerHTML =
|
||||||
|
`<span class="vv-ai-warn">${bugs.length} open</span>`;
|
||||||
|
$('vv-ai-bugs').innerHTML = bugs.map(b => {
|
||||||
|
const when = ago(b.last);
|
||||||
|
// The seen count is the triage signal — once is a curiosity, forty times is a pattern.
|
||||||
|
const seen = b.seen > 1 ? ` · seen ${b.seen}×` : '';
|
||||||
|
return `<div class="vv-ai-bug">
|
||||||
|
<div class="vv-ai-bug-h">
|
||||||
|
<span class="vv-ai-bug-c">${esc(b.component)}</span>
|
||||||
|
<span class="vv-ai-bug-m">${esc(b.id)}${esc(seen)} · ${esc(when)}</span>
|
||||||
|
<button class="vv-ai-btn ghost" onclick="vvAiBugClose('${esc(b.id)}')">Dismiss</button>
|
||||||
|
</div>
|
||||||
|
<div class="vv-ai-bug-s">${esc(b.summary)}</div>
|
||||||
|
<pre class="vv-ai-bug-e">${esc(b.evidence)}</pre>
|
||||||
|
${b.context && b.context.asked
|
||||||
|
? `<div class="vv-ai-bug-q">asked: ${esc(b.context.asked)}</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.vvAiBugClose = function (id) {
|
||||||
|
fetch(API, { method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: new URLSearchParams({ action: 'bug_close', id, open: '0' }) })
|
||||||
|
.then(() => loadBugs()).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
// ── Token accounting ────────────────────────────────────────────────────
|
// ── Token accounting ────────────────────────────────────────────────────
|
||||||
// Fetched on load and after each completed turn, never on the 30s banner tick: the totals
|
// Fetched on load and after each completed turn, never on the 30s banner tick: the totals
|
||||||
// only move when a turn finishes, and the page is the thing that knows when that was.
|
// only move when a turn finishes, and the page is the thing that knows when that was.
|
||||||
@@ -919,5 +985,6 @@ if (is_dir('/var/log/varaverk')) {
|
|||||||
|
|
||||||
loadBanner();
|
loadBanner();
|
||||||
loadTokens();
|
loadTokens();
|
||||||
|
loadBugs();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user