Grossaufgaben und Sonderbudget (#717, Stufe 2+3)
Eine Aufgabe, deren eigene Schaetzung (gemessen, Geschwister oder Autor) ueber der Haelfte der Tageskostengrenze liegt, bekommt zuerst einen Planungslauf: teilen (Teile unter der Elternaufgabe) oder Sonderbudget beantragen. Danach wartet sie auf ihre Teile bzw. die Freigabe und laeuft einmal zum Zusammenfuehren. Sonderbudget ueber Escalation.request_budget, ausnahmslos freigabepflichtig (auch gegen Werkzeugkonfiguration und Dauerfreigabe). Laeufe im Baum ziehen den Topf statt der Tagesgrenzen, tragen BudgetPot in Verbrauch und Beleg und sind in der Kostenanalyse (nach Budget) und auf der Aufgabenkarte gesondert sichtbar. Aufgebraucht zaehlen sie wieder gegen den Tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
183e922278
commit
67649559f5
@@ -369,6 +369,7 @@
|
||||
"tasks.fillBy": "Füllarbeit, spätestens bis",
|
||||
"tasks.fillByHint": "Leer lassen für reguläre Arbeit. Füllarbeit läuft auf Budget, das der Tag übrig lässt; ein Datum meint das Ende dieses Tages.",
|
||||
"tasks.estimateUsd": "Schätzung je Lauf (USD)",
|
||||
"tasks.specialBudget": "Sonderbudget für diese Aufgabe und ihre Teile: {spent} von {amount} verbraucht",
|
||||
|
||||
"services.title": "Dienste",
|
||||
"services.newService": "Neuer Dienst",
|
||||
@@ -769,6 +770,9 @@
|
||||
"timeline.groupBy.agent": "Agent",
|
||||
"timeline.groupBy.model": "Modell",
|
||||
"timeline.groupBy.source": "Ereignis",
|
||||
"timeline.groupBy.budget": "Budget",
|
||||
"timeline.budget.daily": "Tagesbudget",
|
||||
"timeline.budget.special": "Sonderbudget {id}",
|
||||
"timeline.bucket": "Je",
|
||||
"timeline.bucket.hour": "Stunde",
|
||||
"timeline.bucket.day": "Tag",
|
||||
|
||||
@@ -369,6 +369,7 @@
|
||||
"tasks.fillBy": "Fill work, at the latest by",
|
||||
"tasks.fillByHint": "Leave empty for regular work. Fill work runs on budget the day leaves unused; a date means the end of that day.",
|
||||
"tasks.estimateUsd": "Estimate per run (USD)",
|
||||
"tasks.specialBudget": "Special budget for this task and its parts: {spent} of {amount} used",
|
||||
|
||||
"services.title": "Services",
|
||||
"services.newService": "New Service",
|
||||
@@ -769,6 +770,9 @@
|
||||
"timeline.groupBy.agent": "Agent",
|
||||
"timeline.groupBy.model": "Model",
|
||||
"timeline.groupBy.source": "Event",
|
||||
"timeline.groupBy.budget": "Budget",
|
||||
"timeline.budget.daily": "Daily budget",
|
||||
"timeline.budget.special": "Special budget {id}",
|
||||
"timeline.bucket": "Per",
|
||||
"timeline.bucket.hour": "Hour",
|
||||
"timeline.bucket.day": "Day",
|
||||
|
||||
@@ -108,6 +108,9 @@ export async function rendereAufgaben() {
|
||||
await ladeTaskboard();
|
||||
}
|
||||
|
||||
/** #717: Special budgets by task id — filled by ladeTaskboard. */
|
||||
let sonderbudgets = new Map();
|
||||
|
||||
async function ladeTaskboard() {
|
||||
const container = document.getElementById("aufgaben-bereich-inhalt");
|
||||
if (!container) return;
|
||||
@@ -117,7 +120,12 @@ async function ladeTaskboard() {
|
||||
);
|
||||
|
||||
try {
|
||||
const alle = (await api("/tasks?limit=200")) || [];
|
||||
const [alle, toepfe] = await Promise.all([
|
||||
api("/tasks?limit=200").then((r) => r || []),
|
||||
// #717: special budgets, shown on the task they are for; without them the board still loads.
|
||||
api("/special-budgets").then((r) => r || []).catch(() => [])
|
||||
]);
|
||||
sonderbudgets = new Map(toepfe.map((b) => [b.taskId.toLowerCase(), b]));
|
||||
const offen = alle.filter((t) => zeigeAbgeschlossene || !ABGESCHLOSSEN.includes(t.status));
|
||||
const tasks = offen.filter((t) => !brettFilter || (brettFilter === "-" ? !t.board : t.board === brettFilter));
|
||||
container.replaceChildren();
|
||||
@@ -174,6 +182,11 @@ function brettAuswahl(tasks) {
|
||||
erstelleElement("span", { text: t("tasks.board") }), auswahl);
|
||||
}
|
||||
|
||||
/** #717: A budget or an estimate is an amount of money, not a run's cost — cents are enough. */
|
||||
function betrag(usd) {
|
||||
return "$" + Number(usd ?? 0).toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* #717: The date for the input field. A date is stored as the end of that day (the next
|
||||
* midnight, UTC), so one millisecond earlier is the day that was meant.
|
||||
@@ -247,10 +260,19 @@ function erstelleAufgabenKarte(task) {
|
||||
if (task.fillAlertedAt) teile.push(t("tasks.fillAlerted", { time: formatiereDatumZeit(task.fillAlertedAt) }));
|
||||
zeilen.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `◔ ${teile.join(" · ")}` }));
|
||||
}
|
||||
// #717: A special budget is the one exception to the daily limit — never silent.
|
||||
const topf = sonderbudgets.get(task.id.toLowerCase());
|
||||
if (topf) {
|
||||
zeilen.push(erstelleElement("p", {
|
||||
klasse: "text-klein",
|
||||
title: `${topf.reason} (${topf.requestedBy}, ${formatiereDatumZeit(topf.approvedAt)})`,
|
||||
text: `◆ ${t("tasks.specialBudget", { spent: betrag(topf.spentUsd), amount: betrag(topf.amountUsd) })}`
|
||||
}));
|
||||
}
|
||||
if (task.estimateUsd != null) {
|
||||
zeilen.push(erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: t("tasks.estimate", { usd: formatiereKosten(task.estimateUsd) })
|
||||
text: t("tasks.estimate", { usd: betrag(task.estimateUsd) })
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -91,11 +91,22 @@ function buildControls(filters, bucket, groupBy, metric) {
|
||||
|
||||
bar.append(
|
||||
group("metric", ["cost", "tokens", "runs"], metric),
|
||||
group("groupBy", ["agent", "model", "source"], groupBy),
|
||||
group("groupBy", ["agent", "model", "source", "budget"], groupBy),
|
||||
group("bucket", ["hour", "day"], bucket));
|
||||
return bar;
|
||||
}
|
||||
|
||||
/** A group's label — sources translated, #717 budget pots named. */
|
||||
function groupLabel(groupBy, group) {
|
||||
if (groupBy === "source") return sourceLabel(group);
|
||||
if (groupBy === "budget") {
|
||||
return group.startsWith("special:")
|
||||
? t("timeline.budget.special", { id: group.slice("special:".length) })
|
||||
: t("timeline.budget.daily");
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
function bucketsBetween(from, to, bucket) {
|
||||
const step = bucket === "day" ? 864e5 : 36e5;
|
||||
const start = new Date(from);
|
||||
@@ -172,7 +183,7 @@ function buildChart(points, events, filters, bucket, groupBy, metric) {
|
||||
y -= h;
|
||||
const rect = svg("rect", {
|
||||
x: x + 1, y, width: barWidth - 2, height: h, fill: color(p.group), class: "balken", tabindex: 0
|
||||
}, title(`${formatBucket(buckets[i], bucket)} · ${groupBy === "source" ? sourceLabel(p.group) : p.group}: ` +
|
||||
}, title(`${formatBucket(buckets[i], bucket)} · ${groupLabel(groupBy, p.group)}: ` +
|
||||
`${formatValue(valueOf(p), metric)} (${t("timeline.tooltipRuns", { n: p.runs })})`));
|
||||
rect.addEventListener("click", () => openBucket(filters, buckets[i], step, groupBy, p.group));
|
||||
chart.append(rect);
|
||||
@@ -201,7 +212,7 @@ function buildChart(points, events, filters, bucket, groupBy, metric) {
|
||||
const total = points.filter((p) => p.group === group).reduce((sum, p) => sum + valueOf(p), 0);
|
||||
legend.append(erstelleElement("span", { klasse: "legende-eintrag" },
|
||||
erstelleElement("span", { klasse: "legende-farbe", stil: { background: color(group) } }),
|
||||
erstelleElement("span", { text: `${groupBy === "source" ? sourceLabel(group) : group}: ${formatValue(total, metric)}` })));
|
||||
erstelleElement("span", { text: `${groupLabel(groupBy, group)}: ${formatValue(total, metric)}` })));
|
||||
}
|
||||
if (events.length > 0) {
|
||||
legend.append(
|
||||
@@ -229,6 +240,7 @@ function openBucket(filters, start, step, groupBy, group) {
|
||||
delete next.bucket;
|
||||
delete next.groupBy;
|
||||
delete next.metric;
|
||||
next[groupBy === "source" ? "source" : groupBy] = group;
|
||||
// #717: There is no filter by budget pot — the runs of the bucket are shown unfiltered.
|
||||
if (groupBy !== "budget") next[groupBy === "source" ? "source" : groupBy] = group;
|
||||
applyFilters("/verbrauch/runs", next);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user