Anleitung, Etappe 2: Erste Schritte zum Abhaken -- und eine Farbe, die vier Tage falsch war

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 <[email protected]>
This commit is contained in:
2026-10-06 14:22:07 +02:00
co-authored by Claude Opus 5
parent a7b2ad77b8
commit df67574f90
58 changed files with 1519 additions and 714 deletions
+28
View File
@@ -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);
`;
/* =====================================================================
+82 -1
View File
@@ -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");
+136 -2
View File
@@ -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`);
+62 -5
View File
@@ -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
+27 -1
View File
@@ -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
+152 -4
View File
@@ -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 [];
}
}
+14
View File
@@ -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) {