// ClawdDotNet — Token usage › Runs (#660) // // Every run of the selected range: filterable by agent, model, event (source), status, // cost and free text; sortable by every column; totals over the whole filter, not only // the visible page. A row opens the run detail with prompt and response. // // Strictly DOM API, no innerHTML: results and task titles are foreign text. import { api } from "../modules/api.js"; import { erstelleElement, erstelleKarte, erstellePlakette, formatiereDatumZeit, formatiereTokens, formatiereKosten } from "../modules/dom.js"; import { navigiere } from "../modules/router.js"; import { t } from "../modules/i18n.js"; import { readFilters, applyFilters, toApiQuery, loadFacets, buildFilterBar } from "../modules/usage-filters.js"; const PATH = "/verbrauch/runs"; const PAGE_SIZE = 50; const COLUMNS = [ { key: "time", label: "runs.col.time" }, { key: "agent", label: "runs.col.agent" }, { key: "model", label: "runs.col.model" }, { key: "source", label: "runs.col.event" }, { key: "status", label: "runs.col.status" }, { key: "steps", label: "runs.col.steps", numeric: true }, { key: "prompt", label: "runs.col.prompt", numeric: true }, { key: "completion", label: "runs.col.completion", numeric: true }, { key: "cost", label: "runs.col.cost", numeric: true }, { key: "duration", label: "runs.col.duration", numeric: true } ]; export function statusBadge(status) { const kind = { Completed: "erfolg", BudgetExceeded: "warnung", Truncated: "warnung", LoopLimitExceeded: "gefahr", RepeatedFailure: "gefahr", Failed: "gefahr", Cancelled: "neutral" }[status] ?? "neutral"; return erstellePlakette(status, kind); } export function sourceLabel(source) { const key = `source.${source}`; const text = t(key); return text === key ? source : text; } export async function renderUsageRuns() { const container = document.getElementById("ansicht-inhalt"); if (!container) return; const filters = readFilters(); filters.sort ??= "time"; filters.dir ??= "desc"; const layout = erstelleElement("div", { klasse: "verbrauch-ansicht" }); layout.append(erstelleElement("h1", { text: t("runs.title") })); const facets = await loadFacets(filters); layout.append(buildFilterBar(filters, facets, (next) => applyFilters(PATH, next))); const content = erstelleElement("div", {}, erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })); layout.append(content); container.replaceChildren(layout); let page; try { page = await api(`/usage/runs?${toApiQuery(filters, { offset: filters.offset, limit: PAGE_SIZE })}`); } catch (err) { content.replaceChildren(erstelleKarte(t("runs.unavailable"), [ erstelleElement("p", { klasse: "gedaempft", text: err.message }) ])); return; } content.replaceChildren(buildTotals(page.totals), buildTable(page, filters)); } function buildTotals(totals) { const tiles = [ [t("runs.total.runs"), String(totals.runs)], [t("runs.total.prompt"), formatiereTokens(totals.promptTokens)], [t("runs.total.completion"), formatiereTokens(totals.completionTokens)], [t("runs.total.cached"), formatiereTokens(totals.cachedTokens)], [t("runs.total.cost"), formatiereKosten(totals.costUsd)], [t("runs.total.notRun"), String(totals.notRun)] ]; const grid = erstelleElement("div", { klasse: "kennzahlen-raster" }); for (const [label, value] of tiles) { grid.append(erstelleElement("div", { klasse: "kennzahl" }, erstelleElement("div", { klasse: "kennzahl-wert", text: value }), erstelleElement("div", { klasse: "kennzahl-titel", text: label }))); } const parts = [grid]; if (totals.runsWithUnknownCost > 0) { parts.push(erstelleElement("p", { klasse: "hinweis", text: t("runs.unknownCost", { n: totals.runsWithUnknownCost }) })); } return erstelleKarte(t("runs.totals"), parts); } function buildTable(page, filters) { if (page.items.length === 0) { return erstelleKarte(t("runs.empty.title"), [erstelleElement("p", { klasse: "gedaempft", text: t("runs.empty.text") })]); } const table = erstelleElement("table", { klasse: "daten-tabelle lauf-tabelle" }); const head = erstelleElement("tr", {}); for (const column of COLUMNS) { const active = filters.sort === column.key; const arrow = active ? (filters.dir === "asc" ? " ▲" : " ▼") : ""; const button = erstelleElement("button", { type: "button", klasse: "sortier-knopf", text: t(column.label) + arrow, onclick: () => applyFilters(PATH, { ...filters, offset: 0, sort: column.key, dir: active && filters.dir === "desc" ? "asc" : "desc" }) }); head.append(erstelleElement("th", { klasse: column.numeric ? "zahl" : "", "aria-sort": active ? (filters.dir === "asc" ? "ascending" : "descending") : "none" }, button)); } table.append(erstelleElement("thead", {}, head)); const body = erstelleElement("tbody", {}); for (const run of page.items) { const open = () => navigiere(`#${PATH}/${encodeURIComponent(run.runId)}`); const row = erstelleElement("tr", { klasse: "klickbar", tabindex: "0", onclick: open }); row.addEventListener("keydown", (event) => { if (event.key === "Enter") open(); }); const event = erstelleElement("td", {}, erstelleElement("div", { text: sourceLabel(run.source) })); if (run.taskTitle || run.taskId) { event.append(erstelleElement("div", { klasse: "gedaempft klein", text: run.taskTitle || run.taskId })); } row.append( erstelleElement("td", { text: formatiereDatumZeit(run.occurredAt) }), erstelleElement("td", { text: run.agentId }), erstelleElement("td", { klasse: "klein", text: run.model || "—" }), event, erstelleElement("td", {}, statusBadge(run.status)), erstelleElement("td", { klasse: "zahl", text: String(run.stepCount) }), erstelleElement("td", { klasse: "zahl", text: formatiereTokens(run.promptTokens) }), erstelleElement("td", { klasse: "zahl", text: formatiereTokens(run.completionTokens) }), erstelleElement("td", { klasse: "zahl", text: run.costIsKnown || run.costUsd > 0 ? formatiereKosten(run.costUsd) : "—" }), erstelleElement("td", { klasse: "zahl", text: formatDuration(run.durationMs) }) ); body.append(row); } table.append(body); const offset = filters.offset || 0; const pager = erstelleElement("div", { klasse: "blaettern" }, erstelleElement("button", { type: "button", klasse: "schaltflaeche flach klein", text: t("common.previous"), disabled: offset <= 0, onclick: () => applyFilters(PATH, { ...filters, offset: Math.max(0, offset - PAGE_SIZE) }) }), erstelleElement("span", { klasse: "gedaempft", text: t("runs.page", { from: offset + 1, to: offset + page.items.length, total: page.total }) }), erstelleElement("button", { type: "button", klasse: "schaltflaeche flach klein", text: t("common.next"), disabled: offset + PAGE_SIZE >= page.total, onclick: () => applyFilters(PATH, { ...filters, offset: offset + PAGE_SIZE }) })); return erstelleKarte(t("runs.list"), [erstelleElement("div", { klasse: "tabelle-scroll" }, table), pager]); } export function formatDuration(ms) { if (!ms) return "—"; if (ms < 1000) return `${ms} ms`; const seconds = ms / 1000; return seconds < 60 ? `${seconds.toFixed(1)} s` : `${Math.floor(seconds / 60)}:${String(Math.round(seconds % 60)).padStart(2, "0")} min`; }