Files
dogfather-universe/server/workspace-notizen.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

403 lines
16 KiB
JavaScript

/* =====================================================================
DER NOTIZBLOCK — die Wege (26.09.2026)
Die Regeln und ihre Begründung stehen im Kopf von
notiz-tabellen.js. Hier sind die Routen — und die vier Stellen, an
denen etwas schiefgehen kann:
1. EINE NOTIZ DARF NIE BEIM FALSCHEN LANDEN. Jede Abfrage hat
`person_id = ?` fest eingebaut. Es gibt keine Rolle, die mehr
sieht, und deshalb auch keinen Zweig, der die Bedingung
weglassen könnte. `blattFuer()` gibt den Datensatz zurück oder
`null` — nie `true`/`false`. So kann keine Route erst „darf
sehen" beantworten und danach trotzdem das falsche Blatt laden.
2. NUR AUF DER TEAM-ADRESSE. Der Block gehört zum Haus von Team
Dogi. Auf der Agenturadresse ist er ein 404 — auch für
DogFather, der in beiden Häusern arbeitet. Das ist dieselbe
Trennung wie bei Chat, Kalender und Aufgaben, und sie wird hier
geprüft und nicht nur in der Rechtetafel: Die Tafel beantwortet,
wer die SEITE öffnen darf, nicht von welcher Adresse aus.
3. GESPEICHERT WIRD OHNE KNOPF. Die Oberfläche schickt beim Tippen
alle 800 ms. Das ist viel Verkehr für wenig Inhalt — deshalb
antwortet PATCH knapp (nur Zeitstempel), und es gibt eine
Schreibbremse, die ein hängendes Skript stoppt, ohne einen
schnell Tippenden zu treffen.
4. ENDGÜLTIG IST ENDGÜLTIG. Löschen legt ins Papierkorbfach;
erst das zweite Löschen entfernt wirklich, und nur von dort.
Ein direkter Weg von „auf dem Block" zu „weg" existiert nicht.
===================================================================== */
import express from "express";
import { db, sitzungLesen, protokolliere, echteIp } from "./workspace.js";
import { PAPIERKORB_TAGE, TEXT_MAX, BLAETTER_MAX, FARBEN, FARB_NAMEN }
from "./notiz-tabellen.js";
export const notizenRouter = express.Router();
/** Wer einen Block hat.
*
* Genau die vier Rollen aus Filipes Satz. Die Liste steht hier UND
* in rechte.js, und das ist kein Versehen: Die Tafel bewacht die
* Seite, diese Menge die Daten. Wer nur die Tafel prüfte, hätte eine
* Schnittstelle, die jeder Angemeldete aufrufen kann — die Seite ist
* nur HTML, die Daten sind die Sache.
*
* pruef-notizen hält beide gegeneinander und wird rot, wenn sie
* auseinanderlaufen. */
const MIT_BLOCK = new Set(["admin", "hand", "linke", "modi"]);
const jetzt = () => new Date().toISOString();
/* ---------- Schranken --------------------------------------------------- */
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
next();
}
/** Team-Adresse und passende Rolle — sonst gibt es diese Seite nicht.
*
* 404 UND NICHT 403: Ein „verboten" verrät, dass es hier etwas gibt.
* Auf der Agenturadresse soll der Block nicht einmal als verschlossene
* Tür sichtbar sein. */
function nurTeamDogi(req, res, next) {
if (req.person?.haus !== "crew") return res.status(404).json({ fehler: "nicht_gefunden" });
if (!MIT_BLOCK.has(req.person?.rolle)) return res.status(404).json({ fehler: "nicht_gefunden" });
next();
}
/* Schutz gegen Anfragen von fremden Seiten -- wortgleich zu den
übrigen Modulen. Fehlt der Kopf ganz (curl), ist es kein
Browser-Angriff über eine fremde Seite; dann zählt allein das
Cookie. */
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();
}
/* ==== DIE SCHREIBBREMSE ================================================
Automatisches Speichern heißt: Bei jedem Tippen kann eine Anfrage
kommen. Die Oberfläche wartet 800 ms Ruhe ab, ein schneller
Schreiber löst also höchstens rund 75 Anfragen je Minute aus, und
das auch nur, wenn er die ganze Minute durchtippt.
DIE GRENZE IST DAS VIERFACHE DAVON. Sie steht nicht gegen den
Menschen, sondern gegen ein Skript, das in einer Schleife hängt --
und sie ist absichtlich so hoch, dass niemand sie im Alltag je
sieht. Eine Bremse, die beim normalen Arbeiten greift, wird
umgangen und ist ab da wirkungslos (siehe die Hausregel dazu).
Im Speicher und nicht in der Datenbank: Ein Neustart setzt sie
zurück, und das ist richtig -- sie schützt vor einem laufenden
Fehler, nicht vor einem Angreifer mit Geduld. */
const SCHREIB_FENSTER_MS = 60_000;
const SCHREIB_MAX = 300;
const schreibZaehler = new Map(); // personId -> [Zeitpunkte]
function schreibBremse(req, res, next) {
const id = req.person?.id;
if (!id) return next();
const jetztMs = Date.now();
const liste = (schreibZaehler.get(id) || []).filter((t) => jetztMs - t < SCHREIB_FENSTER_MS);
if (liste.length >= SCHREIB_MAX) {
return res.status(429).json({
fehler: "Zu viele Änderungen in kurzer Zeit. Warte einen Moment — "
+ "dein Text ist nicht verloren, er wird gleich erneut gesichert.",
});
}
liste.push(jetztMs);
schreibZaehler.set(id, liste);
/* Damit die Karte nicht ewig wächst: Wer lange nichts geschrieben
hat, fliegt beim nächsten Durchgang heraus. */
if (schreibZaehler.size > 500) {
for (const [k, v] of schreibZaehler) {
if (!v.some((t) => jetztMs - t < SCHREIB_FENSTER_MS)) schreibZaehler.delete(k);
}
}
next();
}
notizenRouter.use("/workspace/api/notizblock", angemeldet, nurTeamDogi);
/* ---------- Hilfen ------------------------------------------------------ */
/** Ein Blatt dieser Person — oder null.
*
* Gibt den DATENSATZ zurück und nicht ein Ja/Nein: Eine Route, die
* erst fragt „darf ich?" und danach selbst lädt, kann die zweite
* Abfrage anders stellen als die erste. So kann sie es nicht. */
function blattFuer(person, id, { ausPapierkorb = false } = {}) {
const n = Number(id);
if (!Number.isInteger(n) || n <= 0) return null;
const zeile = db().prepare("SELECT * FROM notizen WHERE id = ? AND person_id = ?")
.get(n, person.id);
if (!zeile) return null;
if (ausPapierkorb ? !zeile.weg_am : zeile.weg_am) return null;
return zeile;
}
/** Mehrzeiliger Text: Absätze bleiben, Leerzeilenwüsten nicht.
*
* Kein `trim()` am Anfang jeder Zeile — Einrückung ist in einer
* Notiz Inhalt („ - Unterpunkt"). Entfernt werden nur Wagenrückläufe
* (damit die Länge überall dieselbe ist) und mehr als zwei leere
* Zeilen am Stück. */
function textSauber(s) {
return String(s ?? "")
.replace(/\r\n?/g, "\n")
.replace(/\n{4,}/g, "\n\n\n")
.slice(0, TEXT_MAX);
}
/** Die erste nicht leere Zeile, ohne ihre Auszeichnung.
*
* Wird NUR für die Liste gebraucht (und für die Suche). Der Titel
* wird nicht gespeichert: Zwei Orte für dieselbe Sache laufen
* auseinander, sobald jemand die erste Zeile ändert und das Feld
* nicht. Hier wird er jedes Mal aus dem Text abgeleitet und ist
* damit immer der, der wirklich oben steht. */
function titelVon(text) {
const zeile = String(text || "").split("\n").find((z) => z.trim().length) || "";
return zeile
.replace(/^\s*#{1,3}\s*/, "")
.replace(/^\s*[-*]\s+/, "")
.replace(/^\s*\[[ xX]\]\s*/, "")
.trim()
.slice(0, 120);
}
function nachDraussen(z) {
return {
id: z.id,
text: z.text,
titel: titelVon(z.text),
farbe: z.farbe,
angeheftet: !!z.angeheftet,
erstellt: z.erstellt,
geaendert: z.geaendert,
weg_am: z.weg_am || null,
};
}
/* ---------- Lesen -------------------------------------------------------- */
/** Der Block: alle eigenen Blätter, angeheftete zuerst. */
notizenRouter.get("/workspace/api/notizblock", (req, res) => {
try {
const zeilen = db().prepare(`
SELECT * FROM notizen
WHERE person_id = ? AND weg_am IS NULL
ORDER BY angeheftet DESC, geaendert DESC
LIMIT ?`).all(req.person.id, BLAETTER_MAX);
/* DIE ZAHL KOMMT AUS DER DATENBANK, nicht aus der Liste oben: Die
ist bei BLAETTER_MAX abgeschnitten, und eine Zählung darauf
wäre ab dem 301. Blatt still falsch. */
const imKorb = db().prepare(`SELECT COUNT(*) AS n FROM notizen
WHERE person_id = ? AND weg_am IS NOT NULL`).get(req.person.id).n;
res.json({
notizen: zeilen.map(nachDraussen),
papierkorb: imKorb,
text_max: TEXT_MAX,
blaetter_max: BLAETTER_MAX,
papierkorb_tage: PAPIERKORB_TAGE,
farben: FARBEN,
});
} catch (fehler) {
console.error("[notizen] Liste:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Was im Papierkorb liegt — mit dem Tag, an dem es von selbst geht. */
notizenRouter.get("/workspace/api/notizblock/papierkorb", (req, res) => {
try {
const zeilen = db().prepare(`
SELECT * FROM notizen
WHERE person_id = ? AND weg_am IS NOT NULL
ORDER BY weg_am DESC LIMIT ?`).all(req.person.id, BLAETTER_MAX);
res.json({
notizen: zeilen.map((z) => ({
...nachDraussen(z),
/* WANN ES WIRKLICH WEG IST -- gerechnet und nicht „in 30
Tagen" geschrieben. Wer gestern geloescht hat, will wissen,
wie viel Zeit er noch hat, und nicht, wie lang die Frist
ist. */
endgueltig_am: new Date(Date.parse(z.weg_am) + PAPIERKORB_TAGE * 86400000)
.toISOString(),
})),
papierkorb_tage: PAPIERKORB_TAGE,
});
} catch (fehler) {
console.error("[notizen] Papierkorb:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Schreiben ----------------------------------------------------- */
/** Ein neues Blatt. */
notizenRouter.post("/workspace/api/notizblock", gleicheHerkunft, schreibBremse,
express.json({ limit: "256kb" }), (req, res) => {
try {
const offen = db().prepare(`SELECT COUNT(*) AS n FROM notizen
WHERE person_id = ? AND weg_am IS NULL`).get(req.person.id).n;
if (offen >= BLAETTER_MAX) {
return res.status(429).json({
fehler: `Dein Block hat ${BLAETTER_MAX} Blätter — mehr gehen nicht. `
+ "Räum ein paar in den Papierkorb, dann geht es weiter.",
});
}
const text = textSauber(req.body?.text);
const farbe = FARB_NAMEN.includes(req.body?.farbe) ? req.body.farbe : FARB_NAMEN[0];
const n = jetzt();
const { lastInsertRowid } = db().prepare(`
INSERT INTO notizen (person_id, text, farbe, angeheftet, erstellt, geaendert)
VALUES (?,?,?,0,?,?)`).run(req.person.id, text, farbe, n, n);
const frisch = db().prepare("SELECT * FROM notizen WHERE id = ?").get(lastInsertRowid);
res.status(201).json({ notiz: nachDraussen(frisch) });
} catch (fehler) {
console.error("[notizen] Anlegen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Ändern: Text, Farbe, angeheftet. Einzeln oder zusammen.
*
* ANTWORTET KNAPP. Diese Route läuft beim Tippen alle 800 ms; das
* ganze Blatt zurückzuschicken wäre bei einer langen Notiz
* zwanzigtausend Zeichen je Tastendruckpause, für eine Information,
* die der Browser schon hat. Zurück kommt der Zeitstempel — das
* Einzige, was er NICHT weiß. */
notizenRouter.patch("/workspace/api/notizblock/:id(\\d+)", gleicheHerkunft, schreibBremse,
express.json({ limit: "256kb" }), (req, res) => {
try {
const z = blattFuer(req.person, req.params.id);
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
const felder = [], werte = [];
if (req.body?.text !== undefined) {
felder.push("text = ?"); werte.push(textSauber(req.body.text));
}
if (req.body?.farbe !== undefined) {
if (!FARB_NAMEN.includes(req.body.farbe)) {
return res.status(400).json({ fehler: "Diese Papierfarbe gibt es nicht." });
}
felder.push("farbe = ?"); werte.push(req.body.farbe);
}
if (req.body?.angeheftet !== undefined) {
felder.push("angeheftet = ?"); werte.push(req.body.angeheftet ? 1 : 0);
}
if (!felder.length) return res.status(400).json({ fehler: "Nichts zu ändern." });
const n = jetzt();
felder.push("geaendert = ?"); werte.push(n);
werte.push(z.id, req.person.id);
db().prepare(`UPDATE notizen SET ${felder.join(", ")}
WHERE id = ? AND person_id = ?`).run(...werte);
res.json({ geaendert: n });
} catch (fehler) {
console.error("[notizen] Aendern:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** In den Papierkorb. */
notizenRouter.delete("/workspace/api/notizblock/:id(\\d+)", gleicheHerkunft, schreibBremse,
(req, res) => {
try {
const z = blattFuer(req.person, req.params.id);
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
const n = jetzt();
db().prepare("UPDATE notizen SET weg_am = ?, angeheftet = 0 WHERE id = ? AND person_id = ?")
.run(n, z.id, req.person.id);
res.json({ weg_am: n, papierkorb_tage: PAPIERKORB_TAGE });
} catch (fehler) {
console.error("[notizen] Wegtun:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Wieder heraus. */
notizenRouter.post("/workspace/api/notizblock/:id(\\d+)/zurueck", gleicheHerkunft, schreibBremse,
(req, res) => {
try {
const z = blattFuer(req.person, req.params.id, { ausPapierkorb: true });
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
const offen = db().prepare(`SELECT COUNT(*) AS n FROM notizen
WHERE person_id = ? AND weg_am IS NULL`).get(req.person.id).n;
if (offen >= BLAETTER_MAX) {
return res.status(429).json({
fehler: `Dein Block ist voll (${BLAETTER_MAX} Blätter). `
+ "Tu erst eines weg, dann kommt dieses zurück.",
});
}
db().prepare("UPDATE notizen SET weg_am = NULL, geaendert = ? WHERE id = ? AND person_id = ?")
.run(jetzt(), z.id, req.person.id);
const frisch = db().prepare("SELECT * FROM notizen WHERE id = ?").get(z.id);
res.json({ notiz: nachDraussen(frisch) });
} catch (fehler) {
console.error("[notizen] Zurueck:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Endgültig — und nur aus dem Papierkorb.
*
* Es gibt keinen Weg von „auf dem Block" direkt zu „weg". Wer
* danebentippt, landet im Papierkorb; erst wer dort noch einmal
* drückt, meint es. */
notizenRouter.delete("/workspace/api/notizblock/:id(\\d+)/endgueltig", gleicheHerkunft,
schreibBremse, (req, res) => {
try {
const z = blattFuer(req.person, req.params.id, { ausPapierkorb: true });
if (!z) return res.status(404).json({ fehler: "nicht_gefunden" });
db().prepare("DELETE FROM notizen WHERE id = ? AND person_id = ?")
.run(z.id, req.person.id);
/* PROTOKOLLIERT WIRD NUR DAS UNWIDERRUFLICHE -- und ohne eine
Silbe des Inhalts. Jede Textänderung zu protokollieren würde
aus einem privaten Block eine Aktivitätsspur machen, die
andere lesen können; genau das soll er nicht sein. Dass etwas
endgültig verschwunden ist, gehört dagegen in die Ablage:
Sonst sucht jemand eines Tages eine Zeile, die niemand
vermisst hat. */
protokolliere("notiz_endgueltig_weg", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `#${z.id}`,
});
res.json({ weg: true });
} catch (fehler) {
console.error("[notizen] Endgueltig:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* =====================================================================
AUFRAEUMEN
Wird aus der Aufbewahrungsliste gerufen (workspace-aufbewahrung.js)
und nirgends sonst. Das ist die einzige Stelle im Haus, die
wirklich räumt — eine Frist ohne Eintrag dort ist kein Versprechen,
sondern ein Satz in einem Kommentar.
===================================================================== */
export function notizenAufraeumen() {
const grenze = new Date(Date.now() - PAPIERKORB_TAGE * 86400000).toISOString();
const weg = db().prepare("DELETE FROM notizen WHERE weg_am IS NOT NULL AND weg_am < ?")
.run(grenze);
return Number(weg?.changes || 0);
}
/* Für die Prüfung: dieselbe Menge, die `nurTeamDogi` benutzt. Sie
wird exportiert, damit pruef-notizen sie gegen die Rechtetafel
halten kann, statt sie abzuschreiben. */
export const ROLLEN_MIT_BLOCK = MIT_BLOCK;