Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ codexbar serve --port 8080

Typical endpoints: `/health`, `/usage`, `/cost`, and `/dashboard/v1/snapshot`. Loopback default keeps local use simple; treat non-loopback as a threat-model choice because the token for protected requests crosses the network over HTTP.

The dashboard page at `/` has a "Usage display" control (Follow server, Used, Remaining). Follow server uses `host.usageBarsShowUsed` from the snapshot; the other two override it for this browser only, stored in `localStorage` under `codexbar.dashboard.usageDisplay`. Changing it re-renders the cached snapshot without refetching. Bar colour always follows consumption (70% used warns, 90% used is full), whichever value is shown.

Pass `--metrics` to enable the Prometheus text endpoint at `/metrics`; it returns `404` when the flag is absent. The endpoint uses the same Host allowlist, Bearer token, snapshot cache, and single-flight collection as the dashboard snapshot. A scrape never waits for provider I/O: it returns the last successful snapshot while an expired value refreshes in the background, or `codexbar_up 0` until the first collection succeeds.

```powershell
Expand Down
58 changes: 53 additions & 5 deletions rust/src/cli/serve/dashboard/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
<style>
:root { color-scheme: dark; }
body { margin: 0; background: #101014; color: #e8e8ec; font: 13px/1.45 -apple-system, "Segoe UI", Roboto, "Microsoft YaHei", sans-serif; }
header { position: sticky; top: 0; z-index: 2; display: flex; align-items: baseline; gap: 12px; padding: 14px 20px; background: rgba(16,16,20,.92); backdrop-filter: blur(8px); border-bottom: 1px solid #26262e; }
header { position: sticky; top: 0; z-index: 2; display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 12px; padding: 14px 20px; background: rgba(16,16,20,.92); backdrop-filter: blur(8px); border-bottom: 1px solid #26262e; }
h1 { font-size: 15px; margin: 0; font-weight: 650; }
#freshness { color: #8b8b95; font-size: 12px; }
#freshness.stale { color: #f8b04a; }
.display-preference { display: inline-flex; align-items: center; gap: 6px; color: #8b8b95; font-size: 12px; }
.display-preference select { background: #17171d; color: #e8e8ec; border: 1px solid #33333c; border-radius: 6px; padding: 3px 6px; font: inherit; }
#first-load { padding: 40px 20px; color: #8b8b95; }
#error-banner { display: none; margin: 14px 20px 0; padding: 10px 14px; border: 1px solid #7c3f3f; background: #2a1b1b; color: #f0a0a0; border-radius: 8px; }
#auth-bar { display: none; gap: 8px; margin: 14px 20px 0; }
Expand Down Expand Up @@ -56,6 +58,13 @@
<h1>CodexBar</h1>
<span id="freshness">connecting…</span>
<span style="flex:1"></span>
<label class="display-preference" for="usage-display">Usage display
<select id="usage-display">
<option value="server">Follow server</option>
<option value="used">Used</option>
<option value="remaining">Remaining</option>
</select>
</label>
<span id="who" style="color:#63636b;font-size:11px"></span>
</header>
<div id="error-banner" role="alert"></div>
Expand All @@ -72,7 +81,16 @@ <h1>CodexBar</h1>
const DEFAULT_REFRESH = Math.max(15, __REFRESH_SECONDS__);
const SNAPSHOT_URL = "/dashboard/v1/snapshot";
const COST_URL = "/cost";
const state = { token: sessionStorage.getItem("codexbar.dashboard.token") || "", timer: null, refreshSeconds: DEFAULT_REFRESH };
const USAGE_DISPLAY_KEY = "codexbar.dashboard.usageDisplay";
function storedUsageDisplay() {
try {
const value = localStorage.getItem(USAGE_DISPLAY_KEY);
return value === "used" || value === "remaining" ? value : "server";
} catch (e) {
return "server";
}
}
const state = { token: sessionStorage.getItem("codexbar.dashboard.token") || "", timer: null, refreshSeconds: DEFAULT_REFRESH, usageDisplay: storedUsageDisplay(), snapshot: null, dailyCharts: {} };

const els = {
content: document.getElementById("content"),
Expand All @@ -81,6 +99,7 @@ <h1>CodexBar</h1>
error: document.getElementById("error-banner"),
authBar: document.getElementById("auth-bar"),
tokenInput: document.getElementById("token-input"),
usageDisplay: document.getElementById("usage-display"),
};

function authHeaders() {
Expand Down Expand Up @@ -128,6 +147,16 @@ <h1>CodexBar</h1>
if (used >= 70) return "bar warn";
return "bar";
}
// Severity follows consumption in every display mode (70 warn, 90 full), so a
// window with 95% left is never coloured as if it were nearly exhausted.
function windowBarClass(w) {
const used = w && w.usedPercent != null ? Number(w.usedPercent) : NaN;
return Number.isFinite(used) ? pctClass(used) : "bar";
}
function showUsedBars() {
if (state.usageDisplay !== "server") return state.usageDisplay === "used";
return Boolean(state.snapshot && state.snapshot.host && state.snapshot.host.usageBarsShowUsed);
}
function visibleWindows(windows) {
return (windows || []).filter(w => w.idle !== true);
}
Expand All @@ -137,15 +166,15 @@ <h1>CodexBar</h1>
return Number.isFinite(raw) ? Math.max(0, Math.min(100, raw)) : null;
}
function windowRow(w) {
const showUsed = Boolean(state.snapshot && state.snapshot.host && state.snapshot.host.usageBarsShowUsed);
const showUsed = showUsedBars();
const pct = windowPercent(w, showUsed);
const suffix = showUsed ? "used" : "left";
const value = pct === null ? "unknown" : pct.toFixed(0) + "% " + suffix;
const width = pct === null ? 0 : pct;
const ariaNow = pct === null ? "" : ' aria-valuenow="' + width + '"';
return '<div class="wrow">' +
'<span class="wlabel" title="' + esc(w.label) + '">' + esc(w.label) + "</span>" +
'<span class="' + (pct === null ? "bar unknown" : pctClass(pct)) + '" role="progressbar" aria-label="' + esc(w.label || "Usage") + '" aria-valuemin="0" aria-valuemax="100" aria-valuetext="' + esc(value) + '"' + ariaNow + '><i style="width:' + width.toFixed(1) + '%"></i></span>' +
'<span class="' + (pct === null ? "bar unknown" : windowBarClass(w)) + '" role="progressbar" aria-label="' + esc(w.label || "Usage") + '" aria-valuemin="0" aria-valuemax="100" aria-valuetext="' + esc(value) + '"' + ariaNow + '><i style="width:' + width.toFixed(1) + '%"></i></span>' +
'<span class="wpct">' + esc(value) + "</span>" +
'<span class="wreset">' + esc(untilTime(w.resetAt)) + "</span></div>";
}
Expand Down Expand Up @@ -198,7 +227,10 @@ <h1>CodexBar</h1>
}
return inner;
}
function render(snapshot, dailyCharts) {
function renderProviders() {
const snapshot = state.snapshot;
const dailyCharts = state.dailyCharts;
if (!snapshot) return;
els.firstLoad.style.display = "none";
els.content.hidden = false;
const rows = (snapshot.providers || []).slice().sort((a, b) =>
Expand All @@ -215,7 +247,11 @@ <h1>CodexBar</h1>
});
if (!rows.length) html = '<div id="first-load">No enabled providers.</div>';
els.content.innerHTML = html;
}
function render(snapshot, dailyCharts) {
state.snapshot = snapshot;
state.dailyCharts = dailyCharts;
renderProviders();
const host = snapshot.host || {};
state.refreshSeconds = Math.max(15, Number(host.refreshIntervalSeconds) || DEFAULT_REFRESH);
updateFreshness();
Expand Down Expand Up @@ -264,6 +300,18 @@ <h1>CodexBar</h1>
schedule();
}
}
function changeUsageDisplay() {
const value = els.usageDisplay.value;
state.usageDisplay = value === "used" || value === "remaining" ? value : "server";
els.usageDisplay.value = state.usageDisplay;
try {
if (state.usageDisplay === "server") localStorage.removeItem(USAGE_DISPLAY_KEY);
else localStorage.setItem(USAGE_DISPLAY_KEY, state.usageDisplay);
} catch (e) { /* the selection still applies to this page without storage */ }
renderProviders();
}
els.usageDisplay.value = state.usageDisplay;
els.usageDisplay.addEventListener("change", changeUsageDisplay);
document.getElementById("token-save").addEventListener("click", () => {
state.token = els.tokenInput.value.trim();
sessionStorage.setItem("codexbar.dashboard.token", state.token);
Expand Down
87 changes: 87 additions & 0 deletions rust/src/cli/serve/dashboard/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,93 @@ mod tests {
assert!(html.contains("function windowPercent(w, showUsed)"));
}

#[test]
fn usage_display_control_offers_server_used_remaining() {
let html = render_shell(60);
assert!(html.contains(r#"<select id="usage-display">"#));
assert!(html.contains(r#"<option value="server">Follow server</option>"#));
assert!(html.contains(r#"<option value="used">Used</option>"#));
assert!(html.contains(r#"<option value="remaining">Remaining</option>"#));
assert!(html.contains(r#"<label class="display-preference" for="usage-display">"#));
}

#[test]
fn usage_display_preference_is_stored_defensively() {
let html = render_shell(60);
assert!(html.contains(r#"const USAGE_DISPLAY_KEY = "codexbar.dashboard.usageDisplay""#));
// Only `used` and `remaining` are accepted; everything else, including a
// throwing `getItem`, falls back to following the server.
assert!(
html.contains(
r#"return value === "used" || value === "remaining" ? value : "server";"#
)
);
assert!(html.contains("localStorage.removeItem(USAGE_DISPLAY_KEY)"));
assert!(html.contains("localStorage.setItem(USAGE_DISPLAY_KEY, state.usageDisplay)"));
assert!(html.contains("the selection still applies to this page without storage"));
}

#[test]
fn usage_display_change_rerenders_cached_snapshot_without_refetch() {
let html = render_shell(60);
let start = html
.find("function changeUsageDisplay()")
.expect("change handler");
let end = html[start..].find("\n}\n").expect("handler end") + start;
let handler = &html[start..end];
assert!(handler.contains("renderProviders();"));
for forbidden in [
"refresh(",
"fetch(",
"showError(",
"sessionStorage",
"state.token",
] {
assert!(
!handler.contains(forbidden),
"usage display change must not touch {forbidden}"
);
}
assert!(html.contains("els.usageDisplay.addEventListener(\"change\", changeUsageDisplay)"));
}

#[test]
fn usage_display_override_wins_over_server_preference() {
let html = render_shell(60);
assert!(html.contains("const showUsed = showUsedBars();"));
assert!(html.contains(
r#"if (state.usageDisplay !== "server") return state.usageDisplay === "used";"#
));
assert!(html.contains(
"Boolean(state.snapshot && state.snapshot.host && state.snapshot.host.usageBarsShowUsed)"
));
}

#[test]
fn severity_follows_consumption_not_displayed_value() {
let html = render_shell(60);
assert!(html.contains("function windowBarClass(w)"));
assert!(html.contains("return Number.isFinite(used) ? pctClass(used) : \"bar\";"));
assert!(html.contains("windowBarClass(w)"));
assert!(
!html.contains("pctClass(pct)"),
"severity must not be derived from the displayed (possibly remaining) value"
);
}

#[test]
fn snapshot_is_cached_before_rows_render() {
let html = render_shell(60);
let assign = html.find("state.snapshot = snapshot;").expect("assignment");
let render = html
.find("renderProviders();\n const host")
.expect("render call");
assert!(
assign < render,
"rows must read the new snapshot's host preference"
);
}

#[test]
fn daily_chart_gate_reads_upstream_total_cost_key() {
let html = render_shell(60);
Expand Down