Files
ClawdDotNet/src/ClawdDotNet.Manager.Web/wwwroot/views/prompt-history.js
T
RichardandClaude Opus 5 5adf4c1ca6 Prompt History und Verbrauchsauswertung (#660), englische Hauptsprache (#661)
Prompt history: every run records its transcript (task, each model step with
tokens and cost, tool calls and results, engine follow-ups) - bounded in size,
scrubbed of recognisable secrets, system prompts stored once per text.
Retention in days per instance (default 14) and per agent (0 = never, -1 =
forever); an hourly janitor deletes what is past it, "never" also purges.

Usage analysis over run receipts: filter by period, agent, model, event, status,
cost and free text; sort by every column; totals over the whole filter; hourly
or daily series grouped by agent, model or event; markers for budget denials and
model changes; run detail with receipt, transcript, tool calls and approvals.

WebUI: Analysis > Token usage > Runs / Timeline, run detail, Prompt History with
retention settings. New code, comments and texts in English; texts come from
language packs (i18n/en.json, de.json) via modules/i18n.js. CLAUDE.md records
English as the main language.

Version 0.1.12.

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

203 lines
8.6 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 — Analysis › Prompt History (#660)
//
// Recorded prompts and responses of every run, searchable over the full transcript, plus
// how long they are kept: an instance default (14 days unless changed) and an override per
// agent - "never" for an agent that handles secrets, "forever" or many days to debug one.
//
// Transcripts are foreign text and only ever inserted via textContent.
import { api } from "../modules/api.js";
import { erstelleElement, erstelleKarte, formatiereDatumZeit, formatiereTokens, zeigeMeldung } from "../modules/dom.js";
import { navigiere } from "../modules/router.js";
import { t } from "../modules/i18n.js";
import { readFilters, applyFilters, rangeOf, loadFacets, buildFilterBar } from "../modules/usage-filters.js";
import { statusBadge, sourceLabel } from "./usage-runs.js";
const PATH = "/prompt-history";
const PAGE_SIZE = 25;
const NEVER = 0;
const FOREVER = -1;
export async function renderPromptHistory() {
const container = document.getElementById("ansicht-inhalt");
if (!container) return;
const filters = readFilters();
const layout = erstelleElement("div", { klasse: "verbrauch-ansicht" });
layout.append(
erstelleElement("h1", { text: t("history.title") }),
erstelleElement("p", { klasse: "hinweis", text: t("history.intro") }));
const facets = await loadFacets(filters);
layout.append(buildFilterBar(filters, facets, (next) => applyFilters(PATH, next), ["range", "agent", "q"]));
const list = erstelleElement("div", {}, erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") }));
const settings = erstelleElement("div", {});
layout.append(list, settings);
container.replaceChildren(layout);
await Promise.all([loadList(list, filters), loadSettings(settings)]);
}
async function loadList(target, filters) {
const { from, to } = rangeOf(filters);
const query = new URLSearchParams({ from, to, offset: String(filters.offset || 0), limit: String(PAGE_SIZE) });
if (filters.agent) query.set("agent", filters.agent);
if (filters.q) query.set("q", filters.q);
let page;
try {
page = await api(`/prompt-history?${query}`);
} catch (err) {
target.replaceChildren(erstelleKarte(t("history.unavailable"), [erstelleElement("p", { klasse: "gedaempft", text: err.message })]));
return;
}
if (page.items.length === 0) {
target.replaceChildren(erstelleKarte(t("history.empty.title"), [
erstelleElement("p", { klasse: "gedaempft", text: filters.q ? t("history.empty.search") : t("history.empty.text") })
]));
return;
}
const items = page.items.map((item) => {
const open = () => navigiere(`#${PATH}/${encodeURIComponent(item.runId)}`);
const card = erstelleElement("div", { klasse: "verlauf-eintrag klickbar", tabindex: "0", onclick: open });
card.addEventListener("keydown", (event) => { if (event.key === "Enter") open(); });
card.append(
erstelleElement("div", { klasse: "verlauf-kopf" },
erstelleElement("strong", { text: item.agentId }),
erstelleElement("span", { klasse: "gedaempft klein", text: `${formatiereDatumZeit(item.startedAt)} · ${sourceLabel(item.source)} · ${item.model}` }),
statusBadge(item.status)),
erstelleElement("div", { klasse: "verlauf-zeile" },
erstelleElement("span", { klasse: "verlauf-marke", text: t("history.prompt") }),
erstelleElement("span", { klasse: "verlauf-text", text: item.prompt || "—" })),
erstelleElement("div", { klasse: "verlauf-zeile" },
erstelleElement("span", { klasse: "verlauf-marke", text: t("history.response") }),
erstelleElement("span", { klasse: "verlauf-text", text: item.response || "—" })));
if (item.truncated) {
card.append(erstelleElement("div", { klasse: "gedaempft klein", text: t("history.truncated", { chars: formatiereTokens(item.sizeChars) }) }));
}
return card;
});
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 })
}));
target.replaceChildren(erstelleKarte(t("history.list", { n: page.total }), [...items, pager]));
}
// ─── Retention settings ───
function modeOf(days) {
if (days === null || days === undefined) return "inherit";
if (days === NEVER) return "never";
if (days === FOREVER) return "forever";
return "days";
}
/** A mode select plus a day count that is only shown for "days". */
function retentionInput(days, allowInherit) {
const modes = allowInherit ? ["inherit", "never", "days", "forever"] : ["never", "days", "forever"];
const mode = erstelleElement("select", { "aria-label": t("history.settings.mode") });
for (const m of modes) mode.append(erstelleElement("option", { value: m, text: t(`history.settings.mode.${m}`) }));
mode.value = modeOf(days);
const count = erstelleElement("input", {
type: "number", min: "1", step: "1", value: days > 0 ? String(days) : "14", "aria-label": t("history.settings.days")
});
const unit = erstelleElement("span", { klasse: "gedaempft klein", text: t("history.settings.daysUnit") });
const dayBox = erstelleElement("span", { klasse: "gruppe-horizontal" }, count, unit);
const sync = () => { dayBox.hidden = mode.value !== "days"; };
mode.addEventListener("change", sync);
sync();
return {
element: erstelleElement("span", { klasse: "gruppe-horizontal aufbewahrung" }, mode, dayBox),
value() {
switch (mode.value) {
case "inherit": return null;
case "never": return NEVER;
case "forever": return FOREVER;
default: return Math.max(1, Math.round(Number(count.value) || 14));
}
}
};
}
function describe(days) {
if (days === NEVER) return t("history.settings.effective.never");
if (days === FOREVER) return t("history.settings.effective.forever");
return t("history.settings.effective.days", { n: days });
}
async function loadSettings(target) {
let settings;
try {
settings = await api("/prompt-history/settings");
} catch (err) {
target.replaceChildren();
return;
}
const instance = retentionInput(settings.defaultRetentionDays, false);
const rows = [];
const table = erstelleElement("table", { klasse: "daten-tabelle" });
const head = erstelleElement("tr", {});
for (const label of ["history.settings.agent", "history.settings.retention", "history.settings.effective"]) {
head.append(erstelleElement("th", { text: t(label) }));
}
table.append(head);
for (const agent of settings.agents) {
const input = retentionInput(agent.retentionDays, true);
rows.push({ agentId: agent.agentId, input, before: agent.retentionDays ?? null });
table.append(erstelleElement("tr", {},
erstelleElement("td", { text: agent.displayName }),
erstelleElement("td", {}, input.element),
erstelleElement("td", { klasse: "gedaempft", text: describe(agent.effectiveRetentionDays) })));
}
const save = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche primaer klein",
text: t("history.settings.save"),
onclick: async () => {
const body = { defaultRetentionDays: instance.value(), agents: [] };
for (const row of rows) {
const value = row.input.value();
if (value !== row.before) body.agents.push({ agentId: row.agentId, retentionDays: value });
}
save.disabled = true;
try {
const result = await api("/prompt-history/settings", { method: "PUT", body });
zeigeMeldung(result?.message ?? t("history.settings.saved"), "erfolg");
await loadSettings(target);
} catch (err) {
zeigeMeldung(t("history.settings.failed", { error: err.message }), "gefahr");
save.disabled = false;
}
}
});
target.replaceChildren(erstelleKarte(t("history.settings.title"), [
erstelleElement("p", { klasse: "hinweis", text: t("history.settings.intro") }),
erstelleElement("div", { klasse: "gruppe-horizontal" },
erstelleElement("strong", { text: t("history.settings.default") }), instance.element),
erstelleElement("div", { klasse: "tabelle-scroll" }, table),
save
]));
}