// ClawdDotNet — Token-Verbrauch & Analyse // // Diagramme (CSS/DOM-Balken ohne externe Bibliothek), Statistiken und Läufetabelle. // // Feldnamen wie UsageDto (#591): costUsd, costIsKnown, occurredAt, status. Eine // Gesamtsumme liefert die API nicht — sie ist Prompt + Completion. Vorher las die Ansicht // totalTokens, cost und timestamp und zeigte überall 0 bzw. "—". import { api } from "../modules/api.js"; import { erstelleElement, erstelleKarte, formatiereDatumZeit, formatiereTokens, formatiereKosten } from "../modules/dom.js"; export async function rendereVerbrauch() { const container = document.getElementById("ansicht-inhalt"); if (!container) return; container.replaceChildren(); const layout = erstelleElement("div", { klasse: "verbrauch-ansicht" }); layout.append(erstelleElement("h1", { text: "Token-Verbrauch & Analyse" })); const inhaltBereich = erstelleElement("div", { id: "verbrauch-inhalt-bereich" }); layout.append(inhaltBereich); container.append(layout); await ladeVerbrauch(); } function gesamtTokens(lauf) { return (lauf.promptTokens || 0) + (lauf.completionTokens || 0); } async function ladeVerbrauch() { const container = document.getElementById("verbrauch-inhalt-bereich"); if (!container) return; container.replaceChildren( erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Verbrauchsdaten…" }) ); try { const [laeufe, prognose] = await Promise.all([api("/usage?limit=100"), erstellePrognoseKarte()]); container.replaceChildren(); if (prognose) container.append(prognose); if (!laeufe || laeufe.length === 0) { container.append( erstelleKarte("Keine Daten", [ erstelleElement("p", { klasse: "gedaempft", text: "Noch keine Token-Verbrauchsdaten aufgezeichnet." }) ]) ); return; } // Statistiken berechnen let summeTokens = 0; let summePrompt = 0; let summeCompletion = 0; let summeKosten = 0; let unbekannteKosten = 0; const nachAgent = new Map(); const nachModell = new Map(); for (const l of laeufe) { const tokens = gesamtTokens(l); summeTokens += tokens; summePrompt += l.promptTokens || 0; summeCompletion += l.completionTokens || 0; summeKosten += Number(l.costUsd || 0); if (!l.costIsKnown) unbekannteKosten++; const agentKey = l.agentId || "Unbekannt"; nachAgent.set(agentKey, (nachAgent.get(agentKey) || 0) + tokens); const modellKey = l.model || "Unbekannt"; nachModell.set(modellKey, (nachModell.get(modellKey) || 0) + tokens); } // 1. Kacheln (Statistiken) const kachelGitter = erstelleElement("div", { klasse: "kachel-gitter" }); kachelGitter.append( erstelleStatKachel(`Gesamtkosten (${laeufe.length} Läufe)`, formatiereKosten(summeKosten)), erstelleStatKachel("Tokens gesamt", formatiereTokens(summeTokens)), erstelleStatKachel("Eingabe-Tokens (Prompt)", formatiereTokens(summePrompt)), erstelleStatKachel("Ausgabe-Tokens (Completion)", formatiereTokens(summeCompletion)) ); const hinweise = []; if (unbekannteKosten > 0) { hinweise.push( erstelleElement("p", { klasse: "gedaempft text-klein", text: `${unbekannteKosten} Lauf/Läufe ohne bekannten Preis — deren Kosten sind in der Summe nicht enthalten.` }) ); } // 2. Balkendiagramme (rein CSS/DOM) const diagrammGitter = erstelleElement("div", { klasse: "diagramm-gitter" }); const agentBalken = erstelleBalkenDiagramm("Tokens nach Agent", nachAgent, summeTokens); const modellBalken = erstelleBalkenDiagramm("Tokens nach Modell", nachModell, summeTokens); diagrammGitter.append(agentBalken, modellBalken); // 3. Läufe-Tabelle const tabelleKarte = erstelleElement("div", { klasse: "karte" }); tabelleKarte.append(erstelleElement("h2", { text: "Letzte Agenten-Läufe" })); const tabelle = erstelleElement("table", { klasse: "daten-tabelle" }); const thead = erstelleElement("thead"); thead.append( erstelleElement("tr", {}, erstelleElement("th", { text: "Zeitpunkt" }), erstelleElement("th", { text: "Agent" }), erstelleElement("th", { text: "Modell" }), erstelleElement("th", { text: "Status" }), erstelleElement("th", { text: "Prompt", stil: "text-align: right;" }), erstelleElement("th", { text: "Completion", stil: "text-align: right;" }), erstelleElement("th", { text: "Gesamt", stil: "text-align: right;" }), erstelleElement("th", { text: "Kosten", stil: "text-align: right;" }) ) ); const tbody = erstelleElement("tbody"); for (const lauf of laeufe) { const kosten = lauf.costIsKnown ? formatiereKosten(lauf.costUsd) : `${formatiereKosten(lauf.costUsd)} (Preis unbekannt)`; const tr = erstelleElement("tr"); tr.append( erstelleElement("td", { text: formatiereDatumZeit(lauf.occurredAt) }), erstelleElement("td", {}, erstelleElement("strong", { text: lauf.agentId })), erstelleElement("td", { klasse: "gedaempft text-klein", text: lauf.model }), erstelleElement("td", { klasse: "text-klein", text: lauf.status || "—" }), erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.promptTokens) }), erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(lauf.completionTokens) }), erstelleElement("td", { stil: "text-align: right;", text: formatiereTokens(gesamtTokens(lauf)) }), erstelleElement("td", { stil: "text-align: right;", text: kosten }) ); tbody.append(tr); } tabelle.append(thead, tbody); tabelleKarte.append(tabelle); container.append(kachelGitter, ...hinweise, diagrammGitter, tabelleKarte); } catch (err) { container.replaceChildren( erstelleKarte("Verbrauchsdaten nicht verfügbar", [ erstelleElement("p", { klasse: "gedaempft", text: err.message }) ]) ); } } // ─── Kostenprognose (#595) ─── const GRUNDLAGE_TEXT = { Laeufe: (n) => `Ø letzte ${n} Läufe`, Agent: (n) => `Ø des Agenten (${n} Läufe)`, Schaetzung: () => "Schätzung – noch kein Lauf", Unbekannt: () => "keine Grundlage" }; async function erstellePrognoseKarte() { let p; try { p = await api("/usage/forecast"); } catch { return null; // ohne Taskboard keine Prognose - die Verbrauchsansicht bleibt trotzdem } if (!p) return null; const kacheln = erstelleElement("div", { klasse: "kachel-gitter" }); kacheln.append( erstelleStatKachel("Prognose je Tag", formatiereKosten(p.totalPerDay)), erstelleStatKachel("Prognose je Monat (30 Tage)", formatiereKosten(p.totalPerMonth)), erstelleStatKachel("davon Aufgaben je Tag", formatiereKosten(p.tasksPerDay)), erstelleStatKachel("sonstige Läufe je Tag (Ø 7 Tage)", formatiereKosten(p.otherPerDay)) ); const hinweise = []; for (const w of p.budgetWarnings || []) { hinweise.push(erstelleElement("div", { klasse: "hinweis-box warnung", text: `${w.agentId}: Die geplanten Aufgaben kosten laut Prognose ${formatiereKosten(w.costPerDay)} am Tag, ` + `das Tagesbudget erlaubt ${formatiereKosten(w.budgetPerDay)}. Die späten Läufe eines Tages werden abgelehnt.` })); } if (p.withoutBasis > 0) { hinweise.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `${p.withoutBasis} Aufgabe(n) ohne Grundlage (kein Lauf, kein Modellpreis) – nicht in der Summe enthalten.` })); } if (Number(p.oneOffOpen) > 0) { hinweise.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `Offene einmalige Aufgaben kosten zusätzlich etwa ${formatiereKosten(p.oneOffOpen)}.` })); } hinweise.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: "Grundlage je Aufgabe: Mittelwert ihrer letzten Läufe, sonst der des Agenten, sonst eine Schätzung aus Promptlänge und Listenpreis (eher zu hoch)." })); const tabelle = erstelleElement("table", { klasse: "daten-tabelle" }); tabelle.append( erstelleElement("thead", {}, erstelleElement("tr", {}, erstelleElement("th", { text: "Aufgabe" }), erstelleElement("th", { text: "Agent" }), erstelleElement("th", { text: "Läufe/Tag", stil: "text-align: right;" }), erstelleElement("th", { text: "je Lauf", stil: "text-align: right;" }), erstelleElement("th", { text: "Grundlage" }), erstelleElement("th", { text: "je Tag", stil: "text-align: right;" }) ) ) ); const tbody = erstelleElement("tbody"); for (const a of p.tasks || []) { const grundlage = (GRUNDLAGE_TEXT[a.basis] || (() => a.basis))(a.sample); tbody.append( erstelleElement("tr", {}, erstelleElement("td", { text: a.oneOffOpen ? `${a.title} (einmalig)` : a.title }), erstelleElement("td", { klasse: "text-klein", text: a.agentId || "—" }), erstelleElement("td", { stil: "text-align: right;", text: String(a.runsPerDay) }), erstelleElement("td", { stil: "text-align: right;", text: a.costPerRun == null ? "—" : formatiereKosten(a.costPerRun) }), erstelleElement("td", { klasse: "gedaempft text-klein", text: grundlage }), erstelleElement("td", { stil: "text-align: right;", text: formatiereKosten(a.costPerDay) }) ) ); } tabelle.append(tbody); return erstelleKarte("Kostenprognose", [kacheln, ...hinweise, tabelle]); } function erstelleStatKachel(titel, wert) { const kachel = erstelleElement("div", { klasse: "stat-kachel karte" }); kachel.append( erstelleElement("div", { klasse: "stat-titel gedaempft", text: titel }), erstelleElement("div", { klasse: "stat-wert", text: wert }) ); return kachel; } function erstelleBalkenDiagramm(titel, datenMap, gesamt) { const karte = erstelleKarte(titel); const container = erstelleElement("div", { klasse: "balken-liste" }); const sortiert = [...datenMap.entries()].sort((a, b) => b[1] - a[1]); for (const [name, wert] of sortiert) { const prozent = gesamt > 0 ? ((wert / gesamt) * 100).toFixed(1) : "0"; const zeile = erstelleElement("div", { klasse: "balken-zeile" }); const meta = erstelleElement("div", { klasse: "balken-meta" }); meta.append( erstelleElement("span", { klasse: "balken-name", text: name }), erstelleElement("span", { klasse: "balken-wert gedaempft", text: `${formatiereTokens(wert)} (${prozent}%)` }) ); const spur = erstelleElement("div", { klasse: "balken-spur" }); const fuellung = erstelleElement("div", { klasse: "balken-fuellung", stil: `width: ${Math.max(1, Number(prozent))}%;` }); spur.append(fuellung); zeile.append(meta, spur); container.append(zeile); } karte.append(container); return karte; }