From b396e548ab69cda6a93910b50f05c6d209333b51 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 17 Sep 2026 11:59:17 +0200 Subject: [PATCH] Stufe 8: Der Kreislauf -- ein Wunsch kommt als Clip zurueck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/pruef-kreislauf.mjs | 271 +++++++++++++++++++++++++++++++ server/workspace-bereiche.js | 108 +++++++++++- server/workspace.js | 30 ++++ workspace/app.webmanifest | 8 +- workspace/assets/css/bereich.css | 36 ++++ workspace/assets/js/bereich.js | 68 ++++++++ workspace/aufgaben.html | 24 +-- workspace/automation.html | 26 +-- workspace/befinden.html | 24 +-- workspace/bereich.html | 47 ++++-- workspace/calls.html | 28 ++-- workspace/chat.html | 26 +-- workspace/content.html | 30 ++-- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 +-- workspace/entwicklung.html | 24 +-- workspace/index.html | 8 +- workspace/kalender.html | 28 ++-- workspace/leistung.html | 26 +-- workspace/personen.html | 26 +-- workspace/profil.html | 28 ++-- workspace/rechte.html | 24 +-- workspace/report.html | 28 ++-- workspace/scouting.html | 26 +-- workspace/start.html | 24 +-- workspace/startcheck.html | 28 ++-- workspace/steckbrief.html | 28 ++-- workspace/talente.html | 24 +-- workspace/team.html | 26 +-- workspace/teamlage.html | 24 +-- workspace/treff-moderation.html | 24 +-- workspace/treff-regeln.html | 26 +-- workspace/uebersicht.html | 26 +-- workspace/wissen.html | 26 +-- 35 files changed, 883 insertions(+), 361 deletions(-) create mode 100644 server/pruef-kreislauf.mjs diff --git a/server/pruef-kreislauf.mjs b/server/pruef-kreislauf.mjs new file mode 100644 index 00000000..4e684e33 --- /dev/null +++ b/server/pruef-kreislauf.mjs @@ -0,0 +1,271 @@ +/* ===================================================================== + DER KREISLAUF (17.09.2026) -- Stufe 8 des Plans im Vault + + 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. + + --------------------------------------------------------------------- + UND DER PLAN LAG ZUM DRITTEN MAL DANEBEN + + Dort stand: "`aus_eintrag_id` gibt es in der Tabelle bereits". Gibt + es -- an den AUFGABEN. Die Eintraege hatten nichts dergleichen. + Auch ein Plan von heute Nacht ist eine Bestandsliste und altert; + dreimal an einem Tag hat er sich geirrt, und jedes Mal hat es das + Messen gefunden, nicht das Nachdenken. + + --------------------------------------------------------------------- + VIER FRAGEN + + 1. Entsteht die Kette -- und traegt der neue Eintrag den Namen + dessen, der den Wunsch geschrieben hat? + 2. Sieht man sie von BEIDEN Enden? + 3. Zweimal derselbe Weg darf nichts verdoppeln. + 4. Und sie bleibt in der Community: ein Weg in die Content-Planung + eines Creators waere ein Leck. + + Aufrufen mit: node server/pruef-kreislauf.mjs + Bild: SCHIRM=1 node server/pruef-kreislauf.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 } from "./helfer-tag.mjs"; + +const PORT = await portMussFreiSein(4491, "pruef-kreislauf"); +const ordner = mkdtempSync(join(tmpdir(), "ws-kreis-")); +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-kreislauf"); +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("Lena", "modi", "CODE-LENA-0001"); +const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name); +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 lena = await anmelden("creator", "CODE-LENA-0001"); +const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS }); + +async function liste(bereich, keks = dogi) { + const a = await fetch(`${BASIS}/workspace/api/bereich/${bereich}`, { headers: { cookie: keks } }); + return (await a.json().catch(() => null))?.eintraege || []; +} +async function daraus(vonBereich, id, nach, keks = dogi) { + const a = await fetch(`${BASIS}/workspace/api/bereich/${vonBereich}/${id}/daraus`, { + method: "POST", headers: kopf(keks), body: JSON.stringify({ nach }), + }); + return { status: a.status, daten: await a.json().catch(() => null) }; +} + +/* ---------- LENA schreibt einen Wunsch ------------------------------- */ +const w = await fetch(`${BASIS}/workspace/api/bereich/wunsch`, { + method: "POST", headers: kopf(lena), + body: JSON.stringify({ art: "gemeinsam", + titel: "Ein Abend, an dem wir zusammen zocken", datum: heuteLokal() }), +}); +const wunschId = (await w.json().catch(() => null))?.id || 0; + +/* ======================================================================= + 0. DER PLATZ DAFUER + ======================================================================= */ +melde(""); +melde("=== 0. Die Spalte ==="); +ok(spalten.includes("aus_eintrag_id"), + `die Eintraege haben 'aus_eintrag_id' (${spalten.length} Spalten)`); +ok(wunschId > 0, `Lena hat einen Wunsch geschrieben (#${wunschId})`); + +/* ======================================================================= + 1. AUS DEM WUNSCH WIRD EIN TERMIN + ======================================================================= */ +melde(""); +melde("=== 1. Wunsch wird Termin ==="); +let terminId = 0; +{ + const a = await daraus("wunsch", wunschId, "ansteht"); + ok(a.status === 201, `daraus wird ein Termin (${a.status} ${a.daten?.fehler || ""})`); + terminId = a.daten?.id || 0; + + const termine = await liste("ansteht"); + const t = termine.find((x) => x.id === terminId); + ok(t?.titel === "Ein Abend, an dem wir zusammen zocken", + `der Titel wandert mit ("${t?.titel}")`); + + /* DER NAME IST DER GANZE PUNKT. Ein Termin, unter dem steht "Aus + Wunschliste -- von Lena", ist etwas anderes als ein Termin. */ + ok(t?.aus?.bereich === "wunsch", `und er weiss, woher er kam (${t?.aus?.bereich})`); + ok(t?.aus?.von_name === "Lena", + `mit dem Namen dessen, der den Wunsch schrieb ("${t?.aus?.von_name}")`); +} + +/* ======================================================================= + 2. VON BEIDEN ENDEN SICHTBAR + ======================================================================= */ +melde(""); +melde("=== 2. Beide Enden ==="); +{ + /* OHNE DIESEN ABSCHNITT waere die Kette eine Einbahnstrasse: Der + Termin wuesste vom Wunsch, aber wer den Wunsch ansieht, erfuehre + nie, dass etwas daraus wurde -- und genau das ist der Mangel, um + den es hier geht. */ + const wuensche = await liste("wunsch"); + const wu = wuensche.find((x) => x.id === wunschId); + ok((wu?.wurde || []).length === 1, `am Wunsch steht, was daraus wurde (${(wu?.wurde || []).length})`); + ok(wu?.wurde?.[0]?.bereich === "ansteht", + `und wohin (${wu?.wurde?.[0]?.bereich}: ${wu?.wurde?.[0]?.titel})`); + + /* AUCH LENA SIEHT ES. Ein Kreislauf, den nur das Team sieht, ist + keiner -- der Sinn ist, dass die Person es erfaehrt. */ + const beiLena = (await liste("wunsch", lena)).find((x) => x.id === wunschId); + ok((beiLena?.wurde || []).length === 1, + `und Lena sieht es auch (${(beiLena?.wurde || []).length})`); +} + +/* ======================================================================= + 3. DIE KETTE GEHT WEITER -- UND NICHT ZWEIMAL + ======================================================================= */ +melde(""); +melde("=== 3. Weiter zum Highlight ==="); +{ + const a = await daraus("ansteht", terminId, "highlight"); + ok(a.status === 201, `aus dem Termin wird ein Highlight (${a.status})`); + + const h = (await liste("highlight")).find((x) => x.id === a.daten?.id); + ok(h?.aus?.bereich === "ansteht", `es kennt seinen Termin (${h?.aus?.bereich})`); + + /* ZWEIMAL DERSELBE WEG DARF NICHTS VERDOPPELN. Zwei Termine aus + einem Wunsch waeren ein Versehen, kein Wunsch. */ + const z = await daraus("wunsch", wunschId, "ansteht"); + ok(z.status === 200 && z.daten?.schon === true, + `derselbe Weg noch einmal legt nichts nach (${z.status}, schon=${z.daten?.schon})`); + ok(z.daten?.id === terminId, "und verweist auf den vorhandenen"); + + /* GEGENPROBE: Ein ANDERER Weg vom selben Wunsch geht sehr wohl -- + sonst hiesse "schon" nur, dass ab dem zweiten Mal nichts mehr + geht. */ + const g = await daraus("wunsch", wunschId, "highlight"); + ok(g.status === 201, `ein anderer Weg vom selben Wunsch geht (${g.status})`); +} + +/* ======================================================================= + 4. DIE KETTE BLEIBT IN DER COMMUNITY + ======================================================================= */ +melde(""); +melde("=== 4. Wohin sie NICHT fuehrt ==="); +{ + /* Ein Weg in die Content-Planung eines Creators waere ein Leck: + Dort steht Arbeit, die die Community nichts angeht. */ + const a = await daraus("wunsch", wunschId, "content"); + ok(a.status === 404, `nicht in die Content-Planung (${a.status})`); + const b = await daraus("content", 1, "wunsch"); + ok(b.status === 404, `und nicht von dort hierher (${b.status})`); + + /* UND EINE ERFUNDENE NUMMER ist kein Sonderfall. */ + const c = await daraus("wunsch", 999999, "ansteht"); + ok(c.status === 404, `eine erfundene Quelle ebenfalls nicht (${c.status})`); +} + +/* ======================================================================= + 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: 1100, height: 950 } }); + 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=ansteht`, { waitUntil: "networkidle" }); + await seite.waitForSelector(".kette", { timeout: 15_000 }); + await seite.waitForTimeout(400); + + const m = await seite.evaluate(() => ({ + her: [...document.querySelectorAll(".kette--her")].map((z) => z.textContent.trim()), + hin: [...document.querySelectorAll(".kette--hin")].map((z) => z.textContent.trim()), + wege: [...document.querySelectorAll(".kette__weg[href]")].length, + })); + ok(m.her.length >= 1, `die Herkunft steht auf der Karte ("${m.her[0] || ""}")`); + ok(/Lena/.test(m.her[0] || ""), "mit dem Namen"); + ok(/Wunschliste/.test(m.her[0] || ""), "und dem Brett, aus dem es kam"); + ok(m.hin.length >= 1, `und was daraus wurde ("${m.hin[0] || ""}")`); + ok(m.wege >= 2, `beide sind anklickbar (${m.wege} Wege)`); + ok(konsole.length === 0, + konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); + + if (process.env.SCHIRM) { + await seite.screenshot({ path: fileURLToPath(new URL("pruef-kreislauf.png", + import.meta.url)), fullPage: false }); + melde(" -- Bildschirmfoto: server/pruef-kreislauf.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); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 3af6de42..bb309088 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -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) => { diff --git a/server/workspace.js b/server/workspace.js index fe68dbdf..0debe560 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1766,6 +1766,36 @@ function umstellungen(d) { falschen Moment. */ const jetztStempel = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, ""); + /* ---- Der Kreislauf: ein Eintrag kommt aus einem anderen (17.09.2026) + + Aus dem Plan im Vault, Stufe 8 -- und die Stelle, an der der Plan + zum dritten Mal danebenlag: Dort stand "`aus_eintrag_id` gibt es in + der Tabelle bereits". Gibt es -- an den AUFGABEN. Die Eintraege + hatten nichts dergleichen. + + WOZU: Heute sind die acht Bretter acht Silos. Ein Wunsch wird + angenommen, irgendwann passiert etwas im Stream, irgendwann liegt + ein Clip bei den Highlights -- und nichts davon weiss voneinander. + Wer den Wunsch geschrieben hat, erfaehrt nie, dass er erfuellt + wurde. + + Mit dieser einen Spalte wird daraus eine Kette: + + Wunsch -> Termin bei "Was ansteht" -> Clip bei "Highlights" + + ON DELETE SET NULL und nicht CASCADE: Wird der Wunsch geloescht, + bleibt der Clip. Er ist ja trotzdem passiert. */ + try { + const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name); + if (spalten.length && !spalten.includes("aus_eintrag_id")) { + d.exec("ALTER TABLE eintraege ADD COLUMN aus_eintrag_id INTEGER REFERENCES eintraege(id) ON DELETE SET NULL"); + d.exec("CREATE INDEX IF NOT EXISTS idx_eintraege_aus ON eintraege (aus_eintrag_id)"); + console.log("[workspace] Spalte 'aus_eintrag_id' an den Eintraegen ergaenzt."); + } + } catch (fehler) { + console.error("[workspace] Spalte 'aus_eintrag_id':", fehler?.message); + } + /* ---- Ein Bild an einem Community-Beitrag (17.09.2026) ---- "Highlights -- eure Clips und Bilder" konnte bis heute gar keine diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index eb99865b..cdde9b78 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609171148", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609171158", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609171148", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609171158", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171148", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171158", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171148", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171158", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index 786ac962..0e1d8b14 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -951,3 +951,39 @@ margin: 0; flex: 1; font-size: .84rem; line-height: 1.5; color: var(--text-leise); } + +/* ===================================================================== + DIE KETTE (17.09.2026) — Stufe 8 des Plans + + 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 erfüllt wurde. + + ZWEI RICHTUNGEN, ZWEI TÖNE: Woher etwas kam, steht ruhig da — es ist + Herkunft, kein Ergebnis. Was daraus wurde, ist grün: Das ist die + Nachricht, auf die jemand gewartet hat. + + KLEIN GEHALTEN. Die Kette erklärt den Eintrag, sie ersetzt ihn + nicht — eine Zeile, kein Kasten. + ------------------------------------------------------------------- */ +.kette { + --k-ton: #6f89a8; + display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; + margin: 8px 0 0; padding: 6px 10px; + border-left: 2px solid color-mix(in srgb, var(--k-ton) 55%, transparent); + border-radius: 0 var(--radius-klein) var(--radius-klein) 0; + background: color-mix(in srgb, var(--k-ton) 8%, transparent); + font-size: .82rem; +} +/* „Daraus wurde" ist die gute Nachricht — sie darf als Einzige + auffallen. */ +.kette--hin { --k-ton: #4dab77; } + +.kette__wort { + font-weight: 650; letter-spacing: .02em; + color: color-mix(in srgb, var(--k-ton) 70%, #ffffff); +} +.kette__weg { color: var(--text); text-decoration: none; } +.kette__weg:hover { text-decoration: underline; } +.kette__wer { color: var(--text-still); } diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index 8ecd767e..0d805d4f 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -53,6 +53,28 @@ den ABSTAND, nicht die absolute Zahl -- die steht ohnehin daneben. */ let maxStimmen = 0; + /* Wie ein Brett in einem Satz heisst. Kurz, weil es mitten in einer + Zeile steht: „Aus Wunschliste: …" liest sich, „Aus dem Bereich + Wunschliste (wunsch): …" nicht. */ + const BRETT_WORT = { + anschlag: 'Anschlagbrett', ansteht: 'Was ansteht', treff: 'Treff', + wunsch: 'Wunschliste', highlight: 'Highlights', regeln: 'Regeln', + mitmachen: 'Mitmachen', + }; + + /* WELCHER WEG VON WO AUS SINN ERGIBT. Bewusst wenige: Ein Menü mit + sieben Zielen an jeder Karte waere ein Menü, kein Angebot. Was + hier steht, ist der Weg, den eine Sache im echten Betrieb nimmt. */ + const KETTE_ZIELE = { + wunsch: [ + { nach: 'ansteht', wort: 'Daraus einen Termin machen' }, + { nach: 'highlight', wort: 'Daraus ein Highlight machen' }, + ], + ansteht: [{ nach: 'highlight', wort: 'Daraus ein Highlight machen' }], + treff: [{ nach: 'wunsch', wort: 'Daraus einen Wunsch machen' }], + highlight: [{ nach: 'anschlag', wort: 'Am Anschlagbrett zeigen' }], + }; + const $ = (id) => document.getElementById(id); const UNTERZEILE = { live: 'Vorbereitung, Mitschrift und Auswertung – LIVE systematisch verbessern.', @@ -366,6 +388,33 @@ k.append(kopf); + /* ---- DIE KETTE (17.09.2026), Stufe 8 des Plans ----------------- + Woher kam das hier, und was ist daraus geworden? Beides steht + auf der Karte, nicht in einem Detail: Der Zusammenhang ist die + Aussage. Ein Clip, unter dem steht „Euer Wunsch — von Lena", ist + etwas anderes als ein Clip. */ + if (e.aus) { + const z = el('p', 'kette kette--her'); + z.append(el('span', 'kette__wort', 'Aus')); + const a = el('a', 'kette__weg', + `${BRETT_WORT[e.aus.bereich] || e.aus.bereich}: ${e.aus.titel}`); + a.href = `/workspace/bereich.html?b=${encodeURIComponent(e.aus.bereich)}`; + z.append(a); + if (e.aus.von_name) z.append(el('span', 'kette__wer', '· von ' + e.aus.von_name)); + k.append(z); + } + if (Array.isArray(e.wurde) && e.wurde.length) { + const z = el('p', 'kette kette--hin'); + z.append(el('span', 'kette__wort', 'Daraus wurde')); + for (const w of e.wurde) { + const a = el('a', 'kette__weg', + `${BRETT_WORT[w.bereich] || w.bereich}: ${w.titel}`); + a.href = `/workspace/bereich.html?b=${encodeURIComponent(w.bereich)}`; + z.append(a); + } + k.append(z); + } + /* DER RANGBALKEN. Eine Wunschliste ist eine Rangfolge, keine Liste -- und eine Rangfolge sieht man erst, wenn der Abstand sichtbar ist. Die Zahl allein beantwortet "wie viele", der Balken "wie @@ -601,6 +650,25 @@ const knoepfe = el('div', 'eintrag-knoepfe'); + /* ---- DER WEG WEITER --------------------------------------------- + Die Handlung, nicht ein Auswahlfeld: Wer ein Formular ausfuellt, + denkt nicht an den Wunsch von letzter Woche. Hier steht der Knopf + an dem Wunsch, um den es geht. + + Schon gegangene Wege fallen weg -- ein Knopf, der beim zweiten + Druecken nichts tut, ist eine Sackgasse. */ + for (const ziel of (KETTE_ZIELE[bereich] || [])) { + if ((e.wurde || []).some((w) => w.bereich === ziel.nach)) continue; + knoepfe.append(tuKnopf(ziel.wort, async () => { + const a = await hole(`/workspace/api/bereich/${bereich}/${e.id}/daraus`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nach: ziel.nach }), + }); + if (!a.ok) { melde('Ging nicht.'); return; } + await laden(); + })); + } + /* VIER ZUSTAENDE STATT EINES UMSCHALTERS (17.09.2026). "erledigt / wieder offen" reicht fuer einen Vorfall. Ein Wunsch diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 59408ed0..be6dc46e 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -291,10 +291,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 4c8b653e..10f1bfca 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/befinden.html b/workspace/befinden.html index 54dcab68..8c57c13b 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 31122ffe..f3ca8458 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -176,6 +176,18 @@ + +
-

Einträge werden geladen …

- - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index a3f2acba..5b383783 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 3ffa57ae..866ceeeb 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index c9c2b4fc..70f967d2 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index c793f626..84c7f00a 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 022e5c26..ea873937 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609171148", + "src": "/assets/img/app-symbole/crew-192.png?v=202609171158", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609171148", + "src": "/assets/img/app-symbole/crew-512.png?v=202609171158", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171148", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171158", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171148", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171158", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 8dd145b5..33e91b12 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index 2848fcda..655abfe5 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -102,9 +102,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 9f90a10e..3d9b3a52 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 757b1ea2..20e71660 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 00c7d380..ed4ba5ef 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 2c5255ab..8cbc9590 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 92badc5b..918f349c 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index 34d6f8ca..28b0bf60 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index 9bfba68a..0adc0fe0 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index d077ccda..cdae7186 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 0b4b7fea..ad71d76f 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index b366fd62..6578137f 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index ee8026f5..3499c8f7 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index 383f6782..3542fb1b 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index 55729912..f5b15a6b 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 0321a7f5..02891472 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index 585a85b2..86ddba74 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -126,9 +126,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index 29b5c360..117dcf1a 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -211,10 +211,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index b28500ff..c91e8e08 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 5c06ce1f..d1d9f54c 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +