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>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
// 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
|
||||
]));
|
||||
}
|
||||
Reference in New Issue
Block a user