Files
ClawdDotNet/src/ClawdDotNet.Manager.Web/wwwroot/views/usage.js
T
RichardandClaude Opus 5 f5731c50f8 Budgetsperre sichtbar machen (#608), Gedaechtnisabruf kuerzt statt wegzulassen (#607)
#608: Ein wegen erschoepften Budgets abgelehnter Lauf hinterlaesst jetzt einen Beleg
(Status BudgetExceeded) an Aufgabe und Verlauf; die Log-Warnung kommt einmal je Agent,
Grenze und Tag. Die Kostenprognose zaehlt Ablehnungen nicht als Lauf und warnt, wenn die
geplanten Aufgaben eines Agenten sein Tagesbudget uebersteigen (API + Verbrauchsansicht).

#607: Passt schon der erste Treffer nicht in den Abruf, wird er gekuerzt (Anfang und Ende
bleiben) statt ganz zu fehlen. remember weist auf gleichnamige Eintraege ohne Schluessel und
auf Eintraege nahe der Abrufgrenze hin.

Version 0.1.9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:59:14 +02:00

289 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}