treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
This commit is contained in:
parent
5d24bedd60
commit
39b95c2ede
203 changed files with 10090 additions and 6085 deletions
|
|
@ -2,8 +2,8 @@
|
|||
// once on load, then /api/stats?window=... for the chart data — re-fetches
|
||||
// when the operator clicks a window tab.
|
||||
|
||||
import Chart from 'chart.js/auto';
|
||||
import { createTabStrip } from '@hive/shared/tabs.js';
|
||||
import Chart from "chart.js/auto";
|
||||
import { createTabStrip } from "@hive/shared/tabs.js";
|
||||
|
||||
// Expose for the IIFE below — pre-split this was a window global from
|
||||
// the jsDelivr CDN script tag. esbuild now bundles chart.js into
|
||||
|
|
@ -12,26 +12,36 @@ import { createTabStrip } from '@hive/shared/tabs.js';
|
|||
window.Chart = Chart;
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
const cssVar = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
const cssVar = (name) =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
const palette = {
|
||||
bg: cssVar('--bg'),
|
||||
bgElev: cssVar('--bg-elev'),
|
||||
fg: cssVar('--fg'),
|
||||
muted: cssVar('--muted'),
|
||||
purple: cssVar('--purple'),
|
||||
cyan: cssVar('--cyan'),
|
||||
pink: cssVar('--pink'),
|
||||
amber: cssVar('--amber'),
|
||||
green: cssVar('--green'),
|
||||
red: cssVar('--red'),
|
||||
border: cssVar('--border'),
|
||||
bg: cssVar("--bg"),
|
||||
bgElev: cssVar("--bg-elev"),
|
||||
fg: cssVar("--fg"),
|
||||
muted: cssVar("--muted"),
|
||||
purple: cssVar("--purple"),
|
||||
cyan: cssVar("--cyan"),
|
||||
pink: cssVar("--pink"),
|
||||
amber: cssVar("--amber"),
|
||||
green: cssVar("--green"),
|
||||
red: cssVar("--red"),
|
||||
border: cssVar("--border"),
|
||||
};
|
||||
// Distinct hues for categorical charts (top tools / wake mix / result mix).
|
||||
const wheel = [palette.purple, palette.cyan, palette.pink, palette.amber,
|
||||
palette.green, palette.red, '#94e2d5', '#f9e2af',
|
||||
'#74c7ec', '#b4befe'];
|
||||
const wheel = [
|
||||
palette.purple,
|
||||
palette.cyan,
|
||||
palette.pink,
|
||||
palette.amber,
|
||||
palette.green,
|
||||
palette.red,
|
||||
"#94e2d5",
|
||||
"#f9e2af",
|
||||
"#74c7ec",
|
||||
"#b4befe",
|
||||
];
|
||||
|
||||
// Apply Catppuccin defaults globally so each Chart inherits without per-call
|
||||
// overrides. Chart.js v4 reads these on chart construction.
|
||||
|
|
@ -42,16 +52,16 @@ window.Chart = Chart;
|
|||
Chart.defaults.plugins.legend.labels.color = palette.fg;
|
||||
|
||||
const charts = {};
|
||||
let currentWindow = '24h';
|
||||
let currentWindow = "24h";
|
||||
|
||||
function fmtMs(ms) {
|
||||
if (!Number.isFinite(ms) || ms <= 0) return '0';
|
||||
if (ms < 1000) return ms.toFixed(0) + 'ms';
|
||||
return (ms / 1000).toFixed(ms < 10000 ? 2 : 1) + 's';
|
||||
if (!Number.isFinite(ms) || ms <= 0) return "0";
|
||||
if (ms < 1000) return ms.toFixed(0) + "ms";
|
||||
return (ms / 1000).toFixed(ms < 10000 ? 2 : 1) + "s";
|
||||
}
|
||||
|
||||
function fmtInt(n) {
|
||||
if (!Number.isFinite(n)) return '0';
|
||||
if (!Number.isFinite(n)) return "0";
|
||||
return new Intl.NumberFormat().format(Math.round(n));
|
||||
}
|
||||
|
||||
|
|
@ -74,18 +84,21 @@ window.Chart = Chart;
|
|||
destroy(canvasId);
|
||||
const cv = document.getElementById(canvasId);
|
||||
if (!cv) return;
|
||||
const ctx = cv.getContext('2d');
|
||||
const ctx = cv.getContext("2d");
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
ctx.fillStyle = palette.muted;
|
||||
ctx.font = '12px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = "12px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(msg, cv.width / 2, cv.height / 2);
|
||||
}
|
||||
|
||||
// Sum the four token streams across every bucket in the window.
|
||||
function tokenTotals(s) {
|
||||
let input = 0, output = 0, cacheRead = 0, cacheCreation = 0;
|
||||
let input = 0,
|
||||
output = 0,
|
||||
cacheRead = 0,
|
||||
cacheCreation = 0;
|
||||
for (const b of s.buckets || []) {
|
||||
input += b.input_tokens || 0;
|
||||
output += b.output_tokens || 0;
|
||||
|
|
@ -96,54 +109,58 @@ window.Chart = Chart;
|
|||
}
|
||||
|
||||
function renderSummary(s) {
|
||||
const root = document.getElementById('summary');
|
||||
const root = document.getElementById("summary");
|
||||
root.replaceChildren();
|
||||
const chips = [
|
||||
['turns', fmtInt(s.turn_count)],
|
||||
['avg duration', fmtMs(s.duration_summary.avg_ms)],
|
||||
['p50 duration', fmtMs(s.duration_summary.p50_ms)],
|
||||
['p95 duration', fmtMs(s.duration_summary.p95_ms)],
|
||||
['window', s.window],
|
||||
["turns", fmtInt(s.turn_count)],
|
||||
["avg duration", fmtMs(s.duration_summary.avg_ms)],
|
||||
["p50 duration", fmtMs(s.duration_summary.p50_ms)],
|
||||
["p95 duration", fmtMs(s.duration_summary.p95_ms)],
|
||||
["window", s.window],
|
||||
];
|
||||
// Token-efficiency chips: cache hit-rate (cached input vs all
|
||||
// input-side tokens) and average tokens billed per turn.
|
||||
const t = tokenTotals(s);
|
||||
const inputSide = t.input + t.cacheRead + t.cacheCreation;
|
||||
if (inputSide > 0) {
|
||||
chips.push(['cache hit-rate', (100 * t.cacheRead / inputSide).toFixed(1) + '%']);
|
||||
chips.push([
|
||||
"cache hit-rate",
|
||||
((100 * t.cacheRead) / inputSide).toFixed(1) + "%",
|
||||
]);
|
||||
}
|
||||
if (s.turn_count > 0) {
|
||||
const perTurn = (t.input + t.output + t.cacheRead + t.cacheCreation) / s.turn_count;
|
||||
chips.push(['tokens/turn', fmtInt(perTurn)]);
|
||||
const perTurn =
|
||||
(t.input + t.output + t.cacheRead + t.cacheCreation) / s.turn_count;
|
||||
chips.push(["tokens/turn", fmtInt(perTurn)]);
|
||||
}
|
||||
if (s.reminder_stats) {
|
||||
chips.push(
|
||||
['reminders scheduled', fmtInt(s.reminder_stats.scheduled)],
|
||||
['reminders delivered', fmtInt(s.reminder_stats.delivered)],
|
||||
['reminders pending', fmtInt(s.reminder_stats.pending)],
|
||||
["reminders scheduled", fmtInt(s.reminder_stats.scheduled)],
|
||||
["reminders delivered", fmtInt(s.reminder_stats.delivered)],
|
||||
["reminders pending", fmtInt(s.reminder_stats.pending)],
|
||||
);
|
||||
}
|
||||
// Session count: fresh claude sessions started in the window (each
|
||||
// new-session or auto-compaction-fallback mints one). Omitted until
|
||||
// the sessions table exists (older db).
|
||||
if (typeof s.session_count === 'number') {
|
||||
chips.push(['sessions', fmtInt(s.session_count)]);
|
||||
if (typeof s.session_count === "number") {
|
||||
chips.push(["sessions", fmtInt(s.session_count)]);
|
||||
}
|
||||
// First-turn ctx: input tokens of the most recent fresh session's
|
||||
// first turn — the cold system-prompt + CLAUDE.md cost, a sprawl
|
||||
// proxy. Omitted from the JSON (and so absent here) until the
|
||||
// per-session capture has data.
|
||||
if (typeof s.first_turn_ctx === 'number') {
|
||||
chips.push(['first-turn ctx', fmtInt(s.first_turn_ctx)]);
|
||||
if (typeof s.first_turn_ctx === "number") {
|
||||
chips.push(["first-turn ctx", fmtInt(s.first_turn_ctx)]);
|
||||
}
|
||||
for (const [label, value] of chips) {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'chip';
|
||||
const l = document.createElement('span');
|
||||
l.className = 'label';
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "chip";
|
||||
const l = document.createElement("span");
|
||||
l.className = "label";
|
||||
l.textContent = label;
|
||||
const v = document.createElement('span');
|
||||
v.className = 'value';
|
||||
const v = document.createElement("span");
|
||||
v.className = "value";
|
||||
v.textContent = value;
|
||||
chip.append(l, v);
|
||||
root.append(chip);
|
||||
|
|
@ -151,54 +168,67 @@ window.Chart = Chart;
|
|||
}
|
||||
|
||||
function renderTurnsChart(s) {
|
||||
const id = 'chart-turns';
|
||||
const id = "chart-turns";
|
||||
destroy(id);
|
||||
const labels = s.buckets.map((b) => bucketLabel(b.ts, s.bucket_seconds));
|
||||
const data = s.buckets.map((b) => b.turn_count);
|
||||
charts[id] = new Chart(document.getElementById(id), {
|
||||
type: 'bar',
|
||||
type: "bar",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'turns',
|
||||
data,
|
||||
backgroundColor: palette.purple,
|
||||
borderColor: palette.purple,
|
||||
borderWidth: 1,
|
||||
}],
|
||||
datasets: [
|
||||
{
|
||||
label: "turns",
|
||||
data,
|
||||
backgroundColor: palette.purple,
|
||||
borderColor: palette.purple,
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { color: palette.border } },
|
||||
y: { beginAtZero: true, grid: { color: palette.border }, ticks: { precision: 0 } },
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: palette.border },
|
||||
ticks: { precision: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderDurationChart(s) {
|
||||
const id = 'chart-duration';
|
||||
const id = "chart-duration";
|
||||
destroy(id);
|
||||
const labels = s.buckets.map((b) => bucketLabel(b.ts, s.bucket_seconds));
|
||||
const ds = (label, color, key) => ({
|
||||
label, data: s.buckets.map((b) => b[key]),
|
||||
borderColor: color, backgroundColor: color + '33',
|
||||
tension: 0.25, pointRadius: 0, borderWidth: 2, spanGaps: true,
|
||||
label,
|
||||
data: s.buckets.map((b) => b[key]),
|
||||
borderColor: color,
|
||||
backgroundColor: color + "33",
|
||||
tension: 0.25,
|
||||
pointRadius: 0,
|
||||
borderWidth: 2,
|
||||
spanGaps: true,
|
||||
});
|
||||
charts[id] = new Chart(document.getElementById(id), {
|
||||
type: 'line',
|
||||
type: "line",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
ds('p50', palette.cyan, 'p50_duration_ms'),
|
||||
ds('p95', palette.pink, 'p95_duration_ms'),
|
||||
ds('avg', palette.amber, 'avg_duration_ms'),
|
||||
ds("p50", palette.cyan, "p50_duration_ms"),
|
||||
ds("p95", palette.pink, "p95_duration_ms"),
|
||||
ds("avg", palette.amber, "avg_duration_ms"),
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { grid: { color: palette.border } },
|
||||
y: {
|
||||
|
|
@ -212,77 +242,108 @@ window.Chart = Chart;
|
|||
}
|
||||
|
||||
function renderCtxChart(s) {
|
||||
const id = 'chart-ctx';
|
||||
const id = "chart-ctx";
|
||||
destroy(id);
|
||||
const labels = s.buckets.map((b) => bucketLabel(b.ts, s.bucket_seconds));
|
||||
charts[id] = new Chart(document.getElementById(id), {
|
||||
type: 'line',
|
||||
type: "line",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'avg ctx',
|
||||
label: "avg ctx",
|
||||
data: s.buckets.map((b) => b.avg_ctx_tokens),
|
||||
borderColor: palette.cyan,
|
||||
backgroundColor: palette.cyan + '33',
|
||||
tension: 0.25, pointRadius: 0, borderWidth: 2, spanGaps: true,
|
||||
backgroundColor: palette.cyan + "33",
|
||||
tension: 0.25,
|
||||
pointRadius: 0,
|
||||
borderWidth: 2,
|
||||
spanGaps: true,
|
||||
},
|
||||
{
|
||||
label: 'max ctx',
|
||||
label: "max ctx",
|
||||
data: s.buckets.map((b) => b.max_ctx_tokens),
|
||||
borderColor: palette.amber,
|
||||
backgroundColor: palette.amber + '33',
|
||||
tension: 0.25, pointRadius: 0, borderWidth: 2, spanGaps: true,
|
||||
backgroundColor: palette.amber + "33",
|
||||
tension: 0.25,
|
||||
pointRadius: 0,
|
||||
borderWidth: 2,
|
||||
spanGaps: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { grid: { color: palette.border } },
|
||||
y: { beginAtZero: true, grid: { color: palette.border }, ticks: { callback: (v) => fmtInt(v) } },
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: palette.border },
|
||||
ticks: { callback: (v) => fmtInt(v) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderCostChart(s) {
|
||||
const id = 'chart-cost';
|
||||
const id = "chart-cost";
|
||||
destroy(id);
|
||||
const labels = s.buckets.map((b) => bucketLabel(b.ts, s.bucket_seconds));
|
||||
// Stacked bars: cache_read (cheap) / cache_creation / input / output.
|
||||
// Highlights "what's actually getting billed at full rate" vs cache hits.
|
||||
charts[id] = new Chart(document.getElementById(id), {
|
||||
type: 'bar',
|
||||
type: "bar",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{ label: 'cache_read', data: s.buckets.map((b) => b.cache_read_input_tokens),
|
||||
backgroundColor: palette.muted },
|
||||
{ label: 'cache_creation', data: s.buckets.map((b) => b.cache_creation_input_tokens),
|
||||
backgroundColor: palette.cyan },
|
||||
{ label: 'input', data: s.buckets.map((b) => b.input_tokens),
|
||||
backgroundColor: palette.amber },
|
||||
{ label: 'output', data: s.buckets.map((b) => b.output_tokens),
|
||||
backgroundColor: palette.pink },
|
||||
{
|
||||
label: "cache_read",
|
||||
data: s.buckets.map((b) => b.cache_read_input_tokens),
|
||||
backgroundColor: palette.muted,
|
||||
},
|
||||
{
|
||||
label: "cache_creation",
|
||||
data: s.buckets.map((b) => b.cache_creation_input_tokens),
|
||||
backgroundColor: palette.cyan,
|
||||
},
|
||||
{
|
||||
label: "input",
|
||||
data: s.buckets.map((b) => b.input_tokens),
|
||||
backgroundColor: palette.amber,
|
||||
},
|
||||
{
|
||||
label: "output",
|
||||
data: s.buckets.map((b) => b.output_tokens),
|
||||
backgroundColor: palette.pink,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { stacked: true, grid: { color: palette.border } },
|
||||
y: { stacked: true, beginAtZero: true,
|
||||
grid: { color: palette.border }, ticks: { callback: (v) => fmtInt(v) } },
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
grid: { color: palette.border },
|
||||
ticks: { callback: (v) => fmtInt(v) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderModelChart(s) {
|
||||
const id = 'chart-model';
|
||||
const id = "chart-model";
|
||||
destroy(id);
|
||||
const models = s.models || [];
|
||||
if (!models.length) { paintEmpty(id, 'no turns in window'); return; }
|
||||
if (!models.length) {
|
||||
paintEmpty(id, "no turns in window");
|
||||
return;
|
||||
}
|
||||
const labels = s.buckets.map((b) => bucketLabel(b.ts, s.bucket_seconds));
|
||||
// One stacked series per model. Model choice drives token cost,
|
||||
// so this lines up against the cost chart above it.
|
||||
|
|
@ -292,28 +353,36 @@ window.Chart = Chart;
|
|||
backgroundColor: wheel[i % wheel.length],
|
||||
}));
|
||||
charts[id] = new Chart(document.getElementById(id), {
|
||||
type: 'bar',
|
||||
type: "bar",
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { position: 'top', labels: { boxWidth: 12 } } },
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { position: "top", labels: { boxWidth: 12 } } },
|
||||
scales: {
|
||||
x: { stacked: true, grid: { color: palette.border } },
|
||||
y: { stacked: true, beginAtZero: true,
|
||||
grid: { color: palette.border }, ticks: { precision: 0 } },
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
grid: { color: palette.border },
|
||||
ticks: { precision: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderResultTrendChart(s) {
|
||||
const id = 'chart-result-trend';
|
||||
const id = "chart-result-trend";
|
||||
destroy(id);
|
||||
// Series = the result kinds seen in the window (same order +
|
||||
// colours as the result-mix doughnut). One stacked bar series per
|
||||
// kind, so error / rate-limit / compaction spikes line up in time.
|
||||
const kinds = (s.result_mix || []).map((kc) => kc.key);
|
||||
if (!kinds.length) { paintEmpty(id, 'no results'); return; }
|
||||
if (!kinds.length) {
|
||||
paintEmpty(id, "no results");
|
||||
return;
|
||||
}
|
||||
const labels = s.buckets.map((b) => bucketLabel(b.ts, s.bucket_seconds));
|
||||
const datasets = kinds.map((k, i) => ({
|
||||
label: k,
|
||||
|
|
@ -321,15 +390,20 @@ window.Chart = Chart;
|
|||
backgroundColor: wheel[i % wheel.length],
|
||||
}));
|
||||
charts[id] = new Chart(document.getElementById(id), {
|
||||
type: 'bar',
|
||||
type: "bar",
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { position: 'top', labels: { boxWidth: 12 } } },
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { position: "top", labels: { boxWidth: 12 } } },
|
||||
scales: {
|
||||
x: { stacked: true, grid: { color: palette.border } },
|
||||
y: { stacked: true, beginAtZero: true,
|
||||
grid: { color: palette.border }, ticks: { precision: 0 } },
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
grid: { color: palette.border },
|
||||
ticks: { precision: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -345,11 +419,22 @@ window.Chart = Chart;
|
|||
const data = items.map((kc) => kc.count);
|
||||
const colors = items.map((_, i) => wheel[i % wheel.length]);
|
||||
charts[canvasId] = new Chart(document.getElementById(canvasId), {
|
||||
type: 'doughnut',
|
||||
data: { labels, datasets: [{ data, backgroundColor: colors, borderColor: palette.bg, borderWidth: 2 }] },
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
data,
|
||||
backgroundColor: colors,
|
||||
borderColor: palette.bg,
|
||||
borderWidth: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { position: 'right', labels: { boxWidth: 12 } } },
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { position: "right", labels: { boxWidth: 12 } } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -360,30 +445,30 @@ window.Chart = Chart;
|
|||
// doughnut. Runs independently of turn_count (a bash task is tied to
|
||||
// a turn, but we don't want to couple the two reads).
|
||||
function renderBashCard(s) {
|
||||
const card = document.getElementById('card-bash');
|
||||
const card = document.getElementById("card-bash");
|
||||
const items = s.bash_breakdown || [];
|
||||
if (!items.length) {
|
||||
if (card) card.hidden = true;
|
||||
destroy('chart-bash');
|
||||
destroy("chart-bash");
|
||||
return;
|
||||
}
|
||||
if (card) card.hidden = false;
|
||||
renderKeyCount('chart-bash', items, 'no bash commands');
|
||||
renderKeyCount("chart-bash", items, "no bash commands");
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
renderSummary(s);
|
||||
renderBashCard(s);
|
||||
if (s.turn_count === 0) {
|
||||
paintEmpty('chart-turns', 'no turns in window');
|
||||
paintEmpty('chart-duration', 'no turns in window');
|
||||
paintEmpty('chart-ctx', 'no turns in window');
|
||||
paintEmpty('chart-cost', 'no turns in window');
|
||||
paintEmpty('chart-model', 'no turns in window');
|
||||
paintEmpty('chart-tools', 'no tool calls');
|
||||
paintEmpty('chart-wake', 'no wakes');
|
||||
paintEmpty('chart-result', 'no results');
|
||||
paintEmpty('chart-result-trend', 'no results');
|
||||
paintEmpty("chart-turns", "no turns in window");
|
||||
paintEmpty("chart-duration", "no turns in window");
|
||||
paintEmpty("chart-ctx", "no turns in window");
|
||||
paintEmpty("chart-cost", "no turns in window");
|
||||
paintEmpty("chart-model", "no turns in window");
|
||||
paintEmpty("chart-tools", "no tool calls");
|
||||
paintEmpty("chart-wake", "no wakes");
|
||||
paintEmpty("chart-result", "no results");
|
||||
paintEmpty("chart-result-trend", "no results");
|
||||
return;
|
||||
}
|
||||
renderTurnsChart(s);
|
||||
|
|
@ -391,51 +476,63 @@ window.Chart = Chart;
|
|||
renderCtxChart(s);
|
||||
renderCostChart(s);
|
||||
renderModelChart(s);
|
||||
renderKeyCount('chart-tools', s.tool_breakdown, 'no tool calls');
|
||||
renderKeyCount('chart-wake', s.wake_mix, 'no wakes');
|
||||
renderKeyCount('chart-result', s.result_mix, 'no results');
|
||||
renderKeyCount("chart-tools", s.tool_breakdown, "no tool calls");
|
||||
renderKeyCount("chart-wake", s.wake_mix, "no wakes");
|
||||
renderKeyCount("chart-result", s.result_mix, "no results");
|
||||
renderResultTrendChart(s);
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const resp = await fetch('api/stats?window=' + encodeURIComponent(currentWindow));
|
||||
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||
const resp = await fetch(
|
||||
"api/stats?window=" + encodeURIComponent(currentWindow),
|
||||
);
|
||||
if (!resp.ok) throw new Error("http " + resp.status);
|
||||
const snap = await resp.json();
|
||||
render(snap);
|
||||
} catch (e) {
|
||||
document.getElementById('summary').textContent = 'stats fetch failed: ' + e;
|
||||
document.getElementById("summary").textContent =
|
||||
"stats fetch failed: " + e;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadIdentity() {
|
||||
try {
|
||||
const resp = await fetch('api/state');
|
||||
const resp = await fetch("api/state");
|
||||
if (!resp.ok) return;
|
||||
const s = await resp.json();
|
||||
document.title = 'stats · ' + s.label;
|
||||
document.getElementById('title').textContent = '◆ ' + s.label + ' ◆';
|
||||
const dl = document.getElementById('dashboard-link');
|
||||
document.title = "stats · " + s.label;
|
||||
document.getElementById("title").textContent = "◆ " + s.label + " ◆";
|
||||
const dl = document.getElementById("dashboard-link");
|
||||
// When accessed via hive-gateway the page lives at `/agent/<name>/`
|
||||
// on the same origin as the dashboard. Detect via path prefix
|
||||
// rather than the direct port (which is unreachable or wrong scheme
|
||||
// behind HTTPS TLS termination). The dashboard SPA lives at
|
||||
// `dashboard.html` — the `/` root now serves the H0M3 menu hub.
|
||||
dl.href = window.location.pathname.startsWith('/agent/')
|
||||
? window.location.origin + '/dashboard.html'
|
||||
: 'http://' + window.location.hostname + ':' + s.dashboard_port + '/dashboard.html';
|
||||
} catch (_) { /* non-fatal */ }
|
||||
dl.href = window.location.pathname.startsWith("/agent/")
|
||||
? window.location.origin + "/dashboard.html"
|
||||
: "http://" +
|
||||
window.location.hostname +
|
||||
":" +
|
||||
s.dashboard_port +
|
||||
"/dashboard.html";
|
||||
} catch (_) {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
loadIdentity();
|
||||
// The shared hash-routed tab strip drives the stat-window selector,
|
||||
// making it deep-linkable (#1h / #24h / …). onShow updates the
|
||||
// window + reloads; the strip's initial show fires onShow once, so
|
||||
// there's no separate loadStats() call here (avoids a double fetch).
|
||||
createTabStrip(document.getElementById('window-tabs'), {
|
||||
createTabStrip(document.getElementById("window-tabs"), {
|
||||
defaultId: currentWindow,
|
||||
onShow: (w) => { currentWindow = w; loadStats(); },
|
||||
onShow: (w) => {
|
||||
currentWindow = w;
|
||||
loadStats();
|
||||
},
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in a new issue