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:
@@ -189,6 +189,12 @@ button.ghost:hover { color: var(--text); border-color: var(--accent-dim); }
|
||||
.rev-modal .md-preview { padding: 16px 20px; overflow-y: auto; flex: 1; }
|
||||
.rev-modal-foot { padding: 12px 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; }
|
||||
|
||||
/* AI Usage card */
|
||||
.aiu-sec { margin-bottom: 12px; }
|
||||
.aiu-h { font-family: var(--font-display); font-size: 10px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted); margin-bottom: 6px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
|
||||
.aiu-link { display: inline-block; margin-top: 4px; font-size: 11px; color: var(--accent); text-decoration: none; }
|
||||
.aiu-link:hover { text-decoration: underline; }
|
||||
|
||||
/* modal */
|
||||
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||||
.modal {
|
||||
|
||||
41
public/views/cards/ai_usage.js
Normal file
41
public/views/cards/ai_usage.js
Normal 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; }
|
||||
};
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user