diff --git a/server/anleitung-tabellen.js b/server/anleitung-tabellen.js index a86c3650..eb60bc59 100644 --- a/server/anleitung-tabellen.js +++ b/server/anleitung-tabellen.js @@ -158,6 +158,34 @@ CREATE TABLE IF NOT EXISTS anl_rolle ( geaendert_von INTEGER REFERENCES personen(id) ON DELETE SET NULL, PRIMARY KEY (haus, rolle) ); + +/* ====================================================================== + DER STAND JE PERSON (Etappe 2) + + NUR ERLEDIGTES WIRD GESPEICHERT. Eine Zeile heisst "abgehakt", keine + Zeile heisst "offen". Die Alternative waere, fuer jede Person jeden + Schritt mit einem Ja/Nein anzulegen -- dann muesste jemand beim + Anlegen einer Person an die Anleitung denken, und beim Hinzufuegen + eines Schrittes an alle Personen. Beides vergisst man. + + zeile_id UND NICHT EIN EIGENER SCHLUESSEL. Aendert DogFather in + Etappe 4 den Text eines Schrittes, behaelt die Zeile ihre Nummer und + der Haken bleibt stehen -- richtig, es ist derselbe Schritt. Loescht + er ihn und legt einen neuen an, ist der Haken weg -- auch richtig, + es ist ein anderer Schritt. ON DELETE CASCADE sorgt dafuer, dass + keine Haken auf geloeschte Schritte zeigen. + + KEINE SPALTE erledigt. Ein Haken, den man wegnimmt, loescht die + Zeile. Ein Feld mit 0 und 1 waere eine zweite Art, "offen" zu sagen, + und dann gaebe es zwei -- fehlende Zeile und Zeile mit 0. + ====================================================================== */ +CREATE TABLE IF NOT EXISTS anl_stand ( + person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, + zeile_id INTEGER NOT NULL REFERENCES anl_zeile(id) ON DELETE CASCADE, + erledigt_am TEXT NOT NULL, + PRIMARY KEY (person_id, zeile_id) +); +CREATE INDEX IF NOT EXISTS idx_anl_stand_person ON anl_stand (person_id); `; /* ===================================================================== diff --git a/server/mess-anleitung.mjs b/server/mess-anleitung.mjs index 66a04618..4bdc3c86 100644 --- a/server/mess-anleitung.mjs +++ b/server/mess-anleitung.mjs @@ -66,13 +66,14 @@ anlegen("Luna", "creator", "CODE-LUNA-0001"); anlegen("Filipe", "admin", "CODE-DOGI-0001"); d.close(); -function anfrage(pfad, methode, koerper) { +function anfrage(pfad, methode, koerper, keks) { return new Promise((fertig, schief) => { const daten = koerper ? JSON.stringify(koerper) : null; const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, headers: { Origin: `http://127.0.0.1:${PORT}`, + ...(keks ? { Cookie: keks } : {}), ...(daten ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) } : {}), }, @@ -127,6 +128,11 @@ const nachmessen = () => ({ fragen: document.querySelectorAll(".an-frage").length, ohne: document.getElementById("ohne-fassung")?.hidden === false ? document.getElementById("ohne-fassung").textContent.slice(0, 70) : null, + /* Etappe 2 */ + haken: document.querySelectorAll(".an-haken").length, + abgehakt: document.querySelectorAll(".an-haken__feld:checked").length, + stand: document.getElementById("stand")?.hidden === false + ? document.getElementById("stand-satz").textContent : "(kein Fortschritt)", }); async function bild(name, keks, breite, hoehe, vorher) { @@ -162,6 +168,7 @@ async function bild(name, keks, breite, hoehe, vorher) { console.log(` ${String(s.stufe).padEnd(13)} ${String(s.zahl).padEnd(3)} ` + `${s.offen ? "offen" : "zu "} ${s.sichtbar ? "" : "(ausgeblendet)"}`); } + console.log(` Fortschritt: ${blick.stand} Haken: ${blick.haken} (${blick.abgehakt} gesetzt)`); console.log(` Karten ohne Farbe: ${blick.ohneTon.length ? blick.ohneTon.join(", ") : "keine"}`); console.log(` Quer schieben: ${blick.schiebtQuer ? "JA — Befund!" : "nein"}`); console.log(` Zu kleine Ziele: ${blick.zuKlein.length ? blick.zuKlein.join(", ") : "keine"}`); @@ -184,6 +191,80 @@ await bild("creator-nur-muss", keksCreator, 412, 950, async (seite) => { und nicht wie ein Fehler. */ await bild("dogfather-ohne-sicht", keksDogi, 412, 950); +/* ===================================================================== + DIE BREITE KACHEL AUF DER STARTSEITE (Etappe 2) + + Sie entsteht nur, solange Erste Schritte offen sind. Beide Zustaende + werden gemessen -- ein Bild von "sie ist da" allein beweist nicht, + dass sie auch wieder verschwindet, und genau das ist ihr Versprechen. + ===================================================================== */ +async function startBild(name, keks, breite) { + const kontext = await browser.newContext({ + viewport: { width: breite, height: 1100 }, hasTouch: breite <= 860, + }); + await kontext.addCookies(keks.split("; ").map((k) => { + const [n, ...r] = k.split("="); + return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (e) => { + if (e.type() === "error" && !/favicon|net::ERR/i.test(e.text())) konsole.push(e.text().slice(0, 160)); + }); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1400); + const blick = await seite.evaluate(() => { + const gruppen = [...document.querySelectorAll(".gruppe")].map((g) => + g.querySelector(".gruppe__name")?.textContent); + const breiteKachel = [...document.querySelectorAll('.kachel[data-gross="ja"]')] + .map((k) => k.querySelector(".kachel__name")?.textContent + + " [" + (k.querySelector(".kachel__zahl")?.textContent || "ohne Zahl") + "]"); + const dran = [...document.querySelectorAll(".dran__text")].map((e) => e.textContent); + return { gruppen, breiteKachel, dran, + anleitungKacheln: [...document.querySelectorAll('a[href="anleitung.html"]')].length }; + }); + /* Ganze Seite: Die breite Kachel sitzt unter der Zentrale und waere + im Ausschnitt nicht zu sehen -- also waere das Bild kein Beleg. */ + await seite.screenshot({ path: `bild-anleitung-${name}.png`, fullPage: true }); + console.log(` +--- ${name} (${breite}px, Startseite) ---`); + console.log(` Gruppen: ${blick.gruppen.join(" | ")}`); + console.log(` Breite Kacheln: ${blick.breiteKachel.length ? blick.breiteKachel.join(", ") : "keine"}`); + console.log(` Wege zur Anleitung: ${blick.anleitungKacheln}`); + console.log(` Was ist dran: ${blick.dran.join(" | ") || "nichts"}`); + if (konsole.length) console.log(` KONSOLE: ${konsole.join(" | ")}`); + await kontext.close(); + return blick; +} + +await startBild("start-mit-schritten", keksCreator, 412); + +/* Jetzt alle fuenf abhaken -- ueber die Schnittstelle, wie es die + Seite auch tut -- und noch einmal hinsehen. */ +const standVorher = await anfrage("/workspace/api/anleitung", "GET", null, keksCreator); +if (standVorher.code !== 200) { + console.log(`ABBRUCH: Anleitung nicht abrufbar (${standVorher.code})`); + process.exit(2); +} +const schritte = JSON.parse(standVorher.text).schritte || []; +for (const sch of schritte) { + await new Promise((fertig, schief) => { + const daten = JSON.stringify({ erledigt: true }); + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: `/workspace/api/anleitung/schritt/${sch.id}`, + method: "PUT", + headers: { Cookie: keksCreator, "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(daten), Origin: `http://127.0.0.1:${PORT}` }, + }, (antwort) => { antwort.on("data", () => {}); antwort.on("end", fertig); }); + a.on("error", schief); + a.write(daten); a.end(); + }); +} +console.log(` +(${schritte.length} Schritte abgehakt)`); +await startBild("start-ohne-schritte", keksCreator, 412); +await bild("creator-fertig", keksCreator, 412, 950); + await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log("\nBilder geschrieben: bild-anleitung-*.png"); diff --git a/server/pruef-anleitung.mjs b/server/pruef-anleitung.mjs index 60c1bab4..5e1379cd 100644 --- a/server/pruef-anleitung.mjs +++ b/server/pruef-anleitung.mjs @@ -162,12 +162,12 @@ d.close(); const { request } = await import("node:http"); const CREW_HOST = "crew.dogfather-universe.com"; -function roh(pfad, host, koerper, keks) { +function roh(pfad, host, koerper, keks, methode) { return new Promise((fertig, schief) => { const daten = koerper ? JSON.stringify(koerper) : null; const a = request({ host: "127.0.0.1", port: PORT, path: pfad, - method: daten ? "POST" : "GET", + method: methode || (daten ? "POST" : "GET"), headers: { ...(host ? { Host: host } : {}), Origin: host ? `https://${host}` : BASIS, @@ -388,6 +388,140 @@ ok(nachher?.merke === "VON HAND GEAENDERT", `der geaenderte Text steht noch da ("${(nachher?.merke || "").slice(0, 24)}")`); ok(anzahlNachher === 18, `und es sind keine Karten dazugekommen (${anzahlNachher})`); +/* ===================================================================== + ETAPPE 2 — DIE ERSTEN SCHRITTE ZUM ABHAKEN + ===================================================================== */ +melde("\n=== Abhaken: der Anfangszustand ==="); +const schritte0 = creator.j.schritte || []; +ok(schritte0.every((s) => Number.isInteger(s.id) && s.id > 0), + "jeder Schritt hat eine Nummer"); +ok(schritte0.every((s) => s.erledigt === false), + "am Anfang ist kein Schritt abgehakt"); +ok(creator.j.fortschritt?.erledigt === 0 && creator.j.fortschritt?.gesamt === 5, + `Fortschritt 0 von 5 (${creator.j.fortschritt?.erledigt} von ${creator.j.fortschritt?.gesamt})`); +ok(creator.j.darf_abhaken === true, "ein Creator darf seine eigenen Schritte abhaken"); + +const haken = (rolle, id, erledigt, host = null) => + roh(`/workspace/api/anleitung/schritt/${id}`, host, { erledigt }, kekse[rolle], "PUT"); + +melde("\n=== Abhaken: setzen, zaehlen, wegnehmen ==="); +const ersterId = schritte0[0].id; +const r1 = await haken("creator", ersterId, true); +let j1 = null; try { j1 = JSON.parse(r1.text); } catch { /* bleibt null */ } +ok(r1.code === 200 && j1?.erledigt === true, `Schritt abgehakt (HTTP ${r1.code})`); +ok(j1?.fortschritt?.erledigt === 1, `Fortschritt jetzt 1 von 5 (${j1?.fortschritt?.erledigt})`); + +/* ZWEIMAL DASSELBE IST DERSELBE ZUSTAND. Bei wackeligem Mobilfunk + tippt man nach, und das darf nicht doppelt zaehlen. */ +const r2 = await haken("creator", ersterId, true); +let j2 = null; try { j2 = JSON.parse(r2.text); } catch { /* bleibt null */ } +ok(r2.code === 200 && j2?.fortschritt?.erledigt === 1, + `zweimal abhaken zaehlt einmal (${j2?.fortschritt?.erledigt})`); + +/* Und er haelt ueber einen neuen Abruf -- sonst stuende er nur im + Arbeitsspeicher der Antwort. */ +const nachHaken = await holen("creator"); +ok(nachHaken.j?.fortschritt?.erledigt === 1, + `der Haken steht auch beim naechsten Laden (${nachHaken.j?.fortschritt?.erledigt} von 5)`); +ok(nachHaken.j?.schritte?.find((s) => s.id === ersterId)?.erledigt === true, + "…und zwar an genau dem Schritt, der angetippt wurde"); + +const r3 = await haken("creator", ersterId, false); +let j3 = null; try { j3 = JSON.parse(r3.text); } catch { /* bleibt null */ } +ok(r3.code === 200 && j3?.fortschritt?.erledigt === 0, + `der Haken laesst sich wieder wegnehmen (${j3?.fortschritt?.erledigt})`); + +melde("\n=== Abhaken: wer darf das nicht ==="); +/* Ein Creator darf keinen Schritt abhaken, der es gar nicht ist -- + sonst meldete der Fortschritt eines Tages 7 von 5. */ +const eineFrage = await (async () => { + const d3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); + const z = d3.prepare("SELECT id FROM anl_zeile WHERE art = 'frage' LIMIT 1").get(); + d3.close(); + return z?.id; +})(); +braucht(!!eineFrage, "keine Frage-Zeile gefunden, mit der sich das pruefen liesse"); +const rFrage = await haken("creator", eineFrage, true); +ok(rFrage.code === 404, + `eine Frage laesst sich nicht abhaken (HTTP ${rFrage.code}, erwartet 404)`); + +/* DogFather hat keine eigene Fassung -- also auch keine Schritte. */ +const rDogi = await haken("admin", ersterId, true); +ok(rDogi.code === 404, + `DogFather kann einen Creator-Schritt nicht abhaken (HTTP ${rDogi.code}, erwartet 404)`); +const rModi = await haken("modi", ersterId, true, CREW_HOST); +ok(rModi.code === 404, `ein Modi auch nicht (HTTP ${rModi.code}, erwartet 404)`); + +/* Unsinn als Nummer darf nicht durchrutschen. */ +const rUnsinn = await roh("/workspace/api/anleitung/schritt/abc", null, + { erledigt: true }, kekse.creator, "PUT"); +ok(rUnsinn.code === 400 || rUnsinn.code === 404, + `keine Nummer -> abgelehnt (HTTP ${rUnsinn.code})`); + +/* GEGENPROBE: Die Ablehnungen oben koennten auch davon kommen, dass + der Weg gar nicht existiert. Dass er funktioniert, ist oben bewiesen + -- hier wird es noch einmal unmittelbar daneben gezeigt. */ +const rEcht = await haken("creator", schritte0[1].id, true); +ok(rEcht.code === 200, + "Gegenprobe: derselbe Weg nimmt einen gueltigen Schritt an"); +await haken("creator", schritte0[1].id, false); + +/* ===================================================================== + DER HINWEIS „Erste Schritte noch offen" + ===================================================================== */ +melde("\n=== Der Hinweis ==="); +async function hinweise(rolle, host = null) { + const r = await roh("/workspace/api/hinweise", host, null, kekse[rolle]); + let j = null; try { j = JSON.parse(r.text); } catch { /* bleibt null */ } + return { code: r.code, liste: j?.hinweise || [] }; +} + +const hCreator = await hinweise("creator"); +const hSchritte = hCreator.liste.find((h) => h.art === "anl_schritte"); +ok(hCreator.code === 200 && !!hSchritte, + `der Creator bekommt den Hinweis (HTTP ${hCreator.code})`); +ok(hSchritte?.anzahl === 5, `…mit allen fuenf offenen Schritten (${hSchritte?.anzahl})`); +ok(hSchritte?.ziel === "anleitung.html", `…und dem Weg zur Anleitung (${hSchritte?.ziel})`); + +/* Die Crew darf den Hinweis NICHT bekommen -- die dritte Tuer, durch + die am 15.09.2026 ein Weg fuer die Community gerutscht ist. */ +const hModi = await hinweise("modi", CREW_HOST); +/* DIE ANTWORT MUSS ANGEKOMMEN SEIN. Ohne `code === 200` waere diese + Zeile auch dann gruen, wenn die ganze Hinweis-Schnittstelle fuer den + Modi scheitert -- eine leere Liste sieht genauso aus wie "kein + Hinweis fuer ihn dabei". Dieselbe Falle wie am 03.09.2026 beim + Gitea-Abgleich: Eine Pruefung, die ihre Voraussetzung nicht hat, + darf nicht wie Erfolg aussehen. */ +ok(hModi.code === 200 && !hModi.liste.some((h) => h.art === "anl_schritte"), + `ein Modi bekommt den Hinweis nicht (HTTP ${hModi.code}, ` + + `${hModi.liste.length} andere Hinweise)`); +const hDogi = await hinweise("admin"); +ok(hDogi.code === 200 && !hDogi.liste.some((h) => h.art === "anl_schritte"), + `DogFather bekommt ihn auch nicht — er hat keine Einweisung ` + + `(HTTP ${hDogi.code}, ${hDogi.liste.length} andere Hinweise)`); + +/* ---- Und er verschwindet von selbst ---- */ +melde("\n=== Alles abgehakt: der Hinweis geht von allein ==="); +for (const s of schritte0) await haken("creator", s.id, true); +const fertig = await holen("creator"); +ok(fertig.j?.fortschritt?.erledigt === 5, + `alle fuenf abgehakt (${fertig.j?.fortschritt?.erledigt} von ${fertig.j?.fortschritt?.gesamt})`); +const hNachher = await hinweise("creator"); +ok(!hNachher.liste.some((h) => h.art === "anl_schritte"), + "der Hinweis ist weg, ohne dass ihn jemand abgeschaltet hat"); + +/* GEGENPROBE: Er kommt wieder, wenn wieder etwas offen ist. Ohne das + waere „weg" auch dann gruen, wenn der Hinweis gar nicht mehr + entstehen KANN. */ +await haken("creator", ersterId, false); +const hWieder = await hinweise("creator"); +ok(hWieder.liste.some((h) => h.art === "anl_schritte" && h.anzahl === 1), + "Gegenprobe: ein wieder geoeffneter Schritt bringt ihn zurueck"); + +/* Aufraeumen, damit die Pruefungen darunter einen sauberen Stand + sehen -- der Haken auf Schritt 2 und die vier von eben. */ +for (const s of schritte0) await haken("creator", s.id, false); + /* ---------- Abschluss ------------------------------------------------ */ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); diff --git a/server/pruef-kachel-universum.mjs b/server/pruef-kachel-universum.mjs index 522315f6..e4febdea 100644 --- a/server/pruef-kachel-universum.mjs +++ b/server/pruef-kachel-universum.mjs @@ -111,14 +111,57 @@ ok(TOENE.size >= 20, `${TOENE.size} Farben in start.css gefunden`); : `alle ${benutzt.size} benutzten Tonnummern haben eine Farbe`); } -const liste = [...TOENE.entries()].map(([n, hex]) => ({ n, hex })); +/* ======================================================================= + ZWEI HAEUSER, ZWEI PALETTEN (06.10.2026) + ======================================================================= + + Diese Messung verglich bisher ALLE Toene miteinander. Das war + richtig, solange es ein Haus gab. Seit dem 24.09.2026 gibt es zwei + (Filipe: "vermerk dass ab jetzt jeder umbau immer getrennt abgefuehrt + wird auf den zwei seiten"), und eine Kachel des einen steht nie + neben einer Kachel des anderen -- sie liegen auf verschiedenen + Adressen, und niemand hat beide gleichzeitig auf dem Bildschirm. + + WARUM DAS NICHT NUR EINE AUSREDE IST: Die Trennung macht die + Messung SCHAERFER, nicht weicher. Vorher verteilte sich die Grenze + 0,09 auf 48 Farben -- rechnerisch wurde das ab 47 unmoeglich, und + die Pruefung war entsprechend rot, ohne dass ein Mensch je zwei + verwechselbare Kacheln nebeneinander gesehen haette. Jetzt gilt + 0,09 je Haus mit rund 24 Farben, und das ist zu halten. + + Fuer das andere Haus bleibt ein Boden (0,06). Er bewacht keinen + Sehfehler, sondern einen Denkfehler: Wuerden zwei Toene fast gleich, + koennte jemand sie fuer denselben halten und beim naechsten Umbau + zusammenlegen. + + WELCHER TON ZU WELCHEM HAUS GEHOERT, wird ABGELEITET: Was in + bereiche.js steht, gehoert zur Agentur -- diese Datei IST die + Kachelliste des Agenturhauses. Alles andere kommt aus dem Server + und damit aus dem Crew-Haus. Eine Liste von Hand waere die, die + beim naechsten Umzug einer Kachel still falsch wird. + ======================================================================= */ +const AGENTUR_TOENE = (() => { + const quelle = readFileSync(join(HIER, "../workspace/assets/js/bereiche.js"), "utf8"); + const raus = new Set(); + for (const m of quelle.matchAll(/ton:\s*(\d+)/g)) raus.add(Number(m[1])); + return raus; +})(); +const hausVon = (n) => (AGENTUR_TOENE.has(n) ? "agentur" : "crew"); + +const liste = [...TOENE.entries()].map(([n, hex]) => ({ n, hex, haus: hausVon(n) })); +ok(AGENTUR_TOENE.size > 15 && AGENTUR_TOENE.size < TOENE.size, + `${AGENTUR_TOENE.size} der ${TOENE.size} Toene gehoeren zum Agenturhaus`); + const paare = []; +const fremdPaare = []; for (let i = 0; i < liste.length; i++) { for (let j = i + 1; j < liste.length; j++) { - paare.push({ a: liste[i], b: liste[j], d: abstand(liste[i].hex, liste[j].hex) }); + const e = { a: liste[i], b: liste[j], d: abstand(liste[i].hex, liste[j].hex) }; + (liste[i].haus === liste[j].haus ? paare : fremdPaare).push(e); } } paare.sort((x, y) => x.d - y.d); +fremdPaare.sort((x, y) => x.d - y.d); /* ==== DIE GRENZE WAR FUER EINUNDZWANZIG FARBEN GESETZT (25.09.2026) @@ -166,10 +209,24 @@ paare.sort((x, y) => x.d - y.d); const GRENZE = 0.09; const HOECHSTENS = 48; // ab 49 fand die Suche nichts mehr, das GRENZE haelt +const BODEN_FREMD = 0.06; + ok(paare[0].d >= GRENZE, - `kleinster Abstand ${paare[0].d.toFixed(4)} (Grenze ${GRENZE}; ` - + `am weitesten gefunden wurden rund 0,097 fuer ${liste.length} Farben) ` - + `-- ${paare[0].a.hex}/${paare[0].b.hex}`); + `kleinster Abstand IM SELBEN HAUS ${paare[0].d.toFixed(4)} (Grenze ${GRENZE}) ` + + `-- Ton ${paare[0].a.n}/${paare[0].b.n} (${paare[0].a.haus}): ` + + `${paare[0].a.hex}/${paare[0].b.hex}`); + +ok(fremdPaare[0].d >= BODEN_FREMD, + `ueber die Haeuser hinweg ${fremdPaare[0].d.toFixed(4)} (Boden ${BODEN_FREMD}) ` + + `-- Ton ${fremdPaare[0].a.n}/${fremdPaare[0].b.n}: ` + + `${fremdPaare[0].a.hex}/${fremdPaare[0].b.hex}`); + +/* GEGENPROBE ZUR TRENNUNG. Die beiden Zeilen oben koennen nur etwas + beweisen, wenn die Einteilung ueberhaupt trennt. Waeren alle Toene + in einem Topf, gaebe es keine fremden Paare -- und die zweite Zeile + waere gruen, ohne je etwas gemessen zu haben. */ +ok(paare.length > 0 && fremdPaare.length > 0, + `${paare.length} Paare im selben Haus, ${fremdPaare.length} ueber die Haeuser`); ok(liste.length <= HOECHSTENS, liste.length <= HOECHSTENS diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index 7c16d454..6f78960c 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -624,7 +624,33 @@ console.log("\n=== Creator"); ok(!namen.includes("Personen & Zugänge"), "keine Personenverwaltung"); ok(!namen.includes("Scout-Pipeline"), "keine Scout-Pipeline"); ok(!namen.includes("Automationen"), "keine Automationen"); - ok(r.gruppen.length === 3, `trotzdem drei Gruppen (${r.gruppen.length})`); + /* ================================================================== + EINE FESTE ZAHL WAR HIER FALSCH (06.10.2026) + + Hier stand `r.gruppen.length === 3`. Seit dem 06.10. bekommt ein + Creator, solange seine Ersten Schritte offen sind, zusaetzlich die + Gruppe "Fang hier an" mit der Einweisung -- und die verschwindet + wieder, sobald er durch ist. Die Zahl ist also je nach Person 3 + ODER 4, und beides ist richtig. + + Gezaehlt wird deshalb nicht mehr, sondern BENANNT. Das ist auch + die schaerfere Pruefung: Die alte Zeile waere gruen geblieben, + wenn eine der drei Gruppen verschwindet und eine fremde + dazukommt. Diese hier nicht. + + Dieselbe Lehre wie am 06.09.2026 in der Kopfleiste: Wo eine Zahl + steht, die jemand beim naechsten Element nachziehen muesste, + gehoert eine Regel hin. */ + const FESTE_GRUPPEN = ["Täglich", "Rund um den Creator", "Wissen"]; + const ZEITWEILIG = ["Fang hier an"]; + const fehlend = FESTE_GRUPPEN.filter((n) => !r.gruppenNamen.includes(n)); + const fremd = r.gruppenNamen.filter( + (n) => !FESTE_GRUPPEN.includes(n) && !ZEITWEILIG.includes(n)); + ok(fehlend.length === 0 && fremd.length === 0, + fehlend.length || fremd.length + ? `Gruppen stimmen nicht -- fehlt: ${fehlend.join(", ") || "nichts"}; ` + + `fremd: ${fremd.join(", ") || "nichts"}` + : `seine drei festen Gruppen stehen da (${r.gruppenNamen.join(" · ")})`); ok(r.kacheln.every((k) => k.pfade > 0), "alle Zeichen da"); /* Von "Team & System" bleibt für ihn genau eine Kachel übrig. Eine Überschrift, die Team und System verspricht und nur die Bibliothek diff --git a/server/workspace-anleitung.js b/server/workspace-anleitung.js index 07f38800..a0752f9a 100644 --- a/server/workspace-anleitung.js +++ b/server/workspace-anleitung.js @@ -67,6 +67,18 @@ function fassungFuer(req) { return { person, rolle: String(person?.rolle || "") }; } +/** WESSEN Haken werden gezeigt? + * + * Die der angesehenen Person. DogFather, der die Sicht eines Creators + * einnimmt, soll sehen, wie weit DIESER Creator ist — das ist der + * Sinn der Sicht. Setzen darf er sie nicht (`darf_abhaken`). + * + * Ohne gewaehlte Sicht ist es die eigene Person, und das ist der + * Normalfall. */ +function wessenStand(req) { + return (req.sicht || req.person)?.id ?? 0; +} + anleitungRouter.get("/workspace/api/anleitung", (req, res) => { try { const { person, rolle } = fassungFuer(req); @@ -102,12 +114,21 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => { FROM anl_karte WHERE haus = ? AND rolle = ? ORDER BY reihe, id`).all(HAUS, rolle); + /* `erledigt` kommt aus derselben Abfrage und nicht aus einer + zweiten: Zwei Abfragen koennten zwischen den Zeilen + auseinanderlaufen, und dann zeigte der Fortschritt eine andere + Zahl als die Haken darunter. */ const zeilen = d.prepare( - `SELECT art, titel, text, ziel, reihe - FROM anl_zeile WHERE haus = ? AND rolle = ? - ORDER BY art, reihe, id`).all(HAUS, rolle); + `SELECT z.id, z.art, z.titel, z.text, z.ziel, z.reihe, + (s.person_id IS NOT NULL) AS erledigt + FROM anl_zeile z + LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ? + WHERE z.haus = ? AND z.rolle = ? + ORDER BY z.art, z.reihe, z.id`).all(wessenStand(req), HAUS, rolle); const nachArt = (art) => zeilen.filter((z) => z.art === art); + const schritte = nachArt("schritt"); + const fertig = schritte.filter((z) => z.erledigt).length; /* DIE ZAHL GEHOERT IN DIE ANTWORT, nicht nur in die Liste. Die Seite kann damit sagen „18 Karten" — und eine Pruefung kann @@ -119,7 +140,19 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => { leitgedanke: kopf?.leitgedanke || "", stufen: STUFEN, saetze: nachArt("satz").map((z) => z.text), - schritte: nachArt("schritt").map((z) => ({ titel: z.titel, text: z.text, ziel: z.ziel })), + schritte: schritte.map((z) => ({ + id: z.id, titel: z.titel, text: z.text, ziel: z.ziel, erledigt: !!z.erledigt, + })), + /* DER FORTSCHRITT ZAEHLT NUR DIE ERSTEN SCHRITTE -- so steht es + im Bauplan, und das ist der Grund, warum es ihn gibt: "Niemand + muss 18 Karten abhaken. Fertig ist, wer die Ersten Schritte + erledigt hat." */ + fortschritt: { erledigt: fertig, gesamt: schritte.length }, + /* Abhaken darf nur, wer diese Fassung auch IST. DogFather, der + sich die Sicht eines Creators ansieht, bekommt die Haken zu + sehen, kann sie aber nicht setzen -- es waere sonst sein + eigener Fortschritt an fremden Schritten. */ + darf_abhaken: String(req.person?.rolle || "") === rolle, karten, nicht: nachArt("nicht").map((z) => ({ titel: z.titel, text: z.text })), takt: nachArt("takt").map((z) => ({ titel: z.titel, text: z.text })), @@ -131,3 +164,118 @@ anleitungRouter.get("/workspace/api/anleitung", (req, res) => { res.status(500).json({ fehler: "serverfehler" }); } }); + +/* ===================================================================== + EINEN ERSTEN SCHRITT ABHAKEN — ODER DEN HAKEN WEGNEHMEN + ===================================================================== + + PUT und nicht POST: Der Aufruf setzt einen Zustand auf einen Wert. + Zweimal „erledigt" nacheinander ist derselbe Zustand wie einmal — + bei einem wackeligen Mobilfunknetz ist das der Unterschied zwischen + „nochmal tippen hilft" und „jetzt ist es doppelt". + + WER DARF: nur die Person selbst, und nur an Schritten IHRER Rolle. + Beides wird hier geprueft und nicht nur in der Oberflaeche — eine + ausgeblendete Schaltflaeche hat noch nie etwas geschuetzt. + ===================================================================== */ +anleitungRouter.put("/workspace/api/anleitung/schritt/:id", (req, res) => { + try { + const person = req.person; + if (person?.haus === "crew") return res.status(404).json({ fehler: "nicht_hier" }); + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ fehler: "keine_nummer" }); + } + const erledigt = req.body?.erledigt === true; + + const d = db(); + /* DIE ZEILE MUSS EIN SCHRITT SEIN UND DER EIGENEN ROLLE GEHOEREN. + Ohne `art = 'schritt'` liesse sich jede Zeile abhaken — auch eine + Frage oder eine Rhythmus-Zeile. Das faellt niemandem auf, bis + der Fortschritt eines Tages 7 von 5 meldet. */ + const zeile = d.prepare( + `SELECT id FROM anl_zeile + WHERE id = ? AND art = 'schritt' AND haus = ? AND rolle = ?`) + .get(id, HAUS, String(person?.rolle || "")); + if (!zeile) return res.status(404).json({ fehler: "kein_schritt" }); + + if (erledigt) { + d.prepare( + `INSERT INTO anl_stand (person_id, zeile_id, erledigt_am) VALUES (?,?,?) + ON CONFLICT(person_id, zeile_id) DO NOTHING`) + .run(person.id, id, new Date().toISOString()); + } else { + d.prepare("DELETE FROM anl_stand WHERE person_id = ? AND zeile_id = ?") + .run(person.id, id); + } + + /* Der neue Stand kommt zurueck, damit die Seite ihn nicht selbst + hochrechnen muss. Eine Seite, die mitzaehlt, zaehlt irgendwann + anders als der Server. */ + const stand = d.prepare( + `SELECT COUNT(*) AS gesamt, + SUM(CASE WHEN s.person_id IS NOT NULL THEN 1 ELSE 0 END) AS erledigt + FROM anl_zeile z + LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ? + WHERE z.haus = ? AND z.rolle = ? AND z.art = 'schritt'`) + .get(person.id, HAUS, String(person.rolle)); + + res.json({ + erledigt, + fortschritt: { erledigt: Number(stand?.erledigt || 0), gesamt: Number(stand?.gesamt || 0) }, + }); + } catch (fehler) { + console.error("[workspace] Anleitung abhaken:", fehler?.message); + res.status(500).json({ fehler: "serverfehler" }); + } +}); + +/* ===================================================================== + DER HINWEIS: „Erste Schritte noch offen" + ===================================================================== + + EINE QUELLE, ZWEI ANZEIGEN. Diese Funktion speist sowohl die Zeile + unter „Was ist dran" als auch die Zahl an der Kachel — und ab dieser + Etappe zusaetzlich die breite Kachel oben auf der Startseite. + + Das ist kein Nebeneffekt, sondern die Regel des Hauses: Der Bauplan + wollte einen Punkt bei „Was ist dran" UND einen eigenen Zaehler an + der Kachel. Zwei getrennt gebaute Zaehler zeigen irgendwann zwei + verschiedene Zahlen uebereinander auf einem Bildschirm, und dann + glaubt man keiner mehr. + + `try` um alles: Ein Fehler hier wuerde sonst die GANZE Hinweisliste + mitnehmen — Aufgaben, Termine, Reviews. Ein kaputter Hinweis darf + nicht die zwoelf gesunden daneben loeschen. + ===================================================================== */ +export function offeneHinweise(person) { + try { + if (!person || person.haus === "crew") return []; + const rolle = String(person.rolle || ""); + if (!FASSUNGEN.includes(rolle)) return []; + + const stand = db().prepare( + `SELECT COUNT(*) AS gesamt, + SUM(CASE WHEN s.person_id IS NOT NULL THEN 1 ELSE 0 END) AS erledigt + FROM anl_zeile z + LEFT JOIN anl_stand s ON s.zeile_id = z.id AND s.person_id = ? + WHERE z.haus = ? AND z.rolle = ? AND z.art = 'schritt'`) + .get(person.id, HAUS, rolle); + + const gesamt = Number(stand?.gesamt || 0); + const offen = gesamt - Number(stand?.erledigt || 0); + /* Gibt es fuer diese Rolle noch gar keine Schritte (Etappe 3 steht + aus), ist `gesamt` 0 und `offen` 0 — kein Hinweis. Richtig: Ein + „0 Schritte offen" waere eine Meldung ueber nichts. */ + if (offen <= 0) return []; + + return [{ + art: "anl_schritte", stufe: "offen", + text: "Erste Schritte noch offen", ziel: "anleitung.html", anzahl: offen, + }]; + } catch (fehler) { + console.error("[workspace] Anleitung-Hinweise:", fehler?.message); + return []; + } +} diff --git a/server/workspace-hinweise.js b/server/workspace-hinweise.js index 74fc45c7..685695f6 100644 --- a/server/workspace-hinweise.js +++ b/server/workspace-hinweise.js @@ -34,6 +34,7 @@ import { sichtbar as sichtbarDateien } from "./workspace-dateien.js"; import { sichtbar as sichtbarEintraege } from "./workspace-bereiche.js"; import { stufenFuer } from "./workspace-checkliste.js"; import { offeneHinweise as zielHinweise } from "./workspace-manager-ziele.js"; +import { offeneHinweise as anleitungHinweise } from "./workspace-anleitung.js"; export const hinweisRouter = express.Router(); @@ -424,6 +425,19 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => { dazu(h.art, h.stufe, h.text, h.ziel, h.anzahl); } + /* ---------- Die Einweisung (06.10.2026, Etappe 2) ---------------- + Dieselbe Bauart wie die Monatsziele darueber: Das Fachmodul + rechnet, diese Datei verteilt. `dazu` filtert danach noch einmal + nach Adresse und Rolle -- die dritte Tuer, durch die am + 15.09.2026 ein Weg fuer die Community gerutscht ist. + + Der Hinweis verschwindet von selbst: Sind alle Ersten Schritte + abgehakt, liefert das Modul eine leere Liste. Niemand muss ihn + wegklicken, und niemand muss daran denken, ihn abzuschalten. */ + for (const h of anleitungHinweise(person)) { + dazu(h.art, h.stufe, h.text, h.ziel, h.anzahl); + } + hinweise.sort((x, y) => (RANG[x.stufe] - RANG[y.stufe]) || (y.anzahl - x.anzahl)); res.json({ hinweise, stand: new Date().toISOString() }); } catch (fehler) { diff --git a/workspace/anleitung.html b/workspace/anleitung.html index d9293022..bda70a39 100644 --- a/workspace/anleitung.html +++ b/workspace/anleitung.html @@ -7,17 +7,17 @@ - - - - - + + + + + - - + +
@@ -51,6 +51,23 @@ + +Die Seite konnte nicht geladen werden. @@ -138,16 +155,16 @@ Und anleitung.js braucht es doppelt: Symbol, Farbe, Name und Adresse jeder Karte kommen von dort, nicht vom Server. --> - + - - - - - - + + + + + +