Put the assistant on the dashboard, where the questions actually occur to you

This commit is contained in:
Gmer4Lfe
2026-08-08 22:01:13 -04:00
parent 4d00e37ca1
commit ea0c713c89
3 changed files with 179 additions and 2 deletions
+16
View File
@@ -91,6 +91,21 @@ $t = microtime(true);
// Call vv_api_data() once — result is static-cached for the rest of this process. // Call vv_api_data() once — result is static-cached for the rest of this process.
vv_api_data(); vv_api_data();
// Must stay in step with api/monitor.php's own block. This file is what the Monitor tab
// normally reads — the endpoint only assembles a payload on a cache miss — so a key added there
// and not here leaves the card that consumes it loading forever on every ordinary page load,
// and working on the one request that happens to miss the cache.
$_vv_ai = null;
if (vv_ai_ui_on()) {
require_once $_base . '/include/ai.php';
$_vv_ai = [
'model' => vv_ai_config()['model'],
'runtime' => vv_ai_runtime_stats(),
'index' => vv_ai_index_stats(),
'tokens' => vv_ai_token_stats()['today'] ?? null,
];
}
$monitor = [ $monitor = [
'system' => vv_system_info(), 'system' => vv_system_info(),
'fallback' => vv_fallback_state(), 'fallback' => vv_fallback_state(),
@@ -118,6 +133,7 @@ $monitor = [
'vms' => vv_get_vms(), 'vms' => vv_get_vms(),
'docker_folders' => vv_get_docker_folders(), 'docker_folders' => vv_get_docker_folders(),
'remote_hosts' => vv_remote_hosts_stats(), 'remote_hosts' => vv_remote_hosts_stats(),
'ai' => $_vv_ai,
'_api_status' => vv_api_get_status(), '_api_status' => vv_api_get_status(),
'ts' => time(), 'ts' => time(),
]; ];
+20
View File
@@ -64,6 +64,7 @@
// A flat object of the keys listed in the assembly below, plus _api_status and ts. // A flat object of the keys listed in the assembly below, plus _api_status and ts.
// //
// DEPENDS ON // DEPENDS ON
// include/ai.php AI residency and index figures, only when vv_ai_ui_on()
// include/config.php vv_cache_read() // include/config.php vv_cache_read()
// include/common.php raw hardware metrics — system, cpu, mem, net, gpu, disks, // include/common.php raw hardware metrics — system, cpu, mem, net, gpu, disks,
// ups, parity, containers, transcodes, remote roll-ups // ups, parity, containers, transcodes, remote roll-ups
@@ -92,6 +93,24 @@ require_once dirname(__DIR__) . '/include/docker_folders.php';
// Pre-warm the API cache with one request (shared by all API-first functions below). // Pre-warm the API cache with one request (shared by all API-first functions below).
vv_api_data(); vv_api_data();
// AI residency, for the Monitor tab's AI row. Collected here rather than by that card polling
// api/ai.php on its own cycle: vv_ai_runtime_stats() calls out to Ollama and shells nvidia-smi,
// and this payload is written once a minute by the background writer — a card polling it
// directly would pay both costs every five seconds on every open tab.
//
// Null on any host that is not the AI host or has AI_ENABLED false, which is also what makes the
// row absent rather than empty there. Same shape as every other optional subsystem on this page.
$_vv_ai = null;
if (vv_ai_ui_on()) {
require_once dirname(__DIR__) . '/include/ai.php';
$_vv_ai = [
'model' => vv_ai_config()['model'],
'runtime' => vv_ai_runtime_stats(),
'index' => vv_ai_index_stats(),
'tokens' => vv_ai_token_stats()['today'] ?? null,
];
}
echo json_encode([ echo json_encode([
'system' => vv_system_info(), 'system' => vv_system_info(),
'fallback' => vv_fallback_state(), 'fallback' => vv_fallback_state(),
@@ -119,6 +138,7 @@ echo json_encode([
'vms' => vv_get_vms(), 'vms' => vv_get_vms(),
'docker_folders' => vv_get_docker_folders(), 'docker_folders' => vv_get_docker_folders(),
'remote_hosts' => vv_remote_hosts_stats(), 'remote_hosts' => vv_remote_hosts_stats(),
'ai' => $_vv_ai,
'_api_status' => vv_api_get_status(), '_api_status' => vv_api_get_status(),
'ts' => time(), 'ts' => time(),
]); ]);
+143 -2
View File
@@ -29,7 +29,15 @@
// //
// Missing subsystems simply do not render. // Missing subsystems simply do not render.
// No GPU, no UPS, no VMs — the corresponding card is absent rather than showing zeros // No GPU, no UPS, no VMs — the corresponding card is absent rather than showing zeros
// or an error. The page is built to be correct on hardware lacking any given part. // or an error. The page is built to be correct on hardware lacking any given part. The
// AI row is the same rule applied to a subsystem rather than a device: it exists only
// where vv_ai_ui_on() is true, which is the AI host with AI_ENABLED set.
//
// The assistant here starts on General Chat, where the AI tab starts on Varaverk Assistant.
// Different jobs. The tab is where you go to interrogate the installation; this is the
// box you type an idle question into while watching the dashboard. The worker escalates
// anything genuinely about this install to the strict profile on its own, so starting
// loose costs nothing and starting strict would refuse ordinary questions.
// //
// OPERATIONAL SAFEGUARDS // OPERATIONAL SAFEGUARDS
// The health roll-up must not default to healthy. // The health roll-up must not default to healthy.
@@ -63,17 +71,22 @@
// RENDERS // RENDERS
// System header, CPU per core, memory breakdown, GPU cards, storage pools and array disks, // System header, CPU per core, memory breakdown, GPU cards, storage pools and array disks,
// network, UPS, VMs, containers, transcode sessions, media now-playing, watchdog summary, // network, UPS, VMs, containers, transcode sessions, media now-playing, watchdog summary,
// partner node cards // partner node cards, and — on the AI host only — model residency, saved conversations and
// an assistant
// //
// DEPENDS ON // DEPENDS ON
// include/monitor.php required directly for initial render // include/monitor.php required directly for initial render
// include/ai_chat.php the AI row's chat and conversation list, shared with the AI tab
// api/monitor.php full payload, slower cycle // api/monitor.php full payload, slower cycle
// api/monitor_fast.php fast-moving values, 1s // api/monitor_fast.php fast-moving values, 1s
// api/media.php now-playing sessions // api/media.php now-playing sessions
// api/docker_action.php container actions // api/docker_action.php container actions
// api/flag_toggle.php toggles // api/flag_toggle.php toggles
// api/ai.php the AI row's turns and conversation store
// ═══════════════════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once dirname(__DIR__) . '/include/monitor.php'; require_once dirname(__DIR__) . '/include/monitor.php';
require_once dirname(__DIR__) . '/include/ai_chat.php';
if (vv_ai_ui_on()) vv_ai_chat_assets();
?> ?>
<style> <style>
@keyframes vvRsPulse { @keyframes vvRsPulse {
@@ -279,6 +292,59 @@ require_once dirname(__DIR__) . '/include/monitor.php';
<div id="vv-array-body">Loading...</div> <div id="vv-array-body">Loading...</div>
</div> </div>
<?php if (vv_ai_ui_on()): ?>
<!-- Row 5: AI residency | Saved conversations | Assistant -->
<!-- Present only on the AI host with AI_ENABLED true, on the same footing as the GPU and UPS
cards above: a subsystem that is not here does not render an empty card explaining that
it is not here. api/ai.php refuses every action independently, so this is presentation
rather than access control. -->
<div class="vv-card" id="vv-ai-stats-card" style="grid-column:span 1;">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="2.5" width="7" height="7" rx="1.2"/><circle cx="4.7" cy="5" r="0.7" fill="#888" stroke="none"/><circle cx="7.3" cy="5" r="0.7" fill="#888" stroke="none"/><line x1="4.5" y1="7.3" x2="7.5" y2="7.3"/><line x1="6" y1="2.5" x2="6" y2="0.8"/><line x1="2.5" y1="6" x2="0.8" y2="6"/><line x1="9.5" y1="6" x2="11.2" y2="6"/></svg></span>
AI
</span>
<a href="/plugins/varaverk/Varaverk.page?tab=ai" class="vv-card-cog" title="AI tab">⚙</a>
</h3>
<div id="vv-ai-stats-body">Loading...</div>
</div>
<div class="vv-card" id="vv-ai-chats-card" style="grid-column:span 2;">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="13" height="12" viewBox="0 0 13 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><path d="M1 2.2C1 1.5 1.5 1 2.2 1H10.8C11.5 1 12 1.5 12 2.2V6.8C12 7.5 11.5 8 10.8 8H4.5L2 10.5V8H2.2C1.5 8 1 7.5 1 6.8Z"/></svg></span>
Conversations
</span>
</h3>
<?php vv_ai_chat_list_markup('vv-mon-ai'); ?>
</div>
<!-- Named for what it is, not "dock" — the Scheduler tab's vv-ai-dock is a different
component and vvAiDockOn() there tests for that id by name. -->
<div class="vv-card" id="vv-ai-assistant-card" style="grid-column:span 5;">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="5"/><path d="M4.4 4.6C4.4 3.7 5.1 3.1 6 3.1C6.9 3.1 7.6 3.7 7.6 4.5C7.6 5.9 6 5.7 6 7"/><circle cx="6" cy="8.8" r="0.6" fill="#888" stroke="none"/></svg></span>
Assistant
</span>
</h3>
<?php
// Starts on General Chat, unlike the AI tab. This is the box you type an idle question
// into while watching the dashboard, and the worker escalates anything about this install
// to the assistant on its own — so landing on the strict profile here would refuse
// ordinary questions to guard against a mistake the server already prevents.
vv_ai_chat_markup('vv-mon-ai', [
'profile' => 'chat',
'compact' => true,
'height' => '300px',
'empty' => 'Ask anything. Questions about this installation are handed to the '
. 'Varaverk assistant automatically.',
'placeholder' => 'Ask the assistant…',
]);
?>
</div>
<?php endif; ?>
</div> </div>
@@ -1817,10 +1883,63 @@ function vvPollMonitor(live) {
dfData.vms = d.vms ?? { available: false, vms: [] }; dfData.vms = d.vms ?? { available: false, vms: [] };
vvRenderDockerFolders(dfData); vvRenderDockerFolders(dfData);
// ── AI ──────────────────────────────────────────────────────────────────
if (d.ai) vvRenderAi(d.ai);
}) })
.catch(vvPollFailed); .catch(vvPollFailed);
} }
// ── AI residency card ────────────────────────────────────────────────────────
// Leads with offload, not with size. 100% on this card is the difference between ~62 tok/s and
// roughly a quarter of that, and nothing else in the WebGUI surfaces it — a model that has
// quietly fallen back to partial CPU offload is otherwise invisible until answers feel slow.
function vvRenderAi(ai) {
const box = document.getElementById('vv-ai-stats-body');
if (!box) return;
const rt = ai.runtime ?? {}, ix = ai.index ?? {}, tok = ai.tokens ?? null;
const line = (label, value, cls) =>
`<div style="display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:6px;">`
+ `<span style="font-size:11px;color:#666;">${vvEscHtml(label)}</span>`
+ `<span style="font-size:11px;font-family:monospace;${cls || 'color:#999;'}">${vvEscHtml(value)}</span></div>`;
let h = '';
if (!rt.reachable) {
h += line('Ollama', 'unreachable', 'color:#e57;');
} else if (!rt.loaded) {
h += line('Model', 'not loaded', 'color:#ffb74d;');
h += `<div style="font-size:10px;color:#444;margin:-2px 0 7px;">loads on first question</div>`;
} else {
const p = rt.offload_pct;
const full = p === 100;
h += line('GPU offload', p === null ? '—' : p + '%', full ? 'color:#6fcf97;' : 'color:#ffb74d;');
h += `<div style="font-size:10px;color:#444;margin:-2px 0 7px;">`
+ (full ? 'all on GPU' : 'layers on CPU — slow') + `</div>`;
if (rt.context) h += line('Context', rt.context.toLocaleString());
}
if (rt.gpu) {
h += line('VRAM', (rt.gpu.mem_used/1024).toFixed(1) + '/' + (rt.gpu.mem_total/1024).toFixed(1) + ' GB');
h += line('GPU util', rt.gpu.util + '%');
}
// Staleness is stated, not implied. An index older than the newest tracked file answers
// confidently out of code that has since changed — the one failure a grounded answer cannot
// reveal on its own.
if (!ix.exists) {
h += line('Index', 'not built', 'color:#e57;');
} else {
h += line('Index', ix.chunks.toLocaleString() + ' chunks',
ix.stale ? 'color:#ffb74d;' : 'color:#999;');
if (ix.stale) h += `<div style="font-size:10px;color:#8a6a3a;margin:-2px 0 7px;">source newer — reindex</div>`;
}
if (tok && tok.turns) h += line('Today', tok.total.toLocaleString() + ' tok');
box.innerHTML = h;
}
// 5s against a payload the cache writer refreshes once a minute. Polling faster cannot make the // 5s against a payload the cache writer refreshes once a minute. Polling faster cannot make the
// data newer — it only decides how soon the page notices the writer's update. // data newer — it only decides how soon the page notices the writer's update.
vvPollRunner(vvPollMonitor, 5000); vvPollRunner(vvPollMonitor, 5000);
@@ -2481,4 +2600,26 @@ document.addEventListener('click', () => {
// on this page ever called the function — there are no such buttons, on this or any other tab. // on this page ever called the function — there are no such buttons, on this or any other tab.
// Removed rather than left as an unreachable handler for the platform's three most destructive // Removed rather than left as an unreachable handler for the platform's three most destructive
// operations. The endpoint stays; see its header for why it is kept unwired. // operations. The endpoint stays; see its header for why it is kept unwired.
// ── AI row ───────────────────────────────────────────────────────────────────
// Constructed only when the row rendered. The card markup is behind vv_ai_ui_on(), so on any
// other host these ids do not exist and the factories are never called — the row is absent
// rather than broken.
//
// Same store as the AI tab, so a conversation started here is the one you carry on there. The
// instance keys itself on its prefix and tears down any predecessor, which matters on this tab
// specifically: Unraid swaps tab content by AJAX without unloading the previous page's script,
// and this page already has three poll loops that survive that.
if (document.getElementById('vv-mon-ai-chat')) {
let vvMonChatList = null;
const vvMonChat = VvAiChat({
prefix: 'vv-mon-ai',
profile: 'chat',
empty: 'Ask anything. Questions about this installation are handed to the Varaverk '
+ 'assistant automatically.',
onChats: id => { if (vvMonChatList) vvMonChatList.setActive(id); },
});
vvMonChatList = VvAiChatList({ into: 'vv-mon-ai-chats', chat: vvMonChat });
vvMonChatList.reload();
}
</script> </script>