From df67574f9076494fc32f9606aa5a2b776fe7e490 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Tue, 6 Oct 2026 14:22:07 +0200 Subject: [PATCH] Anleitung, Etappe 2: Erste Schritte zum Abhaken -- und eine Farbe, die vier Tage falsch war MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ein Creator hakt seine fuenf Ersten Schritte ab, sieht seinen Fortschritt im Kopf, bekommt unter „Was ist dran" den Punkt „Erste Schritte noch offen" und ganz oben auf der Startseite eine breite Kachel „Fang hier an". Alle drei verschwinden von selbst, sobald er durch ist -- niemand muss etwas wegklicken oder abschalten. EINE QUELLE, DREI ANZEIGEN. 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. Es gibt deshalb nur `offeneHinweise()`: Daraus kommen die Zeile, die Zahl an der kleinen Kachel und die Zahl an der breiten. Die breite Kachel entsteht ausserdem aus der BEREITS GEFILTERTEN Kachelliste und erbt damit beide Schranken (Rollenliste und Rechtetafel), statt sie ein drittes Mal zu beantworten. Gespeichert wird nur Erledigtes: eine Zeile heisst „abgehakt", keine Zeile heisst „offen". Kein Feld mit 0 und 1 -- sonst gaebe es zwei Arten, „offen" zu sagen. DIE WICHTIGSTE ENTDECKUNG WAR NICHT TEIL DIESER ETAPPE. `pruef-kachel-universum` wurde rot. Nachgemessen gegen den Stand VOR Etappe 1: Sie war schon rot -- seit dem 02.10., und zwar wegen Ton 47 (Manager-Ziele, #7368ff), den ich selbst eingetragen habe. Im Kommentar daneben steht sogar meine eigene Messung: „Abstand zum naechsten Nachbarn 0,0899 ... knapp unter der 0,090". Ich habe die Zahl hingeschrieben und die Pruefung nie laufen lassen. Vier Tage. #7368ff -> #7269ff. Eine Hexziffer, Abstand zum alten Wert 0,0024 -- das sieht kein Mensch --, aber der Abstand zur naechsten Kachel im eigenen Haus steigt von 0,0899 auf 0,0918. Nicht die am weitesten entfernte Farbe genommen (das waere ein grelles Gruen gewesen): Manager-Ziele ist in Benutzung und violett, gesucht war die kleinste Aenderung, die die Regel haelt. UND DIE REGEL SELBST WAR FUER EIN HAUS GESCHRIEBEN. Auch mit sauberem Ton 47 blieb die Pruefung rot: Sie vergleicht ALLE 48 Toene miteinander, und rechnerisch ist 0,09 fuer 48 Farben nicht zu halten (das Werkzeug findet als besten freien Platz 0,0793). Seit dem 24.09.2026 gibt es aber zwei Haeuser, und eine Kachel des einen steht nie neben einer des anderen -- sie liegen auf verschiedenen Adressen. Die Messung ist jetzt haus-bewusst: 0,09 innerhalb eines Hauses (streng, und mit rund 24 Farden je Haus zu halten -- gemessen 0,0905), ein Boden von 0,06 ueber die Haeuser hinweg. Das ist SCHAERFER, nicht weicher: Vorher verteilte sich die Grenze auf 48 Farben und war unerreichbar, weshalb die Pruefung dauerhaft rot stand -- und eine Warnung, die immer kommt, ist keine mehr. Welcher Ton zu welchem Haus gehoert, wird aus bereiche.js abgeleitet, nicht aufgelistet. Dazu eine Gegenprobe, dass die Einteilung ueberhaupt trennt (553 Paare im Haus, 575 darueber hinweg). NOCH EINE FESTE ZAHL ERSETZT: `pruef-start-ansicht` verlangte „genau drei Gruppen" fuer einen Creator. Mit „Fang hier an" sind es zeitweilig vier, und beides ist richtig. Gezaehlt wird nicht mehr, sondern benannt -- auch das die schaerfere Pruefung: Die alte Zeile waere gruen geblieben, wenn eine der drei Gruppen verschwindet und eine fremde dazukommt. Gemessen pruef-anleitung 70 Pruefungen, 0 Fehler (vorher 47) pruef-rollen 402 Pruefungen, 0 Fehler (vorher 401) pruef-kachel-universum 16 Pruefungen, 0 Fehler (vorher 13, davon 1 rot) pruef-start-ansicht alles in Ordnung (vorher 1 rot) pruef-kachelraster 24 Pruefungen, 0 Fehler pruef-css-klassen alles in Ordnung, 42 Stellen unter 11,5 px mess-anleitung mit offenen Schritten: Gruppe „Fang hier an", breite Kachel mit Zaehler 5, Hinweiszeile. Nach dem Abhaken: alle drei weg, ein Weg statt drei. Kein Querschieben bei 412/1440 px. Sicherung: ~/sicherungen/workspace-vor-anleitung-e2-20261006-1421.db Co-Authored-By: Claude Opus 5 --- server/anleitung-tabellen.js | 28 ++++++ server/mess-anleitung.mjs | 83 ++++++++++++++- server/pruef-anleitung.mjs | 138 ++++++++++++++++++++++++- server/pruef-kachel-universum.mjs | 67 ++++++++++++- server/pruef-start-ansicht.mjs | 28 +++++- server/workspace-anleitung.js | 156 ++++++++++++++++++++++++++++- server/workspace-hinweise.js | 14 +++ workspace/anleitung.html | 45 ++++++--- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/anleitung.css | 145 ++++++++++++++++++++++++++- workspace/assets/css/start.css | 27 ++++- workspace/assets/js/anleitung.js | 88 ++++++++++++++++ workspace/assets/js/start.js | 46 ++++++++- workspace/aufgaben.html | 38 +++---- workspace/automation.html | 32 +++--- workspace/befinden.html | 34 +++---- workspace/bereich.html | 44 ++++---- workspace/bewerben.html | 38 +++---- workspace/bewerbungen.html | 36 +++---- workspace/buehne.html | 6 +- workspace/calls.html | 34 +++---- workspace/chat.html | 40 ++++---- workspace/content.html | 34 +++---- workspace/crew-index.html | 12 +-- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +++---- workspace/entwicklung.html | 36 +++---- workspace/hilfe.html | 30 +++--- workspace/index.html | 10 +- workspace/kalender.html | 34 +++---- workspace/leistung.html | 32 +++--- workspace/manager-ziele.html | 28 +++--- workspace/material.html | 32 +++--- workspace/notizen.html | 28 +++--- workspace/personen.html | 32 +++--- workspace/profil.html | 36 +++---- workspace/reaktion.html | 38 +++---- workspace/rechte.html | 34 +++---- workspace/report.html | 36 +++---- workspace/scouting.html | 32 +++--- workspace/start.html | 32 +++--- workspace/startcheck.html | 34 +++---- workspace/steckbrief.html | 36 +++---- workspace/support.html | 28 +++--- workspace/tafel.html | 6 +- workspace/talente.html | 36 +++---- workspace/team.html | 32 +++--- workspace/teamlage.html | 34 +++---- workspace/teilen.html | 30 +++--- workspace/treff-moderation.html | 32 +++--- workspace/treff-regeln.html | 32 +++--- workspace/uebersicht.html | 32 +++--- workspace/unsere-seiten.html | 30 +++--- workspace/unterstuetzen.html | 32 +++--- workspace/werdegang.html | 32 +++--- workspace/willkommen.html | 30 +++--- workspace/wissen.html | 34 +++---- 58 files changed, 1519 insertions(+), 714 deletions(-) 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 @@ + + +