Audit und Laufbelege: Tageswerte je Agent und Einheit, Monatsarchiv statt Loeschen (#700)
Nach der Vorgabe vom 2026-09-22 (so wenig loeschen wie moeglich): Laeufe aelter als hotDays (Vorgabe 180, 0 = alles bleibt) wandern aus AuditLog und RunReceipts in Archive/audit-JJJJ-MM.db - kopiert und entfernt in einer Transaktion, Zeilen-IDs bleiben, spaeter dazugekommene Spalten werden im Archiv ergaenzt. Vorher wird jeder Tag in RunDaily verdichtet (Laeufe, Fehlschlaege, Schrittgrenze, Tokens, Kosten, Werkzeugaufrufe, Fehler, Verweigert, Freigaben) mit der Einheit des Agenten; die Tageswerte bleiben. Nachschlagen eines Laufs findet ihn auch im Archiv. API /usage/days, /usage/archive; Karte 'Tageswerte' in der Verbrauchsansicht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0dd8a761b8
commit
095722ae9c
@@ -796,6 +796,22 @@
|
||||
"usage.units.kind.Deviation": "Ungewöhnlicher Tag",
|
||||
"usage.units.kind.NearLimit": "Nahe an den Grenzen",
|
||||
"usage.units.kind.MonthlyThreshold": "Monatssumme überschritten",
|
||||
"usage.days.title": "Tageswerte",
|
||||
"usage.days.hint": "Die letzten 30 Tage, summiert über alle Agenten (UTC-Tage). Jeder Tag wird je Agent und Einheit verdichtet und bleibt für immer — auch wenn seine Läufe ins Archiv wandern. Heute erscheint morgen.",
|
||||
"usage.days.none": "Noch kein Tag verdichtet — die ersten Werte erscheinen wenige Minuten nach dem Start, ab gestern.",
|
||||
"usage.days.day": "Tag",
|
||||
"usage.days.runs": "Läufe",
|
||||
"usage.days.failed": "Gescheitert",
|
||||
"usage.days.loopLimit": "Schrittgrenze",
|
||||
"usage.days.cost": "Kosten",
|
||||
"usage.days.calls": "Werkzeugaufrufe",
|
||||
"usage.days.errors": "Werkzeugfehler",
|
||||
"usage.days.staged": "Zur Freigabe",
|
||||
"usage.archive.title": "Archiv",
|
||||
"usage.archive.hint": "Audit-Einträge und Laufbelege sind Belege und werden nie gelöscht. Nach so vielen Tagen wandern sie aus der Arbeitsdatenbank in eine Archivdatei je Monat; ein Lauf wird dort weiterhin gefunden. 0 behält alles in der Arbeitsdatenbank.",
|
||||
"usage.archive.hotDays": "Tage in der Arbeitsdatenbank",
|
||||
"usage.archive.none": "Noch keine Archivdatei.",
|
||||
"usage.archive.files": "Archivdateien: {list}",
|
||||
"usage.forecastTitle": "Kostenprognose",
|
||||
"usage.forecastDay": "Prognose je Tag",
|
||||
"usage.forecastMonth": "Prognose je Monat (30 Tage)",
|
||||
|
||||
@@ -796,6 +796,22 @@
|
||||
"usage.units.kind.Deviation": "Unusual day",
|
||||
"usage.units.kind.NearLimit": "Near the limits",
|
||||
"usage.units.kind.MonthlyThreshold": "Monthly total crossed",
|
||||
"usage.days.title": "Day values",
|
||||
"usage.days.hint": "The last 30 days, summed over all agents (UTC days). Each day is condensed per agent and unit and stays for good — also when its runs move to the archive. Today appears tomorrow.",
|
||||
"usage.days.none": "No day condensed yet — the first values appear a few minutes after the start, from yesterday on.",
|
||||
"usage.days.day": "Day",
|
||||
"usage.days.runs": "Runs",
|
||||
"usage.days.failed": "Failed",
|
||||
"usage.days.loopLimit": "Step limit",
|
||||
"usage.days.cost": "Cost",
|
||||
"usage.days.calls": "Tool calls",
|
||||
"usage.days.errors": "Tool errors",
|
||||
"usage.days.staged": "For approval",
|
||||
"usage.archive.title": "Archive",
|
||||
"usage.archive.hint": "Audit entries and receipts are evidence and are never deleted. After this many days they move from the working database into one archive file per month; a run is still found there. 0 keeps everything in the working database.",
|
||||
"usage.archive.hotDays": "Days in the working database",
|
||||
"usage.archive.none": "No archive file yet.",
|
||||
"usage.archive.files": "Archive files: {list}",
|
||||
"usage.forecastTitle": "Cost Forecast",
|
||||
"usage.forecastDay": "Forecast per day",
|
||||
"usage.forecastMonth": "Forecast per month (30 days)",
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
erstelleKarte,
|
||||
formatiereDatumZeit,
|
||||
formatiereTokens,
|
||||
formatiereKosten
|
||||
formatiereKosten,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
export async function rendereVerbrauch() {
|
||||
@@ -44,10 +45,11 @@ async function ladeVerbrauch() {
|
||||
);
|
||||
|
||||
try {
|
||||
const [laeufe, prognose, einheiten] = await Promise.all([api("/usage?limit=100"), erstellePrognoseKarte(), erstelleEinheitenKarte()]);
|
||||
const [laeufe, prognose, einheiten, tage] = await Promise.all([api("/usage?limit=100"), erstellePrognoseKarte(), erstelleEinheitenKarte(), erstelleTageswerteKarte()]);
|
||||
container.replaceChildren();
|
||||
if (einheiten) container.append(einheiten);
|
||||
if (prognose) container.append(prognose);
|
||||
if (tage) container.append(tage);
|
||||
|
||||
if (!laeufe || laeufe.length === 0) {
|
||||
container.append(
|
||||
@@ -218,6 +220,81 @@ async function erstelleEinheitenKarte() {
|
||||
return erstelleKarte(t("usage.units.title"), inhalt);
|
||||
}
|
||||
|
||||
// ─── Day values and the month archive (#700) ───
|
||||
|
||||
/**
|
||||
* The last 30 days, summed per day over all agents — from the condensed day values, which stay
|
||||
* when old runs move to the archive. Below: how long runs stay in the working database, and the
|
||||
* archive files. Nothing is ever deleted.
|
||||
*/
|
||||
async function erstelleTageswerteKarte() {
|
||||
let tage, archiv;
|
||||
try {
|
||||
[tage, archiv] = await Promise.all([api("/usage/days"), api("/usage/archive")]);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const summe = new Map();
|
||||
for (const d of tage ?? []) {
|
||||
const s = summe.get(d.day) ?? { runs: 0, failed: 0, loopLimit: 0, cost: 0, calls: 0, errors: 0, staged: 0 };
|
||||
s.runs += d.runs; s.failed += d.failed; s.loopLimit += d.loopLimit; s.cost += Number(d.costUsd);
|
||||
s.calls += d.toolCalls; s.errors += d.toolErrors; s.staged += d.staged;
|
||||
summe.set(d.day, s);
|
||||
}
|
||||
|
||||
const inhalt = [erstelleElement("p", { klasse: "gedaempft text-klein", text: t("usage.days.hint") })];
|
||||
if (summe.size === 0) {
|
||||
inhalt.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: t("usage.days.none") }));
|
||||
} else {
|
||||
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
|
||||
tabelle.append(erstelleElement("thead", {}, erstelleElement("tr", {},
|
||||
...["usage.days.day", "usage.days.runs", "usage.days.failed", "usage.days.loopLimit", "usage.days.cost", "usage.days.calls", "usage.days.errors", "usage.days.staged"]
|
||||
.map((k) => erstelleElement("th", { klasse: k === "usage.days.day" ? "" : "zahl", text: t(k) })))));
|
||||
const koerper = erstelleElement("tbody");
|
||||
for (const [tag, s] of [...summe.entries()].sort((a, b) => b[0].localeCompare(a[0]))) {
|
||||
koerper.append(erstelleElement("tr", { klasse: s.failed + s.loopLimit > 0 ? "zeile-warnung" : undefined },
|
||||
erstelleElement("td", { text: tag }),
|
||||
erstelleElement("td", { klasse: "zahl", text: String(s.runs) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: String(s.failed) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: String(s.loopLimit) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: `${s.cost.toFixed(2)} $` }),
|
||||
erstelleElement("td", { klasse: "zahl", text: String(s.calls) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: String(s.errors) }),
|
||||
erstelleElement("td", { klasse: "zahl", text: String(s.staged) })));
|
||||
}
|
||||
tabelle.append(koerper);
|
||||
inhalt.push(erstelleElement("div", { klasse: "tabelle-scroll" }, tabelle));
|
||||
}
|
||||
|
||||
// How long runs stay in the working database — set by a human; 0 keeps everything there.
|
||||
const tageFeld = erstelleElement("input", { type: "number", min: "0", max: "36500", step: "1", value: String(archiv?.hotDays ?? 180), "aria-label": t("usage.archive.hotDays") });
|
||||
const sichern = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach klein",
|
||||
text: t("common.save"),
|
||||
onclick: async () => {
|
||||
try {
|
||||
const r = await api("/usage/archive", { method: "PUT", body: { hotDays: Number(tageFeld.value) } });
|
||||
zeigeMeldung(r?.message ?? t("common.saved"), "erfolg", 6000);
|
||||
} catch (err) {
|
||||
zeigeMeldung(err.message, "gefahr", 8000);
|
||||
}
|
||||
}
|
||||
});
|
||||
inhalt.push(
|
||||
erstelleElement("h3", { klasse: "fall-verlauf-titel", text: t("usage.archive.title") }),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("usage.archive.hint") }),
|
||||
erstelleElement("div", { klasse: "mcp-token-formular" },
|
||||
erstelleElement("label", { text: t("usage.archive.hotDays") }), tageFeld, sichern));
|
||||
const dateien = archiv?.files ?? [];
|
||||
inhalt.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: dateien.length === 0
|
||||
? t("usage.archive.none")
|
||||
: t("usage.archive.files", { list: dateien.map((f) => `${f.month} (${(f.bytes / 1048576).toFixed(1)} MB)`).join(", ") }) }));
|
||||
|
||||
return erstelleKarte(t("usage.days.title"), inhalt);
|
||||
}
|
||||
|
||||
// ─── Kostenprognose (#595) ───
|
||||
|
||||
function getGrundlageText(basis, sample) {
|
||||
|
||||
Reference in New Issue
Block a user