184aa5e6cb
Ship the in-progress ES-module refactor of the web-ui (new static/modules/ layout, Usage/Settings pages, uplot-based dashboard) alongside a round of security and UX fixes: - main.go: add CSP + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Referrer-Policy middleware on every response; WS CheckOrigin now requires Origin host to match Host (blocks cross-site WebSocket hijacking); upgrade client before dialing upstream so origin check runs first; fatal on unparseable AGENTMON_QUERY_BASE. - app.js: delegated click handler intercepts same-origin <a> clicks for SPA navigation (prev. every nav link caused a full page reload, dropping WS + in-memory state); delegated .copy-btn[data-copy] handler replaces inline onclick=; removed window.navigate / window.copyToClipboard globals and the duplicated handleGlobalSearch. - modules/nav-signal.js: per-route AbortController so in-flight fetches are cancelled when the user navigates away, preventing stale toasts and wasted renders. - modules/api.js: honours the nav signal by default; AbortError is silent. - modules/router.js: resets the nav controller on every route; dropped the fixed 80ms transition delay; breadcrumbs no longer emit inline onclick= (delegated handler picks them up). - modules/utils.js: renderCopyButton emits data-copy=\"...\" instead of nesting a JS string inside an HTML attribute — fixes an XSS where values containing ' broke out via ' decoding. Verified: go build clean; `node --check` clean on all modified modules; manual curl probes confirm security headers present on every response and WS upgrade returns 403 for cross-origin/missing Origin while 101 for same-origin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
108 lines
4.9 KiB
JavaScript
108 lines
4.9 KiB
JavaScript
import { app, isRouteCurrent } from '../router.js';
|
|
import { api } from '../api.js';
|
|
import { escapeHTML, formatTokenCount, formatCost } from '../utils.js';
|
|
|
|
export async function renderUsage(routeToken) {
|
|
app.innerHTML = `
|
|
<div class="page-header"><h2>Usage</h2></div>
|
|
<div id="usage-content"><div class="usage-loading">Loading…</div></div>
|
|
`;
|
|
|
|
const [summary, toolsData, modelsData, tsData] = await Promise.all([
|
|
api('/v1/stats/summary').catch(() => null),
|
|
api('/v1/stats/top-tools?limit=20').catch(() => ({ tools: [] })),
|
|
api('/v1/stats/top-models?limit=10').catch(() => ({ models: [] })),
|
|
api('/v1/stats/timeseries?window=7d').catch(() => ({ series: [] })),
|
|
]);
|
|
if (routeToken && !isRouteCurrent(routeToken)) return;
|
|
|
|
const tools = toolsData.tools || [];
|
|
const models = modelsData.models || [];
|
|
const series = tsData.series || [];
|
|
|
|
// Aggregate 7d totals from timeseries
|
|
const totals7d = series.reduce((acc, b) => {
|
|
acc.runs += b.runs || 0;
|
|
acc.tools += b.tools || 0;
|
|
acc.errors += b.errors || 0;
|
|
acc.tokens += b.tokens || 0;
|
|
acc.cost += b.cost || 0;
|
|
return acc;
|
|
}, { runs: 0, tools: 0, errors: 0, tokens: 0, cost: 0 });
|
|
|
|
const s = summary || {};
|
|
|
|
const content = document.getElementById('usage-content');
|
|
if (!content) return;
|
|
|
|
content.innerHTML = `
|
|
<div class="usage-summary-tiles">
|
|
<div class="meta-tile"><div class="meta-tile-label">Active Sessions</div><div class="meta-tile-value">${s.active_sessions || 0}</div></div>
|
|
<div class="meta-tile"><div class="meta-tile-label">Runs Today</div><div class="meta-tile-value">${s.runs_today || 0}</div></div>
|
|
<div class="meta-tile"><div class="meta-tile-label">Tool Calls Today</div><div class="meta-tile-value">${s.tool_calls_today || 0}</div></div>
|
|
<div class="meta-tile"><div class="meta-tile-label">Errors Today</div><div class="meta-tile-value">${s.errors_today || 0}</div></div>
|
|
<div class="meta-tile"><div class="meta-tile-label">Tokens Today</div><div class="meta-tile-value">${formatTokenCount(s.tokens_today || 0)}</div></div>
|
|
<div class="meta-tile"><div class="meta-tile-label">Cost Today</div><div class="meta-tile-value">${formatCost(s.cost_today || 0)}</div></div>
|
|
</div>
|
|
|
|
<div class="usage-section-row">
|
|
<div class="usage-panel">
|
|
<div class="section-title">7-Day Totals</div>
|
|
<div class="usage-7d-tiles">
|
|
<div class="usage-7d-tile"><span class="usage-7d-label">Runs</span><strong>${totals7d.runs}</strong></div>
|
|
<div class="usage-7d-tile"><span class="usage-7d-label">Tool Calls</span><strong>${totals7d.tools}</strong></div>
|
|
<div class="usage-7d-tile"><span class="usage-7d-label">Errors</span><strong>${totals7d.errors}</strong></div>
|
|
<div class="usage-7d-tile"><span class="usage-7d-label">Tokens</span><strong>${formatTokenCount(totals7d.tokens)}</strong></div>
|
|
<div class="usage-7d-tile"><span class="usage-7d-label">Est. Cost</span><strong>${formatCost(totals7d.cost)}</strong></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="usage-section-row">
|
|
<div class="usage-panel">
|
|
<div class="section-title">Top Models <span class="count">${models.length}</span></div>
|
|
${models.length === 0 ? '<p class="empty-state">No model data yet</p>' : `
|
|
<ul class="stat-list" id="usage-models-list">
|
|
${(() => {
|
|
const max = models[0]?.count || 1;
|
|
return models.map(m => {
|
|
const pct = (m.count / max * 100).toFixed(1);
|
|
return `<li>
|
|
<div class="stat-list-header">
|
|
<span class="stat-list-name">${escapeHTML(m.name)}</span>
|
|
<span class="stat-list-count">${m.count}</span>
|
|
</div>
|
|
<div class="stat-list-bar-track">
|
|
<div class="stat-list-bar-fill model" style="width:${pct}%"></div>
|
|
</div>
|
|
</li>`;
|
|
}).join('');
|
|
})()}
|
|
</ul>`}
|
|
</div>
|
|
|
|
<div class="usage-panel">
|
|
<div class="section-title">Top Tools <span class="count">${tools.length}</span></div>
|
|
${tools.length === 0 ? '<p class="empty-state">No tool data yet</p>' : `
|
|
<ul class="stat-list" id="usage-tools-list">
|
|
${(() => {
|
|
const max = tools[0]?.count || 1;
|
|
return tools.map(t => {
|
|
const pct = (t.count / max * 100).toFixed(1);
|
|
return `<li>
|
|
<div class="stat-list-header">
|
|
<span class="stat-list-name">${escapeHTML(t.name)}</span>
|
|
<span class="stat-list-count">${t.count}</span>
|
|
</div>
|
|
<div class="stat-list-bar-track">
|
|
<div class="stat-list-bar-fill tool" style="width:${pct}%"></div>
|
|
</div>
|
|
</li>`;
|
|
}).join('');
|
|
})()}
|
|
</ul>`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|