Der Treff: eine eigene Tür für die Community, und sie sieht nur, was dasteht

Filipe: "eine community rolle und community kategorie, wo die community
auch dann nur die sieht" · "wieso nur eine kachel — ich will mehrere,
mit mehreren optionen" · "egal welche rolle hinzugefügt wird soll immer
nur das sehen was ich erlaube".

DIE DRITTE ADRESSE. treff.dogfather-universe.com bekommt eine eigene
Weiche (treff-adresse.js), eine eigene Zugangswand, ein eigenes Manifest
und acht eigene Bühnenbilder. Ohne sie stünde dort die Wand der Agentur:
Spicy-Logo, "Creator Workspace", fünf Rollenkacheln mit den Namen Admin,
Manager, Scout, Creator — die komplette Struktur des Unternehmens, vor
der Anmeldung, für jeden mit der Adresse.

Welche Wand zu welcher Tür gehört, steht jetzt in EINER Tafel
(istFremdeWand) statt als Sonderfall im Code. Bei zwei Wänden war ein
Sonderfall richtig; bei drei wären es drei geworden, bei vier sechs.

SIEBEN BRETTER UND EIN ACHTES NUR FÜRS TEAM. Anschlagbrett, Was ansteht,
Der Treff, Wunschliste, Highlights, Regeln & Hilfe, Mitmachen — dazu
"Meldungen & Maßnahmen", das ausdrücklich NICHT in TREFF_BEREICHE steht:
dort wäre es lautlos bei der Community gelandet und hätte ausgesehen wie
die anderen sieben.

WAS AUF DEN BRETTERN PASSIERT: "Will ich auch" (eine Stimme je Person,
erzwungen durch den Schlüssel, nicht durch eine Prüfung) · Anheften,
höchstens drei, gezählt IN der Transaktion · Freigabe vor Sichtbarkeit
für Termine und Highlights, wobei Abwesenheit der Ruhezustand ist ·
Melden mit Pflicht-Grund (DSA Art. 16) · Entfernen mit Grund, der den
Beitrag ÜBERLEBT (DSA Art. 17) · drei Stufen, gerechnet statt
gespeichert · die Bannleiter: Modi bis 7 Tage, dauerhaft nur DogFather
(seine Entscheidung) · "Mitmachen" wird zum Talent, genau einmal.

Der Ausschluss wirkt in sitzungLesen() — also nicht nur beim Schreiben.
Wer ausgeschlossen ist, ist weg, nicht stumm.

ZWEI SEITEN MEHR. treff-regeln.html holt seine ZAHLEN vom Server
(Mindestalter 18, Fristen, Anschläge) — ein Regeltext, der eine andere
Zahl nennt als die Regel, ist schlimmer als keiner. rechte.html rechnet
"Wer sieht was" aus derselben Tafel aus, aus der die Schranke ihre
Entscheidung holt. Die Community steht dort bei 3 von 23.

SECHS BEFUNDE, KEINEN HAT DAS LESEN GEFUNDEN:
 · Ein Modi kam über den verborgenen Zugang in den Treff — die Bedingung
   war durch Ausschluss formuliert und nahm die neue Wand automatisch mit
 · /api/personen gab einem Mitglied Namen und Rolle von DogFather
 · Ein DELETE mit Körper wird von Nodes HTTP-Parser mit einem leeren 400
   abgewiesen, bevor Express ihn sieht (gemessen). Der Grund reist jetzt
   in der Adresse
 · Der Kommentar, der erklärt, warum die Zugangswand keine Rollennamen
   nennt, nannte selbst einen — in einer ausgelieferten Datei
 · Zwei Listen für "welche Seiten sind ohne Anmeldung offen". Sie
   stimmten, weil beide am selben Tag entstanden
 · Ein Portkonflikt, den ich selbst gebaut hatte (4391 gehört
   pruef-chat-aufloesen)

Dazu zwei feste Zahlen abgeschafft (die 20 in pruef-rechtetafel, die
Kachelzahl in pruef-treff) und zwei Werkzeuge, die jetzt SUCHEN statt
aufzuzählen: Der Versionsstempel findet seine Manifeste selbst — das
dritte hätte sonst keinen bekommen, und die neue App hätte bis zu vier
Stunden lang das falsche Zeichen getragen. Das Bildwerkzeug baut beide
Zugangswände aus einer Vorlage mit zwei Werten.

Zwei neue Kacheltöne, gemessen statt gewählt: #2f2f7f (Abstand 31,4 in
Lab) und #7c2771 (29,7). Das schwächste Paar der vorhandenen 34 liegt
bei 24,9.

Geprüft: treff 69 · treff-werkzeuge 60 · rollen 312 · start-ansicht 147 ·
crew-adresse 129 · sicht 84 · modi-verborgen 80 · haus-trennung 62 ·
bereiche-lesend 37 · css-klassen 30 · zwischenspeicher 21 · alle-wege 19 ·
rechtetafel 19 — alles grün.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-11 17:58:34 +02:00
co-authored by Claude Opus 5
parent 47a91e9708
commit 7b21f247eb
63 changed files with 5672 additions and 382 deletions
+353 -13
View File
@@ -19,14 +19,178 @@ import {
db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather,
externPruefen, externSql,
ohneTeamDogi, ohneAgentur, siehtModis, fuehrtTeamDogi, MODI_BEREICHE_ERLAUBT, TEAM_DOGI_ROLLEN,
TREFF_BRETTER, TREFF_ROLLEN, AUSSEN_ROLLEN,
} from "./workspace.js";
import { nachrichtSchicken } from "./workspace-chat.js";
import {
darfSchreiben, freigabeBedingung, entfernenVorbereiten, TREFF_TEAM_ROLLEN,
mitRollenname,
} from "./workspace-treff.js";
export const bereicheRouter = express.Router();
/* Die Bereiche und ihre Arten. Bewusst hier und nicht in der Datenbank:
Es sind Festlegungen aus dem Konzept, keine Nutzdaten. */
export const BEREICHE = {
/* =====================================================================
DIE SIEBEN BRETTER DES TREFFS (11.09.2026)
Der Raum fuer die Zuschauer. Sie laufen ueber dieselbe Maschinerie
wie Ideen-Board und Angebote -- ein Bereich, seine Arten, dieselbe
Seite. Eine eigene Bauart daneben waere eine zweite
Sichtbarkeitsregel, und genau deren Vervielfaeltigung hat am
10.09. ein Leck verursacht.
ALLE SIEBEN HABEN `fuerAlle`: Im Treff sieht jeder jeden Beitrag.
Ein Raum, in dem man nur sieht, was man selbst geschrieben hat,
waere kein Raum. Die Grenze nach aussen zieht nicht `fuerAlle`,
sondern der Riegel in sichtbarEintrag() -- die Agentur sieht kein
einziges dieser Bretter.
ALLE SIEBEN HABEN `ohneCreatorBezug`: Hier geht es um niemanden aus
der Agentur. Das Feld waere nicht nur ueberfluessig, es waere die
Einladung, versehentlich einen Creator mit einem Zuschauer zu
verknuepfen.
===================================================================== */
anschlag: {
name: "Anschlagbrett",
ober: "Vom Team",
arten: {
ansage: "Ansage",
regel: "Neue Regel",
hinweis: "Hinweis",
danke: "Danke an euch",
},
bewertung: false,
dringlichkeit: false,
ohneCreatorBezug: true,
fuerAlle: true,
},
ansteht: {
name: "Was ansteht",
ober: "Termine",
/* AUSDRUECKLICH NICHT AN DEN ECHTEN KALENDER GEKOPPELT.
Ein Termin traegt Titel, Ort und Gegenueber -- und die Regel im
Haus lautet seit dem 07.09., dass ein Kalender als Ganzes privat
ist. Eine Filterung waere eine Einstellung, die man falsch setzen
kann; ein Eintrag hier ist eine Entscheidung, die jemand trifft.
Der Preis ist, dass das Team es zweimal schreibt. Das ist der
guenstigere von beiden. */
arten: {
stream: "Stream",
event: "Event",
turnier: "Turnier",
pause: "Keine Sendung",
},
bewertung: false,
dringlichkeit: false,
ohneCreatorBezug: true,
fuerAlle: true,
},
treff: {
name: "Der Treff",
ober: "Euer Raum",
/* DIE REIHENFOLGE IST ABSICHT. Das erste Feld bestimmt, was
geschrieben wird -- wer ein Formular oeffnet, dessen erste
Auswahl "Problem" heisst, schreibt Probleme auf.
"Hallo, ich bin neu" steht vorn, weil die ersten sieben Tage
darueber entscheiden, ob jemand bleibt: Wer in dieser Zeit
erlebt, dass er hier einen Platz hat, kommt deutlich haeufiger
wieder. Jede Vorstellung bekommt eine Antwort -- das ist der
ganze Trick, und er steht in keinem Code. */
arten: {
hallo: "Hallo, ich bin neu",
frage: "Frage",
lob: "Das war gut",
problem: "Etwas stimmt nicht",
},
bewertung: false,
dringlichkeit: false,
ohneCreatorBezug: true,
fuerAlle: true,
},
wunsch: {
name: "Wunschliste",
ober: "Was ihr euch wuenscht",
arten: {
stream: "Fuer den Stream",
technik: "Technik",
gemeinsam: "Fuer die Community",
sonstiges: "Sonstiges",
},
bewertung: false,
/* KEINE DRINGLICHKEIT VON EINEM EINZELNEN. Was wichtig ist,
entscheidet hier nicht der Schreiber, sondern wie viele
mitwollen -- "Will ich auch" kommt als naechster Schritt. Eine
Dringlichkeit daneben waere eine zweite Rangordnung, und die
beiden wuerden einander widersprechen. */
dringlichkeit: false,
ohneCreatorBezug: true,
fuerAlle: true,
},
highlight: {
name: "Highlights",
ober: "Von euch",
arten: {
clip: "Clip",
bild: "Bild",
fanart: "Fanart",
moment: "Moment",
},
bewertung: false,
dringlichkeit: false,
ohneCreatorBezug: true,
fuerAlle: true,
},
regeln: {
name: "Regeln & Hilfe",
ober: "Wie es hier laeuft",
/* REGELN IN EINFACHER SPRACHE UND AN ECHTEN FAELLEN. Vage Regeln
erzeugen Grenzfaelle, und die muss dann jeder Modi einzeln
entscheiden -- das ist der Weg, auf dem ein Team uneinheitlich
wird und die Community es merkt. */
arten: {
regel: "Regel",
frage: "Haeufige Frage",
hilfe: "Hilfe",
folge: "Was passiert, wenn",
},
bewertung: false,
dringlichkeit: false,
ohneCreatorBezug: true,
fuerAlle: true,
},
mitmachen: {
name: "Mitmachen",
ober: "Nachwuchs",
/* DIE BRUECKE ZU DEN TALENTEN. Wer sich hier meldet, ist ein
Kandidat -- und Kandidaten gehoeren in die Kategorie, die es
dafuer schon gibt, nicht in eine zweite Liste daneben.
Daneben gehoert der ehrliche Satz, dass eine Bewerbung nicht der
Hauptweg ist: Der treffsicherste Weg ist, dass jemandem aus dem
Team auffaellt, wie du dich verhaeltst. Das steht so im Brett,
weil es stimmt -- und weil es die richtige Erwartung setzt. */
arten: {
gesucht: "Wir suchen",
lust: "Ich haette Lust",
frage: "Frage dazu",
},
bewertung: false,
dringlichkeit: false,
ohneCreatorBezug: true,
fuerAlle: true,
},
live: {
name: "LIVE-Analyse",
arten: { vorbereitung: "Vorbereitung", mitschrift: "Während LIVE", auswertung: "Auswertung" },
@@ -483,9 +647,30 @@ bereicheRouter.use("/workspace/api/bereich/:bereich", (req, res, next) => {
sie ueber die Adresszeile in die Agentur-Ablage, die sie nichts
angeht. Genau die Luecke, gegen die diese Schranke gebaut wurde,
nur eine Rolle spaeter. */
const b = String(req.params.bereich);
const nein = () => res.status(404).json({ fehler: "nicht_gefunden" });
/* EINE AUSSENROLLE KOMMT NUR IN DEN TREFF (11.09.2026).
Und zwar als ERSTER Zweig, nicht als letzter. Die Zeile darunter
hiess bis heute "wer nicht zum Team gehoert, darf weiter" -- fuer
einen Gast waere das ein Freifahrtschein durch alle achtzehn
Bereiche gewesen, obwohl die Kacheln ihm nur sieben anbieten. Eine
Regel, die nur im Formular gilt, ist keine Regel. */
if (AUSSEN_ROLLEN.has(req.person?.rolle)) {
return TREFF_BRETTER.includes(b) ? next() : nein();
}
/* UND UMGEKEHRT: Wer nicht zum Treff gehoert, kommt nicht hinein.
Das ist derselbe Riegel wie in sichtbarEintrag(), nur eine Ebene
frueher -- dort entscheidet er, welche ZEILEN jemand sieht, hier,
ob die Seite ueberhaupt antwortet. Zwei Schloesser, absichtlich:
Faellt eines weg, haelt das andere. */
if (TREFF_BRETTER.includes(b) && !TREFF_ROLLEN.has(req.person?.rolle)) return nein();
if (!TEAM_DOGI_ROLLEN.has(req.person?.rolle)) return next();
if (MODI_BEREICHE_ERLAUBT.has(String(req.params.bereich))) return next();
return res.status(404).json({ fehler: "nicht_gefunden" });
if (MODI_BEREICHE_ERLAUBT.has(b)) return next();
return nein();
});
/* ---------- Die Bereiche sind fuer Creator zum LESEN da ----------------
@@ -713,7 +898,65 @@ function ohneVertrauliche(regel, person, praefix) {
};
}
/* =====================================================================
DER TREFF HAT ZWEI RIEGEL, UND SIE ZEIGEN IN ENTGEGENGESETZTE
RICHTUNGEN (11.09.2026)
Filipe: "wo die community auch dan nur die sieht und die modis
rechte hand und ich also dogfather sehen die aber auch. die
community aber nur die dann."
Daraus folgen zwei Saetze, und beide muessen gelten:
1. EINE AUSSENROLLE SIEHT NUR DEN TREFF. Nicht "den Treff und was
sonst noch durchfaellt" -- nur ihn. Deshalb steht dieser Zweig
GANZ VORN und baut seine Regel selbst, statt auf der
allgemeinen aufzusetzen. Wer auf einer bestehenden Regel
aufsetzt, erbt jeden Zweig, den sie hat, auch die, die es
morgen erst gibt.
2. DIE AGENTUR SIEHT DEN TREFF NICHT. Creator, Scouts, Manager und
Spicy Media haben mit den Zuschauern dieses Streams nichts zu
tun. Ohne diesen zweiten Riegel haette `fuerAlle` sie
hereingelassen -- genau das Loch, das am 10.09. schon einmal
entstanden ist, nur andersherum.
BEIDE STEHEN HIER UND NICHT IN sichtbar(). Diese Funktion beantwortet
auch Fragen zu Aufgaben, Dateien und Terminen; ein Bereichsname hat
dort nichts zu suchen. Der Kommentar eine Bildschirmseite weiter oben
warnt woertlich davor. */
export function sichtbarEintrag(person, praefix = "e") {
/* RIEGEL 1 -- die Aussenrolle. Eine eigene, vollstaendige Regel:
genau die Bretter des Treffs, sonst nichts. */
if (AUSSEN_ROLLEN.has(person?.rolle)) {
const platz = TREFF_BRETTER.map(() => "?").join(", ");
const grund = { wo: `${praefix}.bereich IN (${platz})`, werte: [...TREFF_BRETTER] };
/* RIEGEL 1b -- WAS NOCH NICHT FREIGEGEBEN IST, GIBT ES NICHT
(11.09.2026). Termine und Highlights erscheinen der Community
erst, wenn jemand aus dem Team sie freigegeben hat.
DIE BEDINGUNG HAENGT HIER UND NICHT AM LESE-WEG, weil sie sonst
nur dort gaelte: Suche, Ausfuhr und jeder kuenftige Weg, der
sichtbarEintrag benutzt, haetten die unfreigegebenen Zeilen
weiterhin geliefert. Genau so ist am 10.09. ein Modi-Eintrag
durch die Suche gekommen, obwohl das Brett zu war. */
const frei = freigabeBedingung(person, praefix);
if (!frei) return grund;
return { wo: `(${grund.wo}) AND ${frei.wo}`, werte: [...grund.werte, ...frei.werte] };
}
const regel = sichtbarRoh2(person, praefix);
/* RIEGEL 2 -- wer nicht zum Treff gehoert, sieht seine Bretter nicht.
Auch DogFather geht hier durch; er steht in TREFF_ROLLEN und faellt
deshalb nicht heraus. */
if (!regel || TREFF_ROLLEN.has(person?.rolle)) return regel;
const platz = TREFF_BRETTER.map(() => "?").join(", ");
return {
wo: `(${regel.wo}) AND ${praefix}.bereich NOT IN (${platz})`,
werte: [...regel.werte, ...TREFF_BRETTER],
};
}
function sichtbarRoh2(person, praefix = "e") {
const regel = sichtbar(person);
if (!regel) return regel;
if (regel.wo === "1=1") return regel; // DogFather sieht ohnehin alles
@@ -773,6 +1016,34 @@ function saeulePasst(saeuleId, creatorId) {
return null;
}
/* =====================================================================
WAS EIN TREFF-BRETT ZUSAETZLICH MITBRINGT (11.09.2026)
Drei Angaben, die es nur dort gibt: wie viele "Will ich auch" ein
Wunsch hat, ob meine Stimme dabei ist, ob die Ansage angeheftet und
ob der Eintrag freigegeben ist.
SIE HAENGEN NICHT AN SPALTEN, sondern werden nur fuer die sieben
Bretter angehaengt. Drei Unterabfragen je Zeile sind hier
bedeutungslos (die Tabellen haben eine Handvoll Zeilen), auf den
Betreuungsbrettern waeren sie es nicht -- und vor allem waeren sie
dort eine Auskunft ueber etwas, das es in diesem Bereich gar nicht
gibt.
DER ROLLENNAME KOMMT MIT (Entscheidung Filipe, 11.09.2026: "voller
Rollenname zeigen"). Er kommt als ROLLE, der Klartext wird erst in
der Antwort gebildet -- eine Zuordnung Rolle -> Name im
ausgelieferten JavaScript waere eine Liste aller Rollen dieses
Hauses auf jedem Rechner der Community.
===================================================================== */
const TREFF_SPALTEN = `,
(SELECT COUNT(*) FROM treff_stimmen ts WHERE ts.eintrag_id = e.id) AS stimmen,
(SELECT COUNT(*) FROM treff_stimmen ts
WHERE ts.eintrag_id = e.id AND ts.person_id = ?) AS meine_stimme,
(SELECT COUNT(*) FROM treff_angeheftet ta WHERE ta.eintrag_id = e.id) AS angeheftet,
(SELECT COUNT(*) FROM treff_freigaben tf WHERE tf.eintrag_id = e.id) AS freigegeben,
pe.rolle AS erstellt_rolle`;
const VERBUND = `
FROM eintraege e
LEFT JOIN personen pc ON pc.id = e.creator_id
@@ -789,15 +1060,43 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
const regel = sichtbarEintrag(req.sicht || req.person);
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
const eintraege = db().prepare(`
SELECT ${SPALTEN} ${VERBUND}
WHERE ${regel.wo} AND e.bereich = ?
ORDER BY
CASE e.status WHEN 'offen' THEN 0 ELSE 1 END,
CASE e.dringlichkeit WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END,
e.datum DESC, e.id DESC`).all(...regel.werte, bereich);
const treffBrett = TREFF_BRETTER.includes(bereich);
res.json({ bereich, einstellung, eintraege });
/* DIE REIHENFOLGE GEHOERT ZUM BRETT (11.09.2026).
Am Anschlagbrett steht oben, was angeheftet ist -- sonst waere
das Anheften eine Zierde. Auf der Wunschliste steht oben, was die
meisten wollen; genau das ist der Sinn von "Will ich auch", und
eine Liste nach Datum haette die Abstimmung unsichtbar gemacht.
Ueberall sonst bleibt es, wie es war. */
const reihenfolge = bereich === "anschlag"
? "angeheftet DESC, e.datum DESC, e.id DESC"
: bereich === "wunsch"
? "stimmen DESC, e.datum DESC, e.id DESC"
: `CASE e.status WHEN 'offen' THEN 0 ELSE 1 END,
CASE e.dringlichkeit WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END,
e.datum DESC, e.id DESC`;
const wer = (req.sicht || req.person)?.id ?? 0;
const eintraege = db().prepare(`
SELECT ${SPALTEN}${treffBrett ? TREFF_SPALTEN : ""} ${VERBUND}
WHERE ${regel.wo} AND e.bereich = ?
ORDER BY ${reihenfolge}`)
.all(...(treffBrett ? [wer] : []), ...regel.werte, bereich);
/* DASS DIES EIN BRETT DES TREFFS IST, SAGT DER SERVER (11.09.2026).
Die Oberflaeche braucht die Auskunft, um ihre Knoepfe zu bauen
("Will ich auch", anheften, freigeben, melden). Sie koennte den
Bereichsnamen gegen eine Liste im Browser halten -- dann stuenden
die Namen aller sieben Bretter in einer ausgelieferten Datei, und
es gaebe eine zweite Wahrheit darueber, welche es sind. So gibt
es weiterhin genau eine (TREFF_BRETTER), und der Browser fragt
sie, statt sie zu kennen. */
res.json({
bereich, einstellung, treff: treffBrett,
eintraege: treffBrett ? eintraege.map(mitRollenname) : eintraege,
});
} catch (fehler) {
console.error("[workspace] Bereich lesen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
@@ -1062,6 +1361,20 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
const regel = sichtbar(req.person);
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
/* DIE SCHREIBTAFEL DES TREFFS (11.09.2026).
Auf welches der sieben Bretter jemand von aussen schreiben darf,
steht in TREFF_SCHREIBEN -- an einer Stelle, aus der auch die
Oberflaeche ihre Knoepfe holt (/api/treff/lage). Waeren es zwei
Stellen, koennte ein Knopf erscheinen, den der Server ablehnt --
oder, schlimmer, umgekehrt.
DER TEXT WIRD DURCHGEREICHT, nicht durch "nicht_erlaubt" ersetzt:
"Ab Dabei kannst du hier schreiben" beantwortet die Frage, die
der Mensch davor gerade hat. */
const warum = darfSchreiben(req.person, bereich);
if (warum) return res.status(403).json({ fehler: warum });
const { aus, fehler } = pruefe(bereich, req.body || {}, { neu: true });
if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") });
@@ -1226,9 +1539,36 @@ bereicheRouter.delete("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (r
/* Löschen darf das Management und wer den Eintrag selbst geschrieben
hat -- sonst könnte ein Creator eine Notiz des Managements über
seinen eigenen Bereich verschwinden lassen. */
if (!istLeitung(req.person) && eintrag.erstellt_von !== req.person.id) {
return res.status(403).json({ fehler: "nicht_erlaubt" });
}
/* IM TREFF RAEUMT AUCH EIN MODI AUF (11.09.2026). Er ist dort die
Moderation; waere Aufraeumen der Leitung vorbehalten, bliebe ein
Beitrag stehen, bis jemand Zeit hat -- und genau in dieser Zeit
liest ihn die Community. */
const treffBrett = TREFF_BRETTER.includes(bereich);
const darfWeg = treffBrett
? TREFF_TEAM_ROLLEN.has(req.person.rolle) || eintrag.erstellt_von === req.person.id
: istLeitung(req.person) || eintrag.erstellt_von === req.person.id;
if (!darfWeg) return res.status(403).json({ fehler: "nicht_erlaubt" });
/* DER GRUND WIRD FESTGEHALTEN, BEVOR DER BEITRAG VERSCHWINDET
(DSA Art. 17). Danach waere es zu spaet: Titel und Verfasser
stuenden nirgends mehr, und eine Begruendung ohne den Beitrag,
den sie begruendet, ist keine.
DER GRUND KOMMT AUS DER ADRESSE, und das ist keine Vorliebe,
sondern gemessen (11.09.2026): Nodes HTTP-Parser weist ein
DELETE MIT KOERPER mit einem leeren 400 ab, bevor Express die
Anfrage ueberhaupt sieht -- eine Antwort mit genau einem
Kopffeld ("connection: close") und ohne Text. Wer den Fehler
sucht, sucht ihn in dieser Route, und hier ist keiner.
Der Koerper bleibt als zweite Moeglichkeit stehen: Er schadet
nicht, und wenn ein anderer Weg (ein Werkzeug, eine kuenftige
Fassung von Node) doch einen mitschickt, wird er gelesen. Zwei
Quellen fuer denselben Wert sind hier keine zwei Wahrheiten --
es ist derselbe Wert, nur zwei Briefkaesten. */
const nein = entfernenVorbereiten(req.person, eintrag,
req.body?.grund ?? req.query?.grund);
if (nein) return res.status(400).json({ fehler: nein });
db().prepare("DELETE FROM eintraege WHERE id = ?").run(id);
protokolliere("eintrag_geloescht", {