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 @@ - - - - - + + + + +