Stufe 3: Die Wunschliste bekommt einen Rang und einen Weg

Aus dem Plan im Vault. Eine Wunschliste ohne sichtbare Erfuellung ist
ein Briefkasten ohne Postbote -- nach dem dritten unbeantworteten
Wunsch schreibt niemand mehr.

ZWEIMAL LAG MEIN PLAN DANEBEN, beide Male zu pessimistisch:
- Die Sortierung nach Stimmen GAB es schon, seit dem 11.09.
- Die Datenbank erlaubte 'angenommen' und 'abgelehnt' laengst in ihrer
  CHECK-Regel; nur die Pruefung im Server liess sie nicht durch. Die
  Tabelle war der Logik voraus.

Auch ein Plan von gestern Nacht ist eine Bestandsliste und altert.

WAS DAZUGEKOMMEN IST
- Vier Zustaende JE BEREICH statt zwei global: Offen -> Wird gemacht
  -> Gemacht, dazu "Diesmal nicht". Eine gemeinsame Liste haette
  "Diesmal nicht" auch an einem Schutzvorfall erlaubt, und dort
  bedeutet es nichts.
- Ein geplanter Tag dazu: "Wird gemacht" allein ist ein Versprechen,
  "Wird gemacht -- am 24.09." ist ein Termin.
- Der Rangbalken. Eine Rangfolge sieht man erst, wenn der ABSTAND
  sichtbar ist: 12 Stimmen neben 14 sehen sonst aus wie 1 neben 40.
  Fuer ein Vorleseprogramm spricht er in Worten.
- Entschiedenes rutscht nach unten, wird aber NICHT geloescht. Gerade
  der erfuellte Wunsch ist der Beweis, dass sich Schreiben lohnt.

"DIESMAL NICHT" IST DER WICHTIGSTE DER VIER, und es ist bewusst nicht
rot. Ein Nein ist eine Antwort, Schweigen ist keine -- aber wer ein
rotes Schild an seinem Wunsch sieht, schreibt keinen zweiten.

UND AUF DEM BILDSCHIRMFOTO STAND "WAS IHR EUCH WUENSCHT".
Die Umlaut-Pruefung von gestern Nacht sah es nicht: Sie kannte die
Kataloge und die festen Seitentexte, aber nicht die
Bereichseinstellungen, aus denen JEDE Ueberschrift und jede
Art-Beschriftung kommt. Sechs Stellen ("Fuer den Stream", "Wie es hier
laeuft", "Haeufige Frage", "Ich haette Lust" ...).

Gefunden hat sie kein Gedankengang, sondern ein Blick auf das fertige
Bild. Die Wortliste hatte ausserdem Loecher -- "wuensch" fehlte
schlicht; 23 Stuecke nachgetragen. pruef-deutsche-texte deckt jetzt
auch die 120 Beschriftungen der Bereiche ab (9 -> 12 Pruefungen), mit
einer Gegenprobe auf genau den Satz, der heute Morgen durchrutschte.

NEU: pruef-wunschliste (30 Pruefungen)
Der aelteste Wunsch bekommt die meisten Stimmen -- genau der Fall, der
eine Sortierung nach Datum entlarvt. Dazu: alle vier Zustaende setzen,
ein erfundener nicht, "abgelehnt" am Anschlagbrett ABGELEHNT, der
geplante Tag, die Balkenbreiten (100/33/0 %), und dass der gemachte
Wunsch weiter in der Liste steht -- nur nicht mehr oben.

pruef-countdown 22, pruef-treff 66, pruef-bereiche-lesend,
pruef-deutsche-texte 12: gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 11:28:37 +02:00
co-authored by Claude Opus 5
parent 77db1bcc2e
commit 7eec2edf3f
36 changed files with 933 additions and 367 deletions
+11
View File
@@ -32,6 +32,17 @@ export const ERSATZSCHREIBUNGEN = [
"staerk", "stoer", "taegl", "taet", "traeg", "ueber", "uebung",
"ungefaehr", "veraend", "verlaeuf", "vorschlaeg", "waehl", "waehr",
"waere", "weiss", "woert", "zaehl", "zugaeng", "zurueck",
/* NACHGETRAGEN AM 17.09.2026, weil das Netz Loecher hatte: Auf einem
Bildschirmfoto der Wunschliste stand "WAS IHR EUCH WUENSCHT", und
die Liste schwieg -- "wuensch" fehlte schlicht. Ein Netz, das man
nicht nachzieht, faengt beim naechsten Mal wieder nichts.
Deshalb steht hier auch, WIE man das findet: nicht durch Nachdenken,
sondern indem man sich das fertige Bild ansieht. */
"ausfuehr", "duerf", "einfueh", "erklaer", "fuehl", "fuehr", "fuenf",
"gluec", "gruen", "guelt", "haeuf", "kuend", "muede", "natuerl",
"schuetz", "spuer", "stueck", "traeum", "tuer", "uebrig",
"unterstuetz", "wuensch", "wuerd",
];
/** Welche Ersatzschreibungen stecken in diesem Text? Leer heisst sauber. */
+42
View File
@@ -142,6 +142,43 @@ melde("=== 2. Was fest auf den Seiten steht ===");
ok(stellen === 0, `ihr sichtbarer Text ist frei von Ersatzschreibungen (${stellen} Funde)`);
}
/* =======================================================================
2b. DIE BESCHRIFTUNGEN DER COMMUNITY-BEREICHE
======================================================================= */
melde("");
melde("=== 2b. Was an den Brettern steht ===");
{
/* NACHGETRAGEN AM 17.09.2026, und der Weg dorthin gehoert dazu:
Auf einem Bildschirmfoto der Wunschliste stand "WAS IHR EUCH
WUENSCHT" und "FUER DEN STREAM". Diese Pruefung sah es nicht --
sie kannte die Kataloge und die festen Seitentexte, aber nicht die
Bereichseinstellungen, aus denen jede Ueberschrift und jede
Art-Beschriftung kommt.
Sechs Stellen waren es. Gefunden hat sie kein Gedankengang,
sondern ein Blick auf das fertige Bild -- deshalb steht das hier. */
const b = await import("./workspace-bereiche.js");
const texte = [];
const sammeln = (wo, wert) => {
if (typeof wert === "string" && wert.length > 2) texte.push([wo, wert]);
};
for (const [name, e] of Object.entries(b.BEREICHE || {})) {
for (const [feld, wert] of Object.entries(e)) {
sammeln(`${name}.${feld}`, wert);
if (wert && typeof wert === "object" && !Array.isArray(wert)) {
for (const [a, x] of Object.entries(wert)) sammeln(`${name}.${feld}.${a}`, x);
}
}
}
ok(texte.length >= 60, `${texte.length} Beschriftungen gefunden`);
const schmutzig = texte.filter(([, t]) => ersatzschreibungen(t).length);
for (const [wo, t] of schmutzig) {
melde(` ${wo}: [${ersatzschreibungen(t).join(", ")}] ${t}`);
}
ok(schmutzig.length === 0,
`keine schreibt Umlaute als ae/oe/ue/ss (${schmutzig.length} Funde)`);
}
/* =======================================================================
3. GEGENPROBE -- KANN DIE PRUEFUNG UEBERHAUPT ROT WERDEN?
======================================================================= */
@@ -156,6 +193,11 @@ melde("=== 3. Die Gegenprobe ===");
const gefunden = ersatzschreibungen(alt);
ok(gefunden.length >= 3, `der alte Kategorietext schlaegt an (${gefunden.join(", ")})`);
/* UND DER SATZ VON HEUTE, an dem das Netz ein Loch hatte. Ohne ihn
waere nicht bewiesen, dass die Liste nachgezogen wurde. */
ok(ersatzschreibungen("Was ihr euch wuenscht").length > 0,
"auch \"wuenscht\" schlaegt an -- das tat es heute Morgen noch nicht");
/* UND DIE ANDERE RICHTUNG, genauso wichtig: Der reparierte Text darf
NICHT anschlagen. Sonst waere die Liste einfach zu breit. */
const neu = "Was im Livechat passiert, während gesendet wird. "
+301
View File
@@ -0,0 +1,301 @@
/* =====================================================================
DIE WUNSCHLISTE IST EINE RANGFOLGE, KEINE LISTE (17.09.2026)
Aus dem Plan im Vault, Stufe 3.
Eine Wunschliste ohne sichtbare Erfüllung ist ein Briefkasten ohne
Postbote. Nach dem dritten unbeantworteten Wunsch schreibt niemand
mehr.
---------------------------------------------------------------------
ZWEI DINGE, DIE BEIM MESSEN ANDERS WAREN ALS IM PLAN
1. Die Sortierung nach Stimmen GAB es schon (seit 11.09.). Im Plan
stand "fehlt" -- eine Bestandsliste altert, auch die eigene von
gestern Nacht.
2. Die Datenbank kannte 'angenommen' und 'abgelehnt' bereits in ihrer
CHECK-Regel; nur die Pruefung im Server liess sie nicht durch. Die
Tabelle war der Logik voraus.
---------------------------------------------------------------------
DIE VIER FRAGEN
1. Steht der meistgewuenschte oben -- auch wenn er der aelteste ist?
2. Lassen sich alle vier Zustaende setzen, ein erfundener nicht?
3. Gelten sie NUR hier? ("Diesmal nicht" an einem Schutzvorfall
bedeutet nichts.)
4. Bleibt ein GEMACHTER Wunsch sichtbar -- nur nicht mehr oben?
Die vierte ist die wichtigste. Loeschen waere die falsche Antwort:
Gerade der erfuellte Wunsch ist der Beweis, dass sich Schreiben
lohnt.
Aufrufen mit: node server/pruef-wunschliste.mjs
Bild: SCHIRM=1 node server/pruef-wunschliste.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { portMussFreiSein } from "./helfer-port.mjs";
import { heuteLokal, tagLokal } from "./helfer-tag.mjs";
const PORT = await portMussFreiSein(4483, "pruef-wunschliste");
const ordner = mkdtempSync(join(tmpdir(), "ws-wunsch-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(180_000, "pruef-wunschliste");
await new Promise((r) => setTimeout(r, 700));
const BASIS = `http://127.0.0.1:${PORT}`;
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen"
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("VanVan", "hand", "CODE-HAND-0001");
anlegen("Marina", "modi", "CODE-MODI-0001");
anlegen("Nane", "modi", "CODE-NANE-0001");
d.close();
async function anmelden(kachel, code) {
const a = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: kachel, code }),
});
return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
}
const dogi = await anmelden("admin", "CODE-DOGI-0001");
const hand = await anmelden("creator", "CODE-HAND-0001");
const modi = await anmelden("creator", "CODE-MODI-0001");
const nane = await anmelden("creator", "CODE-NANE-0001");
const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS });
async function wunsch(titel, tage) {
const a = await fetch(BASIS + "/workspace/api/bereich/wunsch", {
method: "POST", headers: kopf(dogi),
body: JSON.stringify({ art: "stream", titel, datum: tagLokal(tage) }),
});
return (await a.json().catch(() => null))?.id || 0;
}
async function willIch(id, keks) {
const a = await fetch(`${BASIS}/workspace/api/treff/eintrag/${id}/willich`,
{ method: "PUT", headers: kopf(keks), body: "{}" });
return a.status;
}
async function liste(keks = dogi) {
const a = await fetch(BASIS + "/workspace/api/bereich/wunsch", { headers: { cookie: keks } });
return await a.json().catch(() => null);
}
async function setzen(id, feld, keks = dogi) {
const a = await fetch(`${BASIS}/workspace/api/bereich/wunsch/${id}`, {
method: "PATCH", headers: kopf(keks), body: JSON.stringify(feld),
});
return { status: a.status, daten: await a.json().catch(() => null) };
}
/* ---------- Drei Wuensche, verschieden alt und verschieden beliebt --- */
const alt = await wunsch("Ein Zockerabend mit der Community", -20);
const mittel = await wunsch("Mehr Technik-Streams", -8);
const neu = await wunsch("Ein Q&A mit Casper", -1);
/* DER AELTESTE bekommt die meisten Stimmen -- genau das ist der Fall,
der eine Sortierung nach Datum entlarvt. */
for (const k of [hand, modi, nane]) await willIch(alt, k);
await willIch(mittel, hand);
/* =======================================================================
1. DER MEISTGEWUENSCHTE STEHT OBEN
======================================================================= */
melde("");
melde("=== 1. Die Rangfolge ===");
{
const j = await liste();
const namen = (j?.eintraege || []).map((e) => e.titel);
ok((j?.eintraege || []).length === 3, `drei Wuensche (${namen.length})`);
ok(namen[0] === "Ein Zockerabend mit der Community",
`der meistgewuenschte steht oben, obwohl er der aelteste ist ("${namen[0]}")`);
const stimmen = (j?.eintraege || []).map((e) => Number(e.stimmen) || 0);
ok(stimmen.join(",") === "3,1,0", `und die Stimmen stimmen (${stimmen.join(", ")})`);
/* GEGENPROBE zur Sortierung: Waere sie nach Datum, stuende der
juengste oben. Er steht unten -- ohne Stimmen. */
ok(namen[namen.length - 1] === "Ein Q&A mit Casper",
`der juengste ohne Stimmen steht unten ("${namen[namen.length - 1]}")`);
}
/* =======================================================================
2. DIE VIER ZUSTAENDE
======================================================================= */
melde("");
melde("=== 2. Der Weg eines Wunsches ===");
{
const j = await liste();
const zust = j?.einstellung?.zustaende || {};
ok(Object.keys(zust).length === 4,
`vier Zustaende kommen mit (${Object.values(zust).join(" · ")})`);
ok(!!zust.abgelehnt,
`darunter ein ausdrueckliches Nein ("${zust.abgelehnt}")`);
for (const z of ["angenommen", "erledigt", "abgelehnt", "offen"]) {
const a = await setzen(mittel, { status: z });
ok(a.status === 200, `"${z}" laesst sich setzen (${a.status} ${a.daten?.fehler || ""})`);
}
const x = await setzen(mittel, { status: "vielleicht" });
ok(x.status === 400, `ein erfundener nicht (${x.status})`);
/* DER GEPLANTE TAG: "Wird gemacht" allein ist ein Versprechen,
"Wird gemacht -- am 24.09." ist ein Termin. */
const g = await setzen(mittel, { status: "angenommen", geplant: tagLokal(7) });
ok(g.status === 200, `ein geplanter Tag dazu (${g.status})`);
const jj = await liste();
const m = (jj?.eintraege || []).find((e) => e.id === mittel);
ok(m?.geplant === tagLokal(7), `und er kommt zurueck (${m?.geplant})`);
const gf = await setzen(mittel, { geplant: "übermorgen" });
ok(gf.status === 400, `ein erfundener Tag nicht (${gf.status})`);
}
/* =======================================================================
3. DIE ZUSTAENDE GELTEN NUR HIER
======================================================================= */
melde("");
melde("=== 3. Nicht ueberall ===");
{
/* "Diesmal nicht" an einem Anschlag bedeutet nichts -- eine
gemeinsame Liste haette es trotzdem erlaubt. */
const a = await fetch(BASIS + "/workspace/api/bereich/anschlag", {
method: "POST", headers: kopf(dogi),
body: JSON.stringify({ art: "regel", titel: "Eine Ansage", datum: heuteLokal() }),
});
const id = (await a.json().catch(() => null))?.id;
if (!id) { melde(" -- kein Anschlag anlegbar -- Art unbekannt, Abschnitt uebersprungen"); }
else {
const b = await fetch(`${BASIS}/workspace/api/bereich/anschlag/${id}`, {
method: "PATCH", headers: kopf(dogi), body: JSON.stringify({ status: "abgelehnt" }),
});
ok(b.status === 400, `"abgelehnt" gibt es am Anschlagbrett nicht (${b.status})`);
const c = await fetch(`${BASIS}/workspace/api/bereich/anschlag/${id}`, {
method: "PATCH", headers: kopf(dogi), body: JSON.stringify({ status: "erledigt" }),
});
ok(c.status === 200, `"erledigt" dagegen schon (${c.status})`);
}
}
/* =======================================================================
4. EIN GEMACHTER WUNSCH BLEIBT SICHTBAR
======================================================================= */
melde("");
melde("=== 4. Erfuellt heisst nicht verschwunden ===");
{
/* Der MEISTGEWUENSCHTE wird gemacht. Er darf nicht verschwinden --
gerade er ist der Beweis, dass sich Schreiben lohnt. Er soll nur
nicht mehr die offenen verdraengen. */
await setzen(alt, { status: "erledigt" });
const j = await liste();
const namen = (j?.eintraege || []).map((e) => e.titel);
const drin = namen.includes("Ein Zockerabend mit der Community");
ok(drin, "der gemachte Wunsch steht weiter in der Liste");
ok(namen[0] !== "Ein Zockerabend mit der Community",
`aber nicht mehr oben (oben steht jetzt "${namen[0]}")`);
ok(namen[namen.length - 1] === "Ein Zockerabend mit der Community",
"sondern unten bei den entschiedenen");
}
/* =======================================================================
5. IM BROWSER
======================================================================= */
melde("");
melde("=== 5. Was man sieht ===");
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
try {
const kontext = await browser.newContext({ viewport: { width: 390, height: 844 } });
await kontext.addCookies(dogi.split("; ").map((k) => {
const [name, ...rest] = k.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
await seite.goto(`${BASIS}/workspace/bereich.html?b=wunsch`, { waitUntil: "networkidle" });
await seite.waitForSelector(".wunsch-rang", { timeout: 15_000 });
await seite.waitForTimeout(500);
const m = await seite.evaluate(() => ({
balken: [...document.querySelectorAll(".wunsch-rang__fuellung")]
.map((b) => b.style.width),
spitze: document.querySelectorAll('.wunsch-rang[data-spitze="ja"]').length,
staende: [...document.querySelectorAll(".wunsch-stand")]
.map((z) => `${z.dataset.stand}:${z.textContent.trim()}`),
wahlKnoepfe: document.querySelectorAll(".wunsch-wahl__knopf").length,
gedrueckt: document.querySelectorAll('.wunsch-wahl__knopf[aria-pressed="true"]').length,
sprache: document.querySelector(".wunsch-rang")?.getAttribute("aria-label") || "",
}));
ok(m.balken.length === 3, `drei Rangbalken (${m.balken.join(", ")})`);
/* DER ABSTAND IST DIE AUSSAGE: drei Stimmen = voll, eine = ein
Drittel, keine = leer. Ohne diese Zeile waere ein Balken, der
immer 100 % zeigt, genauso gruen. */
ok(m.balken[0] === "100%" || m.balken.includes("100%"),
`der meistgewuenschte ist voll (${m.balken.join(", ")})`);
ok(m.balken.includes("33%"), `einer bei einem Drittel (${m.balken.join(", ")})`);
ok(m.balken.includes("0%"), "und einer leer");
ok(m.spitze === 1, `genau einer ist als Spitze markiert (${m.spitze})`);
ok(m.staende.length === 3, `jeder Wunsch zeigt seinen Zustand (${m.staende.join(" | ")})`);
ok(m.staende.some((z) => z.startsWith("erledigt")), "darunter der gemachte");
/* VIER KNOEPFE JE WUNSCH, und genau einer ist der aktuelle. */
ok(m.wahlKnoepfe === 12, `vier Knoepfe je Wunsch (${m.wahlKnoepfe})`);
ok(m.gedrueckt === 3, `und je einer ist der aktuelle (${m.gedrueckt})`);
/* EIN BALKEN IST FUER EIN VORLESEPROGRAMM NICHTS -- ausser man sagt
es in Worten. */
ok(/wollen das auch|will das auch|niemand/.test(m.sprache),
`der Balken spricht auch ("${m.sprache}")`);
ok(konsole.length === 0,
konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole");
if (process.env.SCHIRM) {
const pfad = fileURLToPath(new URL("pruef-wunschliste.png", import.meta.url));
await seite.screenshot({ path: pfad, fullPage: false });
melde(" -- Bildschirmfoto: server/pruef-wunschliste.png");
}
await kontext.close();
} finally {
await browser.close();
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);
+64 -8
View File
@@ -117,11 +117,39 @@ export const BEREICHE = {
wunsch: {
name: "Wunschliste",
ober: "Was ihr euch wuenscht",
ober: "Was ihr euch wünscht",
/* DER WEG EINES WUNSCHES (17.09.2026).
Bis heute kannte ein Eintrag zwei Zustaende: offen und erledigt.
Fuer eine Wunschliste ist das zu wenig -- und der Mangel ist
kein Schoenheitsfehler, sondern der Grund, warum solche Listen
sterben:
Eine Wunschliste ohne sichtbare Erfuellung ist ein Briefkasten
ohne Postbote. Nach dem dritten unbeantworteten Wunsch
schreibt niemand mehr.
"Diesmal nicht" gehoert ausdruecklich dazu. Ein Nein ist eine
Antwort; Schweigen ist keine. Wer sieht, dass entschieden wurde,
schreibt beim naechsten Mal wieder -- auch wenn die Entscheidung
gegen ihn ausfiel.
DIE DATENBANK KONNTE DAS SCHON. Ihre CHECK-Regel erlaubt seit
laengerem 'angenommen' und 'abgelehnt'; nur die Pruefung im
Server liess sie nicht durch. Die Tabelle war der Logik voraus. */
zustaende: {
offen: "Offen",
angenommen: "Wird gemacht",
erledigt: "Gemacht",
abgelehnt: "Diesmal nicht",
},
/* Dazu ein Datum: "Wird gemacht" allein ist ein Versprechen,
"Wird gemacht -- am 24.09." ist ein Termin. */
mitGeplant: true,
arten: {
stream: "Fuer den Stream",
stream: "Für den Stream",
technik: "Technik",
gemeinsam: "Fuer die Community",
gemeinsam: "Für die Community",
sonstiges: "Sonstiges",
},
bewertung: false,
@@ -152,14 +180,14 @@ export const BEREICHE = {
regeln: {
name: "Regeln & Hilfe",
ober: "Wie es hier laeuft",
ober: "Wie es hier läuft",
/* 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",
frage: "Häufige Frage",
hilfe: "Hilfe",
folge: "Was passiert, wenn",
},
@@ -182,7 +210,7 @@ export const BEREICHE = {
weil es stimmt -- und weil es die richtige Erwartung setzt. */
arten: {
gesucht: "Wir suchen",
lust: "Ich haette Lust",
lust: "Ich hätte Lust",
frage: "Frage dazu",
},
bewertung: false,
@@ -1072,7 +1100,18 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
const reihenfolge = bereich === "anschlag"
? "angeheftet DESC, e.datum DESC, e.id DESC"
: bereich === "wunsch"
? "stimmen DESC, e.datum DESC, e.id DESC"
/* ENTSCHIEDENES NACH UNTEN -- ABER NICHT WEG (17.09.2026).
Die Sortierung nach Stimmen gibt es seit dem 11.09. Seit es
vier Zustaende gibt, reicht sie nicht mehr: Ein gemachter
Wunsch mit vielen Stimmen stuende dauerhaft ganz oben und
verdraengte die offenen, ueber die noch zu entscheiden ist.
Loeschen waere die falsche Antwort. Gerade der ERFUELLTE
Wunsch ist der Beweis, dass sich Schreiben lohnt -- er
gehoert gesehen, nur nicht an der ersten Stelle. */
? `CASE e.status WHEN 'offen' THEN 0 WHEN 'angenommen' THEN 0 ELSE 1 END,
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`;
@@ -1244,9 +1283,26 @@ function pruefe(bereich, körper, { neu }) {
else aus.uhrzeit = u;
}
if (körper.status !== undefined) {
if (!STATUS.includes(körper.status)) fehler.push("Unbekannter Status.");
/* JE BEREICH, NICHT GLOBAL (17.09.2026). Die Wunschliste kennt
vier Zustaende, alle anderen zwei. Eine gemeinsame Liste haette
"Diesmal nicht" auch an einem Schutzvorfall erlaubt -- und dort
bedeutet es nichts. */
const erlaubt = einstellung.zustaende
? Object.keys(einstellung.zustaende) : STATUS;
if (!erlaubt.includes(körper.status)) fehler.push("Unbekannter Status.");
else aus.status = körper.status;
}
/* DER GEPLANTE TERMIN eines Wunsches. Bisher gab es das Feld nur in
der Content-Planung; hier traegt es dieselbe Bedeutung und darf
deshalb dasselbe Format haben. Leer heisst "noch kein Termin" und
nicht "unveraendert". */
if (einstellung.mitGeplant && körper.geplant !== undefined) {
const g = String(körper.geplant ?? "").trim();
if (!g) aus.geplant = null;
else if (!/^\d{4}-\d{2}-\d{2}$/.test(g) || Number.isNaN(Date.parse(g))) {
fehler.push("Der geplante Tag ist ungültig.");
} else aus.geplant = g;
}
/* Bewertung und Dringlichkeit gibt es nur dort, wo der Bereich sie
vorsieht -- sonst werden sie stillschweigend verworfen. */
if (einstellung.bewertung && körper.bewertung !== undefined) {