=== 1. UNTERSTUETZEN (neue Kachel im Community-Bereich) ===
Filipe: "ich brauch auch noch eine kachel im community bereich. wo mein
paypal und meine amazon liste sein wird. wo die leute alle supporten
koennen auf andere art anstatt nur tiktok. jeder soll diese kachel sehen
aber nur dogfather soll sie veraendern koennen oder vieles mehr sehen."
Neu: unterstuetzen.html + css + js, server/workspace-unterstuetzung.js,
server/unterstuetzung-tabellen.js. Ton 45 (#7567fe) ist gerechnet, das
Kachelzeichen ist ein Herz ueber zwei offenen Haenden.
DIE WEGE STEHEN IN DER DATENBANK, nicht im Quelltext -- ein Recht, das
man nur ueber einen Entwickler ausueben kann, ist keines. DogFather
schreibt Titel, Text, Knopf und Adresse selbst, blendet Wege aus und
nimmt neue dazu.
DER PAYPAL-LINK STEHT LEER UND UNSICHTBAR DA. Filipe schrieb "mein
paypal kennst du ja schon" -- gesucht im ganzen Haus und im Vault,
nirgends gefunden. Eine Zahlungsadresse zu RATEN waere der
gefaehrlichste Fehler dieser Seite: Geld an einen Fremden, und niemand
merkt es. Also steht dort nichts, der Weg ist ausgeblendet, und die
Seite sagt genau EINER Person, dass er fehlt.
DREI ARTEN, UND DIE DRITTE IST DIE WICHTIGSTE: geld, geschenk, frei.
"Kostet nichts" bekommt dieselbe Kartenform und dieselbe Groesse.
Waeren die freien Wege kleiner oder weiter unten, waere die Aussage
"das ist zweite Wahl" -- und die Mehrheit derer, die hier lesen, waere
damit zweite Wahl.
DIE ZAEHLUNG IST ANONYM, UND ZWAR VON DER TABELLE HER. DogFather sieht,
wie oft ein Weg geoeffnet wurde (7/30 Tage/gesamt). Was er NICHT sieht,
ist WER -- weil es in unterstuetzung_striche keine Spalte dafuer gibt.
Geprueft wird das ueber PRAGMA table_info, nicht ueber eine Abfrage:
eine Abfrage liesse sich morgen erweitern, eine fehlende Spalte nicht.
Schreiben haengt an istDogFather, nicht an istLeitung -- die rechte Hand
fuehrt dieses Team mit und kommt trotzdem nicht an diesen Link. Jede
Adresse wird beim SCHREIBEN geprueft (nur https:// oder eine Seite
dieses Hauses); javascript:, data:, http:// und // werden abgewiesen.
Neu: server/pruef-unterstuetzung.mjs -- 70 Pruefungen, alle gruen.
Sie misst ueber die CREW-ADRESSE: Beim ersten Lauf waren vier Rollen
gruen und der Zuschauer rot, und es sah nach einem Rechtefehler aus.
Es war ein Messfehler -- ueber 127.0.0.1 landet man still im
Agenturhaus, und dort gibt es die Rolle "gast" gar nicht.
=== 2. DIE GESPRAECHSLISTE (screen1 + screen2) ===
Filipe: "ich will dass die komplette kachel viel krasser und geiler
aussieht. der hintergrund der kachel soll gleich bleiben."
Der Hintergrund ist unangetastet. Zwei echte Fehler auf seinem Bild:
- Bei "Das Rudel" stand das "@" rechts oben und die orange "6" eine
ZEILE TIEFER. Der Knopf ist ein Raster mit DREI Spalten und bekam
VIER Kinder -- das vierte fiel um. Ausgerechnet die wichtigste
Auskunft der Liste landete an der unauffaelligsten Stelle.
- Jedes Gespraech mit Profilbild haengte das Bild ZWEIMAL ein: ein
Block stand Zeichen fuer Zeichen doppelt da. Zu sehen war nichts,
gekostet hat es die doppelte Ladelast bei jedem Neuzeichnen.
Und eine tote Regel: `.chat-raum__knopf[data-an="ja"]` beschrieb die
Schiene am offenen Gespraech -- gesetzt wird aber `data-offen` am <li>.
Die Regel griff nie, und daneben stand eine zweite, blassere Fassung
derselben Sache. Jetzt steht alles einmal, und die Schiene ist da.
Dazu: Zeilen als Karten, 44px-Gesichter, ein Zaehler neben "Gespraeche",
Suchfeld als Pille mit Lupe, runder Farbfleck statt Quadrat, und der
leere Raum rechts bekommt eine Mitte statt eines Satzes in der Ecke.
=== 3. VIER FUNDE NEBENBEI ===
- supportAufraeumen() war exportiert und wurde NIRGENDS gerufen. Die
Meldungen samt Bildschirmfotos waeren fuer immer liegen geblieben,
obwohl "90 Tage" dokumentiert ist. Jetzt im Loeschkonzept.
- aufgaben_zuteilung fehlte ebenfalls im Loeschkonzept (seit 21.09.).
pruef-aufbewahrung ist damit wieder gruen.
- pruef-start-ansicht war rot, seit "Aufgaben" am 23.09. die silberne
Kachel bekam -- die ueberschreibt ihr --ton absichtlich. Die
Pruefung nimmt sie jetzt aus UND prueft die Ausnahme selbst.
- pruef-buehne meldete auf entwicklung.html 1,79:1 Kontrast bei "Wen
gehst du durch?" -- die Ueberschrift lag direkt auf dem Foto. Sie
steht jetzt auf einer deckenden Flaeche.
OFFEN: pruef-buehne meldet treff-regeln.html mal "0 von 40", mal "3 von
40" -- dieselbe Seite, verschiedene Antworten. Eine Pruefung, die
schwankt, ist schlimmer als eine rote. Nicht in diesem Zug behoben.
Gemessen: pruef-unterstuetzung 70, pruef-start-ansicht, pruef-buehne
(bis auf den Wackler), pruef-workspace-seiten, pruef-aufbewahrung 45,
pruef-rechtetafel 19, pruef-css-klassen, pruef-chatkachel 36,
pruef-chat-ausbau, pruef-entwicklung-kacheln 31 -- gruen.
Neu: server/mess-chat-liste.mjs (zeigt die Liste mit sieben Gespraechen
verschiedener Art, prueft nichts).
Co-Authored-By: Claude Opus 5 <[email protected]>
503 lines
23 KiB
JavaScript
503 lines
23 KiB
JavaScript
/* =====================================================================
|
||
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 { 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(),
|
||
},
|
||
{
|
||
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" });
|
||
}
|
||
});
|