feat(ui): AI Usage Sacred Valley card + /api/ai-usage proxy (Claude tokens + local model perf)

Summarises the Homelab Monitor (CT300 :8080) into a blackflame card: Claude Code
token usage today/week + top model, and OpenClaw/Ollama p50/p95 latency + error rate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-04 23:40:28 +10:00
parent 6d01cb34a7
commit 5aba750102
6 changed files with 142 additions and 1 deletions

View File

@@ -0,0 +1,41 @@
// Sacred Valley card: AI Usage — Claude Code token usage + local (OpenClaw/Ollama)
// performance, summarised from the Homelab Monitor via /api/ai-usage.
import { el, mount } from '../../dom.js';
import { api } from '../../api.js';
let body;
const fmt = (n) => { n = Number(n) || 0; if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'; if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k'; return String(n); };
const dur = (ms) => ms == null ? '—' : (ms >= 1000 ? (ms / 1000).toFixed(1) + 's' : Math.round(ms) + 'ms');
const mono = (t, color) => el('span', { style: { fontFamily: 'var(--font-mono)', ...(color ? { color } : {}) } }, t);
async function load() {
if (!body) return;
try {
const u = await api.get('/api/ai-usage');
if (!u.ok) { mount(body, el('span', { class: 'muted' }, 'Monitor unreachable (' + (u.url || '') + ')')); return; }
const c = u.claude, l = u.local;
mount(body,
el('div', { class: 'aiu-sec' },
el('div', { class: 'aiu-h' }, 'Claude Code'),
el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'today'), mono(`${fmt(c.today.input)}${fmt(c.today.output)}`)),
el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'cache · turns'), mono(`${fmt(c.today.cache)} · ${c.today.turns}`)),
el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'week'), mono(`${fmt(c.week.input + c.week.output)} tok`)),
el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'top model'), mono((c.top_model || '—').replace(/^claude-/, ''), 'var(--accent)'))
),
el('div', { class: 'aiu-sec' },
el('div', { class: 'aiu-h' }, 'Local · OpenClaw'),
l.top
? el('div', {},
el('div', { class: 'sv-row' }, el('span', { class: 'k' }, l.top.model), mono(`${dur(l.top.p50_ms)} p50 · ${dur(l.top.p95_ms)} p95`)),
el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'runs · err'), mono(`${l.runs} · ${(l.top.error_rate * 100).toFixed(0)}%`)))
: el('span', { class: 'muted' }, 'No local runs yet')
),
el('a', { class: 'aiu-link', href: 'http://192.168.1.212:8080/', target: '_blank', rel: 'noopener' }, 'Full dashboard ↗')
);
} catch { mount(body, el('span', { class: 'muted' }, 'No usage data')); }
}
export default {
id: 'ai-usage', title: 'AI Usage', size: 'm',
mount(e) { body = e; load(); }, start() {}, stop() { body = null; }
};

View File

@@ -12,8 +12,9 @@ import jobs from './cards/jobs.js';
import inbox from './cards/inbox.js';
import search from './cards/search.js';
import speedtest from './cards/speedtest.js';
import aiUsage from './cards/ai_usage.js';
const CARD_MODULES = [clock, weather, hostPerf, jobs, inbox, search, speedtest];
const CARD_MODULES = [clock, weather, hostPerf, jobs, inbox, search, speedtest, aiUsage];
const BY_ID = new Map(CARD_MODULES.map(d => [d.id, d]));
let active = []; // mounted cards needing stop()