diff --git a/server/pruef-aufbewahrung.mjs b/server/pruef-aufbewahrung.mjs index 048ad591..f75542b9 100644 --- a/server/pruef-aufbewahrung.mjs +++ b/server/pruef-aufbewahrung.mjs @@ -375,6 +375,105 @@ melde("=== 7. Auf der Seite ==="); konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser"); await browser.close(); } +/* ===================================================================== + WAS ZU ENDE GEHT, MUSS IM KONZEPT STEHEN (20.09.2026) + + So ist `hilfe_faelle` herausgefallen: Die Tabelle kam am 19.09. + dazu, das Löschkonzept wurde am 15.09. geschrieben, und nichts hat + die beiden je miteinander verglichen. In `hilfe_nachrichten` stehen + die empfindlichsten Texte des Hauses; sie wären unbegrenzt liegen + geblieben. Die zugehörige Aufräumfunktion gab es sogar -- nur rief + sie niemand auf. + + --------------------------------------------------------------------- + WARUM NICHT „JEDE TABELLE MUSS DRINSTEHEN" + + Gemessen: 46 Tabellen, davon 41 mit Bezug auf `personen`. Eine + Regel „alle müssen genannt sein" ergäbe 38 Meldungen, von denen + fast alle richtig wären -- sie sind über `personen_geloescht` + abgedeckt, den Eintrag, der sagt: Wer gelöscht wird, nimmt seine + Daten mit, und die Datenbank erzwingt das. + + Eine Prüfung, die 38-mal meldet, wo 1-mal richtig wäre, wird + abgeschaltet. Das ist kein besseres Ergebnis als gar keine. + + --------------------------------------------------------------------- + DAS SCHMALE, TREFFSICHERE MERKMAL + + Gesucht wird nach dem, was `hilfe_faelle` von den anderen 45 + unterschied: eine Spalte, die sagt „hier ist etwas zu Ende" + (`geschlossen_am`, `erledigt_am`, `beendet_am` …). Solche Zeilen + laufen von selbst ab -- unabhängig davon, ob die Person noch da + ist. Für sie MUSS jemand entschieden haben, ob sie verschwinden + oder bleiben. + + Gemessen: Genau ZWEI Tabellen im ganzen Haus tragen so eine + Spalte. Beide sind jetzt im Konzept -- eine mit Frist, eine + ausdrücklich ohne. Die dritte, die morgen dazukommt, macht diese + Prüfung rot. + ===================================================================== */ +melde(""); +melde("=== Was zu Ende geht, steht im Konzept ==="); +{ + const d = oeffne(); + try { + const tabellen = d.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'") + .all().map((r) => r.name); + + /* Welche Tabellen nennt das Konzept? ABGELEITET aus dem SQL der + Einträge, nicht aus einer zweiten Liste -- sonst hätte man + genau das Problem noch einmal, nur eine Ebene höher. */ + const genannt = new Set(); + for (const f of modul.FRISTEN) { + for (const fn of [f.offen, f.raeumen]) { + if (typeof fn !== "function") continue; + for (const m of String(fn).matchAll(/(?:FROM|UPDATE|INTO)\s+([a-z_]+)/gi)) { + genannt.add(m[1]); + } + } + } + + const ENDE = /^(geschlossen|beendet|erledigt|abgeschlossen|abgelaufen|archiviert)_am$/; + const mitEnde = tabellen.filter((t) => + d.prepare(`PRAGMA table_info(${t})`).all().some((c) => ENDE.test(c.name))); + + /* DIE ZAHL GEHÖRT IN DIE BEDINGUNG: Fände die Prüfung gar keine + solche Tabelle (etwa weil sich die Schreibweise ändert), wäre + „alle gedeckt" trivialerweise wahr -- und sie schwiege genau + dann, wenn sie reden müsste. */ + ok(mitEnde.length >= 2, + `es gibt ${mitEnde.length} Tabellen, deren Zeilen von selbst zu Ende gehen ` + + `(${mitEnde.join(", ")})`); + + const fehlend = mitEnde.filter((t) => !genannt.has(t)); + ok(fehlend.length === 0, + fehlend.length + ? `NICHT im Löschkonzept: ${fehlend.join(", ")} — für jede dieser Tabellen ` + + `muss jemand entschieden haben, ob sie verschwindet oder bleibt` + : `und jede davon steht im Löschkonzept (${mitEnde.length} von ${mitEnde.length})`); + + /* Und der Meldeweg ausdrücklich, weil er der Anlass war. */ + ok(genannt.has("hilfe_faelle"), + "der vertrauliche Meldeweg ist darunter — er war der Anlass für diese Prüfung"); + + /* GEGENPROBE: Eine erfundene Tabelle mit Abschluss-Spalte muss + auffallen. Ohne sie wüsste niemand, ob die Prüfung überhaupt + hinsieht. */ + d.exec("CREATE TABLE IF NOT EXISTS probe_ablauf (id INTEGER PRIMARY KEY, beendet_am TEXT)"); + const nochmal = d.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'") + .all().map((r) => r.name) + .filter((t) => d.prepare(`PRAGMA table_info(${t})`).all().some((c) => ENDE.test(c.name))) + .filter((t) => !genannt.has(t)); + ok(nochmal.includes("probe_ablauf"), + `Gegenprobe: eine erfundene Tabelle mit Abschluss-Spalte fällt auf (${nochmal.join(", ")})`); + d.exec("DROP TABLE probe_ablauf"); + } finally { + d.close(); + } +} + /* ------------------------------------------------------------------- */ melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler`); diff --git a/server/pruef-hilfe.mjs b/server/pruef-hilfe.mjs index cf18eed1..c8c042ae 100644 --- a/server/pruef-hilfe.mjs +++ b/server/pruef-hilfe.mjs @@ -299,19 +299,42 @@ melde("=== 6. Drei offene sind genug ==="); melde(""); melde("=== 7. Was niemand mehr braucht, verschwindet ==="); { - const { hilfeAufraeumen } = await import("./workspace-hilfe.js"); + /* GEPRUEFT WIRD, WAS WIRKLICH LAEUFT (20.09.2026). + ------------------------------------------------------------------ + Hier stand `hilfeAufraeumen()` aus `workspace-hilfe.js`. Die + Funktion gab es, sie war richtig -- und NIEMAND rief sie auf. Der + einzige Aufrufer war diese Pruefung. Sie war gruen und bewies + nichts ueber den Betrieb: geschlossene Faelle blieben unbegrenzt + stehen. + + Jetzt steht die Regel im Loeschkonzept, das beim Start und danach + taeglich laeuft -- und geprueft wird genau dieser Eintrag, mit + derselben Rechnung, die auch im Betrieb greift. */ + const { FRISTEN, aufbewahrungLauf } = await import("./workspace-aufbewahrung.js"); const { db } = await import("./workspace.js"); + const eintrag = FRISTEN.find((f) => f.schluessel === "hilfe_geschlossen"); + ok(!!eintrag && typeof eintrag.raeumen === "function", + "der vertrauliche Meldeweg steht im Loeschkonzept und raeumt selbst"); + ok(eintrag?.art === "raeumen" && /90/.test(String(eintrag?.frist)), + `mit der Frist aus hilfe-tabellen.js („${eintrag?.frist}")`); + /* Einen geschlossenen Fall kuenstlich altern lassen. KEIN echtes Warten -- eine Pruefung, die 90 Tage braucht, ist keine. */ const alt = new Date(Date.now() - 100 * 86400000).toISOString(); db().prepare("UPDATE hilfe_faelle SET geschlossen_am = ? WHERE id = ?").run(alt, fallId); + /* ZUERST ZAEHLEN, DANN RAEUMEN. `offen()` ist die Zahl, die in der + Auskunft steht -- stimmt sie nicht mit dem ueberein, was wirklich + verschwindet, steht dort eine Zahl, der niemand glauben darf. */ + const angekuendigt = eintrag.offen(db()); const vorher = db().prepare("SELECT COUNT(*) AS n FROM hilfe_faelle").get().n; - const weg = hilfeAufraeumen(90); + const weg = eintrag.raeumen(db()); const nachher = db().prepare("SELECT COUNT(*) AS n FROM hilfe_faelle").get().n; ok(weg === 1 && nachher === vorher - 1, `ein alter, geschlossener Fall wird geloescht (${weg}, ${vorher} -> ${nachher})`); + ok(angekuendigt === weg, + `und die angekuendigte Zahl stimmt mit der geloeschten ueberein (${angekuendigt} = ${weg})`); /* UND DIE NACHRICHTEN MIT IHM. Ein Fall ohne Text waere geloescht, die Texte laegen aber weiter in der Datenbank -- genau das, was @@ -321,11 +344,27 @@ melde("=== 7. Was niemand mehr braucht, verschwindet ==="); ok(reste === 0, `und seine ${4 - reste} Nachrichten ebenfalls (${reste} uebrig)`); /* GEGENPROBE: Ein OFFENER Fall wird nicht angefasst, egal wie alt. */ + /* Einen OFFENEN Fall kuenstlich uralt machen -- er darf trotzdem + nicht angefasst werden. Ohne diese Gegenprobe wuerde ein + vergessenes `stand = ?` erst auffallen, wenn ein laufender Fall + verschwindet. */ + const offenId = db().prepare( + "SELECT id FROM hilfe_faelle WHERE stand != 'geschlossen' LIMIT 1").get()?.id; + if (offenId) { + db().prepare("UPDATE hilfe_faelle SET erstellt = ?, geaendert = ?, geschlossen_am = ? WHERE id = ?") + .run(alt, alt, alt, offenId); + } const offen = db().prepare("SELECT COUNT(*) AS n FROM hilfe_faelle WHERE stand != 'geschlossen'").get().n; - const weg2 = hilfeAufraeumen(0); + const weg2 = eintrag.raeumen(db()); const offenDanach = db().prepare("SELECT COUNT(*) AS n FROM hilfe_faelle WHERE stand != 'geschlossen'").get().n; ok(offen === offenDanach && offen > 0, - `offene Faelle bleiben, auch bei Aufbewahrung 0 (${offen} vorher, ${offenDanach} danach, ${weg2} geschlossene weg)`); + `ein OFFENER Fall bleibt, selbst mit uraltem Datum (${offen} vorher, ${offenDanach} danach, ${weg2} weg)`); + + /* Und der Lauf, der taeglich stattfindet, geht wirklich durch -- + nicht nur der einzelne Eintrag. */ + let liefDurch = true; + try { aufbewahrungLauf(); } catch { liefDurch = false; } + ok(liefDurch, "der taegliche Aufbewahrungslauf laeuft ohne Fehler durch"); } /* ======================================================================= @@ -395,6 +434,153 @@ melde("=== 8. Im Browser ==="); } } +/* ===================================================================== + 9. WAS NACH DEM ABSCHLUSS DASTEHT (20.09.2026) + + Bis heute stand dort ein Satz: „Dieser Fall ist abgeschlossen + (Datum)." Er beantwortet keine der drei Fragen, die man in diesem + Moment hat: + + Kann ich noch etwas schreiben? Nein -- und das ist endgültig. + Bleibt das jetzt hier stehen? 90 Tage, dann ist es weg. + Und wenn es wieder passiert? Neu melden. + + Die zweite ist die, die man nicht erst merken darf, wenn es weg + ist -- und auf die man ein Recht hat (Art. 13 Abs. 2 lit. a DSGVO). + + Geprüft wird an echten Bildpunkten, nicht am Quelltext: Ein +

in einem

wäre ungültig, der Browser schlösse das äußere + selbst, und die Sätze stünden nebeneinander statt untereinander. + Das sieht man nur, wenn man misst. + ===================================================================== */ +melde(""); +melde("=== 9. Was nach dem Abschluss dasteht ==="); +{ + let pw = null; + try { + pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + } catch { /* dritter Ausgang */ } + + if (!pw) { + melde(" -- Playwright ist nicht da -- im Browser wurde NICHT nachgesehen."); + } else { + const { db } = await import("./workspace.js"); + /* Einen frisch geschlossenen Fall anlegen -- Abschnitt 7 hat den + alten absichtlich geräumt. */ + const nun = new Date().toISOString(); + /* DER FALL MUSS MARA GEHOEREN. Erster Anlauf nahm „irgendeine + Person aus einem vorhandenen Fall" -- und Mara sah ihn dann + zu Recht NICHT: `fallFuer` gibt einem Melder nur die eigenen + Faelle. Die Pruefung meldete zehn Fehler ueber eine Sperre, + die genau richtig funktioniert hat. */ + const wer = db().prepare("SELECT id FROM personen WHERE name = ? LIMIT 1") + .get("Mara")?.id; + ok(!!wer, `Maras Nummer ist bekannt (${wer})`); + db().prepare( + "INSERT INTO hilfe_faelle (person_id, betreff, stand, erstellt, geaendert," + + " geschlossen_am) VALUES (?,?,?,?,?,?)") + .run(wer, "Abgeschlossen zum Ansehen", "geschlossen", nun, nun, nun); + const zuId = db().prepare("SELECT last_insert_rowid() AS id").get().id; + db().prepare( + "INSERT INTO hilfe_nachrichten (fall_id, von_id, von_leitung, text, erstellt)" + + " VALUES (?,?,?,?,?)").run(zuId, wer, 0, "Der Text dieser Meldung.", nun); + + const browser = await pw.chromium.launch(); + try { + for (const [name, keks] of [["Mara", k.mara], ["DogFather", k.dogi]]) { + const ctx = await browser.newContext({ + viewport: { width: 1280, height: 950 }, reducedMotion: "reduce", + }); + await ctx.addCookies(keks.split("; ").filter(Boolean).map((z) => { + const [n, ...r] = z.split("="); + return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await ctx.newPage(); + const kaputt = []; + seite.on("pageerror", (e) => kaputt.push(String(e).slice(0, 140))); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/hilfe.html?fall=${zuId}`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(1000); + + const z = await seite.evaluate(() => { + const kasten = document.getElementById("fall-warten"); + if (!kasten || kasten.hidden) return null; + const zeilen = [...kasten.querySelectorAll(".fall-schluss__zeile")]; + const r = kasten.getBoundingClientRect(); + return { + anzahl: zeilen.length, + text: kasten.textContent.replace(/\s+/g, " ").trim(), + /* UNTEREINANDER, NICHT NEBENEINANDER. Bei ungültiger + Verschachtelung stünden sie in einer Zeile. */ + untereinander: zeilen.length < 2 ? null + : zeilen[1].getBoundingClientRect().top > zeilen[0].getBoundingClientRect().top, + formularZu: document.getElementById("antwort-form")?.hidden === true, + /* Der Knopf zum Schliessen darf bei einem geschlossenen + Fall nicht dastehen -- sonst sieht es aus, als ginge + noch etwas. */ + schliessenWeg: document.getElementById("fall-schliessen")?.hidden === true, + hoehe: Math.round(r.height), + }; + }); + + ok(z !== null, `${name}: der Abschluss-Kasten steht da`); + ok(z?.anzahl === 3, ` drei Sätze statt einem (${z?.anzahl})`); + ok(z?.untereinander === true, " und sie stehen untereinander, nicht nebeneinander"); + ok(/abgeschlossen/i.test(z?.text || ""), " er sagt, dass der Fall zu ist"); + ok(/nicht wieder aufmachen|niemand mehr schreiben/i.test(z?.text || ""), + " und dass das endgültig ist"); + ok(/gelöscht|geloescht/i.test(z?.text || ""), + " er sagt, dass der Fall gelöscht wird"); + ok(/\d{1,2}\.\d{1,2}\.\d{4}/.test(z?.text || ""), + ` und WANN, mit Datum („${(z?.text || "").slice(0, 90)}…“)`); + ok(z?.formularZu === true, " das Antwortfeld ist weg"); + ok(z?.schliessenWeg === true, " und der Schließen-Knopf auch"); + if (name === "Mara") { + ok(/melde es einfach neu/i.test(z?.text || ""), + " und sie erfährt, was bei einer Wiederholung gilt"); + } else { + ok(/neuer Fall/i.test(z?.text || ""), + " und die Leitung erfährt, dass eine Wiederholung ein neuer Fall wird"); + } + ok(kaputt.length === 0, + ` kein Skriptfehler${kaputt.length ? ": " + kaputt.join(" | ") : ""}`); + if (process.env.ZEIG_TEXT === "ja") melde(` [Text] ${z?.text}`); + + /* DER DIALOG BEIM SCHLIESSEN muss die Frist auch nennen -- + dort faellt die Entscheidung. Danach ist es zu spaet. */ + if (name === "DogFather") { + await seite.goto(`http://127.0.0.1:${PORT}/workspace/hilfe.html`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(800); + const offeneZeile = await seite.$(".fall-zeile"); + if (offeneZeile) { + await offeneZeile.click(); + await seite.waitForTimeout(600); + const knopf = await seite.$("#fall-schliessen:not([hidden])"); + if (knopf) { + await knopf.click(); + await seite.waitForSelector("dialog.nachfrage[open]", { timeout: 3000 }); + const dlg = await seite.evaluate(() => { + const d = document.querySelector("dialog.nachfrage[open]"); + const t = (n) => d.querySelector(`[data-teil="${n}"]`)?.textContent || ""; + return { was: t("was"), bleibt: t("bleibt") }; + }); + ok(/90 Tage/.test(dlg.bleibt), + ` der Abschluss-Dialog nennt die Frist („${dlg.bleibt.slice(0, 80)}…“)`); + ok(/gelöscht/.test(dlg.bleibt), + " und sagt, dass danach gelöscht wird"); + await seite.keyboard.press("Escape"); + } else { ok(false, " ein offener Fall zum Schließen war da"); } + } else { ok(false, " es gab eine Fallzeile zum Anklicken"); } + } + await ctx.close(); + } + } finally { + await browser.close(); + } + } +} + melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler`); melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); diff --git a/server/workspace-aufbewahrung.js b/server/workspace-aufbewahrung.js index 7145261a..ccf58081 100644 --- a/server/workspace-aufbewahrung.js +++ b/server/workspace-aufbewahrung.js @@ -38,6 +38,11 @@ ===================================================================== */ import express from "express"; +/* Die Frist steht dort, wo auch die Begruendung steht -- in + `hilfe-tabellen.js`. Sie hier ein zweites Mal hinzuschreiben hiesse + zwei Wahrheiten, von denen eine veraltet. `hilfe-tabellen.js` + importiert absichtlich nichts, es kann also kein Kreis entstehen. */ +import { AUFBEWAHRUNG_TAGE as HILFE_TAGE, FALL_STAND } from "./hilfe-tabellen.js"; import { db, sitzungLesen, istDogFather, protokolliere, echteIp, einstellung, einstellungSetzen, @@ -92,6 +97,62 @@ function zaehle(d, sql, ...werte) { DIE TAFEL ===================================================================== */ export const FRISTEN = [ + { + /* ================================================================= + DER VERTRAULICHE MELDEWEG (nachgetragen am 20.09.2026) + + DIESE TABELLE FEHLTE HIER -- und das ist der teuerste Eintrag, + den man vergessen kann: In `hilfe_nachrichten` stehen die + empfindlichsten Texte des ganzen Hauses. Jemand beschreibt ein + Problem, oft mit anderen Menschen darin. + + Die Frist selbst war nie strittig: `AUFBEWAHRUNG_TAGE = 90` + steht seit dem 19.09. in `hilfe-tabellen.js`, mit Begruendung. + Und `hilfeAufraeumen()` gab es auch. Nur hat sie NIEMAND + aufgerufen: Gesucht im ganzen Verzeichnis war der einzige + Aufrufer ihre eigene Pruefung. Der Kommentar darueber behauptete + „Wird beim Start aufgerufen (siehe index.js)" -- das war nie + wahr. Die Pruefung war gruen und bewies nichts ueber den + Betrieb. + + Jetzt steht es hier, wo es sich selbst durchsetzt: Diese Datei + ist beides -- die Uebersicht, was wie lange aufgehoben wird, UND + das Programm, das es tut. Nebenbei taucht der Meldeweg damit in + der Auskunft auf (Art. 15 DSGVO), und das gehoert sich: Wer + gemeldet hat, darf erfahren, wie lange das noch gespeichert ist. + + GEMESSEN VOR DEM EINSCHALTEN: In der echten Datenbank steht + heute KEIN einziger Fall. Der erste Lauf loescht also nichts, + und der erste Fall kann fruehestens in drei Monaten 90 Tage alt + werden. Einschalten ist jetzt frei -- spaeter waere es der + riskante Moment gewesen. + + DIE NACHRICHTEN GEHEN MIT: `hilfe_nachrichten.fall_id` traegt + `ON DELETE CASCADE`, und `PRAGMA foreign_keys` steht im Dienst + auf 1. Beides nachgemessen an einem echten Fall mit Nachricht: + vorher 1/1, nachher 0/0. Ohne diese Messung waere der Fall + verschwunden und der eigentliche Text liegengeblieben. */ + schluessel: "hilfe_geschlossen", + was: "Abgeschlossene vertrauliche Meldungen samt Verlauf", + art: "raeumen", + frist: `${HILFE_TAGE} Tage nach dem Abschluss`, + zweck: "Ein abgeschlossener Fall kommt manchmal wieder auf " + + "(oft Monate spaeter) – dann muss man nachlesen " + + "können, was damals vereinbart wurde.", + grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse " + + "(Schutz der Mitglieder), Art. 5 Abs. 1 lit. e – Speicherbegrenzung", + wirkung: "Der Fall und ALLE Nachrichten darin verschwinden ganz. " + + "Offene Fälle werden nie geräumt – nur abgeschlossene, und erst " + + `${HILFE_TAGE} Tage danach.`, + offen: (d) => zaehle(d, + "SELECT COUNT(*) AS n FROM hilfe_faelle WHERE stand = ?" + + " AND geschlossen_am IS NOT NULL AND geschlossen_am < ?", + FALL_STAND.zu, vorTagen(HILFE_TAGE)), + raeumen: (d) => d.prepare( + "DELETE FROM hilfe_faelle WHERE stand = ?" + + " AND geschlossen_am IS NOT NULL AND geschlossen_am < ?") + .run(FALL_STAND.zu, vorTagen(HILFE_TAGE)).changes, + }, { schluessel: "protokoll_ip", was: "IP-Adressen im Protokoll", @@ -170,6 +231,37 @@ export const FRISTEN = [ offen: (d) => zaehle(d, "SELECT COUNT(*) AS n FROM personen WHERE alter_bestaetigt_am IS NOT NULL"), }, + { + /* ERLEDIGTE AUFGABEN (nachgetragen am 20.09.2026) + + Aufgefallen beim Suchen nach dem naechsten `hilfe_faelle`: + Genau ZWEI Tabellen im ganzen Haus tragen eine Spalte, die + sagt „hier ist etwas zu Ende" -- `hilfe_faelle.geschlossen_am` + und `aufgaben.erledigt_am`. Die erste war gar nicht im Konzept, + die zweite ebenso wenig. + + Hier ist die Antwort aber eine andere: Sie BLEIBEN. Eine + erledigte Aufgabe ist Arbeitsdokumentation, keine Meldung ueber + einen Menschen -- und wer im Dezember wissen will, was im + September verabredet war, findet es sonst nicht mehr. + + `aufgaben.verantwortlich_id` traegt ON DELETE SET NULL, nicht + CASCADE: Wird eine Person geloescht, bleibt die Aufgabe stehen + und verliert nur den Namen. Auch das ist Absicht -- eine + Aufgabe, die verschwindet, weil jemand das Team verlaesst, + reisst die Geschichte des Projekts mit. */ + schluessel: "aufgaben_erledigt", + was: "Erledigte und abgebrochene Aufgaben", + art: "bleibt", + frist: "unbefristet", + zweck: "Nachlesen können, was wann verabredet und getan wurde.", + grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse an " + + "nachvollziehbarer Zusammenarbeit", + wirkung: "Bleiben. Beim Löschen einer Person verlieren sie nur den Namen " + + "(ON DELETE SET NULL) – die Aufgabe selbst bleibt lesbar.", + offen: (d) => zaehle(d, + "SELECT COUNT(*) AS n FROM aufgaben WHERE erledigt_am IS NOT NULL"), + }, { schluessel: "personen_geloescht", was: "Daten einer gelöschten Person", diff --git a/server/workspace-hilfe.js b/server/workspace-hilfe.js index 54842595..8ed2e909 100644 --- a/server/workspace-hilfe.js +++ b/server/workspace-hilfe.js @@ -19,7 +19,7 @@ import express from "express"; import { db, protokolliere } from "./workspace.js"; -import { FALL_STAND } from "./hilfe-tabellen.js"; +import { FALL_STAND, AUFBEWAHRUNG_TAGE } from "./hilfe-tabellen.js"; import { benachrichtige } from "./workspace-push.js"; export const hilfeRouter = express.Router(); @@ -109,6 +109,10 @@ hilfeRouter.get("/workspace/api/hilfe", (req, res) => { darf, ohne es durch einen Fehlschlag herauszufinden. */ offen_eigene: leitung ? null : zeilen.filter((z) => z.stand !== FALL_STAND.zu).length, offen_max: OFFEN_MAX, + /* Damit der Abschluss-Dialog die Frist nennen kann, ohne sie + ein zweites Mal im Browser stehen zu haben. Eine Zahl, die an + zwei Stellen steht, ist morgen an einer davon falsch. */ + aufbewahrung_tage: AUFBEWAHRUNG_TAGE, }); } catch (fehler) { console.error("[hilfe] Liste:", fehler?.message); @@ -146,6 +150,25 @@ hilfeRouter.get("/workspace/api/hilfe/:id(\\d+)", (req, res) => { id: fall.id, betreff: fall.betreff, stand: fall.stand, erstellt: fall.erstellt, geaendert: fall.geaendert, geschlossen_am: fall.geschlossen_am, + /* BIS WANN ER NOCH DA IST (20.09.2026). + ---------------------------------------------------------- + Ein abgeschlossener Fall wird nach 90 Tagen geloescht -- + das steht im Loeschkonzept und laeuft taeglich. Bis heute + stand auf der Seite nur „Dieser Fall ist abgeschlossen". + Niemand konnte wissen, dass er verschwindet, und erst recht + nicht, wann. + + Das ist keine Kleinigkeit: Wer eine Meldung gemacht hat, + hat ein Recht darauf zu erfahren, wie lange das gespeichert + bleibt (Art. 13 Abs. 2 lit. a DSGVO). Und praktisch braucht + er es auch -- wer nachlesen will, was vereinbart wurde, + soll nicht erst merken, dass es weg ist. + + GERECHNET WIRD HIER, nicht im Browser: Sonst stuende + dieselbe Frist an zwei Stellen, und eine davon altert. */ + lesbar_bis: fall.geschlossen_am + ? new Date(Date.parse(fall.geschlossen_am) + AUFBEWAHRUNG_TAGE * 86400000).toISOString() + : null, ...(leitung ? { melder: melder?.name || "(gelöscht)" } : {}), }, nachrichten: nachrichten.map((n) => ({ @@ -318,44 +341,26 @@ async function schlussBenachrichtigen(fall) { }); } -/* ---------- Aufräumen --------------------------------------------------- */ +/* ---------- Aufräumen: steht jetzt im Löschkonzept -------------------- + + Hier stand `hilfeAufraeumen(tage)` -- mit einem Kommentar darüber, + sie werde „beim Start aufgerufen (siehe index.js)". Das war nie + wahr: Gesucht im ganzen Verzeichnis war der einzige Aufrufer ihre + eigene Prüfung. Die Prüfung war grün und bewies nichts über den + Betrieb. + + Am 20.09.2026 ist die Regel dorthin gewandert, wo sie sich selbst + durchsetzt: `workspace-aufbewahrung.js`, Eintrag + `hilfe_geschlossen`. Diese Datei ist beides -- die Übersicht, was + wie lange aufgehoben wird, UND das Programm, das es tut. Sie läuft + beim Start und danach täglich. + + ZWEI STELLEN WÄREN SCHLIMMER ALS KEINE. Eine zweite Fassung + desselben DELETE hier wäre morgen anders als die dort, und der + Unterschied fiele erst auf, wenn er zählt -- beim Löschen der + empfindlichsten Texte des Hauses. Deshalb ist sie weg, nicht + doppelt. + + Die Frist selbst (`AUFBEWAHRUNG_TAGE = 90`) steht unverändert in + `hilfe-tabellen.js`, samt Begründung. */ -/** Löscht geschlossene Fälle, die älter sind als die Aufbewahrung. - * - * ACHTUNG, NACHGEMESSEN AM 19.09.2026: Diese Funktion wird im Betrieb - * NICHT aufgerufen. Hier stand vorher "Wird beim Start aufgerufen - * (siehe index.js)" -- das war nie wahr. Gesucht im ganzen Verzeichnis: - * der einzige Aufrufer ist `pruef-hilfe.mjs`, also ihre eigene Prüfung. - * - * Die Prüfung ist deshalb grün und beweist nichts über den Betrieb: - * Sie zeigt, dass die Funktion richtig löscht -- nicht, dass jemand - * sie aufruft. Genau das Muster, vor dem die Hausregeln warnen. - * - * FOLGE: Geschlossene Fälle bleiben derzeit unbegrenzt stehen. Das ist - * die sichere Richtung (nichts geht verloren), aber nicht die - * gewollte -- ein vertraulicher Fall über einen Menschen soll nicht - * ewig gespeichert bleiben. - * - * Das EINZUSCHALTEN löscht echte Daten und ist deshalb keine - * Reparatur, die nebenbei passiert, sondern Filipes Entscheidung: - * Wie lange sollen abgeschlossene Fälle stehen bleiben? Die Prüfung - * rechnet mit 90 Tagen, das ist aber nur ihre Annahme. - * - * Läuft still: Wenn es nichts zu tun gibt, sagt es auch nichts -- - * eine Meldung, die jeden Start begleitet, wird nach einer Woche - * nicht mehr gelesen. */ -export function hilfeAufraeumen(tage) { - try { - const grenze = new Date(Date.now() - tage * 86400000).toISOString(); - const weg = db().prepare( - "DELETE FROM hilfe_faelle WHERE stand = ? AND geschlossen_am IS NOT NULL" - + " AND geschlossen_am < ?").run(FALL_STAND.zu, grenze); - if (weg.changes > 0) { - console.log(`[hilfe] ${weg.changes} abgeschlossene Fälle nach ${tage} Tagen gelöscht.`); - } - return weg.changes; - } catch (fehler) { - console.error("[hilfe] aufraeumen:", fehler?.message); - return 0; - } -} diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 6ad585a5..3df3d47e 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +