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 <p> in einem <p> waere ungueltig, der Container
ist jetzt ein <div>), "und WANN, mit Datum".
pruef-aufbewahrung 45/0 (war 41), mit Gegenprobe.
This commit is contained in:
2026-09-20 14:10:38 +02:00
parent 9e9ed67c4a
commit 285038f400
43 changed files with 1039 additions and 564 deletions
+190 -4
View File
@@ -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
<p> in einem <p> 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");