Files
dogfather-universe/server/workspace-aufbewahrung.js
T
DogFatherGitandClaude Opus 5 150555bc33 Der Notizblock -- und eine Pruefung, die zwoelf Kacheln nie angesehen hat
Filipe: „fuer die modis, rechte und linke hand und dogfather eine
kachel hinzufuegst, sie soll: Notizen, heissen. ich will dass du das
auch wie ein notizblock erstellst. ich will dass es uebelst geil und
einzigartig ist."

=================================================================
TEIL 1: DIE FARBE -- UND WAS DABEI AUFFIEL
=================================================================

Fuer die neue Kachel braucht es einen Ton. Beim Suchen fiel auf, dass
tools/kachelton-entzerren.mjs zwoelf der 45 Farben fuer FREI hielt.

Sie sind es nicht. bereicheFuer() gibt fuer die fuenf Rollen des
Agenturhauses null zurueck -- das heisst „nimm die Liste aus dem
Browser", und die steht in workspace/assets/js/bereiche.js. Dort
stehen die Toene 1 bis 22 und 44: Dashboard, Zahlen, Team-Lage,
Scout-Pipeline. Kacheln, die jeden Tag jemand ansieht.

pruef-kachelfarben hatte denselben blinden Fleck. Sie meldete seit
Wochen „33 benutzte Toene" und war gruen. Was sie dadurch NICHT sah:

  * Der Farblauf von gestern Nacht hat zwoelf dieser Kacheln
    verschoben und drei davon unter die Buntheitsgrenze gedrueckt.
    Gruen geblieben.
  * Ton 7, 16 und 22 lagen SEIT JEHER unter der Grenze (0,112 / 0,116
    / 0,068). Nie gemeldet.

Das ist die Sorte gruener Haken, vor der die Hausregel warnt: Er sagt
nur, dass die Bedingung erfuellt war -- nicht, dass sie das Richtige
angesehen hat.

BEHOBEN, und zwar an der Wurzel: Die REGELN (Kontrast, Buntheit)
gelten jetzt fuer jeden Ton, der irgendwo an einer Kachel steht --
gelesen aus denselben fuenf Dateien, die auch pruef-kachel-universum
liest. Dazu die Namen der Kacheln, damit „Ton 1 traegt Dashboard"
ueberhaupt pruefbar ist.

DIE PALETTE WURDE NEU GERECHNET, mit dem dritten Verfahren an einem
Tag -- die ersten zwei stehen als Fehler im Kopf des Werkzeugs:

  1. Alle 45 neu verteilt. Lief ueber zwei ausdrueckliche Wuensche
     hinweg (#ff1a1a, #a8d8ff).
  2. Nur die Kollisionen, aber immer nur EINEN der beiden bewegt.
     Ergebnis: Ton 1 sprang vom Tuerkis ins Altrosa, 0,197 weit.
  3. BEIDE duerfen sich bewegen, und zwar beide nur ein bisschen.
     Zwei Toene, die 0,02 auseinanderstehen und 0,09 brauchen, teilen
     sich das -- jeder rueckt 0,045, und beide bleiben, was sie waren.

  kleinster Abstand   0,0154  ->  0,0905
  zu blass                 4  ->  0
  sichtbar veraendert           7 Kacheln (ueber 0,05)
  kaum zu sehen                28 (13 zwischen 0,02 und 0,05, 15 darunter)

EINE AUSNAHME MIT ZAHL, keine mit Achselzucken: Ton 1 (Dashboard)
bleibt acht Tausendstel unter der Buntheitsgrenze. Gemessen ueber ALLE
16,7 Millionen sRGB-Farben ist die naechste, die alle Regeln haelt,
#7a75c8 -- ein Blauviolett, 0,113 entfernt. Tuerkis erreicht in sRGB
schlicht keine hoehere Buntheit, und das schmale Band teilen sich
schon sieben Toene. Eine Kachel, die ihre Farbfamilie behaelt, ist die
bessere Antwort auf „richtig geil und speziell" als eine, die acht
Tausendstel bunter ist und niemand wiedererkennt. blassBis sagt jetzt
bei jeder Ausnahme, WIE WEIT sie reicht -- vorher hiess blassErlaubt
schlicht „hier wird weggesehen".

DIE NEUE FARBE IST GRUEN UND WOLLTE BERNSTEIN SEIN. Gesucht war die
Farbe von Papier. Gemessen gibt es sie nicht mehr: Der beste Bernstein
im ganzen Farbraum haelt 0,0799 Abstand -- unter der Hausgrenze. Und
Platz schaffen hilft nicht: Setzt man ihn fest, muss ein warmer Ton
das Band verlassen (Ton 45 waere 0,212 weit ins Magenta gewandert).
Die groesste wirklich freie Luecke liegt im Gruen, bei 0,0917. Ein
linierter Block in Gruen ist Papier, seit es Papier gibt.

=================================================================
TEIL 2: DER BLOCK
=================================================================

DREI ENTSCHEIDUNGEN, AUS DENEN DER REST FOLGT:

1. ES GIBT KEINEN SPEICHERN-KNOPF. Nirgends. Wer einen Block
   aufschlaegt und lostippt, drueckt hinterher nicht auf „sichern";
   er klappt ihn zu. Gesichert wird 800 ms nach dem letzten
   Tastendruck. Geht das nicht, bleibt der Text im Browser liegen und
   wird nachgereicht -- und der Fuss sagt ehrlich „noch nicht
   gesichert", statt einen Verlust zu melden, den man gerade nicht
   verhindern kann.

2. DAS BLATT IST EIN SCHREIBFELD MIT EINER DECKSCHICHT. Das Feld
   traegt Text und Cursor und ist unsichtbar; darueber zeichnet eine
   zweite Schicht denselben Text noch einmal -- mit Kaestchen,
   Ueberschriften, Strichen und Links.

   DARAUS FOLGT EINE EISERNE REGEL, und sie steht dreimal im Code:
   KEINE Auszeichnung darf die BREITE eines Zeichens aendern. Kein
   Fettdruck, keine andere Groesse, keine Sperrung. Erlaubt sind nur
   Farbe, Flaeche, Rahmen und Durchstreichen. Ein einziges
   font-weight: 700 verschoebe den Umbruch, und ab der zweiten Zeile
   stuende der sichtbare Text neben dem Cursor.

   pruef-notizen misst das am echten Umbruch: eine Probe mit allen
   Auszeichnungen und einer Zeile, die umbrechen MUSS. Sind beide
   Schichten verschieden hoch, sitzt der Umbruch woanders. Mit
   Gegenprobe -- Fettdruck auf der Deckschicht bricht die Deckung um
   genau eine Zeile (30 px), und die Zeile wird rot.

3. DIE TASTATUR FUEHRT DIE LISTE FORT, NICHT EINE LEISTE. Ein
   Spiegelstrich und Enter macht den naechsten Punkt, ein leeres
   Kaestchen und Enter das naechste Kaestchen -- und ein GESETZTER
   Haken wird dabei nicht mitgenommen, die naechste Aufgabe ist ja
   noch nicht erledigt. Zweimal Enter beendet die Liste. Ein Klick
   aufs Kaestchen hakt ab. Es gibt keine Werkzeugleiste, weil man beim
   Schreiben nie den Stift wechselt.

UND: NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN. Auch DogFather nicht.
Es gibt in workspace-notizen.js keinen siehtAlles()-Zweig, keinen
Umschalter, keine fremde Liste -- jede Abfrage hat person_id = ? fest
eingebaut. Die Kachel steht deshalb in „Fuer dich" und nicht in
„Taeglich": Die Gruppe sagt die wichtigste Eigenschaft, bevor man sie
anfasst.

NUR AUF DER TEAM-ADRESSE. Auf der Agenturadresse ist die Seite ein
404 -- auch fuer DogFather. Das ist dieselbe Trennung wie bei Chat,
Kalender und Aufgaben, und sie steht an zwei Stellen: in
GEHOERT_ZU_ADRESSE (die Datei) und in der Schnittstelle (die Daten).
Die Seite ist nur HTML; die Daten sind die Sache.

Dazu: sechs Papierfarben, Anheften, Suche mit Hervorhebung im Text,
Papierkorb mit dreissig Tagen und einem Eintrag in der
Aufbewahrungsliste (ohne den waere die Frist ein Satz in einem
Kommentar -- genau das ist dem Support am 24.09. passiert).

=================================================================
EIN FUND BEIM BAUEN, DER ALLEN GEHOERT
=================================================================

DELETE /workspace/api/notizen/:id gab es schon -- in
workspace-aufgaben.js, fuer die Notizen AN einer Aufgabe. Weil jener
Router frueher eingehaengt ist, hat er jeden Loeschversuch des Blocks
abgefangen und mit 404 beantwortet.

Von aussen sah das aus wie ein Fehler im neuen Modul: Anlegen ging,
Aendern ging, Loeschen nicht. Man sucht dann im eigenen Code, und dort
ist nichts. Express meldet so etwas nicht -- es nimmt die erste Route,
die passt, und schweigt ueber die zweite.

Der Block liegt jetzt unter /workspace/api/notizblock. Und
pruef-notizen geht seitdem den Routenbaum von express durch und meldet
jedes Paar aus Methode und Pfad, das zweimal vergeben ist. Gemessen:
307 Wege, keine Dublette. Die Pruefung gilt fuers ganze Haus, auch
wenn sie in dieser Datei steht -- hier ist sie gefunden worden.

Nebenbei berichtigt: pruef-crew-adresse verlangte von jedem Eintrag
der Haustafel HTTP 200 mit Inhalt. Das stimmte, solange dort nur
oeffentliche Dateien standen. notizen.html ist die erste Seite hinter
der Anmeldung -- sie antwortet mit 302, und das ist richtig. Gefragt
wird jetzt, was die Tafel wirklich meint: GIBT es das hier.

GEMESSEN, alles nach den Aenderungen:

  pruef-notizen             76 Punkte, 0 Fehler (neu)
  pruef-kachelfarben        31 Punkte, 0 Fehler (vorher 26)
  pruef-kachel-universum    13 Punkte, 0 Fehler
  pruef-crew-adresse       157 Punkte, 0 Fehler
  pruef-buehne             230 Punkte, 0 Fehler -- notizen.html neu in
                           der Liste, schlechtester Kontrast 6,73:1,
                           also 50 % ueber der Grenze
  pruef-rechtetafel, -haus-seiten, -struktur, -css-klassen,
  -jeder-hat-eine-seite, -workspace-seiten, -kachelraster,
  -rueckmeldung            alle 0 Fehler

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-26 12:42:59 +02:00

527 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
Das Löschkonzept — als Tabelle, die sich selbst durchsetzt (15.09.2026)
Stufe 8 aus [[Creator Workspace – Plan zur Perfektion Stufe 2]]:
„Löschkonzept je Datenart · IP-Adressen im Protokoll nach 30 Tagen
weg. Je mehr Daten, desto teurer das Nachholen."
---------------------------------------------------------------------
WARUM DAS KEIN DOKUMENT IST
Ein Löschkonzept als Notiz ist nach dem ersten neuen Feld falsch, und
niemand merkt es. Diese Datei ist deshalb beides: die Übersicht, was
wie lange aufgehoben wird — UND das Programm, das es tut. Was hier
nicht steht, wird nicht geräumt; was hier steht, wird geräumt.
---------------------------------------------------------------------
WAS GEMESSEN WURDE, BEVOR HIER ETWAS STAND
42 Tabellen durchgesehen, drei halten IP-Adressen:
versuche.ip wird beim Anmelden selbst im Fenster geräumt (ok)
sitzungen.ip wird beim Abmelden geloescht -- ABGELAUFENE
Sitzungen aber nie (Luecke)
protokoll.ip wird nirgends geraeumt, waechst ewig (Luecke)
Zwei echte Luecken also, nicht fuenf vermutete.
---------------------------------------------------------------------
DREI SORTEN EINTRAG, UND DIE DRITTE IST DIE WICHTIGSTE
raeumen Diese Datei loescht selbst.
fremd Eine andere Stelle loescht -- hier wird nur GEZAEHLT.
Bricht die andere Stelle, steht die Zahl trotzdem da.
bleibt Wird absichtlich aufgehoben, mit Begruendung.
Ein Loeschkonzept, das nur Loeschungen auffuehrt, ist ein halbes:
Die begruendete Aufbewahrung ist der Teil, nach dem gefragt wird.
===================================================================== */
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 { AUFBEWAHRUNG_TAGE as SUPPORT_TAGE } from "./support-tabellen.js";
import { supportAufraeumen } from "./workspace-support.js";
import { notizenAufraeumen } from "./workspace-notizen.js";
import { PAPIERKORB_TAGE as NOTIZ_TAGE } from "./notiz-tabellen.js";
import { ZAEHLUNG_TAGE } from "./unterstuetzung-tabellen.js";
import { unterstuetzungAufraeumen } from "./workspace-unterstuetzung.js";
import {
db, sitzungLesen, istDogFather, protokolliere, echteIp,
einstellung, einstellungSetzen,
} from "./workspace.js";
export const aufbewahrungRouter = express.Router();
/** Auf welcher Grundlage die Daten eines Menschen verarbeitet werden.
*
* ABGELEITET, NICHT GESPEICHERT. Ein Modi arbeitet hier — das ist ein
* Vertragsverhaeltnis. Ein Mitglied der Community ist freiwillig da —
* das ist eine Einwilligung. Die Grundlage folgt damit aus der Rolle,
* und was sich ableiten laesst, bekommt keine eigene Spalte: Sonst
* gaebe es zwei Wahrheiten, von denen eine veraltet, sobald jemand
* die Rolle wechselt.
*
* Gebraucht wird sie an zwei Stellen: in der Auskunft (Art. 15 Abs. 1
* lit. a — der Mensch darf den Zweck erfahren) und im Loeschkonzept. */
export function grundlageVon(rolle) {
if (rolle === "gast") {
return {
grundlage: "Art. 6 Abs. 1 lit. a DSGVO – Einwilligung",
warum: "Du bist freiwillig im Rudel. Du kannst jederzeit gehen, und deine "
+ "Daten gehen mit.",
};
}
return {
grundlage: "Art. 6 Abs. 1 lit. b DSGVO – zur Durchführung des Vertrags",
warum: "Du arbeitest hier. Ohne diese Daten lässt sich die Zusammenarbeit "
+ "nicht durchführen.",
};
}
/** Wie lange IP-Adressen im Protokoll stehen bleiben.
*
* 30 Tage: lang genug, um einen Missbrauch nachzuvollziehen, den man
* erst spaeter bemerkt -- kurz genug, dass daraus kein Bewegungsbild
* wird. Dieselbe Frist, die Filipe im Plan genannt hat. */
export const IP_TAGE = 30;
const vorTagen = (n) => new Date(Date.now() - n * 86400000).toISOString();
const jetzt = () => new Date().toISOString();
/** Zaehlt, ohne zu aendern. Ein Fehler hier ist kein Befund, sondern
* eine Zahl, die fehlt -- deshalb null statt 0, damit „konnte nicht
* nachsehen" nicht wie „nichts zu tun" aussieht. */
function zaehle(d, sql, ...werte) {
try { return d.prepare(sql).get(...werte)?.n ?? null; } catch { return null; }
}
/* =====================================================================
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",
art: "raeumen",
frist: `${IP_TAGE} Tage`,
zweck: "Einen Missbrauch nachvollziehen können, der erst später auffällt.",
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse",
wirkung: "Nur die IP wird geleert. Wer wann was getan hat, bleibt stehen – "
+ "das Protokoll ist die Sicherung gegen stille Änderungen.",
offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM protokoll WHERE ip IS NOT NULL AND zeitpunkt < ?", vorTagen(IP_TAGE)),
raeumen: (d) => d.prepare(
"UPDATE protokoll SET ip = NULL WHERE ip IS NOT NULL AND zeitpunkt < ?")
.run(vorTagen(IP_TAGE)).changes,
},
{
schluessel: "sitzungen_abgelaufen",
was: "Abgelaufene Anmeldungen (mit IP und Browser)",
art: "raeumen",
frist: "sofort nach Ablauf",
zweck: "Angemeldet bleiben, solange die Sitzung gilt.",
grundlage: "Art. 6 Abs. 1 lit. b DSGVO – zur Durchführung erforderlich",
wirkung: "Die Zeile verschwindet ganz. Nach Ablauf ist sie zu nichts mehr nütze – "
+ "sie stand bisher nur da und trug eine IP mit sich.",
offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM sitzungen WHERE gueltig_bis < ?", jetzt()),
raeumen: (d) => d.prepare("DELETE FROM sitzungen WHERE gueltig_bis < ?")
.run(jetzt()).changes,
},
{
schluessel: "versuche",
was: "Fehlversuche beim Anmelden (IP)",
art: "fremd",
frist: "10 Minuten",
zweck: "Ausprobieren von Codes bremsen.",
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse",
wirkung: "Wird beim Anmelden selbst geräumt (workspace.js). Hier steht die Zahl "
+ "trotzdem – bricht die andere Stelle, sieht man es hier.",
offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM versuche WHERE zeitpunkt < ?",
new Date(Date.now() - 60 * 60000).toISOString()),
},
{
schluessel: "protokoll_eintraege",
was: "Die Protokolleinträge selbst (ohne IP)",
art: "bleibt",
frist: "unbefristet",
zweck: "Nachvollziehen, wer wann einen Zugang angelegt, gesperrt oder gelöscht hat.",
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse an Nachvollziehbarkeit",
wirkung: "Bleibt. Ein Protokoll, das sich selbst löscht, sichert nichts.",
offen: (d) => zaehle(d, "SELECT COUNT(*) AS n FROM protokoll"),
},
{
schluessel: "treff_massnahmen",
was: "Maßnahmen im Rudel (Hinweis, Pause, Ausschluss) mit Begründung",
art: "bleibt",
frist: "solange die Person ein Konto hat",
zweck: "Eine Entscheidung über einen Menschen muss begründet nachlesbar sein – "
+ "das schützt beide Seiten.",
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse",
wirkung: "Bleibt, solange es den Zugang gibt. Wird der Zugang gelöscht, geht die "
+ "Maßnahme mit (ON DELETE CASCADE).",
offen: (d) => zaehle(d, "SELECT COUNT(*) AS n FROM treff_massnahmen"),
},
{
schluessel: "alter_bestaetigt",
was: "Der Zeitpunkt der Altersbestätigung (Community)",
art: "bleibt",
frist: "solange der Zugang besteht",
zweck: "Nachweisen können, dass das Mindestalter bestätigt wurde – "
+ "eine Regel, die man nicht belegen kann, hat man nicht.",
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse an Nachweisbarkeit",
wirkung: "Gespeichert wird NUR der Zeitpunkt, kein Geburtsdatum. Für die Frage "
+ "„hat bestätigt, und wann?“ reicht das – ein Geburtstag wären mehr Daten "
+ "für dieselbe Auskunft.",
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"),
},
/* ==== ZWEI EINTRÄGE, DIE HIER GEFEHLT HABEN (24.09.2026) ========
Beim Einbau des Unterstützen-Bereichs ist aufgefallen, dass
`supportAufraeumen()` in workspace-support.js zwar EXPORTIERT,
aber nirgends AUFGERUFEN wird — gesucht im ganzen Verzeichnis,
null Treffer. Die Meldungen samt Bildschirmfotos wären also für
immer liegen geblieben, obwohl im Kopf der Datei "90 Tage" steht.
Das ist dieselbe Sorte Fehler wie eine Prüfung, die nie läuft:
Es sieht fertig aus, es ist dokumentiert, und es passiert nichts.
Diese Liste ist die einzige Stelle im Haus, die wirklich räumt —
wer eine Frist verspricht und hier keinen Eintrag hat, verspricht
nichts. */
{
schluessel: "support_erledigt",
was: "Erledigte Support-Meldungen samt Bildschirmfoto",
art: "raeumen",
frist: `${SUPPORT_TAGE} Tage nach dem Erledigen`,
zweck: "Ein Fehler kommt oft wieder – dann hilft es, nachlesen zu können, "
+ "was beim letzten Mal daran war.",
grundlage: "Art. 5 Abs. 1 lit. e DSGVO – Speicherbegrenzung",
wirkung: `Die Meldung verschwindet ganz, mitsamt dem Bild auf der Platte. `
+ `Offene Meldungen werden nie geräumt – nur erledigte, und erst `
+ `${SUPPORT_TAGE} Tage danach.`,
offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM support_meldungen WHERE stand = 'erledigt'"
+ " AND erledigt_am IS NOT NULL AND erledigt_am < ?", vorTagen(SUPPORT_TAGE)),
/* ÜBER supportAufraeumen() UND NICHT ÜBER EIN DELETE HIER: Zu
jeder Meldung gehört eine Bilddatei auf der Platte. Ein DELETE
an dieser Stelle würde die Zeile löschen und das Bild liegen
lassen — und ein Bild ohne Zeile findet danach niemand mehr. */
raeumen: () => supportAufraeumen(),
},
/* ==== DER PAPIERKORB DES NOTIZBLOCKS (26.09.2026) ==============
Der Eintrag entsteht ZUSAMMEN mit der Frist und nicht danach.
Am 24.09. ist genau das schiefgegangen: `supportAufraeumen()`
war geschrieben, exportiert und dokumentiert -- und wurde
nirgends aufgerufen. Im Kopf der Datei stand „90 Tage", und
geräumt wurde nie. */
{
schluessel: "notizen_papierkorb",
was: "Weggelegte Notizblätter",
art: "raeumen",
frist: `${NOTIZ_TAGE} Tage im Papierkorb`,
zweck: "Ein versehentlich weggetipptes Blatt zurückholen können.",
grundlage: "Art. 5 Abs. 1 lit. e DSGVO – Speicherbegrenzung",
wirkung: `Das Blatt verschwindet ganz. Was auf dem Block liegt, wird `
+ `nie geräumt – nur was jemand selbst weggelegt hat, und erst `
+ `${NOTIZ_TAGE} Tage danach.`,
offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM notizen WHERE weg_am IS NOT NULL AND weg_am < ?",
vorTagen(NOTIZ_TAGE)),
raeumen: () => notizenAufraeumen(),
},
{
schluessel: "unterstuetzung_striche",
was: "Tagesstriche der Unterstützen-Seite",
art: "raeumen",
frist: `${ZAEHLUNG_TAGE} Tage`,
zweck: "Sehen, ob ein Weg überhaupt benutzt wird – und ob ein neuer anläuft.",
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse",
wirkung: "Die Zeile verschwindet. Es steht ohnehin keine Person darin: "
+ "Die Tabelle hat gar keine Spalte dafür (siehe unterstuetzung-tabellen.js) – "
+ "gezählt wird je Weg und Tag, nicht je Mensch.",
offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM unterstuetzung_striche WHERE tag < ?",
vorTagen(ZAEHLUNG_TAGE).slice(0, 10)),
raeumen: () => unterstuetzungAufraeumen(),
},
{
/* AUFGABEN-ZUTEILUNG (nachgetragen 24.09.2026).
Die Tabelle gibt es seit dem 21.09., im Löschkonzept stand sie
nicht -- gefunden von pruef-aufbewahrung, die jede Tabelle mit
einer "erledigt_am"-Spalte hier einfordert. Die Prüfung meldete
sie zusammen mit support_meldungen, und beide waren echte
Lücken: Für jede Tabelle muss JEMAND entschieden haben, ob sie
verschwindet oder bleibt. "Noch nicht bedacht" ist keine der
beiden Antworten.
DIE ANTWORT HIER IST "BLEIBT", und sie ist begründet: Wer welche
Aufgabe übernommen, abgelehnt oder erledigt hat, ist der
Werdegang eines Menschen im Team -- dieselbe Überlegung wie bei
den Aufgaben selbst eine Zeile darüber. Sie verschwindet mit
ihrer Aufgabe (ON DELETE CASCADE) und mit der Person
(ON DELETE CASCADE), aber nicht nach einer Frist. */
schluessel: "aufgaben_zuteilung",
was: "Wer eine Aufgabe übernommen hat",
art: "bleibt",
frist: "gehen mit der Aufgabe und mit der Person",
zweck: "Nachvollziehen, wer was übernommen hat – und was daraus wurde.",
grundlage: "Art. 6 Abs. 1 lit. f DSGVO – berechtigtes Interesse",
wirkung: "Bleiben. Wird die Aufgabe gelöscht, gehen sie mit (ON DELETE CASCADE); "
+ "wird der Zugang gelöscht, ebenso. Eine eigene Frist gibt es nicht – "
+ "eine Zuteilung ohne ihre Aufgabe wäre ohnehin nicht mehr lesbar.",
offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM aufgaben_zuteilung WHERE erledigt_am IS NOT NULL"),
},
{
schluessel: "personen_geloescht",
was: "Daten einer gelöschten Person",
art: "bleibt",
frist: "gehen mit dem Zugang",
zweck: "—",
grundlage: "Art. 17 DSGVO – Recht auf Löschung",
wirkung: "Wer in „Personen & Zugänge“ gelöscht wird, nimmt Sitzungen, Maßnahmen, "
+ "Entwicklungsstände und Chat-Zugehörigkeit mit – die Datenbank erzwingt das "
+ "über ON DELETE CASCADE, nicht eine Liste, die jemand pflegt.",
offen: (d) => zaehle(d, "SELECT COUNT(*) AS n FROM personen WHERE aktiv = 0"),
},
];
/* =====================================================================
DER LAUF
===================================================================== */
/** Räumt alles, was zu räumen ist.
*
* @returns {{getan: Array, fehler: Array}} was passiert ist
*/
export function aufbewahrungLauf() {
const d = db();
const getan = [];
const fehler = [];
for (const f of FRISTEN) {
if (typeof f.raeumen !== "function") continue;
try {
const anzahl = f.raeumen(d);
if (anzahl) getan.push({ schluessel: f.schluessel, anzahl });
} catch (fe) {
/* EIN FEHLSCHLAG DARF DEN REST NICHT AUFHALTEN -- und er darf
nicht still bleiben. Waere er still, hiesse „aufgeraeumt"
irgendwann „einmal probiert". */
fehler.push({ schluessel: f.schluessel, grund: String(fe?.message || fe).slice(0, 200) });
console.error("[aufbewahrung]", f.schluessel, fe?.message);
}
}
try {
einstellungSetzen("aufbewahrung_letzte",
JSON.stringify({ zeitpunkt: jetzt(), getan, fehler }));
} catch { /* der Lauf zaehlt, nicht sein Vermerk */ }
/* Nur protokollieren, wenn wirklich etwas passiert ist. Ein taeglicher
Eintrag „nichts zu tun" waere genau die Art Rauschen, die das
Protokoll unlesbar macht -- und ausgerechnet dieses Protokoll ist
die Sicherung gegen stille Aenderungen. */
if (getan.length || fehler.length) {
try {
protokolliere("aufbewahrung", {
detail: getan.map((g) => `${g.schluessel}: ${g.anzahl}`).join(", ")
+ (fehler.length ? ` | FEHLER: ${fehler.map((f) => f.schluessel).join(", ")}` : ""),
});
} catch { /* egal */ }
}
return { getan, fehler };
}
/** Einmal am Tag, und einmal kurz nach dem Start.
*
* Nicht im Startmoment: Erst soll die Website antworten. Dieselbe
* Ueberlegung wie bei der Sicherung -- ein Aufraeumlauf im
* Startmoment wuerde die Datenbank nur unnoetig frueh oeffnen und
* einen Fehler dort zum Startfehler machen. */
export function aufbewahrungStarten() {
const ersterLauf = setTimeout(() => { try { aufbewahrungLauf(); } catch { /* egal */ } }, 90_000);
const takt = setInterval(() => { try { aufbewahrungLauf(); } catch { /* egal */ } }, 24 * 3600_000);
ersterLauf.unref?.();
takt.unref?.();
}
/* =====================================================================
DIE TAFEL ZUM ANSEHEN
===================================================================== */
/** Kommt die Anfrage von unserer eigenen Seite?
*
* DERSELBE WORTLAUT WIE IN DEN ANDEREN MODULEN und bewusst keine
* eigene Erfindung: Eine zweite, leicht andere Fassung waere eine
* zweite Wahrheit, und beim naechsten Nachschaerfen wuerde genau die
* eine vergessen.
*
* KEIN `origin` HEISST DURCHLASSEN, und das ist Absicht: Browser
* schicken das Feld bei schreibenden Anfragen immer mit. Fehlt es,
* kommt die Anfrage aus einem Werkzeug (curl, eine Pruefung) -- und
* dort ist die Sitzung ohnehin nur da, wenn jemand sie absichtlich
* mitgibt. Der Angriff, um den es geht, braucht einen Browser. */
function gleicheHerkunft(req, res, next) {
const herkunft = req.get("origin");
if (!herkunft) return next();
let erlaubt;
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
next();
}
function nurDogFather(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
/* 404 und nicht 403: Wer „kein Zugriff" liest, weiss, dass es die
Seite gibt. Dieselbe Antwort wie ueberall im Haus. */
if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
req.person = person;
next();
}
aufbewahrungRouter.get("/workspace/api/aufbewahrung", nurDogFather, (req, res) => {
try {
const d = db();
let letzte = null;
try { letzte = JSON.parse(einstellung("aufbewahrung_letzte") || "null"); } catch { letzte = null; }
const zeilen = FRISTEN.map((f) => ({
schluessel: f.schluessel,
was: f.was,
art: f.art,
frist: f.frist,
zweck: f.zweck,
grundlage: f.grundlage,
wirkung: f.wirkung,
/* `offen` heisst bei „raeumen": so viele warten. Bei „bleibt":
so viele liegen da. Zwei verschiedene Bedeutungen unter einem
Namen waeren eine Falle -- deshalb steht `art` daneben und die
Oberflaeche beschriftet die Zahl danach. */
anzahl: f.offen ? f.offen(d) : null,
}));
res.json({ fristen: zeilen, letzte, ip_tage: IP_TAGE });
} catch (fehler) {
console.error("[aufbewahrung] Tafel:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Von Hand anstossen.
*
* Damit es kein schwarzer Kasten ist: Wer sehen will, ob es wirkt,
* drueckt drauf und sieht die Zahlen fallen. */
/* DER RIEGEL STEHT VOR DER RECHTEPRUEFUNG.
Reihenfolge wie im ganzen Haus: erst "kommt das von uns?", dann
"darf der das?". Andersherum waere es nicht falsch, aber die
Abweichung faellt beim Lesen auf und kostet jedes Mal einen
Moment. */
aufbewahrungRouter.post("/workspace/api/aufbewahrung/jetzt", gleicheHerkunft, nurDogFather, (req, res) => {
try {
const ergebnis = aufbewahrungLauf();
protokolliere("aufbewahrung_von_hand", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: ergebnis.getan.map((g) => `${g.schluessel}: ${g.anzahl}`).join(", ") || "nichts zu tun",
});
res.json(ergebnis);
} catch (fehler) {
console.error("[aufbewahrung] Lauf:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});