const numberFormat = globalThis.CodexUsageNumberFormat;
const state = {
summary: null,
rangeStart: 0,
rangeEnd: 1,
dragging: null,
};
const elements = {
generatedAt: document.querySelector("#generatedAt"),
allTimeTokens: document.querySelector("#allTimeTokens"),
todayTokens: document.querySelector("#todayTokens"),
lastSevenTokens: document.querySelector("#lastSevenTokens"),
eventCount: document.querySelector("#eventCount"),
refreshButton: document.querySelector("#refreshButton"),
dimensionSelect: document.querySelector("#dimensionSelect"),
projectFilter: document.querySelector("#projectFilter"),
modelFilter: document.querySelector("#modelFilter"),
searchInput: document.querySelector("#searchInput"),
rangeReadout: document.querySelector("#rangeReadout"),
warnings: document.querySelector("#warnings"),
timelineChart: document.querySelector("#timelineChart"),
chartTooltip: document.querySelector("#chartTooltip"),
distributionTitle: document.querySelector("#distributionTitle"),
distributionChart: document.querySelector("#distributionChart"),
breakdownChart: document.querySelector("#breakdownChart"),
eventsBody: document.querySelector("#eventsBody"),
tableCount: document.querySelector("#tableCount"),
};
async function loadSummary(refresh = false) {
elements.refreshButton.disabled = true;
const response = await fetch(refresh ? "/api/refresh" : "/api/summary", {
method: refresh ? "POST" : "GET",
});
state.summary = await response.json();
state.rangeStart = 0;
state.rangeEnd = 1;
populateFilters();
render();
elements.refreshButton.disabled = false;
}
function populateFilters() {
const events = state.summary?.events ?? [];
fillSelect(elements.projectFilter, "All projects", unique(events.map((event) => event.project)));
fillSelect(elements.modelFilter, "All models", unique(events.map((event) => event.model)));
}
function fillSelect(select, label, values) {
const previous = select.value;
select.replaceChildren(new Option(label, ""));
values.forEach((value) => select.add(new Option(value || "Unknown", value || "Unknown")));
select.value = values.includes(previous) ? previous : "";
}
function unique(values) {
return [...new Set(values.filter(Boolean))].sort((a, b) => a.localeCompare(b));
}
function render() {
if (!state.summary) {
return;
}
const filtered = filteredEvents();
const totals = totalUsage(filtered);
elements.generatedAt.textContent = `Generated ${formatDateTime(state.summary.generated_at)} from ${state.summary.totals.session_count} sessions`;
setReadableNumber(elements.allTimeTokens, totals.total_tokens);
setReadableNumber(elements.todayTokens, tokensForPreset(filtered, "today"));
setReadableNumber(elements.lastSevenTokens, tokensForPreset(filtered, "7d"));
setReadableNumber(elements.eventCount, filtered.length);
renderWarnings();
renderTimeline();
renderDistribution(filtered);
renderBreakdown(totals);
renderTable(filtered);
}
function filteredEvents() {
const events = state.summary?.events ?? [];
const project = elements.projectFilter.value;
const model = elements.modelFilter.value;
const query = elements.searchInput.value.trim().toLowerCase();
const [start, end] = selectedTimeBounds(events);
return events.filter((event) => {
const time = Date.parse(event.timestamp);
const inRange = !Number.isFinite(start) || !Number.isFinite(end) || (time >= start && time <= end);
const matchesProject = !project || event.project === project;
const matchesModel = !model || event.model === model;
const haystack = `${event.thread} ${event.project} ${event.model} ${event.effort}`.toLowerCase();
const matchesQuery = !query || haystack.includes(query);
return inRange && matchesProject && matchesModel && matchesQuery;
});
}
function selectedTimeBounds(events) {
const bounds = eventTimeBounds(events);
if (!bounds) {
return [Number.NaN, Number.NaN];
}
const [min, max] = bounds;
const span = Math.max(max - min, 1);
return [min + span * state.rangeStart, min + span * state.rangeEnd];
}
function eventTimeBounds(events) {
const times = events.map((event) => Date.parse(event.timestamp)).filter(Number.isFinite);
if (times.length === 0) {
return null;
}
return [Math.min(...times), Math.max(...times)];
}
function totalUsage(events) {
return events.reduce(
(total, event) => {
total.input_tokens += event.usage.input_tokens;
total.cached_input_tokens += event.usage.cached_input_tokens;
total.output_tokens += event.usage.output_tokens;
total.reasoning_output_tokens += event.usage.reasoning_output_tokens;
total.total_tokens += event.usage.total_tokens;
return total;
},
{
input_tokens: 0,
cached_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens: 0,
},
);
}
function tokensForPreset(events, preset) {
const now = new Date();
const today = now.toISOString().slice(0, 10);
const sevenDaysAgo = new Date(now);
sevenDaysAgo.setDate(now.getDate() - 6);
const start = sevenDaysAgo.toISOString().slice(0, 10);
return events
.filter((event) => {
const day = event.timestamp.slice(0, 10);
if (preset === "today") {
return day === today;
}
return day >= start && day <= today;
})
.reduce((sum, event) => sum + event.usage.total_tokens, 0);
}
function renderWarnings() {
const warnings = state.summary.warnings ?? [];
elements.warnings.hidden = warnings.length === 0;
elements.warnings.textContent = warnings.slice(0, 4).map((warning) => warning.message).join(" | ");
}
function renderTimeline() {
const svg = elements.timelineChart;
const points = state.summary.timeline ?? [];
svg.replaceChildren();
if (points.length === 0) {
svg.appendChild(svgText(450, 130, "No token events found"));
elements.rangeReadout.textContent = "No data";
return;
}
const width = 900;
const height = 260;
const padding = { left: 44, right: 24, top: 22, bottom: 42 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
const maxTokens = Math.max(...points.map((point) => point.total_tokens), 1);
const barWidth = chartWidth / points.length;
points.forEach((point, index) => {
const barHeight = (point.total_tokens / maxTokens) * chartHeight;
const x = padding.left + index * barWidth + 2;
const y = padding.top + chartHeight - barHeight;
const rect = svgEl("rect", {
x,
y,
width: Math.max(barWidth - 4, 2),
height: Math.max(barHeight, 1),
fill: "#2563eb",
opacity: "0.78",
tabindex: 0,
"data-tooltip": formatTokenTooltip(point.bucket, point.total_tokens, point.event_count),
});
rect.appendChild(svgEl("title", {}, formatTokenTooltip(point.bucket, point.total_tokens, point.event_count)));
svg.appendChild(rect);
});
svg.appendChild(svgEl("line", {
x1: padding.left,
y1: padding.top + chartHeight,
x2: width - padding.right,
y2: padding.top + chartHeight,
stroke: "#94a3b8",
}));
const startX = padding.left + chartWidth * state.rangeStart;
const endX = padding.left + chartWidth * state.rangeEnd;
svg.appendChild(svgEl("rect", {
x: padding.left,
y: padding.top,
width: startX - padding.left,
height: chartHeight,
fill: "#f8fafc",
opacity: "0.72",
}));
svg.appendChild(svgEl("rect", {
x: endX,
y: padding.top,
width: width - padding.right - endX,
height: chartHeight,
fill: "#f8fafc",
opacity: "0.72",
}));
svg.appendChild(handle("start", startX, padding.top, chartHeight));
svg.appendChild(handle("end", endX, padding.top, chartHeight));
svg.appendChild(svgText(padding.left, height - 14, points[0].bucket, "start"));
svg.appendChild(svgText(width - padding.right, height - 14, points[points.length - 1].bucket, "end"));
updateRangeReadout();
}
function handle(name, x, top, height) {
const group = svgEl("g", { "data-handle": name, cursor: "ew-resize" });
group.appendChild(svgEl("line", {
x1: x,
y1: top,
x2: x,
y2: top + height,
stroke: "#0f766e",
"stroke-width": 3,
}));
group.appendChild(svgEl("rect", {
x: x - 7,
y: top + height / 2 - 22,
width: 14,
height: 44,
rx: 4,
fill: "#0f766e",
}));
return group;
}
function updateRangeReadout() {
const bounds = selectedTimeBounds(state.summary.events ?? []);
if (!Number.isFinite(bounds[0]) || !Number.isFinite(bounds[1])) {
elements.rangeReadout.textContent = "All data";
return;
}
elements.rangeReadout.textContent = `${formatDate(bounds[0])} to ${formatDate(bounds[1])}`;
}
function renderDistribution(events) {
const dimension = elements.dimensionSelect.value;
const labels = {
projects: "Project distribution",
models: "Model distribution",
threads: "Thread distribution",
efforts: "Effort distribution",
context_windows: "Context window distribution",
};
elements.distributionTitle.textContent = labels[dimension] ?? "Distribution";
const points = distribution(events, dimension).slice(0, 12);
renderBars(elements.distributionChart, points);
}
function distribution(events, dimension) {
const totals = new Map();
events.forEach((event) => {
const label = labelFor(event, dimension);
const current = totals.get(label) ?? { label, total_tokens: 0, event_count: 0 };
current.total_tokens += event.usage.total_tokens;
current.event_count += 1;
totals.set(label, current);
});
return [...totals.values()].sort((a, b) => b.total_tokens - a.total_tokens || b.event_count - a.event_count || a.label.localeCompare(b.label));
}
function labelFor(event, dimension) {
if (dimension === "projects") return event.project || "Unknown";
if (dimension === "models") return event.model || "Unknown";
if (dimension === "threads") return event.thread || "Unknown";
if (dimension === "efforts") return event.effort || "Unknown";
if (dimension === "context_windows") return event.context_window ? String(event.context_window) : "Unknown";
return "Unknown";
}
function renderBreakdown(total) {
renderBars(elements.breakdownChart, [
{ label: "Input", total_tokens: total.input_tokens },
{ label: "Cached input", total_tokens: total.cached_input_tokens },
{ label: "Output", total_tokens: total.output_tokens },
{ label: "Reasoning output", total_tokens: total.reasoning_output_tokens },
]);
}
function renderBars(container, points) {
container.replaceChildren();
if (points.length === 0) {
const empty = document.createElement("div");
empty.className = "empty";
empty.textContent = "No data";
container.appendChild(empty);
return;
}
const max = Math.max(...points.map((point) => point.total_tokens), 1);
points.forEach((point) => {
const row = document.createElement("div");
row.className = "bar-row";
row.tabIndex = 0;
row.dataset.tooltip = formatTokenTooltip(point.label, point.total_tokens, point.event_count);
row.title = formatTokenTooltip(point.label, point.total_tokens, point.event_count);
row.innerHTML = `
<div class="bar-label" title="${escapeHtml(point.label)}">${escapeHtml(point.label)}</div>
<div class="bar-track"><div class="bar-fill" style="width:${Math.max((point.total_tokens / max) * 100, 1)}%"></div></div>
<div class="bar-value" title="${formatFullNumber(point.total_tokens)}">${formatCompactNumber(point.total_tokens)}</div>
`;
container.appendChild(row);
});
}
function renderTable(events) {
elements.eventsBody.replaceChildren();
elements.tableCount.textContent = `${formatCompactNumber(events.length)} rows`;
elements.tableCount.title = `${formatFullNumber(events.length)} rows`;
events.slice(0, 300).forEach((event) => {
const row = document.createElement("tr");
row.innerHTML = `
<td>${escapeHtml(formatDateTime(event.timestamp))}</td>
<td>${escapeHtml(event.project)}</td>
<td>${escapeHtml(event.thread)}</td>
<td>${escapeHtml(event.model)}</td>
<td>${escapeHtml(event.effort)}</td>
<td>${escapeHtml(event.context_window ? String(event.context_window) : "Unknown")}</td>
<td title="${formatFullNumber(event.usage.total_tokens)}">${formatCompactNumber(event.usage.total_tokens)}</td>
`;
elements.eventsBody.appendChild(row);
});
}
function setPreset(preset) {
const events = state.summary?.events ?? [];
const bounds = eventTimeBounds(events);
if (!bounds) {
return;
}
if (preset === "all") {
state.rangeStart = 0;
state.rangeEnd = 1;
} else {
const [min, max] = bounds;
const now = max;
const startDate = new Date(now);
startDate.setHours(0, 0, 0, 0);
if (preset === "7d") {
startDate.setDate(startDate.getDate() - 6);
}
const span = Math.max(max - min, 1);
state.rangeStart = clamp((startDate.getTime() - min) / span, 0, 1);
state.rangeEnd = 1;
}
render();
}
function svgEl(tag, attrs, text) {
const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
Object.entries(attrs).forEach(([key, value]) => node.setAttribute(key, value));
if (text !== undefined) {
node.textContent = text;
}
return node;
}
function svgText(x, y, text, anchor = "middle") {
const node = svgEl("text", {
x,
y,
"text-anchor": anchor,
fill: "#657080",
"font-size": 13,
});
node.textContent = text;
return node;
}
function pointerRatio(event) {
const rect = elements.timelineChart.getBoundingClientRect();
const x = event.clientX - rect.left;
const paddingLeft = 44;
const paddingRight = 24;
const width = Math.max(rect.width - paddingLeft - paddingRight, 1);
return clamp((x - paddingLeft) / width, 0, 1);
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function setReadableNumber(element, value) {
element.textContent = formatCompactNumber(value);
element.title = formatFullNumber(value);
}
function formatCompactNumber(value) {
return numberFormat.formatCompactNumber(value);
}
function formatFullNumber(value) {
return numberFormat.formatFullNumber(value);
}
function formatTokenTooltip(label, totalTokens, eventCount) {
return numberFormat.formatTokenTooltip(label, totalTokens, eventCount);
}
function showChartTooltip(text, x, y) {
elements.chartTooltip.textContent = text;
elements.chartTooltip.hidden = false;
moveChartTooltip(x, y);
}
function moveChartTooltip(x, y) {
if (elements.chartTooltip.hidden) {
return;
}
const offset = 14;
const tooltipRect = elements.chartTooltip.getBoundingClientRect();
const left = Math.min(x + offset, window.innerWidth - tooltipRect.width - offset);
const top = Math.min(y + offset, window.innerHeight - tooltipRect.height - offset);
elements.chartTooltip.style.left = `${Math.max(offset, left)}px`;
elements.chartTooltip.style.top = `${Math.max(offset, top)}px`;
}
function hideChartTooltip() {
elements.chartTooltip.hidden = true;
}
function tooltipTargetFromEvent(event) {
return event.target instanceof Element ? event.target.closest("[data-tooltip]") : null;
}
function showTooltipForTarget(target, x, y) {
const text = target?.dataset?.tooltip;
if (!text) {
return;
}
showChartTooltip(text, x, y);
}
function formatDateTime(value) {
return new Date(value).toLocaleString();
}
function formatDate(value) {
return new Date(value).toLocaleDateString();
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
elements.refreshButton.addEventListener("click", () => loadSummary(true));
elements.dimensionSelect.addEventListener("change", render);
elements.projectFilter.addEventListener("change", render);
elements.modelFilter.addEventListener("change", render);
elements.searchInput.addEventListener("input", render);
document.addEventListener("pointerover", (event) => {
const target = tooltipTargetFromEvent(event);
if (target) {
showTooltipForTarget(target, event.clientX, event.clientY);
}
});
document.addEventListener("pointermove", (event) => {
if (!elements.chartTooltip.hidden) {
moveChartTooltip(event.clientX, event.clientY);
}
});
document.addEventListener("pointerout", (event) => {
const target = tooltipTargetFromEvent(event);
if (target && !target.contains(event.relatedTarget)) {
hideChartTooltip();
}
});
document.addEventListener("focusin", (event) => {
const target = tooltipTargetFromEvent(event);
if (!target) {
return;
}
const rect = target.getBoundingClientRect();
showTooltipForTarget(target, rect.left + rect.width / 2, rect.top + rect.height / 2);
});
document.addEventListener("focusout", (event) => {
if (tooltipTargetFromEvent(event)) {
hideChartTooltip();
}
});
document.querySelectorAll("[data-preset]").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll("[data-preset]").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
setPreset(button.dataset.preset);
});
});
elements.timelineChart.addEventListener("pointerdown", (event) => {
const handleNode = event.target.closest("[data-handle]");
if (!handleNode) {
return;
}
state.dragging = handleNode.dataset.handle;
elements.timelineChart.setPointerCapture(event.pointerId);
});
elements.timelineChart.addEventListener("pointermove", (event) => {
if (!state.dragging) {
return;
}
const ratio = pointerRatio(event);
if (state.dragging === "start") {
state.rangeStart = clamp(ratio, 0, state.rangeEnd - 0.02);
} else {
state.rangeEnd = clamp(ratio, state.rangeStart + 0.02, 1);
}
render();
});
elements.timelineChart.addEventListener("pointerup", (event) => {
state.dragging = null;
if (elements.timelineChart.hasPointerCapture(event.pointerId)) {
elements.timelineChart.releasePointerCapture(event.pointerId);
}
});
loadSummary().catch((error) => {
elements.generatedAt.textContent = `Failed to load usage data: ${error}`;
});