Zielkette, Stufe 1: jede Aufgabe kann sagen, wofuer sie da ist (#714)

- TaskItem.ParentId, in der Datei als parent, in der DB als ParentId. Die
  Elternaufgabe ist der Zweck ("wofuer"), blocked_by die Voraussetzung
  ("wann").
- GoalChain: Eltern bis zu acht Stufen, Zyklen brechen den Gang ab und
  werden beim Setzen abgelehnt. Oben steht das Ziel der Einheit - der
  obersten Aufgabe, die zu einer Einheit gehoert (ein Ziel, das ein Mensch
  auf der obersten Ebene setzt, gehoert keiner), sonst der naechsten Einheit
  darueber mit Ziel.
- Der Lauf einer Aufgabe bekommt den Absatz "Why this task exists" und die
  Bitte, zu melden, wenn die Aufgabe ihrem Zweck nicht mehr dient.
- Taskboard-Tool: parent bei task_create/task_update, geprueft auf Existenz,
  Sichtbarkeit (#684) und Zyklen; task_list zeigt "Serves".
- WebUI: Feld im Aufgabendialog, Anzeige auf der Karte; die Verwaltung
  prueft wie das Tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-24 18:45:15 +02:00
co-authored by Claude Opus 5.5
parent b75b68c786
commit b22272345e
16 changed files with 482 additions and 18 deletions
@@ -349,6 +349,8 @@
"tasks.board": "Brett",
"tasks.boardAll": "Alle Bretter",
"tasks.boardTopLevel": "Oberste Ebene",
"tasks.parent": "Dient Aufgabe (Eltern)",
"tasks.serves": "dient {id}",
"services.title": "Dienste",
"services.newService": "Neuer Dienst",
@@ -349,6 +349,8 @@
"tasks.board": "Board",
"tasks.boardAll": "All boards",
"tasks.boardTopLevel": "Top level",
"tasks.parent": "Serves task (parent)",
"tasks.serves": "serves {id}",
"services.title": "Services",
"services.newService": "New Service",
@@ -189,7 +189,7 @@ function erstelleAufgabenKarte(task) {
erstellePlakette(holeStatusText(task.status), STATUS_ART[task.status] || "neutral"),
erstelleElement("span", {
klasse: "gedaempft text-klein",
text: `${task.id} · ${task.assignee || "—"}${task.boardPath ? ` · ▤ ${task.boardPath}` : ""}`
text: `${task.id} · ${task.assignee || "—"}${task.boardPath ? ` · ▤ ${task.boardPath}` : ""}${task.parentId ? ` · ↑ ${t("tasks.serves", { id: task.parentId })}` : ""}`
})
)
);
@@ -343,6 +343,9 @@ async function oeffneAufgabenDialog(task) {
const abnahme = erstelleElement("textarea", { rows: "2", placeholder: "..." });
abnahme.value = task?.acceptance || "";
// #714: The task this one serves — the run is told the chain up to the unit's goal.
const eltern = erstelleElement("input", { type: "text", value: task?.parentId || "", placeholder: "t-…" });
const hinweis = erstelleElement("p", { klasse: "gedaempft text-klein" });
const aktualisiere = () => {
@@ -369,6 +372,7 @@ async function oeffneAufgabenDialog(task) {
erstelleElement("label", { text: "Value:" }), terminWert,
erstelleElement("label", { text: "Timezone:" }), zeitzone,
erstelleElement("label", { text: "Acceptance Criteria:" }), abnahme,
erstelleElement("label", { text: t("tasks.parent") + ":" }), eltern,
erstelleElement("span"), hinweis
);
@@ -399,7 +403,8 @@ async function oeffneAufgabenDialog(task) {
whenValue: terminArt.value ? terminWert.value.trim() : null,
timeZone: terminArt.value ? zeitzone.value.trim() : null,
acceptance: abnahme.value.trim(),
priority: task?.priority ?? 3
priority: task?.priority ?? 3,
parentId: eltern.value.trim()
};
try {