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