Entscheidungen aus dem Testbetrieb umgesetzt (#572): #577 #579 #581 #589 #193 #205 #594 #595 #597 #598 #599 #588

- Engine: leere Antworten nachfassen, Aufgaben enden mit Ergebniszeile (EmptyResponse/Incomplete),
  gleiche Fehlschlaege erkennen (RepeatedFailure), Zeitzeile vor Chatnachrichten,
  Werkzeugergebnisse gerahmt, Vorpruefung vor dem Staging, Belegkosten wie RunUsage
- Staging: gescheiterte Freigaben als Failed; AgentEditor-Aenderungen freigabepflichtig,
  Selbstbearbeitung gesperrt (allowSelfEdit)
- Taskboard: Termine beim Anlegen pruefen, naechster/letzter Lauf, pausieren, Sofortlauf,
  Kostenprognose, Hinweis auf wachsenden Kontext; Jobs-Reiter und /api/jobs entfernt
- Release: Instanzmanager samt Weboberflaeche im Paket, setup.json fragt manager.json ab,
  Token nicht mehr ins systemd-Journal
Version 0.1.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-15 11:23:10 +02:00
co-authored by Claude Opus 5
parent 8ab9d99abe
commit 0b6ea73ec1
42 changed files with 2646 additions and 512 deletions
@@ -43,8 +43,9 @@ async function ladeVerbrauch() {
);
try {
const laeufe = await api("/usage?limit=100");
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(
@@ -162,6 +163,83 @@ async function ladeVerbrauch() {
}
}
// ─── 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 = [];
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(