Stufe 6 und 7: Der Treff wird ein Gespraech, jedes Brett bekommt seine Form

Damit sind alle acht Stufen des Plans gebaut.

STUFE 6 -- DAS GESPRAECH
"Der Treff -- Hallo sagen, fragen, loben" war eine Liste aus Karten.
Der einzige Bereich, in dem Menschen MITEINANDER reden sollen, zwang
sie, nebeneinander zu reden: Jeder schrieb eine eigene Karte, niemand
antwortete jemandem.

Jetzt: Antworten eingerueckt unter ihrem Beitrag, aelteste zuerst (so
liest man ein Gespraech), mit Namen und Rolle. Das Feld steht immer da
statt hinter einem "Antworten"-Knopf -- ein leeres Feld ist die
deutlichste Einladung, die es gibt. Nur EINE Stufe eingerueckt: Auf
390 px ist bei der zweiten Schluss.

Eine EIGENE Tabelle, kein Eintrag mit "antwort_auf": Eine Antwort hat
keine Art, keinen Zustand, keine Frist und gehoert in keinen Filter.
Als Eintrag gefuehrt haette sie zwanzig immer leere Spalten -- und
tauchte in jeder Zaehlung auf, die Beitraege zaehlt.

Und nicht ueberall: nur Treff und Wunschliste. Aus dem Plan, §9 -- ein
Antwortfeld unter jedem Aushang hiesse, jeder Aushang muss betreut
werden.

ZWEI FEHLER, BEIDE VON DER PRUEFUNG GEFUNDEN

1. EIN PFAD, DER AUSSAH WIE EIN BEREICH.
   `/api/bereich/antwort/:id` lief in die Middleware auf
   `/api/bereich/:bereich` -- "antwort" ist kein Bereich. Fuer
   DogFather ging es (ein frueherer Zweig liess ihn durch), fuer einen
   Modi kam 404. Ein Weg, der je nach ROLLE an voellig anderer Stelle
   scheitert, sucht man lange. Liegt jetzt unter `/api/antwort/:id`.

2. UND EIN ECHTES LOCH.
   Die Loeschregel hiess `!darfSchreiben(req.person, "treff")` -- also
   "wer schreiben darf", und das duerfen Gaeste ab der Stufe "dabei".
   JEDER GAST HAETTE JEDE FREMDE ANTWORT LOESCHEN KOENNEN.

   Gefunden hat es eine Pruefung mit einer FALSCHEN Erwartung: Sie
   verlangte, dass ein Modi keine fremde Antwort loeschen kann. Das war
   falsch -- er moderiert ja --, aber der Weg dorthin hat das echte
   Problem freigelegt. Jetzt entscheidet TREFF_TEAM_ROLLEN.

   Der Gast-Fall ist ueber HTTP nicht messbar (er kommt nur ueber die
   crew-Adresse herein, und den Host-Kopf kann fetch nicht setzen).
   Statt stillschweigend zu ueberspringen prueft die Pruefung die REGEL
   selbst -- und dass die Route wirklich diese Menge benutzt.

STUFE 7 -- JEDES BRETT BEKOMMT SEINE FORM
- "Regeln & Hilfe" ist ein DOKUMENT: Sprungmarken oben, vier
  Abschnitte (Regel, Was passiert wenn, Hilfe, Haeufige Frage). Man
  schlaegt es im Streitfall auf und will FINDEN, nicht scrollen.
  scroll-margin-top, sonst verschwindet die angesprungene Ueberschrift
  unter der Kopfleiste und der Sprung sieht aus wie ins Leere.
- "Anschlagbrett" zeigt grosse AUSHAENGE statt Zeilen -- gemessen
  21 px Titel statt 17. Ein Aushang zwischen dreissig anderen ist kein
  Aushang.
- "Mitmachen" gruppiert nach Art, die drei Schritte zuerst.

UND EINE SCHRIFT, DIE ZU KLEIN WAR: Die Rollen-Marke an einer Antwort
stand auf 11,2 px, die Hausgrenze liegt bei 11,5. Die Grenze anzuheben
waere der bequeme Weg gewesen -- und ab da haette pruef-css-klassen
nichts mehr gehalten.

NEU: pruef-gespraech (27 Pruefungen)
pruef-treff-start jetzt 34 (Stufe 7 mitgeprueft)
Alle Community-Pruefungen gruen: gespraech 27, treff-start 34,
kreislauf 23, countdown 22, wunschliste 30, galerie 26, bremse 16,
treff 66, bereiche-lesend, css-klassen, deutsche-texte 12.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 12:12:21 +02:00
co-authored by Claude Opus 5
parent b396e548ab
commit 79e675b846
36 changed files with 1197 additions and 359 deletions
+136
View File
@@ -611,6 +611,13 @@ export const BEITRAEGE_JE_STUNDE = 10;
Content-Planung ist Arbeit am eigenen Kanal; dort waere eine Bremse
eine Behinderung ohne Gegenwert. */
const treffBereich = (b) => TREFF_BRETTER.includes(b);
/* WO EIN GESPRAECH GEMEINT IST (17.09.2026), Stufe 6.
Nicht ueberall. Aus dem Plan, §9: "Keine Kommentarfunktion ueberall.
Nur dort, wo ein Gespraech gemeint ist." Ein Antwortfeld unter jedem
Aushang hiesse: Jeder Aushang muss betreut werden -- und ein
unbeantwortetes Antwortfeld ist schlimmer als keins. */
const MIT_ANTWORTEN = new Set(["treff", "wunsch"]);
const TITEL_MAX = 160;
const TEXT_MAX = 6000;
@@ -1204,6 +1211,33 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
jeQuelle.get(z.aus_eintrag_id).push({ id: z.id, bereich: z.bereich, titel: z.titel });
}
for (const e of eintraege) e.wurde = jeQuelle.get(e.id) || [];
/* DIE ANTWORTEN, in EINER Abfrage fuer die ganze Liste. */
if (MIT_ANTWORTEN.has(bereich)) {
const p4 = eintraege.map(() => "?").join(", ");
const alle = db().prepare(
`SELECT a.id, a.eintrag_id, a.text, a.erstellt, a.person_id,
p.name AS von_name, p.rolle AS von_rolle
FROM eintrag_antworten a
LEFT JOIN personen p ON p.id = a.person_id
WHERE a.eintrag_id IN (${p4})
ORDER BY a.id`).all(...eintraege.map((e) => e.id));
const jeE = new Map();
for (const a of alle) {
if (!jeE.has(a.eintrag_id)) jeE.set(a.eintrag_id, []);
/* DEN ROLLENNAMEN MACHT `mitRollenname`, wie ueberall im
Treff. Eine eigene Uebersetzung hier waere eine zweite
Wahrheit ueber dieselbe Sache -- und Rollennamen sind in
diesem Haus die Sorte Angabe, bei der zwei Stellen einmal
zu viel sind. */
jeE.get(a.eintrag_id).push(mitRollenname({
id: a.id, text: a.text, erstellt: a.erstellt,
von_name: a.von_name, von_rolle: a.von_rolle,
meine: a.person_id === (req.sicht || req.person)?.id,
}));
}
for (const e of eintraege) e.antworten = jeE.get(e.id) || [];
}
}
/* DASS DIES EIN BRETT DES TREFFS IST, SAGT DER SERVER (11.09.2026).
@@ -1685,6 +1719,108 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/:id/daraus",
}
});
/* =====================================================================
ANTWORTEN (17.09.2026) -- Stufe 6 des Plans
"Der Treff" war eine Liste aus Karten. Der einzige Bereich, in dem
Menschen MITEINANDER reden sollen, zwang sie, nebeneinander zu
reden.
DIE BREMSE GILT AUCH HIER. Eine Antwort ist ein Beitrag; wer sie
ausnaehme, haette die Bremse mit drei Klicks umgangen. Sie zaehlt
allerdings ihren EIGENEN Vorrat: Wer zehn Beitraege geschrieben hat,
soll noch antworten koennen -- ein Gespraech, das mitten im Satz
abbricht, ist schlimmer als eins, das nicht angefangen wird. */
const ANTWORTEN_JE_STUNDE = 20;
const ANTWORT_MAX = 1200;
bereicheRouter.post("/workspace/api/bereich/:bereich/:id/antwort",
gleicheHerkunft, (req, res) => {
try {
const bereich = String(req.params.bereich);
const id = Number(req.params.id);
if (!MIT_ANTWORTEN.has(bereich) || !Number.isInteger(id)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const warum = darfSchreiben(req.person, bereich);
if (warum) return res.status(403).json({ fehler: warum });
const text = String(req.body?.text ?? "").trim();
if (!text) return res.status(400).json({ fehler: "Schreib etwas." });
if (text.length > ANTWORT_MAX) return res.status(400).json({ fehler: "Das ist zu lang." });
const regel = sichtbar(req.person);
const e = db().prepare("SELECT id FROM eintraege WHERE id = ? AND bereich = ?")
.get(id, bereich);
if (!e || !regel) return res.status(404).json({ fehler: "nicht_gefunden" });
const seit = new Date(Date.now() - 3600_000).toISOString();
const { anzahl } = db().prepare(
"SELECT COUNT(*) AS anzahl FROM eintrag_antworten WHERE person_id = ? AND erstellt >= ?")
.get(req.person.id, seit);
if (anzahl >= ANTWORTEN_JE_STUNDE) {
return res.status(429).json({
fehler: `Du hast in der letzten Stunde ${anzahl} Antworten geschrieben. `
+ "Mach mal kurz Pause.",
});
}
const { lastInsertRowid } = db().prepare(
"INSERT INTO eintrag_antworten (eintrag_id, person_id, text, erstellt) VALUES (?,?,?,?)")
.run(id, req.person.id, text, jetzt());
res.status(201).json({ id: Number(lastInsertRowid) });
} catch (fehler) {
console.error("[workspace] Antwort:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* DER PFAD LIEGT AUSSERHALB VON `/bereich/` -- und das ist eine
Reparatur, keine Geschmacksfrage (17.09.2026).
Zuerst hiess er `/api/bereich/antwort/:id`. Darueber liegt aber eine
Middleware auf `/api/bereich/:bereich`, die prueft, ob es den
Bereich gibt und ob die Person hineindarf -- und "antwort" ist kein
Bereich. Ergebnis: Fuer DogFather ging es (ein frueherer Zweig liess
ihn durch), fuer einen Modi kam 404. Ein Weg, der je nach Rolle an
einer voellig anderen Stelle scheitert, sucht man lange.
Gefunden hat es die Pruefung, nicht das Lesen: "das Team loescht
auch fremde" war gruen, "Lena loescht ihre eigene" nicht. */
bereicheRouter.delete("/workspace/api/antwort/:id",
gleicheHerkunft, (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
const a = db().prepare(
"SELECT id, person_id FROM eintrag_antworten WHERE id = ?").get(id);
if (!a) return res.status(404).json({ fehler: "nicht_gefunden" });
/* DIE EIGENE IMMER, FREMDE NUR DAS TEAM.
HIER STAND `!darfSchreiben(req.person, "treff")` -- und das war
ein Loch. `darfSchreiben` beantwortet "darf diese Person hier
SCHREIBEN", und das duerfen auch Gaeste ab der Stufe "dabei".
Ein Gast haette also jede fremde Antwort loeschen koennen.
Gefunden hat es die Pruefung, nicht das Lesen: Sie verlangte,
dass ein Modi eine fremde Antwort NICHT loeschen kann, und
bekam 200. Die Erwartung war fuer den Modi falsch -- er
moderiert ja --, aber der Weg dorthin hat das echte Loch
freigelegt.
`TREFF_TEAM_ROLLEN` ist die Liste, die im Haus ohnehin
entscheidet, wer im Treff moderiert: modi, hand, admin. */
const darf = a.person_id === req.person.id
|| TREFF_TEAM_ROLLEN.has(req.person?.rolle);
if (!darf) return res.status(403).json({ fehler: "nicht_erlaubt" });
db().prepare("DELETE FROM eintrag_antworten WHERE id = ?").run(id);
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Antwort loeschen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Anlegen ------------------------------------------------------ */
bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, res) => {