Per mara on #14: 'make agent page not assume root path, links / api calls need to be relative'. atlas's nginx side (#15) will mount the per-agent UI at a prefix like /agent/<name>/ instead of its own port; for the page to keep working under that prefix, every in-page reference needs to resolve document-relative rather than root-anchored. Converted in this pass: - HTML <link>/<script>/<img>/<a> hrefs in index.html, stats.html, screen.html: '/icon' → 'icon', '/static/agent.css' → 'static/agent.css', back links '/' → './'. - app.js fetch() targets ('/api/state' → 'api/state', /api/cancel, /api/loose-ends, etc.), form actions ('/login/start', '/send'), EventSource urls ('/events/stream', '/events/history'). - stats.js fetch() targets. - screen.html WebSocket URL: was hardcoded as ws(s)://host/screen/ws; now derived from document.baseURI via new URL('screen/ws', document.baseURI) so the gateway prefix flows through. Slash-command labels (/cancel, /compact, …) and the dashboard-port link (different port, intentionally absolute) intentionally untouched. Added a new 'Per-agent relative paths' section to docs/web-ui.md covering the rationale + the trailing-slash gotcha (sub-pages like /stats must NOT have a trailing slash, or 'static/app.js' resolves under /stats/ instead of replacing the segment). Functional code unchanged; build clean. Damocles + atlas can proceed with the backend / nginx side without depending on this landing first, but once both ship the agent page works under the gateway-prefixed URL without further changes. refs #14
98 lines
4.1 KiB
HTML
98 lines
4.1 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>hyperhive agent — stats</title>
|
|
<link rel="icon" type="image/svg+xml" href="icon">
|
|
<link rel="stylesheet" href="static/agent.css">
|
|
<style>
|
|
.stats-nav { display: flex; gap: 0.75rem; align-items: baseline; margin-bottom: 0.5rem; }
|
|
.stats-nav a { color: var(--cyan); text-decoration: none; }
|
|
.stats-nav a:hover { text-decoration: underline; }
|
|
.window-tabs { display: flex; gap: 0.4rem; margin: 0.5rem 0 1rem; }
|
|
.window-tabs button {
|
|
background: var(--bg-elev); color: var(--fg);
|
|
border: 1px solid var(--border); padding: 0.3rem 0.8rem;
|
|
font-family: inherit; cursor: pointer;
|
|
}
|
|
.window-tabs button.active { background: var(--purple-dim); border-color: var(--purple); color: var(--purple); }
|
|
.summary { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
.summary .chip {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: stretch;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
padding: 0.5rem 0.9rem;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
min-width: 9rem;
|
|
height: 3.4rem;
|
|
line-height: 1.2;
|
|
}
|
|
.summary .chip .label {
|
|
color: var(--muted);
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.summary .chip .value {
|
|
color: var(--cyan);
|
|
font-weight: bold;
|
|
font-size: 1.05rem;
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
.card {
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
padding: 0.75rem 1rem 1rem;
|
|
border-radius: 4px;
|
|
}
|
|
.card h3 { margin: 0 0 0.5rem; color: var(--purple); font-size: 0.95rem; font-weight: normal; }
|
|
.card .chart-wrap { position: relative; height: 220px; }
|
|
.card.wide { grid-column: 1 / -1; }
|
|
.card.wide .chart-wrap { height: 260px; }
|
|
.empty-note { color: var(--muted); font-style: italic; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<pre class="banner">░▒▓█▓▒░ … ░▒▓█▓▒░ hyperhive ag3nt · stats ░▒▓█▓▒░</pre>
|
|
<div class="stats-nav">
|
|
<a id="back-link" href="./">← live</a>
|
|
<a id="dashboard-link" href="#">dashboard</a>
|
|
<h2 id="title" style="margin: 0;">◆ … ◆</h2>
|
|
</div>
|
|
|
|
<div class="window-tabs" id="window-tabs">
|
|
<button data-w="1h">last 1h</button>
|
|
<button data-w="4h">last 4h</button>
|
|
<button data-w="24h" class="active">last 24h</button>
|
|
<button data-w="3d">last 3d</button>
|
|
<button data-w="7d">last 7d</button>
|
|
<button data-w="30d">last 30d</button>
|
|
</div>
|
|
|
|
<div class="summary" id="summary"></div>
|
|
|
|
<div class="grid">
|
|
<div class="card wide"><h3>turns per bucket</h3><div class="chart-wrap"><canvas id="chart-turns"></canvas></div></div>
|
|
<div class="card wide"><h3>turn duration (ms) — p50 / p95 / avg</h3><div class="chart-wrap"><canvas id="chart-duration"></canvas></div></div>
|
|
<div class="card wide"><h3>context tokens (last inference per turn) — avg / max</h3><div class="chart-wrap"><canvas id="chart-ctx"></canvas></div></div>
|
|
<div class="card wide"><h3>token cost per bucket (sum across inferences)</h3><div class="chart-wrap"><canvas id="chart-cost"></canvas></div></div>
|
|
<div class="card wide"><h3>turns by model per bucket — model drives token cost</h3><div class="chart-wrap"><canvas id="chart-model"></canvas></div></div>
|
|
<div class="card"><h3>top tools</h3><div class="chart-wrap"><canvas id="chart-tools"></canvas></div></div>
|
|
<div class="card"><h3>wake source mix</h3><div class="chart-wrap"><canvas id="chart-wake"></canvas></div></div>
|
|
<div class="card"><h3>result mix</h3><div class="chart-wrap"><canvas id="chart-result"></canvas></div></div>
|
|
</div>
|
|
|
|
<!-- Chart.js is now bundled into stats.js by esbuild (npm dep
|
|
chart.js@4.4.4), so the page works offline / on operator
|
|
machines without internet egress. No SRI hash to maintain. -->
|
|
<script type="module" src="static/stats.js" defer></script>
|
|
</body>
|
|
</html>
|