Admin view in the web UI

This commit is contained in:
Ian Gulliver
2026-10-14 21:06:30 -07:00
parent b2bd894677
commit 4a2bbe0859
34 changed files with 838 additions and 214 deletions
+144
View File
@@ -0,0 +1,144 @@
import { get } from "./api.js";
import { ago, errorText, h, replace } from "./dom.js";
import { findTask, on, onEach, state } from "./state.js";
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created"]);
export function mountAdmin(root) {
let status;
let error = "";
let inFlight = false;
let again = false;
let disposed = false;
const render = () => {
if (!status) {
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
return;
}
replace(root, h("div", { class: "admin-view" }, error && h("p", { class: "error-text" }, error), turnsSection(status), rateLimitsSection(status.rate_limits), serverSection(status)));
};
const refresh = async () => {
if (inFlight) {
again = true;
return;
}
inFlight = true;
try {
const next = await get("/api/admin/status");
if (disposed) {
return;
}
status = next;
error = "";
}
catch (err) {
error = `Couldn't load status: ${errorText(err)}`;
}
inFlight = false;
render();
if (again && !disposed) {
again = false;
void refresh();
}
};
const onEvent = (detail) => {
const ev = detail;
if (refreshKinds.has(ev.kind) || (ev.kind === "agent.vendor" && ev.payload["type"] === "rate_limit_event")) {
void refresh();
return;
}
if (status && ev.seq > status.last_event_seq) {
status.last_event_seq = ev.seq;
render();
}
};
const offs = [
on("event", onEvent),
on("connection", () => {
if (state.connected) {
void refresh();
}
}),
onEach(["projects", "tasks"], render),
];
render();
void refresh();
return () => {
disposed = true;
for (const off of offs) {
off();
}
};
}
function turnsSection(s) {
const vendors = [...new Set([...Object.keys(s.caps.per_vendor), ...s.running_turns.map((t) => t.vendor), ...s.queued_turns.map((t) => t.vendor)])].sort();
const runningFor = (vendor) => s.running_turns.filter((t) => t.vendor === vendor).length;
return h("section", { class: "admin-section" }, h("h2", {}, "Turns"), h("div", { class: "caps" }, capTile("All vendors", s.running_turns.length, s.caps.global), ...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])), h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length))), h("h3", {}, "Running"), turnList(s.running_turns, "running", "No turns running."), h("h3", {}, "Queued"), turnList(s.queued_turns, "queued", "Nothing queued."));
}
function capTile(label, running, cap) {
const full = cap !== undefined && running >= cap;
return h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, label), h("div", { class: full ? "cap-value warn" : "cap-value" }, running, h("span", { class: "cap-max" }, cap === undefined ? " / no cap" : ` / ${cap}`)));
}
function turnList(turns, kind, empty) {
return h("ul", { class: "task-list" }, ...turns.map((t, i) => {
const task = findTask(t.task_id);
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
return h("li", {}, h("a", { href: `#/task/${t.task_id}` }, kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }), h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id), h("span", { class: "task-state" }, t.vendor)));
}), turns.length === 0 && h("li", { class: "empty-row" }, empty));
}
function rateLimitsSection(limits) {
const vendors = Object.keys(limits).sort();
return h("section", { class: "admin-section" }, h("h2", {}, "Rate limits"), ...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})), vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."));
}
function rateLimitCard(vendor, info) {
const { status, rateLimitType, resetsAt, unifiedWindows, ...rest } = info;
const statusText = String(status ?? "unknown");
const tone = statusText === "allowed" ? "ok" : statusText === "rejected" ? "danger" : "warn";
const windows = (unifiedWindows ?? {});
return h("div", { class: "card rate-limit" }, h("header", {}, h("span", { class: "vendor" }, vendor), h("span", { class: `rl-status ${tone}` }, statusText), rateLimitType !== undefined && h("span", {}, String(rateLimitType)), typeof resetsAt === "number" && h("span", { class: "resets" }, "resets ", resetText(resetsAt))), Object.keys(windows).length > 0 &&
h("table", { class: "rl-windows" }, h("tr", {}, h("th", {}, "Window"), h("th", {}, "Used"), h("th", {}, "Resets")), ...Object.entries(windows).map(([name, w]) => h("tr", {}, h("td", {}, name), h("td", { class: utilizationClass(w.utilization) }, w.utilization === undefined ? "—" : `${Math.round(w.utilization * 100)}%`), h("td", {}, w.resetsAt === undefined ? "—" : resetText(w.resetsAt))))), Object.keys(rest).length > 0 &&
h("dl", { class: "kv" }, ...Object.entries(rest).flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, typeof v === "object" ? JSON.stringify(v) : String(v))])));
}
function utilizationClass(u) {
if (u === undefined) {
return "";
}
if (u >= 0.9) {
return "danger";
}
if (u >= 0.7) {
return "warn";
}
return "";
}
function resetText(unixSeconds) {
const at = new Date(unixSeconds * 1000);
const time = at.toLocaleString(undefined, { weekday: "short", hour: "numeric", minute: "2-digit" });
const minutes = Math.round((at.getTime() - Date.now()) / 60000);
if (minutes <= 0) {
return `${time} (passed)`;
}
const rel = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
return `${time} (in ${rel})`;
}
function serverSection(s) {
const rows = [
["Started", `${new Date(s.started_at).toLocaleString()} (${ago(s.started_at)})`],
["Agent processes", String(s.live_agents)],
["Projects", String(s.projects)],
["Active tasks", String(s.active_tasks)],
["Live clients", String(s.sse_clients)],
["Database", bytes(s.db_bytes)],
["Last event", `#${s.last_event_seq}`],
];
return h("section", { class: "admin-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
}
function bytes(n) {
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
let v = n;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
i++;
}
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
//# sourceMappingURL=admin.js.map
+1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
+1 -1
View File
@@ -43,7 +43,7 @@ async function main() {
const left = new Pane("left", "transcript");
const right = new Pane("right", "inbox");
replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el));
mountNavbar(navbar, () => right.show("inbox"));
mountNavbar(navbar, (view) => right.show(view));
identityBanner(banner);
const status = await get("/api/admin/status");
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
+5 -3
View File
@@ -1,10 +1,12 @@
import { h, replace } from "./dom.js";
import { activeTasks, findTask, onEach, state } from "./state.js";
export function mountNavbar(root, showInbox) {
export function mountNavbar(root, show) {
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
crumb.onclick = openSwitcher;
const inbox = h("button", { class: "inbox-count" });
inbox.onclick = showInbox;
inbox.onclick = () => show("inbox");
const admin = h("button", { class: "nav-link" }, "Admin");
admin.onclick = () => show("admin");
const conn = h("span", { class: "connection down", title: "connecting" });
const me = h("span", { class: "me" });
const render = () => {
@@ -19,7 +21,7 @@ export function mountNavbar(root, showInbox) {
replace(me, state.me?.display_name ?? "");
};
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, conn, me);
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, conn, me);
render();
addEventListener("keydown", (e) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -1,3 +1,4 @@
import { mountAdmin } from "./admin.js";
import { mountChanges } from "./changes.js";
import { h, replace } from "./dom.js";
import { mountInbox } from "./inbox.js";
@@ -8,8 +9,9 @@ const views = {
changes: { label: "Changes", mount: (root) => mountChanges(root) },
tasks: { label: "Tasks", mount: mountTasks },
inbox: { label: "Inbox", mount: mountInbox },
admin: { label: "Admin", mount: mountAdmin },
};
const order = ["transcript", "changes", "tasks", "inbox"];
const order = ["transcript", "changes", "tasks", "inbox", "admin"];
export class Pane {
key;
el;
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
+1
View File
@@ -148,5 +148,6 @@ export function handleEvent(ev) {
void loadInbox();
break;
}
notify("event", ev);
}
//# sourceMappingURL=state.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+242
View File
@@ -0,0 +1,242 @@
import { get, type ByreEvent, type Status, type TurnRef } from "./api.js";
import { ago, errorText, h, replace } from "./dom.js";
import { findTask, on, onEach, state } from "./state.js";
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created"]);
export function mountAdmin(root: HTMLElement): () => void {
let status: Status | undefined;
let error = "";
let inFlight = false;
let again = false;
let disposed = false;
const render = () => {
if (!status) {
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
return;
}
replace(
root,
h(
"div",
{ class: "admin-view" },
error && h("p", { class: "error-text" }, error),
turnsSection(status),
rateLimitsSection(status.rate_limits),
serverSection(status),
),
);
};
const refresh = async () => {
if (inFlight) {
again = true;
return;
}
inFlight = true;
try {
const next = await get("/api/admin/status");
if (disposed) {
return;
}
status = next;
error = "";
} catch (err) {
error = `Couldn't load status: ${errorText(err)}`;
}
inFlight = false;
render();
if (again && !disposed) {
again = false;
void refresh();
}
};
const onEvent = (detail?: unknown) => {
const ev = detail as ByreEvent;
if (refreshKinds.has(ev.kind) || (ev.kind === "agent.vendor" && ev.payload["type"] === "rate_limit_event")) {
void refresh();
return;
}
if (status && ev.seq > status.last_event_seq) {
status.last_event_seq = ev.seq;
render();
}
};
const offs = [
on("event", onEvent),
on("connection", () => {
if (state.connected) {
void refresh();
}
}),
onEach(["projects", "tasks"], render),
];
render();
void refresh();
return () => {
disposed = true;
for (const off of offs) {
off();
}
};
}
function turnsSection(s: Status): HTMLElement {
const vendors = [...new Set([...Object.keys(s.caps.per_vendor), ...s.running_turns.map((t) => t.vendor), ...s.queued_turns.map((t) => t.vendor)])].sort();
const runningFor = (vendor: string) => s.running_turns.filter((t) => t.vendor === vendor).length;
return h(
"section",
{ class: "admin-section" },
h("h2", {}, "Turns"),
h(
"div",
{ class: "caps" },
capTile("All vendors", s.running_turns.length, s.caps.global),
...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])),
h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length)),
),
h("h3", {}, "Running"),
turnList(s.running_turns, "running", "No turns running."),
h("h3", {}, "Queued"),
turnList(s.queued_turns, "queued", "Nothing queued."),
);
}
function capTile(label: string, running: number, cap: number | undefined): HTMLElement {
const full = cap !== undefined && running >= cap;
return h(
"div",
{ class: "cap-tile" },
h("div", { class: "cap-label" }, label),
h("div", { class: full ? "cap-value warn" : "cap-value" }, running, h("span", { class: "cap-max" }, cap === undefined ? " / no cap" : ` / ${cap}`)),
);
}
function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string): HTMLElement {
return h(
"ul",
{ class: "task-list" },
...turns.map((t, i) => {
const task = findTask(t.task_id);
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
return h(
"li",
{},
h(
"a",
{ href: `#/task/${t.task_id}` },
kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }),
h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id),
h("span", { class: "task-state" }, t.vendor),
),
);
}),
turns.length === 0 && h("li", { class: "empty-row" }, empty),
);
}
function rateLimitsSection(limits: Status["rate_limits"]): HTMLElement {
const vendors = Object.keys(limits).sort();
return h(
"section",
{ class: "admin-section" },
h("h2", {}, "Rate limits"),
...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})),
vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."),
);
}
function rateLimitCard(vendor: string, info: Record<string, unknown>): HTMLElement {
const { status, rateLimitType, resetsAt, unifiedWindows, ...rest } = info;
const statusText = String(status ?? "unknown");
const tone = statusText === "allowed" ? "ok" : statusText === "rejected" ? "danger" : "warn";
const windows = (unifiedWindows ?? {}) as Record<string, { utilization?: number; resetsAt?: number }>;
return h(
"div",
{ class: "card rate-limit" },
h(
"header",
{},
h("span", { class: "vendor" }, vendor),
h("span", { class: `rl-status ${tone}` }, statusText),
rateLimitType !== undefined && h("span", {}, String(rateLimitType)),
typeof resetsAt === "number" && h("span", { class: "resets" }, "resets ", resetText(resetsAt)),
),
Object.keys(windows).length > 0 &&
h(
"table",
{ class: "rl-windows" },
h("tr", {}, h("th", {}, "Window"), h("th", {}, "Used"), h("th", {}, "Resets")),
...Object.entries(windows).map(([name, w]) =>
h(
"tr",
{},
h("td", {}, name),
h("td", { class: utilizationClass(w.utilization) }, w.utilization === undefined ? "—" : `${Math.round(w.utilization * 100)}%`),
h("td", {}, w.resetsAt === undefined ? "—" : resetText(w.resetsAt)),
),
),
),
Object.keys(rest).length > 0 &&
h(
"dl",
{ class: "kv" },
...Object.entries(rest).flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, typeof v === "object" ? JSON.stringify(v) : String(v))]),
),
);
}
function utilizationClass(u: number | undefined): string {
if (u === undefined) {
return "";
}
if (u >= 0.9) {
return "danger";
}
if (u >= 0.7) {
return "warn";
}
return "";
}
function resetText(unixSeconds: number): string {
const at = new Date(unixSeconds * 1000);
const time = at.toLocaleString(undefined, { weekday: "short", hour: "numeric", minute: "2-digit" });
const minutes = Math.round((at.getTime() - Date.now()) / 60000);
if (minutes <= 0) {
return `${time} (passed)`;
}
const rel = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
return `${time} (in ${rel})`;
}
function serverSection(s: Status): HTMLElement {
const rows: [string, string][] = [
["Started", `${new Date(s.started_at).toLocaleString()} (${ago(s.started_at)})`],
["Agent processes", String(s.live_agents)],
["Projects", String(s.projects)],
["Active tasks", String(s.active_tasks)],
["Live clients", String(s.sse_clients)],
["Database", bytes(s.db_bytes)],
["Last event", `#${s.last_event_seq}`],
];
return h("section", { class: "admin-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
}
function bytes(n: number): string {
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
let v = n;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
i++;
}
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
+18 -2
View File
@@ -655,8 +655,11 @@ export interface components {
Status: {
/** @description Agent processes currently running. */
live_agents: number;
running_turns: number;
queued_turns: number;
/** @description Turns running now, oldest session first. */
running_turns: components["schemas"]["TurnRef"][];
/** @description Turns waiting for room under the caps, in the order they will start. */
queued_turns: components["schemas"]["TurnRef"][];
caps: components["schemas"]["Caps"];
/** @description The latest rate-limit status each vendor reported, keyed by vendor. */
rate_limits: {
[key: string]: Record<string, never>;
@@ -671,6 +674,19 @@ export interface components {
active_tasks: number;
sse_clients: number;
};
Caps: {
/** @description Most turns running at once across all vendors. */
global: number;
/** @description Most turns running at once per vendor, keyed by vendor. */
per_vendor: {
[key: string]: number;
};
};
TurnRef: {
session_id: string;
task_id: string;
vendor: string;
};
};
responses: {
/** @description Error. */
+1
View File
@@ -12,6 +12,7 @@ export type InboxResponseCreate = Schemas["InboxResponseCreate"];
export type Attachment = Schemas["Attachment"];
export type ByreEvent = Schemas["Event"];
export type Status = Schemas["Status"];
export type TurnRef = Schemas["TurnRef"];
type Method = "get" | "post" | "put";
+1 -1
View File
@@ -46,7 +46,7 @@ async function main(): Promise<void> {
const right = new Pane("right", "inbox");
replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el));
mountNavbar(navbar, () => right.show("inbox"));
mountNavbar(navbar, (view) => right.show(view));
identityBanner(banner);
const status = await get("/api/admin/status");
+7 -3
View File
@@ -1,12 +1,16 @@
import { h, replace } from "./dom.js";
import type { View } from "./panes.js";
import { activeTasks, findTask, onEach, state } from "./state.js";
export function mountNavbar(root: HTMLElement, showInbox: () => void): void {
export function mountNavbar(root: HTMLElement, show: (view: View) => void): void {
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
crumb.onclick = openSwitcher;
const inbox = h("button", { class: "inbox-count" });
inbox.onclick = showInbox;
inbox.onclick = () => show("inbox");
const admin = h("button", { class: "nav-link" }, "Admin");
admin.onclick = () => show("admin");
const conn = h("span", { class: "connection down", title: "connecting" });
const me = h("span", { class: "me" });
@@ -26,7 +30,7 @@ export function mountNavbar(root: HTMLElement, showInbox: () => void): void {
};
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, conn, me);
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, conn, me);
render();
addEventListener("keydown", (e) => {
+4 -2
View File
@@ -1,19 +1,21 @@
import { mountAdmin } from "./admin.js";
import { mountChanges } from "./changes.js";
import { h, replace } from "./dom.js";
import { mountInbox } from "./inbox.js";
import { mountTranscript } from "./task.js";
import { mountTasks } from "./tasks.js";
export type View = "transcript" | "changes" | "tasks" | "inbox";
export type View = "transcript" | "changes" | "tasks" | "inbox" | "admin";
const views: Record<View, { label: string; mount: (root: HTMLElement) => () => void }> = {
transcript: { label: "Transcript", mount: mountTranscript },
changes: { label: "Changes", mount: (root) => mountChanges(root) },
tasks: { label: "Tasks", mount: mountTasks },
inbox: { label: "Inbox", mount: mountInbox },
admin: { label: "Admin", mount: mountAdmin },
};
const order: View[] = ["transcript", "changes", "tasks", "inbox"];
const order: View[] = ["transcript", "changes", "tasks", "inbox", "admin"];
export class Pane {
readonly el: HTMLElement;
+3 -1
View File
@@ -1,6 +1,6 @@
import { get, type AgentSession, type ByreEvent, type InboxItem, type Project, type Task, type User } from "./api.js";
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection";
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection" | "event";
export const state = {
me: undefined as User | undefined,
@@ -170,4 +170,6 @@ export function handleEvent(ev: ByreEvent): void {
void loadInbox();
break;
}
notify("event", ev);
}
+130
View File
@@ -1247,3 +1247,133 @@ pre.diff .meta {
padding: 24px;
color: var(--danger);
}
.nav-link {
background: transparent;
}
.admin-view {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 8px 16px 16px;
}
.admin-section h2 {
font-size: 13px;
margin: 16px 0 8px;
}
.admin-section h3 {
font-size: 12px;
color: var(--muted);
margin: 12px 0 4px;
}
.caps {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cap-tile {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
padding: 6px 12px;
min-width: 110px;
}
.cap-label {
font-size: 11px;
color: var(--muted);
}
.cap-value {
font-size: 18px;
font-weight: 700;
}
.cap-value.warn {
color: var(--warn);
}
.cap-max {
font-size: 12px;
font-weight: 400;
color: var(--muted);
}
.queue-pos {
width: 3ch;
color: var(--muted);
flex-shrink: 0;
}
.rate-limit + .rate-limit {
margin-top: 8px;
}
.rate-limit .vendor {
color: var(--text);
font-weight: 700;
font-size: 13px;
}
.rate-limit .resets {
margin-left: auto;
}
.rl-status {
font-weight: 700;
}
.rl-status.ok,
.rl-windows .ok {
color: var(--ok);
}
.rl-status.warn,
.rl-windows .warn {
color: var(--warn);
}
.rl-status.danger,
.rl-windows .danger {
color: var(--danger);
}
.rl-windows {
border-collapse: collapse;
margin-top: 8px;
width: 100%;
}
.rl-windows th,
.rl-windows td {
text-align: left;
padding: 2px 12px 2px 0;
border-bottom: 1px solid var(--border);
}
.rl-windows th {
font-weight: 400;
font-size: 11px;
color: var(--muted);
}
.kv {
display: grid;
grid-template-columns: max-content 1fr;
gap: 2px 16px;
margin: 8px 0 0;
}
.kv dt {
color: var(--muted);
}
.kv dd {
margin: 0;
word-break: break-word;
}