Add config-vs-reality health checks, loaded models, and log evidence

The failures this subsystem actually has are configuration drift, so each
check names the setting to change rather than reporting that retrieval
failed. Notably it catches a conf model tag that is no longer installed, and
an index built by a different embedder than the one configured — vectors
from two models are not comparable, and that failure returns confident
nonsense rather than erroring. Diagnostic questions also get recent log
warnings, attached only then because they cost budget the passages need.
This commit is contained in:
Gmer4Lfe
2026-08-02 17:40:43 -04:00
parent 264ba57cbb
commit 237156c46f
3 changed files with 322 additions and 7 deletions
+73
View File
@@ -104,6 +104,23 @@
.vv-ai-src-s { color:#333; font-family:monospace; margin-left:auto; flex-shrink:0; }
.vv-ai-meta { font-size:10px; color:#333; margin-top:6px; font-family:monospace; }
/* ── Health + loaded models ─────────────────────────────────────────────── */
.vv-ai-diag { display:grid; grid-template-columns:1fr 1fr; gap:1px; background:#1a1a1a;
border:1px solid #262626; border-radius:6px; overflow:hidden; }
@media (max-width:900px) { .vv-ai-diag { grid-template-columns:1fr; } }
.vv-ai-diag-col { background:#0e0e0e; padding:9px 12px; }
.vv-ai-diag-h { font-size:9px; letter-spacing:.08em; text-transform:uppercase; color:#4a4a4a;
margin-bottom:6px; display:flex; align-items:center; gap:7px; }
.vv-ai-chk { display:flex; align-items:flex-start; gap:7px; font-size:11px; padding:2px 0; line-height:1.5; }
.vv-ai-chk-i { flex-shrink:0; font-size:11px; width:12px; }
.vv-ai-chk-l { color:#8a8a8a; flex-shrink:0; }
.vv-ai-chk-d { color:#5a5a5a; }
.vv-ai-chk-f { color:#8a6a3a; display:block; font-size:10px; margin-top:1px; }
.vv-ai-mdl { display:flex; align-items:center; gap:7px; font-size:11px; padding:2px 0; }
.vv-ai-mdl-n { color:#8a8a8a; font-family:monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.vv-ai-mdl-m { color:#444; font-family:monospace; margin-left:auto; flex-shrink:0; font-size:10px; }
.vv-ai-none { font-size:11px; color:#3a3a3a; font-style:italic; }
.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; }
@keyframes vvAiPulse { 0%,100%{opacity:.25;} 50%{opacity:1;} }
@@ -141,6 +158,17 @@
<div class="vv-ai-banner" id="vv-ai-banner"></div>
<div class="vv-ai-diag">
<div class="vv-ai-diag-col">
<div class="vv-ai-diag-h"><span id="vv-ai-health-sum"></span> System checks</div>
<div id="vv-ai-health"></div>
</div>
<div class="vv-ai-diag-col">
<div class="vv-ai-diag-h">Loaded models</div>
<div id="vv-ai-models"></div>
</div>
</div>
<div class="vv-ai-chat" id="vv-ai-chat">
<div class="vv-ai-empty">
Ask Varaverk about itself.<br>
@@ -239,6 +267,51 @@
rt.gpu.name + ' · ' + rt.gpu.util + '% util');
}
$('vv-ai-banner').innerHTML = html;
renderHealth(s.health || []);
renderModels(s.loaded, s.model);
}
// ✓ / ! / ✗ per check. Remedy shown inline on anything not ok — the point is to name the
// setting that is wrong, not to report that something failed.
const ICON = { ok: ['✓','vv-ai-ok'], warn: ['!','vv-ai-warn'], bad: ['✗','vv-ai-bad'] };
function renderHealth(checks) {
if (!checks.length) { $('vv-ai-health').innerHTML = '<div class="vv-ai-none">no checks</div>'; return; }
let bad = 0, warn = 0, html = '';
checks.forEach(c => {
if (c.state === 'bad') bad++; else if (c.state === 'warn') warn++;
const [ic, cl] = ICON[c.state] || ICON.warn;
html += `<div class="vv-ai-chk"><span class="vv-ai-chk-i ${cl}">${ic}</span>`
+ `<span><span class="vv-ai-chk-l">${esc(c.label)}</span> `
+ `<span class="vv-ai-chk-d">— ${esc(c.detail)}</span>`
+ (c.fix ? `<span class="vv-ai-chk-f">→ ${esc(c.fix)}</span>` : '')
+ `</span></div>`;
});
$('vv-ai-health').innerHTML = html;
const sum = $('vv-ai-health-sum');
if (bad) sum.innerHTML = `<span class="vv-ai-bad">✗ ${bad} problem${bad>1?'s':''}</span>`;
else if (warn) sum.innerHTML = `<span class="vv-ai-warn">! ${warn} warning${warn>1?'s':''}</span>`;
else sum.innerHTML = `<span class="vv-ai-ok">✓ all good</span>`;
}
function renderModels(loaded, active) {
const box = $('vv-ai-models');
if (loaded === null || loaded === undefined) {
box.innerHTML = '<div class="vv-ai-none">Ollama unreachable</div>'; return;
}
if (!loaded.length) {
box.innerHTML = '<div class="vv-ai-none">none resident — loads on first use</div>'; return;
}
let html = '';
loaded.forEach(m => {
const full = m.offload === 100;
const [ic, cl] = full ? ICON.ok : ICON.warn;
html += `<div class="vv-ai-mdl"><span class="vv-ai-chk-i ${cl}">${ic}</span>`
+ `<span class="vv-ai-mdl-n"${m.name===active?' style="color:#a8c8a8"':''}>`
+ esc(m.name.replace(/^hf\.co\/[^/]+\//,'')) + `</span>`
+ `<span class="vv-ai-mdl-m">${m.offload===null?'—':m.offload+'% GPU'}`
+ `${m.context?' · '+m.context.toLocaleString()+' ctx':''} · ${(m.vram/1073741824).toFixed(1)}GB</span></div>`;
});
box.innerHTML = html;
}
function loadBanner() {
fetch(API + '?action=stats').then(r => r.json())