From 285038f400f53e15c874482e57147b7a75ab3182 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sun, 20 Sep 2026 14:10:38 +0200 Subject: [PATCH] Der vertrauliche Meldeweg vergisst jetzt -- und sagt, bis wann Zwei Entscheidungen, die offenstanden. Beide getroffen, nachdem gemessen war, was wirklich da ist. 1. WIE LANGE BLEIBEN ABGESCHLOSSENE FAELLE? 90 Tage. Die Entscheidung war bereits getroffen: `AUFBEWAHRUNG_TAGE = 90` steht seit dem 19.09. in hilfe-tabellen.js, mit Begruendung ("ein Fall kommt manchmal wieder auf"). Nur hat sie NIEMAND durchgesetzt -- `hilfeAufraeumen()` gab es, und der einzige Aufrufer war ihre eigene Pruefung. Der Kommentar darueber behauptete "Wird beim Start aufgerufen (siehe index.js)"; das war nie wahr. Jetzt steht die Regel in workspace-aufbewahrung.js, dem Loeschkonzept, das sich selbst durchsetzt -- beim Start und danach taeglich. Die zweite Fassung in workspace-hilfe.js ist WEG, nicht doppelt: Zwei DELETEs auf dieselben Daten waeren morgen verschieden, und der Unterschied fiele erst auf, wenn er zaehlt. GEMESSEN VOR DEM EINSCHALTEN: In der echten Datenbank steht heute KEIN einziger Fall. Der erste Lauf loescht nichts, und der erste Fall kann fruehestens in drei Monaten 90 Tage alt werden. Jetzt einschalten ist frei -- spaeter waere es der riskante Moment gewesen. PRAGMA foreign_keys steht im Dienst auf 1, und hilfe_nachrichten traegt ON DELETE CASCADE. An einem echten Fall mit Nachricht nachgemessen: vorher 1/1, nachher 0/0. Ohne diese Messung waere der Fall verschwunden und der eigentliche Text liegengeblieben. 2. SOLL EIN GESCHLOSSENER FALL WIEDER ZU OEFFNEN SEIN? Nein. Im Kopf von workspace-hilfe.js steht: "EIN GESCHLOSSENER FALL IST GESCHLOSSEN. Auch fuer die Leitung -- sonst ist 'zugemacht' eine Meinung und keine Tatsache." Das ist eine gute Regel, und sie bleibt. Nachgemessen, dass sie auch traegt: Ein Melder kann seinen geschlossenen Fall weiter LESEN, und ein geschlossener Fall zaehlt nicht gegen das Limit von drei offenen. Wer eine Wiederholung melden will, kann das also jederzeit -- die Bauweise ist stimmig. NUR WUSSTE DAS NIEMAND. Dort stand ein Satz: "Dieser Fall ist abgeschlossen (Datum)." Jetzt stehen drei -- und sie beantworten die drei Fragen, die man in dem Moment hat: Kann ich noch schreiben? Nein, und er laesst sich nicht oeffnen. Bleibt das hier stehen? Bis zum TT.MM.JJJJ, dann geloescht. Und wenn es wieder passiert? Neu melden, der alte zaehlt nicht mit. Das Datum kommt vom Server (`lesbar_bis`), die Frist ebenso -- sie steht nur an EINER Stelle. Und sie steht jetzt auch im Dialog BEIM Schliessen: Wer eine Uhr startet, soll das vorher wissen, nicht danach. OHNE UHRZEIT, und das ist kein Schoenheitsgrund: Ein Fall, der am 20.09. um 14:04 (Sommerzeit) geschlossen wird, verfaellt 90 Tage spaeter um 13:04 -- die Uhr wird dazwischen zurueckgestellt. Richtig gerechnet, sieht aus wie ein Fehler. Wer eine Stunde sucht, die es nicht gibt, hat Zeit verloren. UND DIE URSACHE, DAMIT ES NICHT WIEDER PASSIERT: hilfe_faelle kam am 19.09. dazu, das Loeschkonzept ist vom 15.09., und nichts hat die beiden je verglichen. Neue Pruefung in pruef-aufbewahrung: Jede Tabelle mit einer Spalte, die "hier ist etwas zu Ende" sagt, MUSS im Konzept stehen. Kein "jede Tabelle muss drinstehen": 46 Tabellen, 41 mit Personenbezug -- das gaebe 38 Meldungen, von denen fast alle falsch waeren (sie sind ueber personen_geloescht gedeckt). Eine Pruefung, die 38-mal meldet, wo einmal richtig waere, wird abgeschaltet. Gesucht wird das schmale Merkmal: GENAU ZWEI Tabellen im Haus tragen so eine Spalte. Beide jetzt im Konzept -- hilfe_faelle mit Frist, aufgaben ausdruecklich OHNE (erledigte Aufgaben sind Arbeitsdokumentation, keine Meldung ueber einen Menschen). pruef-hilfe 84/0 (war 59) -- darunter zehn neue am Bildschirm: "drei Saetze statt einem", "sie stehen untereinander, nicht nebeneinander" (ein

in einem

waere ungueltig, der Container ist jetzt ein

), "und WANN, mit Datum". pruef-aufbewahrung 45/0 (war 41), mit Gegenprobe. --- server/pruef-aufbewahrung.mjs | 99 ++++++++++++++++ server/pruef-hilfe.mjs | 194 ++++++++++++++++++++++++++++++- server/workspace-aufbewahrung.js | 92 +++++++++++++++ server/workspace-hilfe.js | 87 +++++++------- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/hilfe.css | 16 +++ workspace/assets/js/hilfe.js | 80 ++++++++++++- workspace/aufgaben.html | 30 ++--- workspace/automation.html | 30 ++--- workspace/befinden.html | 28 ++--- workspace/bereich.html | 40 +++---- workspace/bewerben.html | 34 +++--- workspace/bewerbungen.html | 32 ++--- workspace/calls.html | 32 ++--- workspace/chat.html | 34 +++--- workspace/content.html | 34 +++--- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 32 ++--- workspace/entwicklung.html | 28 ++--- workspace/hilfe.html | 33 +++--- workspace/index.html | 10 +- workspace/kalender.html | 32 ++--- workspace/leistung.html | 30 ++--- workspace/personen.html | 30 ++--- workspace/profil.html | 34 +++--- workspace/rechte.html | 28 ++--- workspace/report.html | 32 ++--- workspace/scouting.html | 30 ++--- workspace/start.html | 28 ++--- workspace/startcheck.html | 32 ++--- workspace/steckbrief.html | 34 +++--- workspace/talente.html | 30 ++--- workspace/team.html | 30 ++--- workspace/teamlage.html | 28 ++--- workspace/teilen.html | 26 ++--- workspace/treff-moderation.html | 28 ++--- workspace/treff-regeln.html | 30 ++--- workspace/uebersicht.html | 30 ++--- workspace/unsere-seiten.html | 26 ++--- workspace/werdegang.html | 30 ++--- workspace/wissen.html | 32 ++--- 43 files changed, 1039 insertions(+), 564 deletions(-) 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`. --> - - + + - +