Compare commits
2
Commits
9c150532fc
...
77fb1abb85
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77fb1abb85 | ||
|
|
b20a46ba81 |
@@ -65,6 +65,8 @@
|
||||
// GET ?action=memory_get the operator memory file and its budget
|
||||
// GET ?action=chats stored conversations, newest first, metadata only
|
||||
// GET ?action=chat_get&id=<hex32> one stored conversation with its transcript
|
||||
// GET ?action=findings [all=1] repair findings, open only unless all=1
|
||||
// POST action=finding_action id=<hex12> act=fix|ack|dismiss|reopen|cancel [note=…]
|
||||
// POST action=ask question=… [history=<JSON>] [kind=…] [think=0|1]
|
||||
// POST action=memory_set memory=… replace the memory file
|
||||
// POST action=clear token=<hex32> discard a finished job
|
||||
@@ -77,11 +79,14 @@
|
||||
// poll {"ok":true,"job":{"status":"retrieving|generating|done|error",…}}
|
||||
// clear {"ok":true}
|
||||
// chats {"ok":true,"chats":[{id,ts,profile,title,turns}],"max":N}
|
||||
// findings {"ok":true,"repair":{enabled,autofix,last},"findings":[…],"counts":{…}}
|
||||
// finding_action {"ok":true,"action":"fix"}
|
||||
// chat_save {"ok":true,"id":"<hex32>","title":…}
|
||||
// {"ok":false,"error":…}
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/ai.php vv_ai_stats(), vv_ai_config(), vv_ai_job_*()
|
||||
// include/ai_repair.php the findings store — loaded only by the two actions that read it
|
||||
// Tools/ai_chat_worker.php the detached worker
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// First executable statement, deliberately dependency-free. A request that is rejected by the
|
||||
@@ -290,6 +295,59 @@ if ($action === 'bug_close') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── findings / finding_action ─────────────────────────────────────────────────
|
||||
// What the repair sweep found, and the operator's answer to it. include/ai_repair.php is pulled
|
||||
// in here rather than at the top of the file: it is the largest include in the plugin and poll
|
||||
// runs once a second per open tab, so it is loaded by the two actions that need it and by nothing
|
||||
// else.
|
||||
//
|
||||
// Neither action is gated on AI_REPAIR_ENABLED. Findings filed while it was on do not stop being
|
||||
// true when it goes off, and answering them — including saying "this was never a problem" — is
|
||||
// exactly what an operator turning the feature off is likely to want to do first. The gate states
|
||||
// are reported instead, so the card can say what is running rather than the endpoint pretending
|
||||
// the store is empty.
|
||||
if ($action === 'findings' || $action === 'finding_action') {
|
||||
require_once dirname(__DIR__) . '/include/ai_repair.php';
|
||||
|
||||
if ($action === 'findings') {
|
||||
// Closed findings are the history — what was dismissed, what a fix actually fixed — and
|
||||
// they are asked for explicitly rather than shipped with every poll of the open list.
|
||||
$rows = [];
|
||||
$open = 0; $needs = 0;
|
||||
foreach (vv_ai_findings_list(($_GET['all'] ?? '') === '1' ? [] : ['open', 'needs_operator']) as $f) {
|
||||
$state = (string)($f['state'] ?? 'open');
|
||||
if ($state === 'open') $open++;
|
||||
elseif ($state === 'needs_operator') $needs++;
|
||||
// The three things the page must not decide for itself: which actions this row
|
||||
// offers, and what its state and kind mean in words.
|
||||
$f['actions'] = vv_ai_finding_actions($f);
|
||||
$f['state_label'] = VV_AI_FINDING_STATES[$state] ?? '';
|
||||
$f['kind_label'] = VV_AI_FINDING_KINDS[(string)($f['kind'] ?? '')] ?? '';
|
||||
$rows[] = $f;
|
||||
}
|
||||
echo json_encode(['ok' => true,
|
||||
'repair' => ['enabled' => vv_ai_repair_enabled(),
|
||||
'autofix' => vv_ai_repair_autofix_enabled(),
|
||||
'last' => vv_ai_sweep_last()],
|
||||
'findings' => $rows,
|
||||
'counts' => ['open' => $open, 'needs_operator' => $needs, 'shown' => count($rows)]]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// POST, because fix writes conf through the guarded path and every other answer writes state.
|
||||
// Which actions are legal for a given row is vv_ai_finding_apply_action()'s call, not this
|
||||
// endpoint's — a tab left open overnight is holding buttons the store has moved past.
|
||||
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
|
||||
|
||||
$fid = trim($_POST['id'] ?? '');
|
||||
$act = trim($_POST['act'] ?? '');
|
||||
$r = vv_ai_finding_apply_action($fid, $act, trim($_POST['note'] ?? ''));
|
||||
vv_ai_log(sprintf('finding_action id=%s act=%s %s', $fid, $act,
|
||||
$r['ok'] ? 'ok' : 'FAILED: ' . ($r['error'] ?? '?')));
|
||||
echo json_encode($r);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── incident_add ──────────────────────────────────────────────────────────────
|
||||
// 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
|
||||
|
||||
@@ -308,11 +308,13 @@ function vv_ai_finding_write(array $f): array {
|
||||
$rec['closed_at'] = $old['closed_at'] ?? null;
|
||||
}
|
||||
// An acknowledgement holds only while the thing acknowledged is still true. Compare
|
||||
// the live value against what it read when the ack was given: unchanged means stay
|
||||
// quiet, changed means the note is stale and the finding comes back by itself.
|
||||
// what it is pinned to now against what it read when the ack was given: unchanged
|
||||
// means stay quiet, changed means the note is stale and the finding comes back by
|
||||
// itself. $rec carries this sighting's evidence, so a fault that has changed shape
|
||||
// fails this comparison even when no conf key is involved.
|
||||
if (($old['state'] ?? '') === 'acknowledged') {
|
||||
$ackedAt = (string)($old['ack_value'] ?? '');
|
||||
if ($ackedAt === (string)(vv_conf_vars()[$confKey] ?? '')) {
|
||||
if ($ackedAt === vv_ai_finding_ack_pin($rec)) {
|
||||
$rec['state'] = 'acknowledged';
|
||||
$rec['ack_value'] = $ackedAt;
|
||||
$rec['closed_at'] = $old['closed_at'] ?? null;
|
||||
@@ -365,17 +367,32 @@ function vv_ai_finding_dismiss(string $id, string $note = ''): bool {
|
||||
return vv_ai_finding_set_state($id, 'dismissed', $note);
|
||||
}
|
||||
|
||||
// What an acknowledgement is pinned to — the thing that has to stay the same for the ack to keep
|
||||
// meaning what it meant.
|
||||
//
|
||||
// For a conf-bound finding that is the key's value, which is what the operator was looking at
|
||||
// when they said "I know". A finding with no key had nothing to pin to and so compared '' with
|
||||
// '' — every ack on an arr health item was silently permanent, which is dismiss wearing ack's
|
||||
// label. Those pin to the shape of the fault instead: "indexers unavailable: NzbNoob" and
|
||||
// "indexers unavailable: NzbNoob, Miatrix" are one finding getting worse, and an ack given for
|
||||
// the first has not been given for the second.
|
||||
function vv_ai_finding_ack_pin(array $f): string {
|
||||
$key = (string)($f['conf_key'] ?? '');
|
||||
if ($key !== '') return (string)(vv_conf_vars()[$key] ?? '');
|
||||
return 'ev:' . substr(sha1((string)($f['evidence'] ?? '')), 0, 16);
|
||||
}
|
||||
|
||||
// "I know about this — leave it, and tell me if it changes."
|
||||
//
|
||||
// Stamps the key's current value onto the record. Every later sighting compares against that
|
||||
// stamp, so the acknowledgement covers this state and not the key forever. Acking that critical
|
||||
// Stamps what it is pinned to onto the record. Every later sighting compares against that stamp,
|
||||
// so the acknowledgement covers this state and not the finding forever. Acking that critical
|
||||
// rsync is off says nothing about critical rsync being on.
|
||||
function vv_ai_finding_ack(string $id, string $note = ''): bool {
|
||||
$r = vv_ai_finding_get($id);
|
||||
if ($r === null) return false;
|
||||
|
||||
$r['state'] = 'acknowledged';
|
||||
$r['ack_value'] = (string)(vv_conf_vars()[$r['conf_key'] ?? ''] ?? '');
|
||||
$r['ack_value'] = vv_ai_finding_ack_pin($r);
|
||||
$r['closed_at'] = time();
|
||||
if ($note !== '') $r['note'] = mb_substr(vv_ai_redact($note), 0, 1000);
|
||||
|
||||
@@ -384,12 +401,23 @@ function vv_ai_finding_ack(string $id, string $note = ''): bool {
|
||||
}
|
||||
|
||||
// What the operator can do about a finding, and what each choice means. Returned rather than
|
||||
// hardcoded in the UI so the chat and the page cannot offer different options for the same row.
|
||||
// hardcoded in the UI so the chat and the page cannot offer different options for the same row,
|
||||
// and enforced in vv_ai_finding_apply_action() so neither can act on one it was not offered.
|
||||
//
|
||||
// Fix appears for anything with a proposed value, toggle or not — the prohibition is on the
|
||||
// sweep choosing, never on the operator choosing. Everything carries ack and cancel, because
|
||||
// "I know" and "not now" are always valid answers to being told something.
|
||||
// sweep choosing, never on the operator choosing. Open rows also carry ack, dismiss and cancel,
|
||||
// because "I know", "this is never a problem" and "not now" are all valid answers to being told
|
||||
// something, and they are three different answers.
|
||||
function vv_ai_finding_actions(array $f): array {
|
||||
// A closed finding has one question left, and it is not the original one: was closing it
|
||||
// right? Offering fix or ack on a row that is already dismissed is offering to decide
|
||||
// something that has been decided. Reopen is here because dismiss is otherwise permanent —
|
||||
// the write path keeps a dismissed finding dismissed however many times the fault recurs,
|
||||
// so a mis-click would need someone editing JSON on disk to undo.
|
||||
if (!in_array((string)($f['state'] ?? 'open'), ['open', 'needs_operator'], true)) {
|
||||
return ['reopen' => 'Put it back on the list — either closing it was wrong, or it is back'];
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
|
||||
if (($f['proposed'] ?? null) !== null) {
|
||||
@@ -398,7 +426,10 @@ function vv_ai_finding_actions(array $f): array {
|
||||
: 'Write the proven value to ' . $f['conf_key'];
|
||||
}
|
||||
|
||||
$actions['ack'] = 'Known and intended. Stays quiet until ' . ($f['conf_key'] ?? 'it') . ' changes';
|
||||
$actions['ack'] = ($f['conf_key'] ?? '') !== ''
|
||||
? 'Known and intended. Stays quiet until ' . $f['conf_key'] . ' changes'
|
||||
: 'Known and intended. Stays quiet until the fault itself changes';
|
||||
$actions['dismiss'] = 'Not a problem, ever. Stays closed even when it is seen again';
|
||||
$actions['cancel'] = 'Leave it alone for now';
|
||||
|
||||
return $actions;
|
||||
@@ -597,13 +628,32 @@ const VV_AI_ACTION_PATTERNS = [
|
||||
],
|
||||
// Not now — no state written, it comes back next sweep.
|
||||
'cancel' => [
|
||||
'/\b(not now|later|leave it (alone|for now)|skip( it)?|cancel|ignore for now)\b/u',
|
||||
// "leave it" is matched bare, not only as "leave it alone" / "leave it for now". The
|
||||
// ambiguity this function is built to refuse — "leave it, I know" reading as both cancel
|
||||
// and ack — did not actually arise with the longer forms, so that sentence resolved to
|
||||
// ack and silenced the finding until the fault changed. The looser pattern is what makes
|
||||
// the two readings collide and sends it back to be restated.
|
||||
'/\b(not now|later|leave it\b|skip( it)?|cancel|ignore for now)\b/u',
|
||||
'/\b(no|nope|nah)\b[\s,.!]*$/u',
|
||||
'/\b(don\'?t|do not) (fix|touch|change|write|apply)\b/u',
|
||||
],
|
||||
// Never a problem. Deliberately narrow: this is the one answer that cannot expire on its
|
||||
// own, so it is only read from a sentence that says so outright. Anything vaguer than these
|
||||
// is meant to land on ack, which comes back by itself when the fault changes.
|
||||
'dismiss' => [
|
||||
'/\bdismiss\b/u',
|
||||
'/\b(this|that|it)(?:\'s| is) not (a |an )?(problem|bug|issue|real)\b/u',
|
||||
'/\bnever (a problem|an issue|report this)\b/u',
|
||||
],
|
||||
// Undo a close.
|
||||
'reopen' => [
|
||||
'/\breopen\b/u',
|
||||
'/\bun-?dismiss\b/u',
|
||||
],
|
||||
];
|
||||
|
||||
// Returns 'fix' | 'ack' | 'cancel', or null when the reply does not clearly mean one of them.
|
||||
// Returns one of the keys in VV_AI_ACTION_PATTERNS, or null when the reply does not clearly mean
|
||||
// exactly one of them.
|
||||
//
|
||||
// Only call this when a finding is actually pending. A bare "yes" means fix in answer to "shall
|
||||
// I fix it" and means nothing at all on its own, and the difference is context this function
|
||||
@@ -634,10 +684,27 @@ function vv_ai_finding_apply_action(string $id, string $action, string $note = '
|
||||
$f = vv_ai_finding_get($id);
|
||||
if ($f === null) return ['ok' => false, 'error' => 'no such finding'];
|
||||
|
||||
// Only what this finding actually offers, in the state it is actually in. The page renders
|
||||
// its buttons from the same function, but a stale tab holds buttons the store has moved past
|
||||
// — a row acked in one window is still showing Fix in another — and the endpoint is reachable
|
||||
// without either. Checking here is what makes vv_ai_finding_actions() the authority rather
|
||||
// than a suggestion.
|
||||
if (!isset(vv_ai_finding_actions($f)[$action])) {
|
||||
return ['ok' => false, 'error' => 'not offered for this finding: ' . $action];
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'ack':
|
||||
return ['ok' => vv_ai_finding_ack($id, $note), 'action' => 'ack'];
|
||||
|
||||
case 'dismiss':
|
||||
return ['ok' => vv_ai_finding_dismiss($id, $note), 'action' => 'dismiss'];
|
||||
|
||||
// Back to open, never straight back to needs_operator: whether it still cannot be
|
||||
// repaired here is the next sweep's finding to make, not a state to restore.
|
||||
case 'reopen':
|
||||
return ['ok' => vv_ai_finding_set_state($id, 'open', $note), 'action' => 'reopen'];
|
||||
|
||||
case 'cancel':
|
||||
// Deliberately writes nothing at all. "Not now" is not a state, it is the absence of
|
||||
// one — recording it would make the finding look decided when it is still open.
|
||||
|
||||
+199
-7
@@ -47,17 +47,27 @@
|
||||
// Polling stops on a terminal state, on error, and on a wall-clock ceiling.
|
||||
// A worker that dies without writing would otherwise be polled forever.
|
||||
//
|
||||
// Read-only with respect to the system. Nothing here runs a script, edits conf, or changes
|
||||
// any Varaverk state — it asks questions about documentation.
|
||||
// The conversation is read-only with respect to the system. The findings card is not.
|
||||
// Asking questions changes nothing, and every profile reachable from the composer holds
|
||||
// zero capabilities. The one control on this page that changes Varaverk state is Fix on a
|
||||
// repair finding, which writes a single conf key through vv_conf_edit()'s guarded path —
|
||||
// lock, backup, syntax check, read-back, rollback. It is confirmed first, it names the
|
||||
// file, key and both values before it is clicked, and it is the only way a toggle is ever
|
||||
// written by this subsystem, because reaching it means the operator asked for it by name.
|
||||
//
|
||||
// The page never decides what a finding may do.
|
||||
// Buttons are rendered from the actions api/ai.php returned for that row, and the endpoint
|
||||
// checks the same list again before acting. A tab left open overnight holds buttons the
|
||||
// store has moved past, so the row the operator sees is not the authority on what is legal.
|
||||
//
|
||||
// RENDERS
|
||||
// Status banner (index, model residency, GPU, staleness), chat transcript with collapsible
|
||||
// reasoning and audited sources, composer with retrieval-scope and reasoning controls,
|
||||
// source viewer overlay
|
||||
// Status banner (index, model residency, GPU, staleness), repair findings with their actions,
|
||||
// assistant-filed bug reports, chat transcript with collapsible reasoning and audited sources,
|
||||
// composer with retrieval-scope and reasoning controls, source viewer overlay
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/ai_chat.php the shared conversation surface, also used by the Monitor tab's AI row
|
||||
// api/ai.php stats / tokens / bugs / ask / poll / clear / chats
|
||||
// api/ai.php stats / tokens / bugs / findings / finding_action / ask / poll / chats
|
||||
// api/readscript.php source viewer contents
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
@@ -149,6 +159,37 @@ if (is_dir('/var/log/varaverk')) {
|
||||
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; }
|
||||
|
||||
/* ── Repair findings ────────────────────────────────────────────────────── */
|
||||
/* Deliberately not styled like the bug reports above. A bug report is a note to send someone
|
||||
else; a finding is a decision waiting on the operator, and the row carries buttons that write
|
||||
conf. The left border colours by severity so a page of them is scannable without reading. */
|
||||
.vv-ai-fnd { border-left:2px solid #3a3a3a; background:#0d0d0d; border-radius:0 3px 3px 0;
|
||||
padding:8px 10px; margin-bottom:8px; }
|
||||
.vv-ai-fnd.sev-error { border-left-color:#7a3040; background:#140c0e; }
|
||||
.vv-ai-fnd.sev-warn { border-left-color:#6a5228; background:#130f0a; }
|
||||
.vv-ai-fnd.closed { opacity:.55; }
|
||||
.vv-ai-fnd-h { display:flex; align-items:center; gap:9px; margin-bottom:4px; flex-wrap:wrap; }
|
||||
.vv-ai-fnd-s { font-family:monospace; font-size:11px; color:#c8c8c8; }
|
||||
.vv-ai-fnd-k { font-size:9px; letter-spacing:.06em; text-transform:uppercase; color:#4a4a4a;
|
||||
border:1px solid #262626; border-radius:3px; padding:1px 5px; }
|
||||
.vv-ai-fnd-m { font-size:10px; color:#4a4a4a; font-family:monospace; margin-left:auto; }
|
||||
.vv-ai-fnd-r { font-size:12px; color:#b8b8b8; line-height:1.5; margin-bottom:5px; }
|
||||
.vv-ai-fnd-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; }
|
||||
/* The proposed write, spelled out in full before anything is clicked. This is the one line that
|
||||
says what Fix will actually do to conf, so it is not abbreviated and not hidden. */
|
||||
.vv-ai-fnd-w { font-size:11px; font-family:monospace; color:#8a8a8a; margin-top:6px;
|
||||
padding:5px 8px; background:#0b0b0b; border:1px solid #1e1e1e; border-radius:3px; }
|
||||
.vv-ai-fnd-w b { color:#c8c8c8; font-weight:normal; }
|
||||
.vv-ai-fnd-w .arrow { color:#4a4a4a; }
|
||||
.vv-ai-fnd-n { font-size:10px; color:#5a5a5a; margin-top:5px; font-style:italic; }
|
||||
.vv-ai-fnd-a { display:flex; gap:7px; margin-top:7px; align-items:center; flex-wrap:wrap; }
|
||||
/* Each button explains itself on hover from the server's own text, so the page never has to
|
||||
restate what an action means and cannot restate it differently. */
|
||||
.vv-ai-fnd-a .vv-ai-btn { padding:3px 11px; font-size:11px; }
|
||||
.vv-ai-fnd-msg { font-size:10px; color:#5a5a5a; margin-left:4px; }
|
||||
|
||||
/* ── Settings card ──────────────────────────────────────────────────────── */
|
||||
/* Collapsed by default and by markup, not by JS: the card is closed because the class is
|
||||
simply absent, so it cannot flash open on a slow load or stick open if a script throws.
|
||||
@@ -187,6 +228,25 @@ if (is_dir('/var/log/varaverk')) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- What the repair sweep found. Above the bug reports because these are decisions waiting on
|
||||
the operator rather than notes to file somewhere, and because until this card existed the
|
||||
only way to see a finding was to run the sweep tool with --status over SSH.
|
||||
|
||||
Unlike the bugs card it does not hide itself when empty. "Nothing found, last swept 6
|
||||
minutes ago" is the single most useful thing this card ever says, and a card that vanishes
|
||||
on good news cannot say it — it just leaves a gap that reads as broken. -->
|
||||
<div class="vv-ai-tok" id="vv-ai-fnd-wrap">
|
||||
<div class="vv-ai-diag-col" style="grid-column:1/-1">
|
||||
<div class="vv-ai-diag-h">
|
||||
<span id="vv-ai-fnd-sum"></span> Self-repair findings
|
||||
<span class="vv-ai-set-sum" style="margin-left:auto" id="vv-ai-fnd-gate"></span>
|
||||
<button class="vv-ai-btn ghost" id="vv-ai-fnd-all" type="button"
|
||||
style="padding:2px 9px;font-size:10px;text-transform:none;letter-spacing:0">Show closed</button>
|
||||
</div>
|
||||
<div id="vv-ai-fnd"></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. -->
|
||||
@@ -527,6 +587,133 @@ vv_ai_chat_markup('vv-ai', [
|
||||
.then(() => loadBugs()).catch(() => {});
|
||||
};
|
||||
|
||||
// ── Repair findings ─────────────────────────────────────────────────────
|
||||
// The sweep files these every 15 minutes and, until this card, the only way to read one was
|
||||
// ai_repair_sweep.sh --status over SSH. Every button here is an answer the operator is giving
|
||||
// to something the system noticed — including Fix, which writes conf through the guarded path.
|
||||
let fndAll = false, fndRows = [];
|
||||
|
||||
// Labels are the page's; meanings are not. The title on each button is the server's own
|
||||
// description of that action, so the wording an operator hovers is the same wording the chat
|
||||
// uses for the same row.
|
||||
const FND_LABEL = { fix: 'Fix', ack: 'I know', dismiss: 'Never a problem', reopen: 'Reopen' };
|
||||
|
||||
function loadFindings() {
|
||||
fetch(API + '?action=findings' + (fndAll ? '&all=1' : '')).then(r => r.json())
|
||||
.then(d => { if (d.ok) renderFindings(d); })
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function renderFindings(d) {
|
||||
fndRows = d.findings || [];
|
||||
const rep = d.repair || {}, c = d.counts || {};
|
||||
|
||||
// Gate state is stated on the card rather than left to be inferred from an empty list. An
|
||||
// empty list means "nothing found" when repair is on and "nothing is looking" when it is off,
|
||||
// and those are opposite pieces of news.
|
||||
const gate = [];
|
||||
gate.push(rep.enabled ? (rep.autofix ? 'repair on · autofix on'
|
||||
: 'repair on · detect only')
|
||||
: 'repair off — nothing is looking');
|
||||
if (rep.last) gate.push('swept ' + ago(rep.last));
|
||||
$('vv-ai-fnd-gate').innerHTML = rep.enabled
|
||||
? esc(gate.join(' · '))
|
||||
: `<span class="vv-ai-warn">${esc(gate.join(' · '))}</span>`;
|
||||
|
||||
// needs_operator is counted separately from open because it is the one that is actually
|
||||
// waiting on a person — an open finding may still be repaired by the next sweep.
|
||||
const sum = $('vv-ai-fnd-sum');
|
||||
if (c.needs_operator) sum.innerHTML = `<span class="vv-ai-warn">${c.needs_operator} need${c.needs_operator > 1 ? '' : 's'} you</span>`;
|
||||
else if (c.open) sum.innerHTML = `<span class="vv-ai-warn">${c.open} open</span>`;
|
||||
else sum.innerHTML = `<span class="vv-ai-ok">✓ nothing open</span>`;
|
||||
|
||||
if (!fndRows.length) {
|
||||
$('vv-ai-fnd').innerHTML = fndAll
|
||||
? '<div class="vv-ai-none">nothing filed yet</div>'
|
||||
: '<div class="vv-ai-none">nothing open — the last sweep found no new faults</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$('vv-ai-fnd').innerHTML = fndRows.map(f => {
|
||||
const closed = ['open', 'needs_operator'].indexOf(f.state) === -1;
|
||||
const meta = [f.id, f.seen > 1 ? 'seen ' + f.seen + '×' : null, ago(f.last),
|
||||
closed ? f.state : null].filter(Boolean).join(' · ');
|
||||
|
||||
// Buttons come from what the server offered for this row and nothing else. cancel is the
|
||||
// exception and is dropped: "leave it alone for now" writes nothing by design, which in a
|
||||
// page is spelled "do not click anything".
|
||||
const acts = Object.keys(f.actions || {}).filter(a => a !== 'cancel').map(a =>
|
||||
`<button class="vv-ai-btn${a === 'fix' ? '' : ' ghost'}" data-act="${esc(a)}" `
|
||||
+ `data-id="${esc(f.id)}" title="${esc(f.actions[a])}">${esc(FND_LABEL[a] || a)}</button>`
|
||||
).join('');
|
||||
|
||||
return `<div class="vv-ai-fnd sev-${esc(f.severity || 'warn')}${closed ? ' closed' : ''}">
|
||||
<div class="vv-ai-fnd-h">
|
||||
<span class="vv-ai-fnd-s">${esc(f.subject)}</span>
|
||||
<span class="vv-ai-fnd-k" title="${esc(f.kind_label || '')}">${esc(f.kind)}</span>
|
||||
<span class="vv-ai-fnd-m">${esc(meta)}</span>
|
||||
</div>
|
||||
<div class="vv-ai-fnd-r">${esc(f.ref)}</div>
|
||||
<pre class="vv-ai-fnd-e">${esc(f.evidence)}</pre>
|
||||
${f.proposed !== null && f.proposed !== undefined
|
||||
? `<div class="vv-ai-fnd-w">${esc(f.conf_file)} · <b>${esc(f.conf_key)}</b> `
|
||||
+ `${esc(f.observed || '(empty)')} <span class="arrow">→</span> <b>${esc(f.proposed)}</b>`
|
||||
+ (f.proven ? '' : ' <span class="vv-ai-warn">· unproven</span>') + `</div>`
|
||||
: ''}
|
||||
${f.note ? `<div class="vv-ai-fnd-n">${esc(f.note)}</div>` : ''}
|
||||
<div class="vv-ai-fnd-a">${acts}<span class="vv-ai-fnd-msg" data-msg="${esc(f.id)}"></span></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Delegated, so the markup carries no inline handler and nothing has to be escaped into an
|
||||
// attribute that runs as code.
|
||||
$('vv-ai-fnd').addEventListener('click', e => {
|
||||
const btn = e.target.closest('button[data-act]');
|
||||
if (!btn) return;
|
||||
const id = btn.dataset.id, act = btn.dataset.act;
|
||||
const f = fndRows.find(x => x.id === id);
|
||||
if (!f) return;
|
||||
|
||||
// The two that cannot be walked back get asked about first. Fix edits a conf file, and
|
||||
// dismiss is the one state the sweep will never reopen on its own however many times the
|
||||
// fault comes back.
|
||||
if (act === 'fix' && !confirm(
|
||||
`Write ${f.conf_key} = ${f.proposed}\n\nin ${f.conf_file}, replacing ${f.observed || '(empty)'}.`
|
||||
+ (f.proven ? '' : '\n\nNothing has probed this value — it is a proposal, not a proven fix.')))
|
||||
return;
|
||||
if (act === 'dismiss' && !confirm(
|
||||
`Dismiss "${f.subject} — ${f.ref}" permanently?\n\nIt stays closed even when it is seen `
|
||||
+ `again. To be told if it changes, use "I know" instead.`)) return;
|
||||
|
||||
const msg = $('vv-ai-fnd').querySelector(`[data-msg="${id}"]`);
|
||||
const btns = Array.from(btn.parentNode.querySelectorAll('button'));
|
||||
btns.forEach(b => b.disabled = true);
|
||||
if (msg) msg.textContent = 'working…';
|
||||
|
||||
// Reload on success, never on failure. A refused conf write is the case that must not
|
||||
// disappear quietly: the guarded path rolled back, the finding is unchanged, and a reload
|
||||
// would repaint the row identically half a second later and take the error with it. So the
|
||||
// failed row keeps its message and its buttons, and the operator decides what to do next.
|
||||
const failed = text => {
|
||||
if (msg) msg.innerHTML = `<span class="vv-ai-bad">${esc(text)}</span>`;
|
||||
btns.forEach(b => b.disabled = false);
|
||||
};
|
||||
|
||||
fetch(API, { method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: new URLSearchParams({ action: 'finding_action', id, act }) })
|
||||
.then(r => r.json())
|
||||
.then(d => { if (d.ok) loadFindings(); else failed(d.error || 'failed'); })
|
||||
.catch(e => failed(String(e)));
|
||||
});
|
||||
|
||||
$('vv-ai-fnd-all').addEventListener('click', () => {
|
||||
fndAll = !fndAll;
|
||||
$('vv-ai-fnd-all').textContent = fndAll ? 'Open only' : 'Show closed';
|
||||
loadFindings();
|
||||
});
|
||||
|
||||
// ── Token accounting ────────────────────────────────────────────────────
|
||||
// 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.
|
||||
@@ -699,11 +886,16 @@ vv_ai_chat_markup('vv-ai', [
|
||||
// way, keyed on its prefix, so it is not handled here.
|
||||
if (window.__vvAiTeardown) { try { window.__vvAiTeardown(); } catch (e) {} }
|
||||
const bannerTimer = setInterval(loadBanner, 30000);
|
||||
window.__vvAiTeardown = function () { clearInterval(bannerTimer); };
|
||||
// The sweep runs every 15 minutes, so anything faster than this is polling for news that
|
||||
// cannot have arrived. Five minutes keeps "swept N ago" honest without the card costing
|
||||
// anything to leave open.
|
||||
const fndTimer = setInterval(loadFindings, 300000);
|
||||
window.__vvAiTeardown = function () { clearInterval(bannerTimer); clearInterval(fndTimer); };
|
||||
|
||||
loadBanner();
|
||||
loadTokens();
|
||||
loadBugs();
|
||||
loadFindings();
|
||||
chatList.reload();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user