From 4eab64bd203b0bfa298a5dd09b5698de647b63f6 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Mon, 7 Sep 2026 07:12:48 +0200 Subject: [PATCH] Eine neue Form fuer jedes Modul -- und der Kalender zeigt nur noch, was einen angeht Filipe: "DU HAST WIEDER EINE KLEINE AENDERUNG UEBERALL GEMACHT ANSTATT EINE RIESEN AENDERUNG." Er hatte recht, und der Grund war jedes Mal derselbe: Ich habe das MATERIAL getauscht (Mattglas, Leuchtschiene, Verlauf) und die FORM gelassen. Ein abgerundetes Rechteck bleibt ein abgerundetes Rechteck -- und die Silhouette ist das Einzige, was man aus fuenf Metern erkennt. Jetzt ist die Form eine andere: abgeschnittene Ecke oben links (clip-path, echte Silhouette), ein Kantenlicht in der Kategoriefarbe darauf, Eckwinkel unten rechts, ein feines Raster statt Koernung. 35 Bauteile auf allen 18 Seiten, in einer eigenen Datei (module.css). DREI DINGE, DIE DABEI SCHIEFGINGEN UND JETZT ABGESICHERT SIND 1. Die Regeln standen in :where() -- Spezifitaet null, also gewann jede aeltere .kachel::before-Regel. Jetzt :is(). 2. module.css stand an DRITTER Stelle im Ladeweg. Auf Calls, Automationen, Bereich und Dateien hat sie damit gar nichts bewirkt: Die Seitendateien setzen dort selbst border-radius und box-shadow und kommen spaeter. Genau das ergab wieder "ueberall ein bisschen". Sie wird jetzt als LETZTE geladen, geprueft auf allen 18 Seiten. 3. Die Klassenliste steht siebenmal in der Datei (CSS kennt keine Variable fuer Selektoren). Eine vergessene Kopie faellt niemandem auf -- pruef-css-klassen vergleicht sie deshalb alle, mit Gegenprobe. DER KALENDER: NUR NOCH, WAS EINEN ANGEHT Filipe: "calls oder termine soll jeder nur sehen die er selber macht oder jeden betrifft." Das gilt auch fuer DogFather, und das ist die eigentliche Aenderung -- er bekam bisher 1=1. "Jeden betrifft" heisst: ohne Gegenueber und ohne Teilnehmerliste. Wer niemanden eintraegt, meint alle. Die Rollenfrage entfaellt im Kalender damit vollstaendig. SECHS PRUEFUNGEN, DIE EINE WELT GEMESSEN HABEN, DIE ES NICHT MEHR GIBT Alle sechs wurden auf die neue Regel umgeschrieben, keine geloescht -- loeschen haette die Zahl gesenkt und den neuen Weg ungeprueft gelassen: ics, serien, sicht, spicy, teilnehmer, tagesblick, team. UND VIER ECHTE FUNDE, DIE DABEI HERAUSFIELEN * pruef-workspace-seiten war seit dem Bildumbau von heute Frueh auf ALLEN 32 Seiten rot: Sie verlangte noch das eine Motiv. Die neue Bedingung ist schaerfer als beide alten -- das geladene Bild muss zu dem Merkmal passen, das die Seite selbst traegt. Und die Meldung sagt jetzt, WELCHE Bedingung gefallen ist. * pruef-leistung-optik hat sich selbst ausgesperrt (meldete sich als Manager an, der die Zahlen seit Filipes Anweisung nicht mehr sehen darf) und stuerzte danach ab: 2 Pruefungen statt 56. Der Absturz hat verdeckt, dass sie sieben Fingerziele als "zu klein" meldete -- es waren die unsichtbaren Knoepfe der zugeklappten Woche, 0x0. * personen.html beim Manager: Der Erklaersatz ("Codes, Sperren und das Protokoll bleiben bei DogFather") kam nie an -- das Element gab es nicht, und das && davor hat den Fehlgriff verschluckt. Die Liste stand ausserdem dauerhaft auf aria-busy="true": fuer einen Screenreader lud die Seite fuer immer. * pruef-schranke hielt personen.html noch fuer DogFather-only. Die Seite wechselt jetzt die Erwartung, statt aus der Pruefung zu verschwinden: Seite auf fuer die Leitung, Verwaltungsdaten dahinter weiterhin zu. Die Anmeldeseite wurde nicht angefasst (nur ihr Versionsstempel). Co-Authored-By: Claude Opus 5 --- server/pruef-css-klassen.mjs | 64 +++++++ server/pruef-ics.mjs | 27 ++- server/pruef-leistung-optik.mjs | 213 +++++++++++++--------- server/pruef-personen-formular.mjs | 61 +++++-- server/pruef-schranke.mjs | 51 +++++- server/pruef-serien.mjs | 14 +- server/pruef-sicht.mjs | 31 +++- server/pruef-spicy.mjs | 19 +- server/pruef-start-ansicht.mjs | 42 ++++- server/pruef-tagesblick.mjs | 29 ++- server/pruef-team.mjs | 10 +- server/pruef-teilnehmer.mjs | 29 ++- server/pruef-workspace-seiten.mjs | 44 ++++- server/workspace.js | 41 ++++- tools/ansicht.mjs | 10 +- workspace/assets/css/module.css | 199 ++++++++++++++++++++ workspace/assets/css/start.css | 282 +++++------------------------ workspace/assets/js/personen.js | 26 ++- workspace/aufgaben.html | 17 +- workspace/automation.html | 19 +- workspace/bereich.html | 27 +-- workspace/calls.html | 21 ++- workspace/chat.html | 17 +- workspace/content.html | 23 +-- workspace/dateien.html | 19 +- workspace/index.html | 4 +- workspace/kalender.html | 21 ++- workspace/leistung.html | 19 +- workspace/personen.html | 27 ++- workspace/profil.html | 21 ++- workspace/report.html | 21 ++- workspace/scouting.html | 19 +- workspace/start.html | 17 +- workspace/startcheck.html | 21 ++- workspace/steckbrief.html | 19 +- workspace/uebersicht.html | 17 +- workspace/wissen.html | 19 +- 37 files changed, 997 insertions(+), 563 deletions(-) create mode 100644 workspace/assets/css/module.css diff --git a/server/pruef-css-klassen.mjs b/server/pruef-css-klassen.mjs index 511a8c1d..173b8304 100644 --- a/server/pruef-css-klassen.mjs +++ b/server/pruef-css-klassen.mjs @@ -426,5 +426,69 @@ console.log("\n=== Gegenprobe ==="); "aus classList.add(bedingung ? 'a' : 'b') werden beide Klassen gelesen, aber nicht die Variable"); } +/* ======================================================================= + DIE SIEBEN KOPIEN DER MODULLISTE MÜSSEN GLEICH SEIN + + module.css beschreibt die Form aller Module -- Silhouette, + Kantenlicht, Raster, Handy, Kontrastmodus. Dieselbe Klassenliste + steht dafür siebenmal in der Datei (im Kontrastmodus dreimal); CSS + kennt keine Variable für Selektoren. + + Das ist die verwundbare Stelle: Wer eine Karte hinzufügt und nur + sechs der sieben Stellen pflegt, bekommt eine Karte MIT Schräge und + OHNE Kantenlicht -- und niemandem fällt es auf, weil nichts kaputt + geht. Genau diese Sorte stiller Abweichung fängt hier eine Prüfung + ab, statt sich auf Sorgfalt zu verlassen. + ======================================================================= */ +console.log("\n=== module.css: die Modulliste steht siebenmal — und überall gleich"); +{ + const pfad = join(WURZEL, "workspace/assets/css/module.css"); + const text = readFileSync(pfad, "utf8"); + + /* Jede Kopie ist mit `/* MODULLISTE *​/` markiert; gelesen wird die + Klammer des `:is(...)` direkt danach. */ + const listen = []; + const marke = /\/\* MODULLISTE \*\/\s*:is\(([^)]*)\)/g; + for (let t; (t = marke.exec(text)) !== null; ) { + listen.push(t[1].split(",").map((s) => s.trim()).filter(Boolean)); + } + + /* Die Anzahl gehört in die BEDINGUNG, nicht nur in den Meldetext: + Fände die Prüfung keine einzige Liste, wäre "alle gleich" sonst + trivial erfüllt -- grün, ohne etwas angesehen zu haben. */ + ok(listen.length === 7, `sieben markierte Kopien gefunden (${listen.length})`); + + const erste = listen[0] || []; + ok(erste.length >= 30, `die Liste umfasst ${erste.length} Klassen`); + + const abweichung = listen.findIndex( + (l) => l.length !== erste.length || l.some((k, i) => k !== erste[i])); + ok(abweichung === -1, abweichung === -1 + ? "alle sieben Kopien sind Zeichen für Zeichen gleich" + : `Kopie ${abweichung + 1} weicht ab (${listen[abweichung].length} statt ${erste.length} Klassen)`); + + /* GEGENPROBE: Der Vergleich muss auch NEIN sagen können. Ohne sie + bewiese die Zeile darüber nur, dass irgendetwas verglichen wurde. */ + const geprobt = [erste, erste.slice(0, -1)]; + const findetEs = geprobt.findIndex( + (l) => l.length !== erste.length || l.some((k, i) => k !== erste[i])); + ok(findetEs === 1, "eine künstlich verkürzte Kopie wird erkannt"); + + /* Und die Datei muss auf jeder Seite ZULETZT stehen -- sonst + überschreiben die Seitendateien die Form wieder, so wie es bis zum + 07.09.2026 auf vier Seiten der Fall war. */ + let zuletzt = 0, gesamt = 0; + for (const seite of readdirSync(join(WURZEL, "workspace")).filter((f) => f.endsWith(".html"))) { + if (seite === "index.html") continue; // die Anmeldeseite lädt sie nicht + const html = readFileSync(join(WURZEL, "workspace", seite), "utf8"); + const bl = [...html.matchAll(/]+rel="stylesheet"[^>]*>/g)].map((m) => m[0]); + if (!bl.some((z) => z.includes("css/module.css"))) continue; + gesamt++; + if (bl[bl.length - 1].includes("css/module.css")) zuletzt++; + } + ok(gesamt >= 15 && zuletzt === gesamt, + `module.css steht auf allen ${gesamt} Seiten als letzte Stilvorlage (${zuletzt})`); +} + console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`); process.exit(fehler ? 1 : 0); diff --git a/server/pruef-ics.mjs b/server/pruef-ics.mjs index d4914ac4..180b8a5c 100644 --- a/server/pruef-ics.mjs +++ b/server/pruef-ics.mjs @@ -114,6 +114,13 @@ const idLunaTermin = termin(HEIKEL, idLuna, 3, "https://meet.example.invalid/rau termin(LANG, idLuna, 5); termin("Ganz normal", idLuna, 9); const idNoraTermin = termin("NORAS GEHEIMER TERMIN", idNora, 4); +/* Beas eigener Termin. Er ist seit dem 07.09.2026 noetig: Bis dahin war + Beas Kalender allein dadurch gefuellt, dass sie Nora betreut. Die + Betreuung entscheidet im Kalender aber nicht mehr mit (siehe + termineSichtbar in workspace.js) -- ohne eigenen Termin waere ihr + Kalender leer, und ein leerer Kalender ist der sauberste Befund von + allen: Er beweist gar nichts. */ +termin("BEAS EIGENER TERMIN", idBea, 6); d.close(); /* ---- Werkzeug: ICS zurueckLESEN ---------------------------------------- @@ -302,13 +309,23 @@ melde("\n=== Sichtbarkeit — der Weg ohne Anmeldung darf nicht mehr zeigen ===" ok(!icsLuna.includes("NORAS GEHEIMER TERMIN"), "Lunas Kalender enthaelt Noras Termin NICHT"); - /* GEGENPROBE: Beas Kalender MUSS ihn enthalten -- sie betreut Nora. - Ohne diesen Nachweis bewiese die Zeile darueber nur, dass der - Termin nirgends steht. */ + /* SEIT DEM 07.09.2026 SIEHT AUCH BEA NORAS TERMIN NICHT MEHR. + Hier stand vorher das Gegenteil, und es war damals richtig: Bea + betreut Nora, also stand Noras Termin in ihrem Kalender. Der + Kalender fragt seit dem Umbau nicht mehr nach Rolle oder + Betreuung, sondern nur noch nach Beteiligung -- und Bea ist an + diesem Termin nicht beteiligt. + + Die GEGENPROBE bleibt notwendig, sie braucht nur einen anderen + Beleg: Beas eigener Termin muss in ihrem Kalender stehen. Ohne + ihn bewiesen die beiden Zeilen darueber nur, dass ihr Kalender + leer ist. */ const linkBea = await linkHolen(keksBea); const icsBea = await (await fetch(linkBea)).text(); - ok(icsBea.includes("NORAS GEHEIMER TERMIN"), - "Beas Kalender enthaelt ihn sehr wohl — die Messung unterscheidet also"); + ok(!icsBea.includes("NORAS GEHEIMER TERMIN"), + "Beas Kalender enthaelt ihn ebenfalls nicht — Betreuung zaehlt hier nicht mehr"); + ok(icsBea.includes("BEAS EIGENER TERMIN"), + "ihr eigener steht sehr wohl darin — die Messung unterscheidet also"); ok(!icsBea.includes("Rückblick"), "und Lunas heikler Termin steht NICHT in Beas Kalender"); diff --git a/server/pruef-leistung-optik.mjs b/server/pruef-leistung-optik.mjs index 61a8ffa6..30eba385 100644 --- a/server/pruef-leistung-optik.mjs +++ b/server/pruef-leistung-optik.mjs @@ -137,9 +137,16 @@ async function alsRolle(rolle, code, wohin = "leistung.html", groesse = { width: 1. Die Seite ist überhaupt erreichbar — und über die Kachel ======================================================================= */ melde("\n=== Der Weg zur Seite ==="); -const man = await alsRolle("manager", "CODE-MANA-0001", "start.html"); +/* SEIT FILIPES ANWEISUNG SIEHT NUR DOGFATHER DIE ZAHLEN. + Hier stand "manager", und bis zur Umstellung war das richtig. Seit + die Kachel und die Seite auf DogFather beschraenkt sind, hat sich + diese Pruefung selbst ausgesperrt: Sie meldete sieben Fehler, von + denen keiner ein Mangel war -- sie sah nur eine Seite, die sie nicht + mehr sehen darf. Gemessen wird jetzt als DogFather; dass der Manager + NICHT hineinkommt, steht als eigene Gegenprobe darunter. */ +const chef = await alsRolle("admin", "CODE-DOGI-0001", "start.html"); { - const kachel = await man.seite.$$eval(".kachel", (k) => + const kachel = await chef.seite.$$eval(".kachel", (k) => k.map((x) => ({ name: x.querySelector(".kachel__name")?.textContent.trim(), /* Das href sitzt am .kachel__link INNEN, nicht am
  • aussen -- @@ -156,13 +163,28 @@ const man = await alsRolle("manager", "CODE-MANA-0001", "start.html"); /* GEGENPROBE zum Selektor: Ein Name, den es nicht gibt, darf NICHT gefunden werden -- sonst prüfte der Block oben nichts. */ - const erfunden = await man.seite.$$eval(".kachel", (k) => + const erfunden = await chef.seite.$$eval(".kachel", (k) => k.some((x) => x.querySelector(".kachel__name")?.textContent.trim() === "Zahlenzauber")); ok(!erfunden, "eine erfundene Kachel wird nicht gefunden"); - await man.seite.goto(`${BASIS}/workspace/leistung.html`, { waitUntil: "networkidle" }); - await man.seite.waitForTimeout(1100); - const titel = await man.seite.$eval(".titel", (e) => e.textContent.trim()); + /* GEGENPROBE ZUR REGEL SELBST: Der Manager darf sie NICHT sehen und + NICHT hineinkommen. Ohne diese beiden Zeilen bewiese der Block + oben nur, dass DogFather etwas sieht -- nicht, dass sonst niemand + es sieht. Und genau das war die Anweisung. */ + const mgr = await alsRolle("manager", "CODE-MANA-0001", "start.html"); + const beiIhm = await mgr.seite.$$eval(".kachel", (k) => + k.map((x) => x.querySelector(".kachel__name")?.textContent.trim())); + ok(beiIhm.length > 3 && !beiIhm.includes("Zahlen"), + `beim Manager fehlt sie (${beiIhm.length} Kacheln, keine davon "Zahlen")`); + + const versuch = await mgr.seite.evaluate(async (b) => + (await fetch(b + "/workspace/leistung.html", { redirect: "manual" })).status, BASIS); + ok(versuch !== 200, `und die Seite selbst laesst ihn nicht herein (HTTP ${versuch})`); + await mgr.ctx.close(); + + await chef.seite.goto(`${BASIS}/workspace/leistung.html`, { waitUntil: "networkidle" }); + await chef.seite.waitForTimeout(1100); + const titel = await chef.seite.$eval(".titel", (e) => e.textContent.trim()); ok(titel.length > 0, `die Seite ist offen: "${titel}"`); } @@ -171,14 +193,14 @@ const man = await alsRolle("manager", "CODE-MANA-0001", "start.html"); ======================================================================= */ melde("\n=== Einen Tag eintragen ==="); { - const vorher = await man.seite.$$eval(".zahlkarte__wert", (w) => w.map((x) => x.textContent)); + const vorher = await chef.seite.$$eval(".zahlkarte__wert", (w) => w.map((x) => x.textContent)); ok(vorher.length >= 5, `die Wochenkarten stehen da (${vorher.length} Werte) — sonst prüft alles Folgende nichts`); /* Die laufende Woche ist leer: Diamanten müssen 0 sein. Steht hier schon etwas, ist der Bestand oben falsch aufgebaut und die Prüfung misst etwas anderes als sie behauptet. */ - const diaVorher = await man.seite.$$eval(".zahlkarte", (k) => { + const diaVorher = await chef.seite.$$eval(".zahlkarte", (k) => { const t = k.find((x) => x.querySelector(".zahlkarte__name")?.textContent === "Diamanten"); return t?.querySelector(".zahlkarte__wert")?.textContent; }); @@ -186,19 +208,19 @@ melde("\n=== Einen Tag eintragen ==="); /* Der erste "eintragen"-Knopf ist der von HEUTE (die Liste läuft rückwärts). */ - const knoepfe = await man.seite.$$(".tagzeile__knopf"); + const knoepfe = await chef.seite.$$(".tagzeile__knopf"); ok(knoepfe.length === 14, `es gibt 14 Tageszeilen mit Knopf (${knoepfe.length})`); await knoepfe[0].click(); - await man.seite.waitForSelector("#tag-dialog[open]", { timeout: 5000 }); + await chef.seite.waitForSelector("#tag-dialog[open]", { timeout: 5000 }); - await man.seite.fill("#f-diamanten", "1250"); - await man.seite.fill("#f-dauer", "120"); - await man.seite.fill("#f-verweildauer", "260"); - await man.seite.fill("#f-notiz", "PK gegen Mia"); - await man.seite.click("#tag-speichern"); - await man.seite.waitForTimeout(1400); + await chef.seite.fill("#f-diamanten", "1250"); + await chef.seite.fill("#f-dauer", "120"); + await chef.seite.fill("#f-verweildauer", "260"); + await chef.seite.fill("#f-notiz", "PK gegen Mia"); + await chef.seite.click("#tag-speichern"); + await chef.seite.waitForTimeout(1400); - const dia = await man.seite.$$eval(".zahlkarte", (k) => { + const dia = await chef.seite.$$eval(".zahlkarte", (k) => { const t = k.find((x) => x.querySelector(".zahlkarte__name")?.textContent === "Diamanten"); return t?.querySelector(".zahlkarte__wert")?.textContent; }); @@ -206,7 +228,7 @@ melde("\n=== Einen Tag eintragen ==="); /* DER VERGLEICH. Ohne ihn ist die Zahl eine Behauptung -- das ist die Regel, wegen der dieses Modul überhaupt so gebaut ist. */ - const wandel = await man.seite.$$eval(".zahlkarte", (k) => { + const wandel = await chef.seite.$$eval(".zahlkarte", (k) => { const t = k.find((x) => x.querySelector(".zahlkarte__name")?.textContent === "Diamanten"); const w = t?.querySelector(".zahlkarte__wandel"); return { text: w?.textContent || "", richtung: w?.dataset.richtung || "" }; @@ -227,7 +249,7 @@ melde("\n=== Einen Tag eintragen ==="); /* Und die Farbe muss der Richtung folgen -- sonst steht ein Minus in Grün da und niemand merkt es. */ - const farbe = await man.seite.$$eval(".zahlkarte", (k) => { + const farbe = await chef.seite.$$eval(".zahlkarte", (k) => { const t = k.find((x) => x.querySelector(".zahlkarte__name")?.textContent === "Diamanten"); return getComputedStyle(t.querySelector(".zahlkarte__wandel")).color; }); @@ -236,12 +258,12 @@ melde("\n=== Einen Tag eintragen ==="); `und die Farbe passt dazu (${farbe})`); /* Die Notiz ist der Kontext, ohne den eine Zahl in die Irre führt. */ - const notiz = await man.seite.$$eval(".tagzeile__notiz", (n) => n.map((x) => x.textContent)); + const notiz = await chef.seite.$$eval(".tagzeile__notiz", (n) => n.map((x) => x.textContent)); ok(notiz.includes("PK gegen Mia"), `die Notiz steht in der Tageszeile (${notiz.length} Notizen)`); /* GEGENPROBE: Ein Wert, der nie eingetragen wurde, darf nicht dastehen. */ - const erfunden = await man.seite.$$eval(".zahlkarte__wert", (w) => + const erfunden = await chef.seite.$$eval(".zahlkarte__wert", (w) => w.some((x) => x.textContent === "9.999")); ok(!erfunden, "ein nie eingetragener Wert erscheint nicht"); } @@ -257,7 +279,7 @@ melde("\n=== Die kleine Verlaufslinie ==="); und der Browser zeichnete wortlos gar nichts. Ein SVG mit points="NaN,NaN" ist im DOM vorhanden und auf dem Bildschirm unsichtbar. Deshalb werden die PUNKTE gemessen. */ - const linien = await man.seite.$$eval(".zahlkarte__linie polyline", (ls) => + const linien = await chef.seite.$$eval(".zahlkarte__linie polyline", (ls) => ls.map((l) => l.getAttribute("points") || "")); ok(linien.length >= 3, `${linien.length} Verlaufslinien sind gezeichnet`); @@ -272,20 +294,20 @@ melde("\n=== Die kleine Verlaufslinie ==="); `jede Linie hat mindestens drei Punkte (${zuKurz.length} zu kurz)`); /* Auch der Endpunkt-Kreis. */ - const kreise = await man.seite.$$eval(".zahlkarte__linie circle", (cs) => + const kreise = await chef.seite.$$eval(".zahlkarte__linie circle", (cs) => cs.map((c) => `${c.getAttribute("cx")},${c.getAttribute("cy")}`)); ok(kreise.length === linien.length && !kreise.some((k) => /NaN/.test(k)), `jede Linie hat einen Endpunkt (${kreise.length})`); /* GEGENPROBE zum Selektor: Nach dem Entfernen darf nichts mehr gefunden werden. */ - const nachher = await man.seite.evaluate(() => { + const nachher = await chef.seite.evaluate(() => { document.querySelectorAll(".zahlkarte__linie").forEach((e) => e.remove()); return document.querySelectorAll(".zahlkarte__linie polyline").length; }); ok(nachher === 0, "der Selektor trifft wirklich die Linien (nach dem Entfernen 0)"); - await man.seite.reload({ waitUntil: "networkidle" }); - await man.seite.waitForTimeout(1200); + await chef.seite.reload({ waitUntil: "networkidle" }); + await chef.seite.waitForTimeout(1200); } /* ======================================================================= @@ -293,17 +315,17 @@ melde("\n=== Die kleine Verlaufslinie ==="); ======================================================================= */ melde("\n=== Ziele ==="); { - const leerText = await man.seite.$eval("#zielbalken", (e) => e.textContent.trim()); + const leerText = await chef.seite.$eval("#zielbalken", (e) => e.textContent.trim()); ok(/keine Ziele/i.test(leerText), `ohne Ziel steht ein Satz da: "${leerText.slice(0, 60)}…"`); - await man.seite.click("#ziele-aendern"); - await man.seite.waitForSelector("#ziele-dialog[open]", { timeout: 5000 }); - await man.seite.fill("#z-diamanten", "2500"); - await man.seite.fill("#z-tage", "5"); - await man.seite.click("#ziele-speichern"); - await man.seite.waitForTimeout(1300); + await chef.seite.click("#ziele-aendern"); + await chef.seite.waitForSelector("#ziele-dialog[open]", { timeout: 5000 }); + await chef.seite.fill("#z-diamanten", "2500"); + await chef.seite.fill("#z-tage", "5"); + await chef.seite.click("#ziele-speichern"); + await chef.seite.waitForTimeout(1300); - const balken = await man.seite.$$eval(".zielbalken__zeile", (zs) => + const balken = await chef.seite.$$eval(".zielbalken__zeile", (zs) => zs.map((z) => ({ name: z.querySelector(".zielbalken__name")?.textContent, zahl: z.querySelector(".zielbalken__zahl")?.textContent, @@ -316,12 +338,12 @@ melde("\n=== Ziele ==="); ok(dia?.breite === "50%", `der Balken ist zur Hälfte gefüllt (${dia?.breite})`); /* KEIN BALKEN ÜBER 100 % -- er hinge sonst aus dem Kasten. */ - await man.seite.click("#ziele-aendern"); - await man.seite.waitForSelector("#ziele-dialog[open]", { timeout: 5000 }); - await man.seite.fill("#z-diamanten", "500"); - await man.seite.click("#ziele-speichern"); - await man.seite.waitForTimeout(1300); - const voll = await man.seite.$$eval(".zielbalken__zeile", (zs) => { + await chef.seite.click("#ziele-aendern"); + await chef.seite.waitForSelector("#ziele-dialog[open]", { timeout: 5000 }); + await chef.seite.fill("#z-diamanten", "500"); + await chef.seite.click("#ziele-speichern"); + await chef.seite.waitForTimeout(1300); + const voll = await chef.seite.$$eval(".zielbalken__zeile", (zs) => { const z = zs.find((x) => x.querySelector(".zielbalken__name")?.textContent === "Diamanten"); return { breite: z?.querySelector(".zielbalken__fuell")?.style.width, @@ -346,23 +368,23 @@ melde("\n=== Aus Datei einlesen ==="); "kein-datum;500;60;100", ].join("\n"); - await man.seite.click("#import-oeffnen"); - await man.seite.waitForSelector("#import-dialog[open]", { timeout: 5000 }); + await chef.seite.click("#import-oeffnen"); + await chef.seite.waitForSelector("#import-dialog[open]", { timeout: 5000 }); - const gesperrt = await man.seite.$eval("#import-los", (b) => b.disabled); + const gesperrt = await chef.seite.$eval("#import-los", (b) => b.disabled); ok(gesperrt, "der Übernehmen-Knopf ist gesperrt, solange keine Datei da ist"); - await man.seite.setInputFiles("#datei", { + await chef.seite.setInputFiles("#datei", { name: "export.csv", mimeType: "text/csv", buffer: Buffer.from(csv, "utf8"), }); - await man.seite.waitForSelector("#vorschau:not([hidden])", { timeout: 8000 }); + await chef.seite.waitForSelector("#vorschau:not([hidden])", { timeout: 8000 }); - const zahlen = await man.seite.$$eval(".import-zahl", (zs) => + const zahlen = await chef.seite.$$eval(".import-zahl", (zs) => zs.map((z) => `${z.querySelector(".import-zahl__wert")?.textContent} ${z.querySelector(".import-zahl__name")?.textContent}`)); ok(zahlen.some((z) => /^2 neu/.test(z)), `die Vorschau zählt: ${zahlen.join(" · ")}`); ok(zahlen.some((z) => /^1 unlesbar/.test(z)), "die unlesbare Zeile wird gemeldet, nicht verschluckt"); - const spalten = await man.seite.$eval(".import-spalten", (e) => e.textContent); + const spalten = await chef.seite.$eval(".import-spalten", (e) => e.textContent); ok(/Diamanten/.test(spalten) && /LIVE-Dauer/.test(spalten), `die erkannten Spalten stehen da: "${spalten}"`); @@ -370,22 +392,22 @@ melde("\n=== Aus Datei einlesen ==="); geschrieben worden sein. Eine Vorschau, die schon schreibt, ist keine Vorschau. */ const vorAntwort = await fetch(`${BASIS}/workspace/api/leistung/${idLuna}/tage?tage=7`, { - headers: { cookie: (await man.ctx.cookies()).map((c) => `${c.name}=${c.value}`).join("; ") }, + headers: { cookie: (await chef.ctx.cookies()).map((c) => `${c.name}=${c.value}`).join("; ") }, }); const vorTage = (await vorAntwort.json()).tage || []; const schonDa = vorTage.some((t) => t.tag === tagVor(3)); ok(!schonDa, "vor dem Bestätigen steht noch nichts in der Datenbank"); - await man.seite.click("#import-los"); - await man.seite.waitForTimeout(1600); + await chef.seite.click("#import-los"); + await chef.seite.waitForTimeout(1600); - const nachTage = await man.seite.$$eval(".tagzeile", (zs) => + const nachTage = await chef.seite.$$eval(".tagzeile", (zs) => zs.filter((z) => z.dataset.quelle === "import").length); ok(nachTage === 2, `nach dem Bestätigen sind 2 Zeilen aus dem Import da (${nachTage})`); /* DER TAUSENDERPUNKT. "1.480" darf nicht als 1,48 ankommen -- Faktor tausend daneben, und die Zahl sieht trotzdem plausibel aus. */ - const werte = await man.seite.$$eval(".tagzeile", (zs) => + const werte = await chef.seite.$$eval(".tagzeile", (zs) => zs.filter((z) => z.dataset.quelle === "import") .map((z) => z.querySelector(".tagzeile__wertzahl")?.textContent)); ok(werte.includes("1.480"), `"1.480" ist als 1480 angekommen (${werte.join(", ")})`); @@ -397,26 +419,23 @@ melde("\n=== Aus Datei einlesen ==="); 6. Der Creator sieht seine Zahlen — und darf sie nicht eintragen ======================================================================= */ melde("\n=== Was der Creator sieht ==="); -const luna = await alsRolle("creator", "CODE-CREA-0001"); +const luna = await alsRolle("creator", "CODE-CREA-0001", "start.html"); { - const karten = await luna.seite.$$eval(".zahlkarte__wert", (w) => w.map((x) => x.textContent)); - ok(karten.length >= 5, `Luna sieht ihre Zahlen (${karten.length} Werte)`); - /* 1250 von Hand + 1480 + 970 aus dem Import = 3700. Die Zahl steht - hier ausgerechnet und nicht abgeschrieben: So fällt auf, wenn der - Import etwas dazuzählt oder verschluckt. */ - ok(karten.includes("3.700"), - `darunter ihre Diamanten, 1250+1480+970 (${karten.slice(0, 4).join(", ")})`); + /* HIER STAND DAS GEGENTEIL, UND ES WAR RICHTIG -- bis Filipe die + Regel geändert hat: "nimm die kategorie zahlen bei jedem weg, nur + die rolle dogfather soll die sehen." Vorher sah Luna ihre eigenen + Zahlen (nur ohne Eintragen-Knopf), jetzt kommt sie gar nicht mehr + hinein. Die Prüfung misst deshalb nicht mehr, WAS sie dort sieht, + sondern DASS sie nicht hineinkommt -- und dass die Schranke am + Server sitzt, nicht bloß in der Oberfläche. */ + const keineKachel = await luna.seite.$$eval(".kachel", (k) => + k.map((x) => x.querySelector(".kachel__name")?.textContent.trim())); + ok(keineKachel.length > 2 && !keineKachel.includes("Zahlen"), + `bei Luna fehlt die Kachel "Zahlen" (${keineKachel.length} Kacheln)`); - const knoepfe = await luna.seite.$$(".tagzeile__knopf"); - ok(knoepfe.length === 0, `sie hat keinen Eintragen-Knopf (${knoepfe.length})`); - - const zieleKnopf = await luna.seite.$eval("#ziele-aendern", (b) => b.hidden); - ok(zieleKnopf, "und kann auch keine Ziele setzen"); - - /* Kein Auswahlfeld: Sie hat nur sich selbst, und eine Liste mit einem - Eintrag ist eine Frage ohne Anlass. */ - const wahl = await luna.seite.$eval("#creator-wahl", (e) => e.hidden); - ok(wahl, "es gibt kein Creator-Auswahlfeld für sie"); + const rein = await luna.seite.evaluate(async () => + (await fetch("/workspace/leistung.html", { credentials: "same-origin" })).redirected); + ok(rein === true, "und die Seite selbst weist sie ab (Umleitung)"); /* DIE SCHRANKE SITZT AM SERVER, nicht in der Oberfläche. Ein versteckter Knopf ist keine Sicherung. */ @@ -439,7 +458,7 @@ melde("\n=== Lesbar und bedienbar ==="); /* Kleiner als 12 px liest niemand auf einem Bildschirm, an dem er jeden Tag sitzt. Zweimal ist mir das schon durchgerutscht (10,56 und 10,88 px) -- deshalb wird es gemessen, nicht angesehen. */ - const klein = await man.seite.$$eval( + const klein = await chef.seite.$$eval( ".zahlkarte *, .zielbalken *, .tagzeile *, .import-vorschau *", (es) => es .filter((e) => e.textContent.trim() && !e.children.length) @@ -451,19 +470,33 @@ melde("\n=== Lesbar und bedienbar ==="); /* Die ZAHL der geprüften Elemente gehört in die Bedingung, nicht nur in den Meldetext. Eine leere Auswahl wäre sonst grün, ohne etwas angesehen zu haben. */ - const gezaehlt = await man.seite.$$eval( + const gezaehlt = await chef.seite.$$eval( ".zahlkarte *, .zielbalken *, .tagzeile *", (es) => es.filter((e) => e.textContent.trim() && !e.children.length).length); ok(gezaehlt > 40, `und es wurden wirklich Elemente gemessen (${gezaehlt})`); /* HANDY: Der Eintragen-Knopf muss mit dem Daumen zu treffen sein. */ - const handy = await alsRolle("manager", "CODE-MANA-0001", "leistung.html", + const handy = await alsRolle("admin", "CODE-DOGI-0001", "leistung.html", { width: 390, height: 844 }); - const masse = await handy.seite.$$eval(".tagzeile__knopf", (bs) => - bs.map((b) => { const r = b.getBoundingClientRect(); return { h: Math.round(r.height), w: Math.round(r.width) }; })); + /* NUR SICHTBARE KNÖPFE. Die Seite zeigt zwei Wochen, und die zweite + ist zugeklappt -- ihre Knöpfe messen 0×0. Genau die meldete die + Prüfung als "zu klein": ein Fingerziel, das niemand sehen kann, + ist kein zu kleines Fingerziel, sondern gar keins. Sieben + angebliche Mängel, kein einziger echter. Deshalb wird die Zahl der + wirklich sichtbaren mitgeführt und in die Bedingung genommen -- + wäre plötzlich keiner mehr sichtbar, muss das auffallen und darf + nicht als "alle groß genug" durchgehen. */ + const masse = await handy.seite.$$eval(".tagzeile__knopf", (bs) => bs + .filter((b) => b.checkVisibility ? b.checkVisibility() : b.offsetParent !== null) + .map((b) => { const r = b.getBoundingClientRect(); return { h: Math.round(r.height), w: Math.round(r.width) }; }) + .filter((m) => m.w > 0 && m.h > 0)); const zuKlein = masse.filter((m) => m.h < 38 || m.w < 60); - ok(masse.length > 0 && zuKlein.length === 0, - `alle ${masse.length} Eintragen-Knöpfe sind groß genug (${zuKlein.length} zu klein)`); + /* Die Maße der Ausreißer gehören in die Meldung. "7 zu klein" sagt + nur, DASS etwas nicht stimmt; "38×44" sagt, WAS -- und erspart + einen zweiten Lauf, nur um nachzusehen. */ + ok(masse.length >= 7 && zuKlein.length === 0, + `alle ${masse.length} Eintragen-Knöpfe sind groß genug (${zuKlein.length} zu klein` + + `${zuKlein.length ? ": " + zuKlein.slice(0, 4).map((m) => `${m.w}×${m.h}`).join(", ") : ""})`); /* NICHTS DARF SEITLICH HERAUSHÄNGEN. Waagerechtes Scrollen auf dem Handy ist der sicherste Weg, eine Seite unbenutzbar zu machen. */ @@ -494,10 +527,10 @@ melde("\n=== Lesbar und bedienbar ==="); ======================================================================= */ melde("\n=== Dashboard ==="); { - await man.seite.goto(`${BASIS}/workspace/uebersicht.html`, { waitUntil: "networkidle" }); - await man.seite.waitForTimeout(1600); + await chef.seite.goto(`${BASIS}/workspace/uebersicht.html`, { waitUntil: "networkidle" }); + await chef.seite.waitForTimeout(1600); - const zahlen = await man.seite.$$eval(".kzahlen", (zs) => + const zahlen = await chef.seite.$$eval(".kzahlen", (zs) => zs.map((z) => ({ ziel: z.getAttribute("href"), felder: [...z.querySelectorAll(".kzahlen__feld")].map((f) => @@ -511,21 +544,21 @@ melde("\n=== Dashboard ==="); /* DIE FRÜHWARNUNG. Der Bestand oben fällt über drei Wochen ab und hat diese Woche weniger LIVE-Tage -- da MUSS etwas stehen. */ - const warnung = await man.seite.$eval("#fruehwarnung", (e) => e.hidden); + const warnung = await chef.seite.$eval("#fruehwarnung", (e) => e.hidden); ok(!warnung, "die Frühwarnung ist sichtbar"); - const signale = await man.seite.$$eval(".warnsignal__text", (ts) => ts.map((t) => t.textContent)); + const signale = await chef.seite.$$eval(".warnsignal__text", (ts) => ts.map((t) => t.textContent)); ok(signale.length > 0, `${signale.length} Signale: ${signale.join(" | ").slice(0, 140)}`); /* KEINE PUNKTZAHL. Genau das war die Entscheidung: sechs benannte Beobachtungen statt eines Werts, dem man nicht widersprechen kann. */ - const punktzahl = await man.seite.evaluate(() => + const punktzahl = await chef.seite.evaluate(() => /risiko|score|punkte?zahl/i.test(document.getElementById("fruehwarnung")?.textContent || "")); ok(!punktzahl, "und nirgends steht eine Risiko-Punktzahl"); /* Der Weg zum Gespräch. Ein Hinweis ohne Weg dahin ist nur ein schlechtes Gewissen. */ - const wege = await man.seite.$$eval(".warnkarte__weg", (ws) => ws.map((w) => w.getAttribute("href"))); + const wege = await chef.seite.$$eval(".warnkarte__weg", (ws) => ws.map((w) => w.getAttribute("href"))); ok(wege.length > 0 && wege.every((w) => /^chat\.html\?mit=\d+$/.test(w)), `jede Warnkarte führt ins Gespräch (${wege.join(", ")})`); @@ -533,11 +566,11 @@ melde("\n=== Dashboard ==="); Fläche mit eigenem CSS. Beim ersten Lauf stand .kzahlen__name auf .66 rem = 10,56 px; genau derselbe Wert, der mir schon zweimal durchgerutscht ist, weil ihn niemand misst. */ - const kleinDash = await man.seite.$$eval(".kzahlen *, .warnkarte *", (es) => es + const kleinDash = await chef.seite.$$eval(".kzahlen *, .warnkarte *", (es) => es .filter((e) => e.textContent.trim() && !e.children.length) .map((e) => ({ t: e.textContent.trim().slice(0, 24), px: parseFloat(getComputedStyle(e).fontSize) })) .filter((x) => x.px < 12)); - const gezaehltDash = await man.seite.$$eval(".kzahlen *, .warnkarte *", + const gezaehltDash = await chef.seite.$$eval(".kzahlen *, .warnkarte *", (es) => es.filter((e) => e.textContent.trim() && !e.children.length).length); ok(gezaehltDash > 8 && kleinDash.length === 0, `im Dashboard keine Schrift unter 12 px (${gezaehltDash} gemessen, ${kleinDash.length} zu klein` @@ -546,9 +579,9 @@ melde("\n=== Dashboard ==="); /* Und dieser Weg muss WIRKLICH funktionieren. ?mit= war bis zum 06.09.2026 gar nicht vorgesehen -- ein Link, der auf einer leeren Gesprächsliste endet, ist schlimmer als keiner. */ - await man.seite.goto(`${BASIS}/workspace/${wege[0]}`, { waitUntil: "networkidle" }); - await man.seite.waitForTimeout(1800); - const offen = await man.seite.$eval("#verlauf-titel", (e) => e.textContent.trim()); + await chef.seite.goto(`${BASIS}/workspace/${wege[0]}`, { waitUntil: "networkidle" }); + await chef.seite.waitForTimeout(1800); + const offen = await chef.seite.$eval("#verlauf-titel", (e) => e.textContent.trim()); ok(offen === "Luna", `der Link öffnet das Gespräch mit "${offen}"`); } @@ -573,7 +606,7 @@ melde("\n=== Was der Creator NICHT sieht ==="); } await luna.ctx.close(); -await man.ctx.close(); +await chef.ctx.close(); await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } diff --git a/server/pruef-personen-formular.mjs b/server/pruef-personen-formular.mjs index 6b041875..29167738 100644 --- a/server/pruef-personen-formular.mjs +++ b/server/pruef-personen-formular.mjs @@ -114,7 +114,11 @@ const mass = await dogi.evaluate(() => { }; }); -ok(mass.karten === 4, `alle vier Rollen stehen zur Wahl (${mass.karten})`); +/* FÜNF, seit es Spicy Media gibt. Die Zahl steht hier absichtlich fest + und wird nicht aus der Oberfläche abgeleitet: Sie ist die Erwartung, + gegen die gemessen wird. Fiele eine Rolle aus der Wahl, müsste das + auffallen -- eine Zahl, die sich selbst nachzählt, kann das nicht. */ +ok(mass.karten === 5, `alle fünf Rollen stehen zur Wahl (${mass.karten})`); ok(mass.wahlUnten <= mass.formUnten, `die Rollenauswahl endet INNERHALB des Formulars (${mass.wahlUnten} ≤ ${mass.formUnten})`); ok(!mass.ueberRand, "keine Karte ragt seitlich aus dem Formular"); @@ -150,27 +154,60 @@ await handy.screenshot({ path: "pruef-personen-formular-handy.png", fullPage: fa TEIL 2 — Die beiden Bereiche gehören DogFather allein =================================================================== */ -console.log("\n— Personen & Automationen: nur DogFather —"); +console.log("\n— Automationen: nur DogFather. Personen: auch die Leitung —"); +/* AM 07.09.2026 GETEILT. Hier standen beide Bereiche unter derselben + Regel: „gehören DogFather allein". Für die Automationen gilt das + weiter. Für die Personenseite hat Filipe es umgedreht -- Manager + sollen „creator und auch wirklich nur creator hinzufügen können", + und dafür müssen sie hinein. + + Aus einer Regel sind damit ZWEI geworden, und beide werden gemessen: + Die Seite geht auf, die Verwaltungsdaten dahinter bleiben zu. Genau + diese Trennung IST die Sicherung. Hätte ich hier nur die Zeilen + gelöscht, wäre der neue Weg ab sofort ungeprüft -- und die Zahl der + Prüfungen wäre gesunken, ohne dass jemand hinsieht. */ const cigdem = await anmelden("manager", "CODE-CIGDEM-01"); await cigdem.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); await cigdem.waitForTimeout(800); const kacheln = await cigdem.evaluate(() => [...document.querySelectorAll(".kachel")].map((k) => k.innerText.split("\n")[0].trim())); -ok(!kacheln.some((t) => /Personen/.test(t)), - "1. die Kachel „Personen & Zugänge“ fehlt bei der Managerin"); +ok(kacheln.some((t) => /Personen/.test(t)), + "1. die Kachel „Personen & Zugänge“ steht jetzt bei der Managerin"); ok(!kacheln.some((t) => /Automationen/.test(t)), - "1. die Kachel „Automationen“ fehlt ebenfalls"); + "1. die Kachel „Automationen“ fehlt weiterhin"); ok(kacheln.length > 8, - ` – aber der Rest steht noch da (${kacheln.length} Kacheln), es ist nicht alles weg`); + ` – und der Rest steht auch da (${kacheln.length} Kacheln)`); -/* 2. Die SEITE, falls jemand die Adresse kennt. */ -for (const pfad of ["personen.html", "automation.html"]) { - const a = await cigdem.goto(BASIS + "/workspace/" + pfad, { waitUntil: "networkidle" }); - const gelandet = cigdem.url(); - ok(gelandet.endsWith("/start.html"), - `2. ${pfad} direkt aufgerufen führt zurück zur Startseite (${gelandet.split("/").pop()})`); +/* 2. Die SEITEN, falls jemand die Adresse kennt. */ +{ + await cigdem.goto(BASIS + "/workspace/automation.html", { waitUntil: "networkidle" }); + ok(cigdem.url().endsWith("/start.html"), + `2. automation.html führt zurück zur Startseite (${cigdem.url().split("/").pop()})`); + + await cigdem.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); + await cigdem.waitForTimeout(900); + ok(cigdem.url().endsWith("/personen.html"), + `2. personen.html geht bei ihr auf (${cigdem.url().split("/").pop()})`); + + /* Und sie sieht dort NUR das Anlegen -- keine Liste, kein Protokoll, + und keinen Ladehinweis ohne Ende. Der stand dort wirklich: + `laden()` wird für sie mit Absicht nicht aufgerufen, und niemand + hat aria-busy zurückgesetzt. Für einen Screenreader lud die Seite + dann für immer. */ + const seitenbild = await cigdem.evaluate(() => ({ + nurAnlegen: document.body.dataset.nurAnlegen || null, + laedtNoch: document.querySelectorAll('[aria-busy="true"]').length, + erklaerung: document.getElementById("unterzeile")?.textContent.trim() || "", + formularDa: !!document.getElementById("neu") && !document.getElementById("neu").hidden, + })); + ok(seitenbild.nurAnlegen === "ja" && seitenbild.formularDa === true, + "2. sie sieht dort das Anlegen-Formular"); + ok(seitenbild.laedtNoch === 0, + `2. und nichts hängt auf „wird geladen“ (${seitenbild.laedtNoch})`); + ok(seitenbild.erklaerung.includes("DogFather"), + `2. ein Satz erklärt, warum hier so wenig steht ("${seitenbild.erklaerung.slice(0, 40)}…")`); } /* 3. Die SCHNITTSTELLE, falls jemand ganz an der Seite vorbeigeht. diff --git a/server/pruef-schranke.mjs b/server/pruef-schranke.mjs index 6f05f8de..981b53e4 100644 --- a/server/pruef-schranke.mjs +++ b/server/pruef-schranke.mjs @@ -181,7 +181,18 @@ console.log("\n=== Falsche Rolle ==="); /* Was jede Rolle NICHT sehen darf, steht ebenfalls in workspace.js. Hier wird gegengeprueft, dass die Umleitung wirklich greift. */ -const NUR_DOGFATHER = ["/workspace/personen.html", "/workspace/automation.html"]; +/* PERSONEN.HTML IST SEIT DEM 07.09.2026 NICHT MEHR NUR FUER DOGFATHER. + Filipe: Manager sollen "creator und auch wirklich nur creator + hinzufuegen koennen" -- dafuer muessen sie die Seite oeffnen duerfen. + Sie steht deshalb nicht mehr in dieser Liste, sondern in der Liste + darunter, mit ihrer eigenen, engeren Erwartung. Automationen bleiben + allein bei DogFather. + + Wichtig fuer die Aussagekraft: Die Seite VERSCHWINDET nicht aus der + Pruefung, sie wechselt die Erwartung. Haette ich sie nur + herausgenommen, waere die Zahl der Pruefungen gesunken und der + Manager-Weg ab da ungeprueft geblieben. */ +const NUR_DOGFATHER = ["/workspace/automation.html"]; for (const seite of NUR_DOGFATHER) { for (const rolle of ["manager", "scout", "creator"]) { const a = await hol(seite, kekse[rolle]); @@ -191,6 +202,25 @@ for (const seite of NUR_DOGFATHER) { ok(a.status === 200, `DogFather auf ${seite} -> ${a.status} (erwartet 200)`); } +/* Die Personenseite: Leitung ja, Scout und Creator nein. */ +const NUR_LEITUNG = "/workspace/personen.html"; +{ + for (const rolle of ["scout", "creator"]) { + const a = await hol(NUR_LEITUNG, kekse[rolle]); + ok(a.status === 302, `${rolle} auf ${NUR_LEITUNG} -> ${a.status} (erwartet 302)`); + } + for (const rolle of ["admin", "manager"]) { + const a = await hol(NUR_LEITUNG, kekse[rolle]); + ok(a.status === 200, `${rolle} auf ${NUR_LEITUNG} -> ${a.status} (erwartet 200)`); + } + /* Und die Verwaltungsschnittstelle dahinter bleibt trotzdem zu: + Die Seite aufmachen zu duerfen heisst nicht, die Personenliste + lesen zu duerfen. Genau daran haengt, dass ein Manager nur + anlegen und nicht verwalten kann. */ + const v = await hol("/workspace/api/verwaltung/personen", kekse.manager); + ok(v.status === 404, `aber die Verwaltungsdaten bleiben zu (${v.status}, erwartet 404)`); +} + const NUR_TEAM = "/workspace/scouting.html"; { const a = await hol(NUR_TEAM, kekse.creator); @@ -204,9 +234,11 @@ const NUR_TEAM = "/workspace/scouting.html"; console.log("\n=== Falsche Rolle + andere Schreibweise ==="); const rolleDurchgerutscht = []; let rolleGeprueft = 0; -for (const seite of NUR_DOGFATHER) { +for (const seite of [...NUR_DOGFATHER, NUR_LEITUNG]) { for (const [pfad, wie] of VARIANTEN(seite)) { - for (const rolle of ["manager", "creator"]) { + /* Fuer die Personenseite zaehlt hier nur der Creator -- der Manager + DARF sie inzwischen sehen, also waere ein 200 dort kein Loch. */ + for (const rolle of seite === NUR_LEITUNG ? ["creator"] : ["manager", "creator"]) { let a; try { a = await hol(pfad, kekse[rolle]); } catch { continue; } rolleGeprueft++; @@ -219,7 +251,18 @@ for (const seite of NUR_DOGFATHER) { } } } -ok(rolleGeprueft > 40, `${rolleGeprueft} Versuche mit falscher Rolle`); +/* DIE ZAHL WIRD AUSGERECHNET, NICHT ABGESCHRIEBEN. Vorher stand hier + "> 40" -- eine Zahl aus dem Moment, in dem zwei Seiten mal zwei + Rollen geprueft wurden. Als die Personenseite fuer Manager aufging + und dort nur noch der Creator zu pruefen war, fielen es 33, und die + Pruefung wurde rot, ohne dass ein Weg ungeprueft geblieben waere. + Jetzt wird die Erwartung aus denselben Listen gerechnet, aus denen + auch geprueft wird: Sie stimmt automatisch, und ein WIRKLICHER + Ausfall (eine Schleife, die nicht laeuft) faellt trotzdem auf. */ +const erwarteteVersuche = VARIANTEN(NUR_DOGFATHER[0]).length * NUR_DOGFATHER.length * 2 + + VARIANTEN(NUR_LEITUNG).length; +ok(rolleGeprueft === erwarteteVersuche && rolleGeprueft > 25, + `${rolleGeprueft} Versuche mit falscher Rolle (erwartet ${erwarteteVersuche})`); ok(rolleDurchgerutscht.length === 0, rolleDurchgerutscht.length ? `ROLLE UMGANGEN: ${rolleDurchgerutscht.slice(0, 6).join(" · ")}` diff --git a/server/pruef-serien.mjs b/server/pruef-serien.mjs index 3d726d78..2c021f4f 100644 --- a/server/pruef-serien.mjs +++ b/server/pruef-serien.mjs @@ -389,7 +389,19 @@ const liste = await seiteDogi.evaluate(() => ({ })); ok(liste.sichtbar, "die Leiste „Läuft von allein\" ist sichtbar"); -ok(liste.serien.length === 2, `beide Serien stehen darin (${liste.serien.length})`); +/* SEIT DEM 07.09.2026 SIEHT AUCH DOGFATHER NUR SEINE EIGENEN. + Hier stand "beide Serien stehen darin", und das war richtig, solange + DogFather jeden Termin sah. Beide Serien gehören Tili; angelegt hat + er nur die erste (den Wochen-Call mit ihr), die zweite hat sie + selbst angelegt. Nach der neuen Regel steht in seinem Kalender + deshalb genau eine -- und dass es die RICHTIGE ist, wird gleich + mitgeprüft: eine Zahl allein wäre auch erfüllt, wenn die falsche + übrig geblieben wäre. */ +ok(liste.serien.length === 1, `seine eigene Serie steht darin (${liste.serien.length})`); +ok(liste.serien[0]?.titel === "Wochen-Call mit Tili", + `und zwar die, die er selbst angelegt hat (${liste.serien[0]?.titel})`); +ok(!liste.serien.some((s) => s.titel === "Eigener Drehtag"), + "Tilis eigene Wiederholung steht NICHT in seinem Kalender"); ok(liste.serien.some((s) => s.satz === "jeden Dienstag um 18:00 Uhr, bis du sie abstellst") || liste.serien.some((s) => /^jeden \w+ um 18:00 Uhr, bis du sie abstellst$/.test(s.satz)), "der Satz nennt Rhythmus, Uhrzeit und dass sie ohne Ende läuft"); diff --git a/server/pruef-sicht.mjs b/server/pruef-sicht.mjs index bc65b3ac..0c41047d 100644 --- a/server/pruef-sicht.mjs +++ b/server/pruef-sicht.mjs @@ -246,15 +246,23 @@ for (const [pfad, name, feld] of [ ok(a.status === 200 && b.status === 200, `${name}: beide Abfragen gehen durch`); } -/* Der Kalender ist der klarste Beleg: Patrick sieht nur seine Termine. */ +/* Der Kalender ist der klarste Beleg -- und seit dem 07.09.2026 gilt + die Regel "nur was mich betrifft" fuer JEDEN, DogFather + eingeschlossen (siehe termineSichtbar in workspace.js). Fremde + Verabredungen sieht er nicht mehr in seinem eigenen Kalender; dafuer + gibt es den Umschalter. Genau dieser Wechsel wird hier gemessen: + ohne Umschalter keins der beiden, mit Umschalter genau das eine. + Und ein Termin ohne Teilnehmer geht weiterhin alle an -- sonst + koennte die Regel auch einfach alles ausblenden und waere trotzdem + gruen. */ { const d2 = new DatabaseSync(process.env.WORKSPACE_DB); - d2.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von) - VALUES (?,?,?,?,?,?,?)`) - .run("PATRICK-TERMIN", "call", heute + "T10:00", 30, idTili, jetzt, idPat); - d2.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von) - VALUES (?,?,?,?,?,?,?)`) - .run("BEN-TERMIN", "call", heute + "T12:00", 30, idLuna, jetzt, idBen); + const rein = d2.prepare( + `INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?)`); + rein.run("PATRICK-TERMIN", "call", heute + "T10:00", 30, idTili, jetzt, idPat); + rein.run("BEN-TERMIN", "call", heute + "T12:00", 30, idLuna, jetzt, idBen); + rein.run("TEAM-TERMIN", "termin", heute + "T14:00", 30, null, jetzt, idPat); d2.close(); const j = async (k, s) => { @@ -263,9 +271,12 @@ for (const [pfad, name, feld] of [ }; const alleT = await j(kDogi); const patT = await j(kDogi, idPat); - ok(alleT.length === 2, `DogFather sieht beide Termine (${alleT.join(", ")})`); - ok(patT.length === 1 && patT[0] === "PATRICK-TERMIN", - `als Patrick nur dessen (${patT.join(", ")})`); + ok(!alleT.includes("PATRICK-TERMIN") && !alleT.includes("BEN-TERMIN"), + `DogFather sieht fremde Verabredungen nicht mehr (${alleT.join(", ") || "nichts"})`); + ok(alleT.includes("TEAM-TERMIN"), + "den Termin ohne Teilnehmer aber schon -- der geht alle an"); + ok(patT.includes("PATRICK-TERMIN") && !patT.includes("BEN-TERMIN"), + `mit dem Umschalter auf Patrick genau dessen (${patT.join(", ")})`); } /* ================================================================ diff --git a/server/pruef-spicy.mjs b/server/pruef-spicy.mjs index e1f815ac..be1740a8 100644 --- a/server/pruef-spicy.mjs +++ b/server/pruef-spicy.mjs @@ -372,16 +372,27 @@ const ruf = async (art, weg, keks, koerper) => { `auch seine Aufgabe nicht (sie sieht: ${aTitel.join(", ") || "nichts"})`); ok(aTitel.includes("Max seine Aufgabe"), "Max seine schon"); + /* Der Kalender folgt seit dem 07.09.2026 einer ANDEREN Regel als + Bereiche und Aufgaben, und das ist Absicht (Begruendung steht bei + termineSichtbar in workspace.js): Dort entscheidet die Rolle, hier + entscheidet allein die BETEILIGUNG. Wer niemanden eintraegt, meint + alle -- also sieht Spicy Media DogFathers teilnehmerlosen Termin + sehr wohl, Max' Verabredung mit Luna dagegen nicht. Genau + umgekehrt zu den beiden Bloecken darueber. Die zwei Zeilen stehen + hier zusammen, weil erst beide zeigen, dass wirklich die + Beteiligung zaehlt und nicht die Rolle -- eine allein waere auch + erfuellt, wenn schlicht alles oder gar nichts ankaeme. */ await ruf("POST", "/workspace/api/termine", keksDogi, - { titel: "DogFathers Termin", art: "termin", beginn: heute + "T20:00", dauer_min: 30 }); + { titel: "DogFathers Team-Termin", art: "termin", beginn: heute + "T20:00", dauer_min: 30 }); await ruf("POST", "/workspace/api/termine", keksMax, { titel: "Max sein Termin", art: "termin", beginn: heute + "T21:00", dauer_min: 30, creator_id: idLuna }); const term = await ruf("GET", `/workspace/api/termine?von=${heute}&bis=${heute}`, keksSpicy); const tTitel = (term.daten?.termine || []).map((t) => t.titel); - ok(!tTitel.includes("DogFathers Termin"), - `und seinen Termin nicht (sie sieht: ${tTitel.join(", ") || "nichts"})`); - ok(tTitel.includes("Max sein Termin"), "Max seinen schon"); + ok(tTitel.includes("DogFathers Team-Termin"), + `der Termin ohne Teilnehmer geht alle an -- auch sie (${tTitel.join(", ") || "nichts"})`); + ok(!tTitel.includes("Max sein Termin"), + "Max' Verabredung mit Luna dagegen nicht -- die geht nur die beiden an"); /* GEGENPROBE: DogFather selbst sieht weiterhin BEIDE. Ohne sie bewiesen die Zeilen oben nur, dass irgendetwas gefiltert wird. */ diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index 1d683f09..2b4b0ba1 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -615,7 +615,21 @@ console.log("\n=== Das Licht folgt dem Zeiger"); const soll = hexTreffer ? [0, 2, 4].map((i) => parseInt(hexTreffer[1].slice(i, i + 2), 16)) : zahlen.slice(0, 3); - return { x: r.x + r.width / 2, y: r.y + r.height / 2, soll, ton }; + /* WO DIE FARBE LIEGT, HAT SICH AM 07.09.2026 GEAENDERT. + Bis dahin lag ein Farbschleier ueber der ganzen Kachel, und + die Mitte war der richtige Messpunkt. Die neue Form traegt + die Farbe auf der KANTE -- die Flaeche ist mit Absicht + neutral. An der alten Stelle gemessen kam deshalb Grau + heraus, und die Pruefung meldete einen Fehler, den es nicht + gab. + + Gemessen wird jetzt das obere Kantenlicht: ein schmales Band + von 4 px Hoehe ueber dem linken Drittel, beginnend hinter + der Schraege. Nicht EIN Bildpunkt -- eine 1,6 px dicke Linie + auf einer 1-zu-1-Aufloesung trifft man mit einem Punkt nur + mit Glueck. Aus dem Band wird der farbigste Punkt genommen. */ + return { x: r.x + r.width / 2, y: r.y + r.height / 2, soll, ton, + band: { x: r.x + 22, y: r.y, breite: Math.max(40, r.width / 3), hoehe: 4 } }; }, name); if (!b || b.soll.length !== 3) { ok(false, `"${name}": Farbton nicht lesbar (${b?.ton})`); @@ -624,14 +638,34 @@ console.log("\n=== Das Licht folgt dem Zeiger"); await seite.mouse.move(b.x, b.y); await seite.waitForTimeout(350); const bild = await seite.screenshot({ type: "png" }); - const p = await seite.evaluate(async ({ b64, x, y }) => { + const gemessen = await seite.evaluate(async ({ b64, band }) => { const i = new Image(); i.src = "data:image/png;base64," + b64; await i.decode(); const c = document.createElement("canvas"); c.width = i.width; c.height = i.height; const g = c.getContext("2d"); g.drawImage(i, 0, 0); const s = i.width / window.innerWidth; - return [...g.getImageData(Math.round(x * s), Math.round(y * s), 1, 1).data]; - }, { b64: bild.toString("base64"), x: b.x, y: b.y }); + const d = g.getImageData(Math.round(band.x * s), Math.round(band.y * s), + Math.round(band.breite * s), Math.round(band.hoehe * s)).data; + /* Der farbigste Punkt des Bandes: groesster Abstand zwischen + staerkstem und schwaechstem Kanal. Ein grauer Streifen hat + dort fast null -- daran erkennt man auch, wenn gar kein + Licht da ist. */ + let best = [0, 0, 0], spanne = -1, punkte = 0; + for (let k = 0; k + 3 < d.length; k += 4) { + punkte++; + const v = [d[k], d[k + 1], d[k + 2]]; + const sp = Math.max(...v) - Math.min(...v); + if (sp > spanne) { spanne = sp; best = v; } + } + return { punkte, spanne, farbe: best }; + }, { b64: bild.toString("base64"), band: b.band }); + const p = gemessen.farbe; + /* DIE ANZAHL DER GEMESSENEN PUNKTE GEHOERT IN DIE BEDINGUNG. + Ein leeres Band haette sonst "Reihenfolge stimmt" gemeldet -- + 0 gegen 0 ist immer gleich. */ + ok(gemessen.punkte > 50 && gemessen.spanne >= 6, + `"${name}": auf der Oberkante liegt ueberhaupt Farbe ` + + `(${gemessen.punkte} Punkte, Spanne ${gemessen.spanne})`); /* Die REIHENFOLGE der Kanaele muss uebereinstimmen. Absolute Werte koennen es nicht: Das Licht ist stark abgedunkelt und liegt auf einem dunklen Grund, seine Zahlen sind ein Bruchteil diff --git a/server/pruef-tagesblick.mjs b/server/pruef-tagesblick.mjs index 45942683..9efb5fad 100644 --- a/server/pruef-tagesblick.mjs +++ b/server/pruef-tagesblick.mjs @@ -142,14 +142,33 @@ melde("\n=== Eine Kachel, mittig geteilt ==="); const da = await seite.isVisible("#dran-block"); ok(da, da ? "die Kachel ist da" : "die Kachel fehlt"); - /* EIN Rahmen um beides -- nicht zwei Kacheln nebeneinander. */ + /* EINE Umrandung um beides -- nicht zwei Kacheln nebeneinander. + + WIE DIESE UMRANDUNG AUSSIEHT, HAT SICH AM 07.09.2026 GEÄNDERT. + Vorher: ein `border` mit Radius. Jetzt: eine Silhouette mit + abgeschnittener Ecke (`clip-path`) und ein Kantenlicht auf + `::before`. Hier stand deshalb eine Bedingung, die zwangsläufig + rot wurde, ohne dass an der Kachel etwas fehlte -- gemessen wurde + eine Bauweise, die es nicht mehr gibt. + + Die Aussage bleibt dieselbe und wird jetzt an der neuen Bauweise + gemessen: ein Kasten, eine Silhouette, ein Licht darauf. */ const rahmen = await seite.$eval("#dran-block", (e) => { const s = getComputedStyle(e); - return { breite: s.borderTopWidth, radius: s.borderTopLeftRadius, anzeige: s.display }; + const v = getComputedStyle(e, "::before"); + return { + anzeige: s.display, + silhouette: s.clipPath, + radius: s.borderTopLeftRadius, + lichtDa: v.content !== "none" && v.position === "absolute", + }; }); - ok(rahmen.anzeige === "grid" && parseFloat(rahmen.breite) >= 1 - && parseFloat(rahmen.radius) > 4, - `ein Rahmen um beides (${rahmen.anzeige}, ${rahmen.breite}, Radius ${rahmen.radius})`); + ok(rahmen.anzeige === "grid" + && /polygon/.test(rahmen.silhouette) + && parseFloat(rahmen.radius) === 0 + && rahmen.lichtDa, + `eine Silhouette um beides (${rahmen.anzeige}, ${rahmen.silhouette.slice(0, 24)}…, ` + + `Radius ${rahmen.radius}, Kantenlicht ${rahmen.lichtDa ? "da" : "fehlt"})`); /* NEBENEINANDER und gleich breit -- das ist der Kern des Wunsches. */ const seiten = await seite.evaluate(() => { diff --git a/server/pruef-team.mjs b/server/pruef-team.mjs index 5f80b16e..24259a0a 100644 --- a/server/pruef-team.mjs +++ b/server/pruef-team.mjs @@ -226,9 +226,17 @@ const team = await a1.seite.evaluate(() => { automation.css und wissen.css -- diese Seite laedt beide nicht. Genau so sind hier schon zweimal Stile ins Leere gelaufen. Gemessen wird deshalb der tatsaechliche Stil, nicht die Klasse. */ + /* SEIT DEM 07.09.2026 IST DER RAND KEINE `border` MEHR, sondern + eine Silhouette mit abgeschnittener Ecke aus module.css. Die + alte Bedingung (border >= 1) wurde dadurch zwangsläufig rot, + ohne dass ein Stil fehlte. Die FRAGE bleibt aber genau dieselbe + und ist so wichtig wie zuvor: Ist der Stil auf DIESER Seite + angekommen? Gemessen wird sie jetzt an der neuen Bauweise -- + Polsterung wie bisher, dazu die Silhouette, die es nur gibt, + wenn module.css wirklich geladen wurde. */ gestaltet: karten[0] ? (() => { const st = getComputedStyle(karten[0]); - return parseFloat(st.borderTopWidth) >= 1 && parseFloat(st.paddingLeft) >= 8; + return parseFloat(st.paddingLeft) >= 8 && /polygon/.test(st.clipPath); })() : null, anzahl: karten.length, sichtbare: karten.filter(sichtbar).length, diff --git a/server/pruef-teilnehmer.mjs b/server/pruef-teilnehmer.mjs index 7d6f78c5..7b69ef16 100644 --- a/server/pruef-teilnehmer.mjs +++ b/server/pruef-teilnehmer.mjs @@ -278,20 +278,39 @@ console.log("\n=== Unsinn in der Teilnehmerliste ==="); ======================================================================= */ console.log("\n=== Gegenprobe ==="); { + /* SEIT DEM 07.09.2026 IST EIN TERMIN OHNE TEILNEHMER DER ÖFFENTLICHE + FALL, nicht der geheime (siehe termineSichtbar in workspace.js): + "Wer niemanden einträgt, meint alle." Hier stand deshalb vorher + das Gegenteil — ein teilnehmerloser Termin, von dem erwartet + wurde, dass Luna ihn NICHT sieht. + + Für eine Gegenprobe braucht es aber weiterhin einen Termin, den + sie wirklich nicht sehen darf. Das ist jetzt der Termin MIT einem + anderen Gegenüber: eine Verabredung zwischen zwei anderen. */ const a = await ruf("POST", "/workspace/api/termine", { - titel: "Nur fuer DogFather", art: "termin", beginn: heute + "T20:00", + titel: "Verabredung ohne Luna", art: "termin", beginn: heute + "T20:00", + teilnehmer: [idBea], }, K.dogi); const geheim = a.daten?.id; - ok(geheim > 0, `ein Termin ohne Teilnehmer angelegt (#${geheim})`); - ok(await siehtTermin(K.dogi, geheim), "DogFather sieht ihn"); + ok(geheim > 0, `ein Termin mit Bea, ohne Luna, angelegt (#${geheim})`); + ok(await siehtTermin(K.dogi, geheim), "DogFather sieht ihn — er hat ihn angelegt"); ok(!(await siehtTermin(K.luna, geheim)), "Luna sieht ihn NICHT — die Messung erkennt also 'nicht sichtbar'"); const b = await ruf("PATCH", `/workspace/api/termine/${geheim}`, - { teilnehmer: [idLuna] }, K.dogi); - ok(b.status === 200, `Luna eingetragen (${b.status})`); + { teilnehmer: [idBea, idLuna] }, K.dogi); + ok(b.status === 200, `Luna dazugenommen (${b.status})`); ok(await siehtTermin(K.luna, geheim), "jetzt sieht sie ihn — die Messung erkennt auch 'sichtbar'"); + + /* Und der neue Fall selbst, damit er nicht ungeprüft bleibt: OHNE + Teilnehmer sieht ihn jeder. Ohne diese Zeile bliebe die Regel, die + den Umbau ausgelöst hat, ausgerechnet hier ungemessen. */ + const c = await ruf("POST", "/workspace/api/termine", { + titel: "Geht alle an", art: "termin", beginn: heute + "T21:30", + }, K.dogi); + ok(await siehtTermin(K.luna, c.daten?.id), + "einen Termin ganz ohne Teilnehmer sieht dagegen jeder"); } console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`); diff --git a/server/pruef-workspace-seiten.mjs b/server/pruef-workspace-seiten.mjs index 34fe1e72..b9d95f0f 100644 --- a/server/pruef-workspace-seiten.mjs +++ b/server/pruef-workspace-seiten.mjs @@ -145,14 +145,39 @@ for (const [breite, hoehe, wie] of [[1440, 1000, "Computer"], [390, 844, "Handy" CSS-Regel wäre still wirkungslos, und niemand würde es merken -- die Seite sähe nur "etwas dunkler" aus. - Zusätzlich: Es muss auf JEDER Seite DASSELBE sein. Ohne diese - zweite Bedingung wäre die Prüfung auch dann grün, wenn irgendwo - eine der alten Szenen zurückkäme. */ - const buehneGut = r.buehneBild === "dogfather"; - const gut = r.ueber <= 0 && konsole.length === 0 && r.abmelden !== false - && markeGut && kopfGut && buehneGut - && (r.breit === null || r.breit === "1240px") - && (r.kopfzeile === null || r.kopfzeile === "flex"); + AM 07.09.2026 NOCH EINMAL UMGESTELLT -- und diesmal war die + Prüfung schlicht liegengeblieben. Filipe hat die Motive + zurückgeholt (Commit "Sieben Motive statt einem"), hier stand + aber weiter `=== "dogfather"`. Ergebnis: ALLE 32 Seiten rot, in + einer Meldung, der man nicht ansieht, welche Bedingung gefallen + ist -- 32 Fehlalarme, kein einziger Mangel. Wer eine Entscheidung + ändert, muss die Prüfung mitändern, sonst misst sie eine Welt, + die es nicht mehr gibt. + + Die neue Bedingung ist SCHÄRFER als beide vorherigen: Das + geladene Bild muss zu dem Merkmal passen, das die Seite selbst + trägt. Ein `data-buehne="garage"` ohne passende CSS-Regel fiele + damit auf -- bei "überall dasselbe Bild" wäre es nie + aufgefallen, und bei "irgendein Motiv" auch nicht. */ + /* Die Startseite trägt KEIN data-buehne -- sie hat ihre feste + Grundszene aus `body.start::before`. Für sie zählt deshalb nur, + dass überhaupt ein Motiv ankommt; für alle anderen, dass das + ankommende Motiv das ist, das sie sich aussuchen. */ + const buehneGut = r.buehne ? r.buehneBild === r.buehne : !!r.buehneBild; + /* JEDE BEDINGUNG EINZELN, MIT NAMEN. Vorher war das ein einziges + langes UND -- fiel es, stand da nur "FEHL" und eine Zeile, in + der alles in Ordnung aussah. Man musste die Prüfung aufmachen + und raten. Die Gründe stehen jetzt in der Meldung. */ + const gruende = []; + if (!(r.ueber <= 0)) gruende.push(`ragt ${r.ueber}px heraus`); + if (konsole.length) gruende.push(`Konsole: ${konsole.join(" / ")}`); + if (r.abmelden === false) gruende.push("Abmelden-Knopf ausserhalb"); + if (!markeGut) gruende.push("Schriftzug/Rückweg"); + if (!kopfGut) gruende.push("Seitenkopf (Ton/Zeichen/Wasserzeichen)"); + if (!buehneGut) gruende.push(`Bühne: ${r.buehneBild} statt ${r.buehne}`); + if (!(r.breit === null || r.breit === "1240px")) gruende.push(`Breite ${r.breit}`); + if (!(r.kopfzeile === null || r.kopfzeile === "flex")) gruende.push(`Kopfzeile ${r.kopfzeile}`); + const gut = gruende.length === 0; if (!gut) fehler++; console.log(` ${gut ? "ok " : "FEHL"} ${String(s).padEnd(20)}` + `über=${String(r.ueber).padStart(3)}px abmelden=${r.abmelden}` @@ -162,7 +187,8 @@ for (const [breite, hoehe, wie] of [[1440, 1000, "Computer"], [390, 844, "Handy" + ` bühne=${r.buehneBild || "FEHLT"}${r.buehne && r.buehneBild !== r.buehne ? ` (gewollt: ${r.buehne})` : ""}` + ` breit=${r.breit}` + (konsole.length ? " KONSOLE: " + konsole.join(" | ") : "") - + (r.schuld.length ? " -> " + r.schuld.join(" | ") : "")); + + (r.schuld.length ? " -> " + r.schuld.join(" | ") : "") + + (gruende.length ? "\n GRUND: " + gruende.join(" ; ") : "")); } await kontext.close(); } diff --git a/server/workspace.js b/server/workspace.js index 99a968dc..1a6ecb57 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -2367,10 +2367,35 @@ export const externSql = (personSpalte, externSpalte) => spaeter auseinandergelaufen -- und dann haette eine Wiederholung jemandem etwas gezeigt, was der einzelne Termin ihm verbirgt. */ export function termineSichtbar(person, praefix = "t") { - if (istSpicy(person)) return { wo: ohneDogFather(praefix), werte: [] }; - if (siehtAlles(person)) return { wo: "1=1", werte: [] }; const p = praefix; + /* ===================================================================== + JEDER SIEHT NUR SEINE EIGENEN TERMINE -- ODER DIE, DIE ALLE ANGEHEN + (07.09.2026) + + Wunsch Filipe: "calls oder termine soll jeder nur sehen die er + selber macht oder jeden betrifft." + + DAS GILT AUCH FUER DOGFATHER, und das ist die eigentliche + Aenderung. Bis hierher bekam er `1=1` -- er sah jeden Termin von + jedem. Bei einer Handvoll Leuten war das ein Ueberblick; bei + dreissig ist es eine Wand aus fremden Verabredungen, in der die + eigenen untergehen. Ein Kalender, in dem alles steht, ist kein + Kalender mehr. + + "JEDEN BETRIFFT" braucht eine Definition, sonst ist es Auslegung. + Sie steht hier: ein Termin OHNE Gegenueber und OHNE Teilnehmerliste. + Wer einen Termin anlegt und niemanden eintraegt, meint alle -- das + ist die Team-Besprechung, die Schulung, der Feiertag. Sobald auch + nur EINE Person eingetragen ist, ist es eine Verabredung und geht + nur die Beteiligten etwas an. + + Diese Regel ersetzt die Rollenfrage vollstaendig: Es gibt hier + nichts mehr, was DogFather sieht und ein Creator nicht. Was ein + Betreuer ueber seine Creator wissen muss, steht in deren Bereichen + und Aufgaben -- nicht in ihrem Kalender. + ===================================================================== */ + /* MITGEZAEHLT WIRD AUCH DIE TEILNEHMERLISTE (05.09.2026). Seit ein Termin mehrere Teilnehmer haben kann, reicht @@ -2394,9 +2419,15 @@ export function termineSichtbar(person, praefix = "t") { const eigen = `(${p}.creator_id = ? OR ${p}.teilnehmer_id = ? OR ${p}.erstellt_von = ?` + ` OR ${dabei})`; const werte = [person.id, person.id, person.id, person.id]; - const b = betreutWo(person, `${p}.creator_id`); - if (!b) return { wo: eigen, werte }; - return { wo: `(${eigen} OR ${b.wo})`, werte: [...werte, ...b.werte] }; + + /* "Geht alle an": kein Gegenueber, kein Teilnehmer. `NOT EXISTS` und + nicht `COUNT(*) = 0` -- der Server hoert beim ersten Treffer auf zu + suchen, statt jedes Mal die ganze Nebentabelle zu zaehlen. */ + const fuerAlle = `(${p}.creator_id IS NULL AND ${p}.teilnehmer_id IS NULL` + + ` AND NOT EXISTS (SELECT 1 FROM ${nebenTabelle.tabelle} ta` + + ` WHERE ta.${nebenTabelle.spalte} = ${p}.id))`; + + return { wo: `(${eigen} OR ${fuerAlle})`, werte }; } /* ===================================================================== diff --git a/tools/ansicht.mjs b/tools/ansicht.mjs index 1ba18a0e..36cbaebc 100644 --- a/tools/ansicht.mjs +++ b/tools/ansicht.mjs @@ -50,7 +50,13 @@ await start(); const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); -const ctx = await browser.newContext({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 1 }); +/* Breite ueber die Umgebung: node tools/ansicht.mjs start -> Rechner + BREITE=412 node tools/ansicht.mjs start -> Handy */ +const BREITE = Number(process.env.BREITE || 1600); +const HOEHE = Number(process.env.HOEHE || (BREITE < 700 ? 915 : 1000)); +const ctx = await browser.newContext({ + viewport: { width: BREITE, height: HOEHE }, deviceScaleFactor: 1, + isMobile: BREITE < 700, hasTouch: BREITE < 700 }); const seite = await ctx.newPage(); await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); @@ -62,7 +68,7 @@ await seite.waitForURL("**/start.html", { timeout: 25000 }); for (const s of SEITEN) { await seite.goto(`${BASIS}/workspace/${s}.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1600); - await seite.screenshot({ path: `/tmp/a-${s}.png`, fullPage: false }); + await seite.screenshot({ path: `/tmp/a-${s}${BREITE < 700 ? "-handy" : ""}.png`, fullPage: false }); console.log(`/tmp/a-${s}.png`); } diff --git a/workspace/assets/css/module.css b/workspace/assets/css/module.css new file mode 100644 index 00000000..dc5f8c33 --- /dev/null +++ b/workspace/assets/css/module.css @@ -0,0 +1,199 @@ +/* ===================================================================== + module.css — DIE FORM ALLER MODULE + + Herausgeloest aus start.css am 07.09.2026. Diese Datei beschreibt die + SILHOUETTE der ganzen Anwendung: die abgeschnittene Ecke, das + Kantenlicht, die Eckwinkel und das Raster. + + Sie wird von JEDER Seite geladen -- ausser von der Anmeldeseite + (index.html), die unangetastet bleibt. + + SIE WIRD ALS LETZTE GELADEN, und das ist keine Kosmetik. Bis zum + 07.09.2026 stand sie an dritter Stelle, VOR den Seitendateien. Dort + hat sie auf vier Seiten schlicht nichts bewirkt: `.call` in + calls.css, `.regel`/`.zustand`/`.ki-kasten` in automation.css, + `.unterweisung` in bereich.css und `.fortschritt` in dateien.css + setzen alle selbst `border-radius` und `box-shadow` -- gleiche + Spezifitaet, spaeter im Ladeweg, also gewinnen sie. Ergebnis war + genau das, was Filipe zu Recht bemaengelt hat: die neue Form auf + manchen Seiten, die alte auf anderen, "ueberall ein bisschen". + Als letzte Datei gewinnt die Form ueberall. + + WER EINE NEUE KARTE BAUT, traegt ihren Klassennamen in die Listen + unten ein. Die Liste steht siebenmal in dieser Datei (Grundform, + Kantenlicht, Raster, Handy und dreimal Kontrastmodus) -- das ist die + schwache Stelle dieser Bauart, denn eine vergessene Kopie faellt + niemandem auf. Deshalb steht vor jeder Kopie die Marke + MODULLISTE, und `pruef-css-klassen.mjs` vergleicht alle sieben + Vorkommen miteinander: Weicht eines ab, wird die Pruefung rot. + ===================================================================== */ + +/* ===================================================================== + HUD-MODULE — die FORM ändert sich, nicht der Lack (07.09.2026) + + Filipe: "DU HAST WIEDER EINE KLEINE ÄNDERUNG ÜBERALL GEMACHT ANSTATT + EINE RIESEN ÄNDERUNG." + + --------------------------------------------------------------------- + ER HAT RECHT, UND DER GRUND WAR IMMER DERSELBE + + Ich habe dreimal das MATERIAL getauscht -- Mattglas, Leuchtschiene, + Verlauf mit Schatten -- und dreimal die FORM gelassen. Ein + abgerundetes Rechteck bleibt ein abgerundetes Rechteck. Die + Silhouette ist aber das Einzige, was man aus fünf Metern erkennt; + deshalb sah jede Änderung "wie vorher" aus. + + DIE NEUE FORM: + + 1. DIE ECKE OBEN LINKS IST ABGESCHNITTEN. Eine Schräge statt einer + Rundung -- eine andere geometrische Klasse, und genau deshalb + fällt sie auf. Gebaut mit `clip-path`, also eine echte + Silhouette und kein aufgemalter Fleck. + 2. EIN LICHT LÄUFT AUF DER KANTE, oben und über die Schräge, in der + Farbe der Kategorie -- nicht ein Rahmen rundherum (das wäre + wieder ein Kasten), sondern eine beleuchtete Oberkante. + 3. ECKWINKEL UNTEN RECHTS. Zwei kurze Striche, die eine Ecke + andeuten, ohne sie zu zeichnen -- die Sprache von Peilung und + Zielmarke. + 4. EIN FEINES RASTER statt Körnung. Körnung sagt "Papier", ein + Raster sagt "Messgerät". + + --------------------------------------------------------------------- + DREI DINGE, DIE BEIM ERSTEN ANLAUF SCHIEFGINGEN + + `clip-path` SCHNEIDET DEN SCHLAGSCHATTEN AB. Deshalb liegt die Höhe + hier in `filter: drop-shadow()` und nicht in `box-shadow`: drop-shadow + folgt der WIRKLICHEN Form, box-shadow immer dem Rechteck -- er würde + einen Schatten für eine Ecke zeichnen, die es nicht mehr gibt. + + DIE REGELN STANDEN IN `:where()`. Das hat Spezifität NULL, also + gewann jede ältere `.kachel::before`-Regel. Die Silhouette kam durch + (die setzt sonst niemand), das Kantenlicht nicht -- heraus kam ein + Zwitter aus neuer Form und alter Kante. Jetzt steht hier `:is()`: + Das übernimmt die Spezifität seines stärksten Arguments, also die + einer einzelnen Klasse. Damit liegt die Form gleichauf mit + `.kachel` und gewinnt über den Ladeweg -- deshalb muss diese Datei + zuletzt kommen (siehe oben). + + UND SIE STAND ZU FRÜH IM LADEWEG. Siehe der Absatz ganz oben; das + war derselbe Fehler noch einmal, nur eine Ebene höher. + ===================================================================== */ + +:root { + /* Die Schräge steht als Größe da, damit Silhouette, Kantenlicht und + Eckwinkel garantiert dieselbe benutzen. */ + --fase: 18px; + --raster: + repeating-linear-gradient(0deg, rgba(150, 186, 220, .26) 0 1px, transparent 1px 26px), + repeating-linear-gradient(90deg, rgba(150, 186, 220, .26) 0 1px, transparent 1px 26px); +} + +/* MODULLISTE */ +:is(.eintrag-karte, .call, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .k-ansicht, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .unterweisung, .warnkarte, .kachel, .willkommen, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu) { + position: relative; + clip-path: polygon(var(--fase) 0, 100% 0, 100% 100%, 0 100%, 0 var(--fase)); + border-radius: 0; + border: 0; + box-shadow: none; + filter: + drop-shadow(0 2px 3px rgba(0, 0, 0, .55)) + drop-shadow(0 12px 22px rgba(0, 0, 0, .55)); +} + +/* DAS KANTENLICHT. Ein Layer mit derselben Silhouette, 1,6 px kleiner -- + was übrig bleibt, ist eine Linie, die exakt der Form folgt, Schräge + inklusive. Ein `border` könnte das nicht: Er folgt dem Rechteck. */ +/* MODULLISTE */ +:is(.eintrag-karte, .call, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .k-ansicht, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .unterweisung, .warnkarte, .kachel, .willkommen, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu)::before { + content: ""; + position: absolute; inset: 0; + pointer-events: none; + z-index: 2; + width: auto; height: auto; + opacity: 1; + background: linear-gradient(158deg, + color-mix(in srgb, var(--ton, var(--akzent)) 90%, #ffffff) 0%, + color-mix(in srgb, var(--ton, var(--akzent)) 60%, transparent) 15%, + rgba(150, 186, 220, .20) 45%, + rgba(150, 186, 220, .07) 100%); + clip-path: polygon( + var(--fase) 0, 100% 0, 100% 100%, 0 100%, 0 var(--fase), + 1.6px calc(var(--fase) + 0.7px), 1.6px calc(100% - 1.6px), + calc(100% - 1.6px) calc(100% - 1.6px), calc(100% - 1.6px) 1.6px, + calc(var(--fase) + 0.7px) 1.6px, 1.6px calc(var(--fase) + 0.7px), + 0 var(--fase)); +} + +/* RASTER UND ECKWINKEL. Beides auf ::after -- ein drittes Element gibt + es nicht, und beides ist reine Optik ohne Klicks. */ +/* MODULLISTE */ +:is(.eintrag-karte, .call, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .k-ansicht, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .unterweisung, .warnkarte, .kachel, .willkommen, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu)::after { + content: ""; + position: absolute; inset: 0; + pointer-events: none; + z-index: 0; + background: + linear-gradient(var(--ton, var(--akzent)), var(--ton, var(--akzent))) no-repeat, + linear-gradient(var(--ton, var(--akzent)), var(--ton, var(--akzent))) no-repeat, + var(--raster); + background-size: 18px 1.5px, 1.5px 18px, 26px 26px, 26px 26px; + background-position: right 10px bottom 10px, right 10px bottom 10px, 0 0, 0 0; + /* .3 war zu laut: Das Raster soll man nicht LESEN, sondern spueren. + Bei 26 px Abstand und einem Viertel Deckkraft bleibt der Eindruck + "Messgeraet", ohne dass die Linien mit dem Text konkurrieren. */ + opacity: .26; + transform: none; +} + +/* Der Inhalt gehört ÜBER Raster und Kantenlicht. Ohne diese Zeile + liegt bei Karten ohne eigenen Stapelwert der Text unter dem Raster + und wird von ihm überzeichnet. */ +:is(.eintrag-karte, .call, .datei, .kachel, .willkommen, .leer-hinweis, + .team-karte, .zahl, .k-listentag, .person, .creator) > * { + position: relative; + z-index: 1; +} + +/* Beim Anfassen kommt das Modul NÄHER, es leuchtet nicht auf. */ +:is(.kachel, .datei, .pdf, .creator, .person, .kk, .call, .team-karte):hover { + transform: translateY(-3px); + box-shadow: none; + filter: + drop-shadow(0 4px 6px rgba(0, 0, 0, .6)) + drop-shadow(0 20px 34px rgba(0, 0, 0, .6)) + drop-shadow(0 0 14px color-mix(in srgb, var(--ton, var(--akzent)) 30%, transparent)); +} +:is(.kachel, .datei, .pdf, .creator, .person, .kk, .call, .team-karte) { + transition: transform var(--tempo), filter var(--tempo); +} + +@media (max-width: 620px) { + /* Dieselbe Form, kleiner -- keine zweite Gestaltung. */ + :root { --fase: 12px; } + /* MODULLISTE */ + :is(.eintrag-karte, .call, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .k-ansicht, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .unterweisung, .warnkarte, .kachel, .willkommen, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu) { + filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .7)); + } +} + +@media (prefers-reduced-motion: reduce) { + :is(.kachel, .datei, .pdf, .creator, .person, .kk, .call, .team-karte):hover { + transform: none; + } +} +@media (forced-colors: active) { + /* Im Kontrastmodus zählt die Kante, und die muss ein echter `border` + sein. Die Schräge bleibt: Sie ist Geometrie, keine Farbe. */ + /* MODULLISTE */ + :is(.eintrag-karte, .call, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .k-ansicht, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .unterweisung, .warnkarte, .kachel, .willkommen, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu) { + filter: none; border: 1px solid CanvasText; + } + /* MODULLISTE */ + :is(.eintrag-karte, .call, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .k-ansicht, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .unterweisung, .warnkarte, .kachel, .willkommen, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu)::after { + display: none; + } + /* MODULLISTE */ + :is(.eintrag-karte, .call, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .k-ansicht, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .unterweisung, .warnkarte, .kachel, .willkommen, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu)::before { + display: none; + } +} diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index bb593aeb..5aa4dab7 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -845,37 +845,15 @@ body.start > main { position: relative; z-index: 1; } /* Die farbige Kante links. Sie ist das Einzige, was den Ton kräftig zeigt -- drei Pixel reichen, um eine Kachel wiederzuerkennen. */ -.kachel::before { - content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; - background: linear-gradient(180deg, var(--ton), transparent 92%); - opacity: .55; - transition: opacity var(--tempo), width var(--tempo); -} +/* Die Kacheln bekommen Form, Kante und Schatten seit dem 07.09.2026 aus + dem Abschnitt HUD-MODULE am Ende dieser Datei. Was hier stand -- die + Leuchtschiene oben, der wandernde Glanz und der eigene Schatten -- ist + dort vollstaendig ersetzt und war ab dann nur noch Ballast: Bytes auf + jeder Seite und, schlimmer, eine zweite Stelle, an der dieselbe Kachel + beschrieben wird. Wer etwas an der Form aendert, sucht ab jetzt unten. -/* Das Licht. Es liegt schwach auf der Kachel und FOLGT beim Überfahren - dem Zeiger -- das ist die Kleinigkeit, die man nicht sieht, sondern - erst beim Benutzen merkt. Kein Blinken, keine Bewegung des Inhalts: - Es wird nur an einer anderen Stelle heller. + Es bleibt nur, was der neue Abschnitt NICHT selbst setzt. */ - Ruhend sitzt es in der oberen linken Ecke (--my: 0%), damit die - Kacheln auch ohne Zeiger von einer Seite beleuchtet wirken. */ -.kachel::after { - content: ""; position: absolute; inset: 0; pointer-events: none; - background: radial-gradient(130% 150% at var(--mx) var(--my), - color-mix(in srgb, var(--ton) 20%, transparent), transparent 60%); - opacity: .38; - transition: opacity var(--tempo); -} - -.kachel:hover { - border-color: color-mix(in srgb, var(--ton) 45%, var(--rand)); - transform: translateY(-2px); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, .09), - 0 10px 26px -14px color-mix(in srgb, var(--ton) 55%, transparent); -} -.kachel:hover::before { opacity: 1; width: 4px; } -.kachel:hover::after { opacity: 1; } .kachel:has(:focus-visible) { border-color: var(--ton); } .kachel__link { @@ -3342,29 +3320,10 @@ body[data-fremde-sicht="ja"]::after { 0 10px 26px -18px rgba(0, 0, 0, .9), 0 2px 6px -4px rgba(0, 0, 0, .5); } -.kachel::after { - content: ""; - position: absolute; top: 0; left: 14%; right: 14%; height: 1px; - background: linear-gradient(90deg, - transparent, - color-mix(in srgb, var(--ton) 75%, #fff) 50%, - transparent); - opacity: .5; - pointer-events: none; - z-index: 2; - transition: opacity var(--tempo); -} -.kachel:hover { - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, .07), - 0 18px 40px -20px rgba(0, 0, 0, .95), - 0 3px 10px -5px color-mix(in srgb, var(--ton) 45%, transparent); -} -.kachel:hover::after { opacity: .95; } - -@media (forced-colors: active) { - .kachel::after { display: none; } -} +/* Lichtkante, Hover-Schatten und Kontrastmodus der Kachel stehen seit + dem 07.09.2026 im Abschnitt HUD-MODULE am Ende dieser Datei. Was hier + stand, war die aelteste von drei Generationen und wurde von jeder + Seite geladen, ohne je zu wirken. */ /* ---------- Der Benachrichtigungs-Knopf ------------------------------ Wunsch Filipe (05.09.2026): ein Knopf wie "Benachrichtigungen aus", @@ -4237,66 +4196,17 @@ body[data-fremde-sicht="ja"]::after { /* ===================================================================== - MODULKARTEN — die Kacheln werden ein Raster, keine Liste - (07.09.2026, dritter Anlauf) + MODULKARTEN — die Kacheln sind ein Raster, keine Liste (07.09.2026) - Filipe zum zweiten Anlauf: "sorry aber ich glaub du verstehst nicht - was ich meine, ich sag ja ich will eine komplette änderung von den - kacheln, viel übersichtlicher ... es soll ganz anders aussehen wie - jetzt." + Aus drei breiten Zeilen wurde ein Raster aus sechs Karten. Eine + breite Zeile zwingt das Auge, siebzehnmal von links nach rechts zu + LESEN; eine kompakte Karte erkennt man an Form und Zeichen -- das + Auge springt, statt zu wandern. - --------------------------------------------------------------------- - ER HAT RECHT, UND ICH WEISS JETZT WARUM - - Zweimal habe ich die OBERFLÄCHE geändert -- Mattglas, dann - Leuchtschiene -- und beide Male blieb die FORM gleich: eine breite - Zeile, Zeichen links, Text daneben, Pfeil rechts. Drei davon passen - nebeneinander, siebzehn ergeben sechs Reihen. Wer eine Zeile - umlackiert, bekommt eine lackierte Zeile. - - Was sich ändern musste, ist die Form: - - AUS DREI BREITEN ZEILEN WIRD EIN RASTER AUS SECHS KARTEN. - - Das ist der eigentliche Punkt hinter "übersichtlicher". Eine breite - Zeile zwingt das Auge, von links nach rechts zu LESEN -- siebzehnmal. - Eine kompakte Karte erkennt man an ihrer Form und ihrem Zeichen, - ohne ein Wort zu lesen; das Auge springt statt zu wandern. Bei - 1380 px passen jetzt sechs Karten nebeneinander statt drei: Der - ganze Arbeitsbereich steht auf einem Bildschirm, ohne zu scrollen. - - --------------------------------------------------------------------- - WAS SICH IM EINZELNEN ÄNDERT - - FORM Hochformat statt Querformat. Zeichen oben, Name unten -- - die Leserichtung eines Menschen, der etwas SUCHT (erst - das Bild, dann der Name), nicht eines, der liest. - - SCHIENE Sie wandert von links nach OBEN. An einer hochkanten - Karte ist die lange Seite senkrecht; ein Lichtbalken - dort würde die Karte optisch halbieren. Oben liegt er - wie eine beschriftete Kante an einem Gerät. - - ZEICHEN Größer und in einer eigenen, beleuchteten Mulde. Es ist - jetzt das erste, was man sieht, und trägt deshalb mehr - Gewicht als vorher. - - ZAHL Nach oben rechts, als eigene Marke. Vorher stand sie in - derselben Zeile wie der Name und hat ihn gekürzt. - - WASSER- Größer und weiter unten -- sie füllt jetzt die untere - ZEICHEN Hälfte der Karte und gibt jeder Kachel eine eigene - Silhouette. Bei sechs Karten nebeneinander ist genau das - der Unterschied zwischen "Raster" und "Sammlung". - - GROSSE `data-gross="ja"` nimmt zwei Spalten und bekommt das - KACHEL Querformat von früher -- so hebt sie sich WIRKLICH ab, - statt nur breiter zu sein. - - Material, Farben und Verhalten bleiben: beleuchtete Platte, Körnung, - Anheben statt Aufleuchten, jede Kategorie in ihrer Farbe. Das war - nicht das Problem. - ===================================================================== */ + Die SILHOUETTE dieser Karten (Schraege, Kantenlicht, Eckwinkel) steht + im Abschnitt HUD-MODULE am Ende der Datei. Hier steht nur noch, wie + der Inhalt darin angeordnet ist. + ===================================================================== .kacheln { /* 214 px ist gemessen, nicht geraten: Bei 1380 px Inhaltsbreite und @@ -4330,46 +4240,7 @@ body[data-fremde-sicht="ja"]::after { rgba(8, 11, 18, .95) 100%); } -/* DIE SCHIENE OBEN. Volle Breite, hell in der Mitte, zu den Ecken - auslaufend -- so sieht eine beleuchtete Kante aus, wenn die Lampe - dahinter mittig sitzt. Ein gleichmäßiger Balken sähe aufgeklebt aus. */ -.kachel::before { - left: 0; right: 0; top: 0; bottom: auto; - width: auto; height: 3px; - border-radius: 16px 16px 0 0; - background: linear-gradient(90deg, - color-mix(in srgb, var(--ton) 30%, transparent) 0%, - color-mix(in srgb, var(--ton) 55%, #fff) 34%, - var(--ton) 62%, - color-mix(in srgb, var(--ton) 34%, transparent) 100%); - box-shadow: - 0 0 9px 0 color-mix(in srgb, var(--ton) 45%, transparent), - 0 3px 20px -3px color-mix(in srgb, var(--ton) 38%, transparent); - opacity: .85; -} -.kachel:hover::before { - opacity: 1; - box-shadow: - 0 0 14px 0 color-mix(in srgb, var(--ton) 70%, transparent), - 0 5px 32px -3px color-mix(in srgb, var(--ton) 55%, transparent); -} - -/* Der Glanz zieht jetzt von oben links nach unten rechts -- an einer - hochkanten Fläche ist das die Richtung, in der Licht fällt. */ -.kachel::after { - background: - linear-gradient(148deg, - transparent 24%, - rgba(255, 255, 255, .05) 38%, - rgba(255, 255, 255, .10) 46%, - rgba(255, 255, 255, .03) 54%, - transparent 70%), - radial-gradient(130% 120% at var(--mx) var(--my), - color-mix(in srgb, var(--ton) 24%, transparent), transparent 62%); - background-size: 100% 240%, 100% 100%; - background-position: 0 -30%, 0 0; -} -.kachel:hover::after { background-position: 0 130%, 0 0; } +/* Schiene und Glanz stehen jetzt im Abschnitt HUD-MODULE. /* ---------- Der Aufbau der Karte ------------------------------------- */ .kachel__link { @@ -4551,58 +4422,20 @@ body[data-fremde-sicht="ja"]::after { /* ===================================================================== - EIN MATERIAL FÜR DIE GANZE WEBSITE (07.09.2026) + EIN MATERIAL FUER DIE GANZE WEBSITE (07.09.2026) - Filipe: "ICH WILL DASS DU DIE KACHELN DER KOMPLETTEN WEBSITE KOMPLETT - VERÄNDERST ... SEHR HOCHWERTIG UND EPISCH ... EINE EINZIGARTIGE - WEBSITE ... PERFEKT AUF DEM PC UND AUF DEM HANDY." + Fuenfundvierzig Stellen benutzten `var(--flaeche)` als Kartenflaeche + -- Seite fuer Seite nachzuziehen war der Grund, warum es nie fertig + wurde. Die Liste unten ist deshalb GEMESSEN und nicht erfunden: Es + sind genau diese Klassen. - --------------------------------------------------------------------- - WARUM DAS BISHER NICHT GEKLAPPT HAT + `:where()` hat Spezifitaet null -- die Regel legt nur den Boden und + ueberschreibt nichts, was eine Seite selbst festlegt. Eine Warnkarte + bleibt rot, eine Spalte behaelt ihre Statusfarbe. - Die Startseite hat seit heute ein Material -- beleuchtete Platte, - Lichtkante, Schatten, Körnung. Die anderen sechzehn Seiten hatten - weiterhin `background: var(--flaeche)` und einen Rand: flache - Rechtecke. Man wechselte die Seite und fiel aus einer Oberfläche in - eine andere. - - Ich habe das bisher Seite für Seite nachgezogen -- und genau deshalb - war es nie fertig. Bei fünfundvierzig Stellen ist das kein Weg. - - --------------------------------------------------------------------- - EINE STELLE, NICHT FÜNFUNDVIERZIG - - Die Liste unten ist nicht erfunden, sie ist GEMESSEN: Es sind genau - die Klassen, die in den siebzehn Stildateien `var(--flaeche)` als - Kartenfläche benutzen. Wer eine neue Karte baut und diesen Token - nimmt, gehört dazu -- und sollte hier eingetragen werden. - - `:where()` ist der entscheidende Kniff: Es hat SPEZIFITÄT NULL. - Dadurch überschreibt diese Regel nichts, was eine Seite selbst - festlegt -- eine Warnkarte bleibt rot, eine Spalte behält ihre - Statusfarbe, die Kacheln der Startseite ihre eigene Form. Sie legt nur - den Boden, auf dem alle stehen. Ohne `:where()` müsste ich an - dreißig Stellen `!important` schreiben oder Reihenfolgen sortieren -- - beides Wege, die beim nächsten Umbau kippen. - - --------------------------------------------------------------------- - WAS DAS MATERIAL AUSMACHT - - GEWICHT Zwei Schatten: ein enger, dunkler direkt darunter und - ein weiter, weicher. Ein einzelner sieht immer nach - Aufkleber aus. - MATERIALSTÄRKE Licht oben, Schatten unten -- beide innen. Nur die - obere Kante ergibt einen aufgeklebten Strich; erst - beide zusammen ergeben ein Blech. - TIEFE Ein Verlauf von oben nach unten. Eine einfarbige - Fläche ist Papier. - KÖRNUNG Auf einer eigenen Ebene mit `overlay` und 3 % -- - genau der Fehler, den ich heute zweimal gemacht habe: - Körnung auf derselben Ebene wie ein Glanz erbt dessen - Deckkraft und wird zum grauen Schleier. - - AUF DEM HANDY DASSELBE MATERIAL, nur flacher: Schatten kosten dort - Rechenzeit und bringen wenig, weil ohnehin nichts nebeneinander liegt. + FORM, KANTE UND SCHATTEN kommen dagegen aus dem Abschnitt HUD-MODULE + am Ende der Datei -- mit voller Spezifitaet, weil sie die aelteren + Regeln ABLOESEN sollen und nicht unter ihnen liegen. ===================================================================== */ :where( @@ -4645,40 +4478,8 @@ body[data-fremde-sicht="ja"]::after { 0 12px 28px -18px rgba(0, 0, 0, .85); } -/* Die Körnung. Eigene Ebene, `overlay`, 3 % -- und `pointer-events: - none`, sonst läge eine unsichtbare Fläche über jedem Knopf in jeder - Karte der ganzen Website. */ -:where( - .eintrag-karte, .call, .datei, .ablage, .zahlkarte, .regel, .zustand, - .ki-kasten, .k-ansicht, .spalte, .balance, .saeulen-kasten, .neu, - .call-raum, .creator, .summe, .pdf, .person, .kk, .unterweisung -)::after { - content: ""; - position: absolute; inset: 0; - pointer-events: none; - border-radius: inherit; - background-image: var(--koernung); - background-size: 140px 140px; - opacity: .03; - mix-blend-mode: overlay; - z-index: 0; -} - -/* Karten, die man anklickt, kommen beim Zeigen NÄHER -- sie werden - nicht heller. So verhält sich ein Gegenstand, den man anhebt, und - deshalb wirkt es echt, ohne dass etwas blinkt. */ -:where(a, button):where(.datei, .pdf, .creator, .person, .kk, .call):hover, -:where(.datei, .pdf, .creator, .person, .kk):hover { - transform: translateY(-2px); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, .10), - inset 0 -1px 0 rgba(0, 0, 0, .42), - 0 4px 9px -4px rgba(0, 0, 0, .55), - 0 18px 36px -20px rgba(0, 0, 0, .9); -} -:where(.datei, .pdf, .creator, .person, .kk) { - transition: transform var(--tempo), box-shadow var(--tempo), border-color var(--tempo); -} +/* Körnung, Anheben und Schatten stehen im Abschnitt HUD-MODULE am + Ende dieser Datei. Hier bleibt nur die Fläche selbst. */ @media (max-width: 620px) { /* Flacher auf dem Handy: Nebeneinander liegt dort ohnehin nichts, und @@ -4713,3 +4514,18 @@ body[data-fremde-sicht="ja"]::after { .call-raum, .creator, .summe, .pdf, .person, .kk, .unterweisung )::after { display: none; } } + + +/* Die FORM aller Module -- abgeschnittene Ecke, Kantenlicht, Eckwinkel, + Raster -- steht seit dem 07.09.2026 in einer eigenen Datei: + assets/css/module.css. + + Zwei Gruende. Erstens war start.css ueber 200 KB gewachsen und lief in + die Grenze von pruef-struktur -- zu Recht, sie ist ueber Wochen aus + Anbauten entstanden. Zweitens ist die Silhouette eine EIGENE Sache: + Sie gilt fuer jede Karte auf jeder Seite, unabhaengig davon, was die + Seite sonst tut. Wer an der Form etwas aendert, soll eine Datei + oeffnen und nicht in einer von 4900 Zeilen suchen. + + module.css wird von JEDER Seite geladen (anders als heim.css, das nur + die Startseite braucht). */ diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index 5920bfe6..55ae8b89 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -744,8 +744,30 @@ const nurAnlegen = ich.rolle !== 'admin'; if (nurAnlegen) { document.body.dataset.nurAnlegen = 'ja'; - $('unterzeile') && ($('unterzeile').textContent = - 'Hier legst du neue Leute an. Codes, Sperren und das Protokoll bleiben bei DogFather.'); + /* DER ERKLÄRSATZ. Er stand hier schon, kam aber nie an: Ein + Element mit der Kennung `unterzeile` gab es auf der Seite + nicht, und das `&&` davor hat den Fehlgriff verschluckt. + Gemessen im Browser als Manager -- die Seite bestand aus + Überschrift und Formular, ohne ein Wort dazu, warum hier so + wenig steht. Jetzt gibt es das Element (personen.html), und + der Satz wird auch sichtbar gemacht. */ + const zeile = $('unterzeile'); + if (zeile) { + zeile.textContent = + 'Hier legst du neue Leute an. Codes, Sperren und das Protokoll bleiben bei DogFather.'; + zeile.hidden = false; + } + /* UND DIE LISTE HÖRT AUF ZU LADEN. Sie steht mit + aria-busy="true" in der Seite, und `laden()` -- das es wieder + abschaltet -- wird für sie mit Absicht nie aufgerufen. Damit + meldete die Seite einem Screenreader dauerhaft "wird geladen", + und es kam nie etwas. Ein Ladehinweis, der nie endet, ist + schlimmer als keiner. */ + const liste = $('liste'); + if (liste) { + liste.setAttribute('aria-busy', 'false'); + liste.textContent = ''; + } baueRollenwahl(); /* Das Formular steht offen da -- es ist der einzige Grund, warum diese Seite fuer sie ueberhaupt aufgeht. Ein Knopf, der eine diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index b49c08e2..a78a6efd 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -9,9 +9,10 @@ - - - + + + + @@ -211,10 +212,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 8d4ff40c..76061787 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -134,10 +135,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index a98883c7..86aa9a6b 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -143,14 +144,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index af057a05..5da22414 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -76,11 +77,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index f36becfa..079f2f0a 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -9,16 +9,17 @@ - - + + - - + + + @@ -154,9 +155,9 @@ - - - - + + + + diff --git a/workspace/content.html b/workspace/content.html index 622408f3..70cc68bb 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -166,12 +167,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 8087b6c7..b63ec745 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -95,10 +96,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index efa1a1bc..aa8ee0ca 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -27,7 +27,7 @@ - + @@ -202,6 +202,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 914d711e..7e08fbdd 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -257,11 +258,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 7e61e5bc..ad201889 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -211,10 +212,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index abbdbd97..5ff3f4e4 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -54,6 +55,14 @@

    Zugänge

    Personen & Zugangscodes

    + +
    - + + @@ -263,10 +264,10 @@

    - - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 588fa588..3776ab59 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -72,11 +73,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index ce7f3cc1..c2ab3206 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -113,10 +114,10 @@ - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index c079843e..d24f3d95 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -9,12 +9,13 @@ - - + + - - + + + @@ -136,10 +137,10 @@ - + - - - + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 476e3d5e..9737d916 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -9,10 +9,11 @@ - - - - + + + + + @@ -177,10 +178,10 @@ - - - - - + + + + +