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