From 95d68fd392ecb49c169a55750e9c5260a892eb82 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sat, 3 Oct 2026 15:00:03 +0200 Subject: [PATCH] Rechte Hand und DogFather duerfen Aufgaben immer loeschen, in jeder Etappe Filipe: "ich will noch dass die rechte hand und dogfather bei den aufgaben immer die option haben aufgaben zu loeschen. die soll es immer geben in jeder etape." NACHGEMESSEN STATT ANGENOMMEN, was vorher galt (darfAufgabenVerteilen && darfAendern): DogFather (admin) -> Leitung, konnte schon immer ueberall loeschen. rechte Hand (hand) -> NUR an Aufgaben, bei denen sie selbst Erstellerin, Verantwortliche oder Zielperson war. An allen anderen fehlte der Knopf. Die Luecke war also allein die rechte Hand -- und sie hing nicht an der Etappe, sondern an der Zugehoerigkeit. Genau das faellt weg. ZUR ETAPPE: Der Loeschweg hat auch vorher keinen Status geprueft, und die Oberflaeche zeigt den Stift an JEDER Karte. Es gab hier nichts freizuschalten -- die Zusage "in jeder etape" ist jetzt aber festgenagelt, damit sie niemand mit einer gut gemeinten Statusbedingung wieder aufhebt. `istRechteHand` NEU, UND NICHT `istHand`: Letzteres umfasst auch die LINKE Hand. Haette ich das genommen, haette sie das Recht lautlos mitbekommen -- im ganzen Haus hat sie durchgaengig das kleinere Recht, und es waren zwei Rollen genannt, nicht drei. Der Vergleich `rolle === "hand"` stand bisher VIERMAL verstreut im Haus (workspace-chat zweimal, workspace-material, workspace-rechte); eine fuenfte Abschrift waere die naechste, die irgendwann abweicht. Die vier alten bleiben vorerst unberuehrt -- nebenbei umgebaut waeren sie vier ungepruefte Rechteaenderungen. pruef-verteilen 30 -> 42. Darin steht jetzt das GEGENTEIL einer bisherigen Zeile: "Gegenprobe: eine fremde Aufgabe loescht sie nicht (403)" war am 25.09. richtig, als der Auftrag "ihre eigenen" hiess. Sie ist mitgewandert statt stehenzubleiben -- eine Pruefung, die eine zurueckgenommene Regel weiter verteidigt, haelt die Aenderung auf und sieht dabei aus wie Sorgfalt. Alle vier Etappen einzeln (offen, arbeit, review, erledigt), jeweils mit der Kontrolle, dass die Etappe wirklich steht -- sonst waeren es in Wahrheit viermal "offen" und die Pruefung gruen, ohne je etwas anderes gesehen zu haben. Dazu je Etappe die Auskunft an die Oberflaeche (darf_loeschen), der echte DELETE und ein Blick in die Datenbank. GEGENPROBE GEFAHREN: Mit `istHand` statt `istRechteHand` meldet die Pruefung "die LINKE Hand darf es in keiner (0/4 mal abgelehnt)", 2 Fehler. Sie faengt also genau den Fehler, der am teuersten gewesen waere. Gegengemessen: pruef-haerte 20, pruef-aufgabenbrett 49, pruef-rechtetafel 19, pruef-zuteilung 98, pruef-haus-trennung 100 -- alle 0 Fehler. Das andere Haus ist unberuehrt; "hand" gibt es dort nicht. Co-Authored-By: Claude Opus 5 --- server/pruef-verteilen.mjs | 98 ++++++++++++++++++++++++++++++++++-- server/workspace-aufgaben.js | 34 ++++++++++++- server/workspace.js | 19 +++++++ 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/server/pruef-verteilen.mjs b/server/pruef-verteilen.mjs index 8b267823..b19dd307 100644 --- a/server/pruef-verteilen.mjs +++ b/server/pruef-verteilen.mjs @@ -79,7 +79,7 @@ melde("\n=== 1. Die Regel ==="); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const jetzt = new Date().toISOString(); -let idDogi, idHand, idModi, idModi2; +let idDogi, idHand, idModi, idModi2, idLinke; { const d = new DatabaseSync(process.env.WORKSPACE_DB); const k = randomBytes(32).toString("hex"); @@ -97,6 +97,10 @@ let idDogi, idHand, idModi, idModi2; idHand = anlegen("VanVan", "hand", "CODE-H"); idModi = anlegen("Diene", "modi", "CODE-M"); idModi2 = anlegen("Ghost", "modi", "CODE-M2"); + /* DIE LINKE HAND IST DIE GEGENPROBE (03.10.2026). `istHand` fasst + beide Haende zusammen -- haette das neue Loeschrecht daran + gehangen, bekaeme sie es lautlos mit. */ + idLinke = anlegen("Funny", "linke", "CODE-L"); d.close(); } @@ -132,6 +136,7 @@ melde("\n=== 2. Durch die Schnittstelle ==="); const kDogi = await anmelden("admin", "CODE-A"); const kHand = await anmelden("hand", "CODE-H"); const kModi = await anmelden("modi", "CODE-M"); +const kLinke = await anmelden("linke", "CODE-L"); ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet"); /* Sagt der Server es auch der Seite? */ @@ -355,8 +360,95 @@ melde("\n=== 5. Die rechte Hand an ihren eigenen Aufgaben ==="); { titel: "Nur fuer DogFather", verantwortlich_id: idDogi, prioritaet: "mittel" }); const id3 = json(fremd).id; const handFremd = await ruf(`/workspace/api/aufgaben/${id3}`, kHand, "DELETE"); - ok(fremd.code === 201 && handFremd.code === 403, - `Gegenprobe: eine fremde Aufgabe loescht sie nicht (${handFremd.code})`); + ok(fremd.code === 201 && handFremd.code === 200, + `und auch eine fremde Aufgabe von DogFather (${handFremd.code})`); +} + +/* ===================================================================== + 6. IMMER UND IN JEDER ETAPPE (03.10.2026) + + Filipe: "ich will noch dass die rechte hand und dogfather bei den + aufgaben immer die option haben aufgaben zu loeschen. die soll es + immer geben in jeder etape." + + HIER STAND BIS HEUTE DAS GEGENTEIL. Die letzte Zeile des Abschnitts + darueber lautete: "Gegenprobe: eine fremde Aufgabe loescht sie + nicht (403)". Am 25.09. war das richtig -- damals hiess der Auftrag + "ihre eigenen". Jetzt heisst er "immer", und die Zeile ist + mitgewandert statt stehenzubleiben. Eine Pruefung, die eine + zurueckgenommene Regel weiter verteidigt, haelt die Aenderung auf + und sieht dabei aus wie Sorgfalt. + + WARUM ALLE VIER ETAPPEN EINZELN: Der Loeschweg fragt heute keinen + Status ab -- diese Pruefung zielt also nicht auf einen bekannten + Fehler, sondern auf einen kuenftigen. "In jeder etape" ist eine + Zusage; ohne die vier Durchlaeufe koennte sie jemand mit einer gut + gemeinten Statusbedingung aufheben, ohne dass etwas rot wird. + + GEGENGEWICHT: Die LINKE Hand bekommt das Recht NICHT. `istHand` + fasst beide Haende zusammen, und haette ich das genommen, waere es + lautlos an sie mitgegangen -- Filipe hat zwei Rollen genannt, nicht + drei. Diese Gegenprobe ist der einzige Ort, an dem der Unterschied + auffallen wuerde. + ===================================================================== */ +melde("\n=== 6. Loeschen: immer, in jeder Etappe ==="); +{ + const ETAPPEN = ["offen", "arbeit", "review", "erledigt"]; + let konnteHand = 0, konnteDogi = 0, konnteLinkeNicht = 0, wirklichWeg = 0; + + for (const stand of ETAPPEN) { + /* Je Etappe drei Aufgaben, alle von DogFather an einen Modi -- + die rechte Hand hat mit keiner davon etwas zu tun. */ + const bauen = async (titel) => { + const r = await ruf("/workspace/api/aufgaben", kDogi, "POST", + { titel, verantwortlich_id: idModi, prioritaet: "mittel" }); + const id = json(r).id; + if (stand !== "offen") { + await ruf(`/workspace/api/aufgaben/${id}`, kDogi, "PATCH", { status: stand }); + } + return id; + }; + + const fuerHand = await bauen(`Fremd fuer die rechte Hand (${stand})`); + const fuerDogi = await bauen(`Fremd fuer DogFather (${stand})`); + const fuerLinke = await bauen(`Fremd fuer die linke Hand (${stand})`); + + /* Steht die Etappe wirklich? Ohne das waeren alle vier Durchlaeufe + in Wahrheit viermal "offen" -- und die Pruefung gruen, ohne je + etwas anderes gesehen zu haben. */ + const liste = json(await ruf("/workspace/api/aufgaben", kDogi)).aufgaben || []; + const echt = liste.find((a) => a.id === fuerHand); + ok(echt?.status === stand, `Etappe "${stand}" steht wirklich (${echt?.status})`); + + /* Und die Oberflaeche bekommt es auch gesagt -- sonst gaebe es den + Knopf gar nicht, egal was der Server erlaubt. */ + const beiHand = (json(await ruf("/workspace/api/aufgaben", kHand)).aufgaben || []) + .find((a) => a.id === fuerHand); + ok(beiHand?.darf_loeschen === true, + ` der rechten Hand wird Loeschen angeboten (${beiHand?.darf_loeschen})`); + + if ((await ruf(`/workspace/api/aufgaben/${fuerHand}`, kHand, "DELETE")).code === 200) konnteHand++; + if ((await ruf(`/workspace/api/aufgaben/${fuerDogi}`, kDogi, "DELETE")).code === 200) konnteDogi++; + if ((await ruf(`/workspace/api/aufgaben/${fuerLinke}`, kLinke, "DELETE")).code === 403) konnteLinkeNicht++; + + /* Weg heisst weg -- in der Datenbank, nicht nur im Rueckgabewert. */ + const d3 = new DatabaseSync(process.env.WORKSPACE_DB); + const da = d3.prepare("SELECT COUNT(*) AS n FROM aufgaben WHERE id IN (?,?)") + .get(fuerHand, fuerDogi).n; + const linkeNochDa = d3.prepare("SELECT COUNT(*) AS n FROM aufgaben WHERE id = ?") + .get(fuerLinke).n; + d3.close(); + if (da === 0 && linkeNochDa === 1) wirklichWeg++; + } + + const n = ETAPPEN.length; + ok(n === 4 && konnteHand === n, + `die rechte Hand loescht in allen ${n} Etappen (${konnteHand}/${n})`); + ok(konnteDogi === n, `DogFather ebenso (${konnteDogi}/${n})`); + ok(konnteLinkeNicht === n, + `Gegenprobe: die LINKE Hand darf es in keiner (${konnteLinkeNicht}/${n} mal abgelehnt)`); + ok(wirklichWeg === n, + `und geloescht ist geloescht -- die abgelehnte steht noch da (${wirklichWeg}/${n})`); } console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index bcc0c38e..155c548d 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -10,7 +10,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, darfAufgabenVerteilen, darfAufgabenAnlegen, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, betreuteIds, + db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, istRechteHand, darfAufgabenVerteilen, darfAufgabenAnlegen, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, MODI_KATEGORIEN, kategorienFuer, kategoriePersonen, KANAELE, kanaeleFuer, @@ -374,6 +374,38 @@ function darfAendern(person, aufgabe) { ohnehin. Die Zeile ist damit eine Erweiterung, keine Einschraenkung -- nachgewiesen in pruef-aufgaben-loeschen mit einer Gegenprobe. */ function darfLoeschen(person, aufgabe) { + /* ==== DOGFATHER UND DIE RECHTE HAND: IMMER (03.10.2026) ========== + + Filipe: „ich will noch dass die rechte hand und dogfather bei den + aufgaben immer die option haben aufgaben zu loeschen. die soll es + immer geben in jeder etape." + + Vorher galt `darfAufgabenVerteilen && darfAendern`. Nachgemessen, + was das hiess: + + DogFather (admin) -> Leitung, also schon immer ueberall. + rechte Hand (hand) -> NUR an Aufgaben, bei denen sie selbst + Erstellerin, Verantwortliche oder + Zielperson war. An allen anderen war + der Knopf weg. + + Die Luecke war also allein die rechte Hand -- und zwar nicht nach + Etappe, sondern nach Zugehoerigkeit. Genau das faellt jetzt weg. + + KEIN STATUS WIRD GEFRAGT, und das ist der zweite Teil des + Auftrags („in jeder etape"). Der Loeschweg hat auch vorher keinen + Status geprueft; die Oberflaeche zeigt den Stift an JEDER Karte, + unabhaengig von offen/arbeit/review/erledigt. Es gibt hier + deshalb nichts freizuschalten -- aber es muss auch nie wieder + jemand eine Statusbedingung einbauen, und darauf passt + pruef-verteilen jetzt in allen vier Etappen auf. + + `istRechteHand` UND NICHT `istHand`: Letzteres umfasst auch die + LINKE Hand. Sie steht hier bewusst nicht drin -- im ganzen Haus + hat sie das kleinere Recht, und Filipe hat zwei Rollen genannt, + nicht drei. Haette ich `istHand` genommen, haette die linke Hand + das Recht lautlos mitbekommen. */ + if (istLeitung(person) || istRechteHand(person)) return true; return darfAufgabenVerteilen(person) && darfAendern(person, aufgabe); } diff --git a/server/workspace.js b/server/workspace.js index fb9d809f..36390d4a 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -145,6 +145,25 @@ export const WIE_RECHTE_HAND = new Set(["hand", "linke"]); /** Hat diese Person die Team-Rechte einer Hand (rechts oder links)? */ export const istHand = (person) => !!person && WIE_RECHTE_HAND.has(person.rolle); +/** Genau die RECHTE Hand -- nicht die linke. + * + * WARUM ES BEIDES BRAUCHT (03.10.2026). `istHand` fasst beide Haende + * zusammen, und das ist fuer die meisten Fragen richtig. Fuer manche + * nicht: Die linke Hand hat im Haus durchgaengig das kleinere Recht + * (sie darf keine Aufgaben annehmen, nicht in den vertraulichen + * Meldeweg). Wer dort `istHand` nimmt, gibt ihr versehentlich ein + * Recht, das niemand ihr geben wollte -- und sieht es nicht, weil + * der Name so harmlos aussieht. + * + * Der Vergleich `rolle === "hand"` stand dafuer bisher VIERMAL + * verstreut im Haus (workspace-chat zweimal, workspace-material, + * workspace-rechte). Vier Abschriften derselben Regel sind vier + * Gelegenheiten, dass eine nachgezogen wird und die anderen nicht. + * Neue Stellen nehmen deshalb diese hier; die vier alten sind + * unveraendert und warten auf eine eigene, gemessene Umstellung -- + * nebenbei umgebaut waeren sie vier ungepruefte Rechteaenderungen. */ +export const istRechteHand = (person) => !!person && person.rolle === "hand"; + /* ===== WER FUEHRT DIE ZUGAENGE? (25.09.2026) ======================== Filipe, zum wiederholten Mal: „die rechte hand sieht das immer noch