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 <[email protected]>
This commit is contained in:
2026-09-07 07:12:48 +02:00
co-authored by Claude Opus 5
parent e8478c9cae
commit 4eab64bd20
37 changed files with 997 additions and 563 deletions
+64
View File
@@ -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(/<link[^>]+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);
+22 -5
View File
@@ -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");
+123 -90
View File
@@ -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 <li> 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 */ }
+49 -12
View File
@@ -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.
+47 -4
View File
@@ -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(" · ")}`
+13 -1
View File
@@ -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");
+21 -10
View File
@@ -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(", ")})`);
}
/* ================================================================
+15 -4
View File
@@ -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. */
+38 -4
View File
@@ -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
+24 -5
View File
@@ -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(() => {
+9 -1
View File
@@ -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,
+24 -5
View File
@@ -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`}`);
+35 -9
View File
@@ -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();
}
+36 -5
View File
@@ -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 };
}
/* =====================================================================
+8 -2
View File
@@ -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`);
}
+199
View File
@@ -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;
}
}
+49 -233
View File
@@ -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). */
+24 -2
View File
@@ -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
+9 -8
View File
@@ -9,9 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -211,10 +212,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/aufgaben.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/aufgaben.js?v=202609070620" defer></script>
</body>
</html>
+10 -9
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -134,10 +135,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/automation.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/automation.js?v=202609070620" defer></script>
</body>
</html>
+14 -13
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -143,14 +144,14 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/checkliste.js?v=202609070539" defer></script>
<script src="assets/js/ampel.js?v=202609070539" defer></script>
<script src="assets/js/schulung.js?v=202609070539" defer></script>
<script src="assets/js/vorlagen.js?v=202609070539" defer></script>
<script src="assets/js/bereich.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/checkliste.js?v=202609070620" defer></script>
<script src="assets/js/ampel.js?v=202609070620" defer></script>
<script src="assets/js/schulung.js?v=202609070620" defer></script>
<script src="assets/js/vorlagen.js?v=202609070620" defer></script>
<script src="assets/js/bereich.js?v=202609070620" defer></script>
</body>
</html>
+11 -10
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -76,11 +77,11 @@
</main>
<script src="assets/js/ki.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/calls.js?v=202609070539" defer></script>
<script src="assets/js/ki.js?v=202609070620" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/calls.js?v=202609070620" defer></script>
</body>
</html>
+9 -8
View File
@@ -9,16 +9,17 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -154,9 +155,9 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609070539"></script>
<script src="assets/js/kopf.js?v=202609070539"></script>
<script src="assets/js/glocke.js?v=202609070539"></script>
<script src="assets/js/chat.js?v=202609070539"></script>
<script src="assets/js/bereiche.js?v=202609070620"></script>
<script src="assets/js/kopf.js?v=202609070620"></script>
<script src="assets/js/glocke.js?v=202609070620"></script>
<script src="assets/js/chat.js?v=202609070620"></script>
</body>
</html>
+12 -11
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/content.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/content.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -166,12 +167,12 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/checkliste.js?v=202609070539" defer></script>
<script src="assets/js/vorlagen.js?v=202609070539" defer></script>
<script src="assets/js/content.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/checkliste.js?v=202609070620" defer></script>
<script src="assets/js/vorlagen.js?v=202609070620" defer></script>
<script src="assets/js/content.js?v=202609070620" defer></script>
</body>
</html>
+10 -9
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -95,10 +96,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/dateien.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/dateien.js?v=202609070620" defer></script>
</body>
</html>
+2 -2
View File
@@ -27,7 +27,7 @@
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
</head>
<body class="gate">
@@ -202,6 +202,6 @@
</main>
</div>
<script src="assets/js/gate.js?v=202609070539" defer></script>
<script src="assets/js/gate.js?v=202609070620" defer></script>
</body>
</html>
+11 -10
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -257,11 +258,11 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/kalender.js?v=202609070539" defer></script>
<script src="assets/js/kalender-abo.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/kalender.js?v=202609070620" defer></script>
<script src="assets/js/kalender-abo.js?v=202609070620" defer></script>
</body>
</html>
+10 -9
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -211,10 +212,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609070539"></script>
<script src="assets/js/bereiche.js?v=202609070539"></script>
<script src="assets/js/kopf.js?v=202609070539"></script>
<script src="assets/js/glocke.js?v=202609070539"></script>
<script src="assets/js/leistung.js?v=202609070539"></script>
<script src="assets/js/wahl.js?v=202609070620"></script>
<script src="assets/js/bereiche.js?v=202609070620"></script>
<script src="assets/js/kopf.js?v=202609070620"></script>
<script src="assets/js/glocke.js?v=202609070620"></script>
<script src="assets/js/leistung.js?v=202609070620"></script>
</body>
</html>
+18 -9
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -54,6 +55,14 @@
<div>
<p class="marke">Zugänge</p>
<h1 class="titel">Personen &amp; Zugangscodes</h1>
<!-- Der Satz gilt nur für Manager und Spicy Media: Sie sehen hier
allein das Anlegen. personen.js blendet ihn dafür ein.
Er stand vorher nur im Skript (`$('unterzeile')`), und dieses
Element gab es nie -- der Aufruf war mit `&&` abgesichert und
tat deshalb schweigend nichts. Eine abgesicherte Zeile, die
ins Leere greift, sieht auf Dauer aus wie eine, die
funktioniert. -->
<p class="leise" id="unterzeile" hidden></p>
</div>
<div class="kopf-knoepfe">
<!-- Standardmäßig steht hier nur DogFather. Alle anderen kommen auf
@@ -128,10 +137,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/personen.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/personen.js?v=202609070620" defer></script>
</body>
</html>
+11 -10
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -201,11 +202,11 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/steckbrief.js?v=202609070539" defer></script>
<script src="assets/js/profil.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/steckbrief.js?v=202609070620" defer></script>
<script src="assets/js/profil.js?v=202609070620" defer></script>
</body>
</html>
+11 -10
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/report.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/report.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -93,11 +94,11 @@
</main>
<script src="assets/js/ki.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/report.js?v=202609070539" defer></script>
<script src="assets/js/ki.js?v=202609070620" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/report.js?v=202609070620" defer></script>
</body>
</html>
+10 -9
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -126,10 +127,10 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/scouting.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/scouting.js?v=202609070620" defer></script>
</body>
</html>
+9 -8
View File
@@ -9,11 +9,12 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
<link rel="stylesheet" href="assets/css/heim.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/heim.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -263,10 +264,10 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/start.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/start.js?v=202609070620" defer></script>
</body>
</html>
+11 -10
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -72,11 +73,11 @@
</main>
<script src="assets/js/ki.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/startcheck.js?v=202609070539" defer></script>
<script src="assets/js/ki.js?v=202609070620" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/startcheck.js?v=202609070620" defer></script>
</body>
</html>
+10 -9
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -113,10 +114,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/steckbrief.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/steckbrief.js?v=202609070620" defer></script>
</body>
</html>
+9 -8
View File
@@ -9,12 +9,13 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -136,10 +137,10 @@
</main>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/uebersicht.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/uebersicht.js?v=202609070620" defer></script>
</body>
</html>
+10 -9
View File
@@ -9,10 +9,11 @@
<meta name="theme-color" content="#0674b9" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609070539" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/start.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609070620" />
<link rel="stylesheet" href="assets/css/module.css?v=202609070620" />
</head>
<body class="start">
@@ -177,10 +178,10 @@
</main>
<script src="assets/js/wahl.js?v=202609070539" defer></script>
<script src="assets/js/bereiche.js?v=202609070539" defer></script>
<script src="assets/js/kopf.js?v=202609070539" defer></script>
<script src="assets/js/glocke.js?v=202609070539" defer></script>
<script src="assets/js/wissen.js?v=202609070539" defer></script>
<script src="assets/js/wahl.js?v=202609070620" defer></script>
<script src="assets/js/bereiche.js?v=202609070620" defer></script>
<script src="assets/js/kopf.js?v=202609070620" defer></script>
<script src="assets/js/glocke.js?v=202609070620" defer></script>
<script src="assets/js/wissen.js?v=202609070620" defer></script>
</body>
</html>