Stufe 8: Der Kreislauf -- ein Wunsch kommt als Clip zurueck

Bisher waren die acht Bretter acht Silos. Ein Wunsch wurde angenommen,
irgendwann passierte etwas im Stream, irgendwann lag ein Clip bei den
Highlights -- und nichts davon wusste voneinander. Wer den Wunsch
geschrieben hatte, erfuhr NIE, dass er erfuellt wurde.

    Wunsch  ->  Termin bei "Was ansteht"  ->  Clip bei "Highlights"

Das ist der Unterschied zwischen einem Briefkasten und einer Community.
Jemand schreibt einen Wunsch und sieht ihn vier Wochen spaeter als Clip
wieder -- mit seinem Namen daneben.

ZUM DRITTEN MAL LAG DER PLAN DANEBEN. Dort stand "`aus_eintrag_id`
gibt es in der Tabelle bereits". Gibt es -- an den AUFGABEN. Die
Eintraege hatten nichts dergleichen. Dreimal an einem Tag, und jedes
Mal hat es das Messen gefunden, nicht das Nachdenken.

EINE HANDLUNG, KEIN AUSWAHLFELD. Der Zusammenhang entsteht durch
"daraus wird ein Termin" -- als Knopf an dem Wunsch, um den es geht.
Wer ein Formular ausfuellt, denkt nicht an den Wunsch von letzter
Woche, und ein Auswahlfeld mit vierzig Eintraegen benutzt niemand.

Die Kette steht auf beiden Karten: "Aus Wunschliste: ... von Lena"
(ruhig, es ist Herkunft) und "Daraus wurde: Highlights ..." (gruen, das
ist die Nachricht, auf die jemand gewartet hat).

ON DELETE SET NULL, nicht CASCADE: Wird der Wunsch geloescht, bleibt
der Clip. Er ist ja trotzdem passiert.

UND DIE PRUEFUNG VON STUFE 2 HAT STUFE 5 ERWISCHT.
Der Startkatalog stand ueber dem Countdown und schob die Antwort auf
"wann ist der naechste Stream" von 458 px auf 1488 px -- aus dem
ersten Handybildschirm heraus. Gefunden hat das nicht das Auge,
sondern das Abnahmekriterium aus §7 des Plans, das seit heute Vormittag
in pruef-countdown steht.

  DIE REGEL DARAUS: Ein WERKZEUG (fuer das Team) draengt nie eine
  ANTWORT (fuer alle) nach unten.

Der Countdown steht jetzt ganz oben, noch vor den Filtern -- gemessen
342 px statt 458.

NEU: pruef-kreislauf (23 Pruefungen)
Lena schreibt einen Wunsch, daraus wird ein Termin, daraus ein
Highlight. Geprueft: Der Titel wandert mit, der NAME wandert mit, beide
Enden sehen die Kette, Lena sieht sie auch (ein Kreislauf, den nur das
Team sieht, ist keiner), zweimal derselbe Weg verdoppelt nichts -- und
die Gegenprobe, dass ein ANDERER Weg vom selben Wunsch sehr wohl geht.

Und wohin sie NICHT fuehrt: nicht in die Content-Planung eines
Creators. Dort steht Arbeit, die die Community nichts angeht.

pruef-countdown 22, pruef-treff-start 27, pruef-wunschliste 30,
pruef-galerie 26, pruef-treff 66, pruef-bremse 16, pruef-css-klassen:
alle gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 11:59:17 +02:00
co-authored by Claude Opus 5
parent 7701f5e5e5
commit b396e548ab
35 changed files with 883 additions and 361 deletions
+107 -1
View File
@@ -1040,7 +1040,7 @@ function sichtbarRoh2(person, praefix = "e") {
}
const SPALTEN = `
e.id, e.bereich, e.art, e.titel, e.text, e.datum, e.uhrzeit, e.bewertung,
e.id, e.bereich, e.art, e.titel, e.text, e.datum, e.uhrzeit, e.aus_eintrag_id, e.bewertung,
e.dringlichkeit, e.status, e.creator_id, e.erstellt, e.erstellt_von, e.geaendert,
e.hook, e.format, e.saeule_id, e.geplant,
e.event_ende, e.event_aufgaben, e.event_regeln,
@@ -1174,6 +1174,36 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
je.get(b.eintrag_id).push({ id: b.id, name: b.name_original });
}
for (const e of eintraege) e.bilder = je.get(e.id) || [];
/* DIE KETTE IN BEIDE RICHTUNGEN (17.09.2026). Woher kam dieser
Eintrag -- und was ist aus ihm geworden? Beides in je EINER
Abfrage; vierzig Eintraege einzeln zu fragen waeren achtzig
Anfragen.
WER DEN WUNSCH GESCHRIEBEN HAT, kommt mit: Das ist der ganze
Punkt. Ein Clip, unter dem steht "Euer Wunsch -- von Lena",
ist etwas anderes als ein Clip. */
const quellen = eintraege.map((e) => e.aus_eintrag_id).filter(Boolean);
if (quellen.length) {
const p2 = quellen.map(() => "?").join(", ");
const her = new Map(db().prepare(
`SELECT e.id, e.bereich, e.titel, p.name AS von_name
FROM eintraege e LEFT JOIN personen p ON p.id = e.erstellt_von
WHERE e.id IN (${p2})`).all(...quellen).map((z) => [z.id, z]));
for (const e of eintraege) {
if (e.aus_eintrag_id) e.aus = her.get(e.aus_eintrag_id) || null;
}
}
const p3 = eintraege.map(() => "?").join(", ");
const draus = db().prepare(
`SELECT id, bereich, titel, aus_eintrag_id FROM eintraege
WHERE aus_eintrag_id IN (${p3})`).all(...eintraege.map((e) => e.id));
const jeQuelle = new Map();
for (const z of draus) {
if (!jeQuelle.has(z.aus_eintrag_id)) jeQuelle.set(z.aus_eintrag_id, []);
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) || [];
}
/* DASS DIES EIN BRETT DES TREFFS IST, SAGT DER SERVER (11.09.2026).
@@ -1579,6 +1609,82 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (r
}
});
/* =====================================================================
DER KREISLAUF (17.09.2026) -- aus dem Plan im Vault, Stufe 8.
Bisher waren die acht Bretter acht Silos. Ein Wunsch wurde
angenommen, irgendwann passierte etwas im Stream, irgendwann lag ein
Clip bei den Highlights -- und nichts davon wusste voneinander. Wer
den Wunsch geschrieben hatte, erfuhr nie, dass er erfuellt wurde.
Wunsch -> Termin bei "Was ansteht" -> Clip bei "Highlights"
DAS IST DER UNTERSCHIED ZWISCHEN EINEM BRIEFKASTEN UND EINER
COMMUNITY. Jemand schreibt einen Wunsch und sieht ihn vier Wochen
spaeter als Clip wieder, mit seinem Namen daneben.
EINE EIGENE ROUTE UND KEIN FELD AM FORMULAR: Der Zusammenhang
entsteht durch eine HANDLUNG ("daraus wird ein Termin"), nicht durch
ein Auswahlfeld, in dem man einen von vierzig Eintraegen sucht. Wer
ein Formular ausfuellt, denkt nicht an den Wunsch von letzter Woche.
===================================================================== */
bereicheRouter.post("/workspace/api/bereich/:bereich/:id/daraus",
gleicheHerkunft, (req, res) => {
try {
const vonBereich = String(req.params.bereich);
const id = Number(req.params.id);
const nach = String(req.body?.nach || "");
if (!BEREICHE[vonBereich] || !BEREICHE[nach] || !Number.isInteger(id)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
/* NUR INNERHALB DER COMMUNITY. Eine Kette, die in die
Content-Planung eines Creators fuehrt, waere ein Leck: Dort
steht Arbeit, die die Community nichts angeht. */
if (!TREFF_BRETTER.includes(vonBereich) || !TREFF_BRETTER.includes(nach)) {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const warum = darfSchreiben(req.person, nach);
if (warum) return res.status(403).json({ fehler: warum });
const regel = sichtbar(req.person);
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
/* DIE QUELLE MUSS SICHTBAR SEIN. Sonst liesse sich durch
Ausprobieren herausfinden, welche Nummern es gibt -- und der
Titel kaeme im neuen Eintrag gleich mit. */
const quelle = db().prepare(
"SELECT id, bereich, titel, erstellt_von FROM eintraege WHERE id = ? AND bereich = ?")
.get(id, vonBereich);
if (!quelle) return res.status(404).json({ fehler: "nicht_gefunden" });
/* SCHON GEMACHT? Dann dieselbe Antwort wie beim Startkatalog:
nichts Neues, kein Fehler. Zwei Termine aus einem Wunsch
waeren ein Versehen, kein Wunsch. */
const schon = db().prepare(
"SELECT id FROM eintraege WHERE aus_eintrag_id = ? AND bereich = ?").get(id, nach);
if (schon) return res.status(200).json({ id: schon.id, schon: true });
const arten = Object.keys(BEREICHE[nach].arten);
const art = arten.includes(String(req.body?.art)) ? String(req.body.art) : arten[0];
const { lastInsertRowid } = db().prepare(`
INSERT INTO eintraege (bereich, art, titel, text, datum, status,
erstellt, erstellt_von, aus_eintrag_id)
VALUES (?,?,?,?,?, 'offen', ?,?,?)`)
.run(nach, art, quelle.titel,
String(req.body?.text ?? "").trim().slice(0, TEXT_MAX) || null,
jetzt().slice(0, 10), jetzt(), req.person.id, quelle.id);
protokolliere("bereich_daraus", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `${vonBereich}#${id} -> ${nach}`,
});
res.status(201).json({ id: Number(lastInsertRowid), schon: false });
} catch (fehler) {
console.error("[workspace] Kreislauf:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Anlegen ------------------------------------------------------ */
bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, res) => {