diff --git a/server/pruef-kachel-universum.mjs b/server/pruef-kachel-universum.mjs new file mode 100644 index 00000000..5a102ed5 --- /dev/null +++ b/server/pruef-kachel-universum.mjs @@ -0,0 +1,602 @@ +/* ===================================================================== + DER GRUND JEDER KACHEL IM HAUS VON TEAM DOGI (11.09.2026) + + Filipe: "veraender die kacheln und den hintergrund, ich will dass + die hintergrunde von den kacheln immer unviersum artig ist, es muss + richtig geil sein aber immer so dass man alles noch gut erkkent. + und das IN DER GANZEN WEBSITE VON TEAM DOGI. nur die kacheln. [...] + wichtig ist die form der kacheln soll gleich bleiben." + + Daraus werden vier pruefbare Saetze, und jeder einzelne kann + scheitern: + + 1. UEBERALL. Nicht "auf der Startseite", sondern auf jeder Kachel + des Hauses -- auch auf denen, die es heute noch nicht gibt. + Deshalb wird geprueft, dass crew-haus.css KEINE Kachelklasse + nennt: Sie setzt vier Werte um, aus denen module.css den Grund + jeder Kachel baut. Eine Datei, die stattdessen Kachel fuer + Kachel aufzaehlt, waere bei der naechsten neuen unvollstaendig + -- lautlos. + 2. NUR DIE KACHELN. Der Grund der SEITE bleibt, wie er war. + 3. DIE FORM BLEIBT GLEICH. Fase, Silhouette und die drei + Eckwinkel werden vor und nach dem Hauswechsel verglichen, + Zeichen fuer Zeichen. + 4. MAN ERKENNT ALLES NOCH GUT. Und das ist der Teil, der wirklich + Arbeit macht. + + --------------------------------------------------------------------- + WIE LESBARKEIT AUF EINEM STERNENHIMMEL GEMESSEN WIRD + + Ein Sternenfeld ist kein Hintergrund, sondern viele. Hinter einem + Buchstaben liegt mal fast schwarzer Grund, mal Nebel, mal -- selten + -- der Kern eines Sterns. Eine einzige Durchschnittszahl wuerde + genau den Fall verschlucken, der stoert; eine Regel auf den + einzelnen hellsten Punkt dagegen waere eine Regel GEGEN Sterne, denn + ein Stern ist per Definition der hellste Punkt seiner Umgebung. + + Deshalb drei Zahlen je Textstelle, alle aus denselben echten + Bildpunkten eines Bildschirmfotos: + + * MITTEL -- die Flaeche, die ein Buchstabe wirklich sieht. + Muss 4,5:1 halten. Das ist die Regel. + * ANTEIL -- wie viel Prozent der Punkte hinter dem Text unter + 4,5:1 liegen. Das ist die eigentliche Messung: Sie + unterscheidet einen PUNKT von einer FLAECHE. Ein + Nebel, der zu hell wird, faellt hier sofort auf, + auch wenn der Mittelwert noch durchgeht. Grenze: + 1,5 Prozent. Gemessen wurden 0,25 Prozent, im + Agenturhaus an derselben Stelle 0,115 Prozent. + * HELLSTER -- der einzelne hellste Bildpunkt, also ein Sternkern. + Grenze 3,4:1 -- bewusst unter der Regel, denn dieser + Punkt ist kleiner als ein Buchstabenstrich breit. + Ohne diese Zahl koennte jemand die Sterne beliebig + hell machen, solange nur wenige davon da sind. + + Gemessen wird an ECHTEN BILDPUNKTEN, nicht an Farbangaben. Das ist + hier keine Vorsicht, sondern Notwendigkeit: Was hinter dem Text + liegt, entsteht erst beim Zeichnen aus Grund, Nebel, Raster und + Schleier. Keine Farbangabe kennt das Ergebnis. + + UND DER INHALT DER KACHEL WIRD DABEI UNSICHTBAR GESCHALTET. Der + erste Anlauf hat das nicht getan und Kontraste von 1,95:1 gemeldet + -- gegen das grosse Zeichen der Kachel, das gar keinen Text + verdeckt. Gefragt ist, was HINTER dem Text liegt, und das gehoert + der Kachel, nicht ihren Kindern. + + --------------------------------------------------------------------- + WARUM DIE ZWEITE ADRESSE HIER NICHT IM BROWSER AUFGERUFEN WIRD + + Das Haus liefert `upgrade-insecure-requests` aus. Ruft man + crew.dogfather-universe.com im Testbrowser ueber eine umgebogene + Namensaufloesung auf, macht der Browser daraus https und findet + nichts. Diese Grenze ist bekannt und wird wie bei den anderen + Haus-Pruefungen umgangen, indem die Frage in zwei geteilt wird: + + a) KOMMT DIE DATEI DORT AN? Das beantwortet eine echte Anfrage mit + gesetztem Host-Kopf ueber node:http -- ohne Browser, ohne CSP. + b) WIE SIEHT SIE AUS? Das beantwortet der Browser, indem genau + dieselbe Datei nachgeladen wird, an derselben Stelle wie im + Betrieb: als letzte Stilvorlage. pruef-css-klassen sichert ab, + dass sie im Betrieb auch wirklich zuletzt steht. + + Nachgeladen wird ueber ihre ADRESSE, nicht ueber ihren Dateipfad. + Das ist kein Detail: Bei einer eingefuegten Datei rechnet der + Browser relative Verweise gegen die SEITE, und aus `../img/…` wird + ein 404, den es im Betrieb nicht gibt. Zwei Minuten Fehlersuche an + einem Fehler, den die Pruefung selbst erzeugt hatte. + ===================================================================== */ +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4391, "pruef-kachel-universum"); + +const ordner = mkdtempSync(join(tmpdir(), "ws-universum-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(240_000, "pruef-kachel-universum"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = `http://127.0.0.1:${PORT}`; + +/* EIGENE ZUHOERER. index.js haengt fuer den Betrieb Auffangnetze an + `uncaughtException` -- eine Pruefung, die dieselbe Datei in denselben + Prozess laedt, erbt sie und wuerde nach einem Absturz einfach + weiterhaengen, statt rot zu werden. */ +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); + +const schluessel = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", schluessel, jetzt); + +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, + createHmac("sha256", schluessel).update(code).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} + +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Rieke", "hand", "CODE-HAND-0001"); +anlegen("Frida", "modi", "CODE-MODI-0001"); +anlegen("Tomma", "modi", "CODE-MODI-0002"); +/* Die Teamseite zeichnet ihre Personenkarten aus dem, was das jeweilige + Haus zu sehen bekommt. Auf DIESER Adresse sind das Manager und + Scouts -- ohne sie bliebe `.tperson` leer, und die Messung der + Personenkarte waere ein uebersprungener Test, der nichts beweist. + Die Kachel ist in beiden Haeusern dieselbe; gemessen wird ihr Grund, + nicht wessen Name darin steht. */ +anlegen("Mara", "manager", "CODE-MANA-0001"); +anlegen("Sten", "scout", "CODE-SCOU-0001"); +d.close(); + +const CREW_CSS_ADRESSE = "/workspace/assets/css/crew-haus.css"; +const CREW_CSS = readFileSync( + new URL("../workspace/assets/css/crew-haus.css", import.meta.url).pathname + .replace(/^\/([A-Za-z]:)/, "$1"), "utf8"); + +/* ======================================================================= + 1. DIE DATEI KOMMT AUF DER ZWEITEN ADRESSE AN -- UND NUR DORT + ======================================================================= */ +melde(""); +melde("=== 1. Ausgeliefert wird sie da, wo sie hingehoert ==="); + +function holen(host) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: "/workspace/assets/css/haus.css", + method: "GET", headers: { Host: host } }, (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text })); + }); + a.on("error", schief); + a.end(); + }); +} +{ + const crew = await holen("crew.dogfather-universe.com"); + const agentur = await holen("workspace.dogfather-universe.com"); + ok(crew.code === 200 && crew.text.includes("--sternenfeld:"), + `die zweite Wand bekommt das Sternenfeld (${crew.code}, ${crew.text.length} Zeichen)`); + ok(crew.text.includes("--modul-schleier: .30"), + "und den Schleier, der dazugehoert"); + ok(agentur.code === 200 && !agentur.text.includes("--sternenfeld"), + `die Agenturadresse bekommt es NICHT (${agentur.text.length} Zeichen)`); + ok(crew.text.length > agentur.text.length + 500, + "die beiden Haeuser liefern wirklich verschiedene Dateien aus"); +} + +/* KEINE ACHTE ABSCHRIFT DER KACHELLISTE. + Der eigentliche Gedanke hinter dem ganzen Umbau: crew-haus.css darf + keine Kachel beim Namen nennen. Sonst waere sie beim naechsten neuen + Bauteil unvollstaendig, ohne dass es auffiele. */ +{ + const kachelklassen = [".eintrag-karte", ".t-kachel", ".tperson", ".liste-kachel", + ".steckbrief", ".zahlkarte", ".k-anlasskachel", ".leer-hinweis"]; + const gefunden = kachelklassen.filter((k) => CREW_CSS.includes(k)); + ok(gefunden.length === 0, + `crew-haus.css zaehlt keine Kachelklassen auf (${gefunden.join(" ") || "keine"})`); + ok(CREW_CSS.includes("--sternenfeld:") && CREW_CSS.includes("--sternenfeld-mass:") + && CREW_CSS.includes("--sternenfeld-lage:"), + "sie setzt stattdessen die Werte, aus denen module.css den Grund baut"); + + const modul = readFileSync( + new URL("../workspace/assets/css/module.css", import.meta.url).pathname + .replace(/^\/([A-Za-z]:)/, "$1"), "utf8"); + ok((modul.match(/var\(--sternenfeld,/g) || []).length === 1, + "und module.css benutzt das Bild an genau EINER Stelle"); + ok(modul.includes("--sternenfeld: none;"), + "im Agenturhaus steht dort `none` -- eine Ebene ohne Bild zeichnet nichts"); +} + +/* KEIN KARO. Wiederholen sich zwei Sternebenen im selben Abstand, sieht + man sofort ein Muster statt eines Feldes. Die Abstaende duerfen + deshalb keinen gemeinsamen Teiler haben -- das ist keine Kosmetik, + sondern der Unterschied zwischen Himmel und Tapete. */ +{ + const zeile = CREW_CSS.split("--sternenfeld-mass:")[1].split(";")[0]; + const zahlen = (zeile.match(/(\d+)px/g) || []).map((z) => parseInt(z, 10)); + const ggt = (a, b) => (b === 0 ? a : ggt(b, a % b)); + let groesster = 0; + for (let i = 0; i < zahlen.length; i++) + for (let j = i + 1; j < zahlen.length; j++) + groesster = Math.max(groesster, ggt(zahlen[i], zahlen[j])); + ok(zahlen.length >= 6, `sechs Abstaende fuer drei Sternebenen (${zahlen.join(" ")})`); + ok(groesster === 1, `kein gemeinsamer Teiler, also kein Karo (groesster: ${groesster})`); +} + +/* ======================================================================= + 2. DER BROWSER: erst das Agenturhaus, dann dasselbe mit dem Himmel + ======================================================================= */ +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: 1440, height: 1100 } }); +const seite = await ctx.newPage(); +const konsole = []; +seite.on("pageerror", (f) => konsole.push("Absturz: " + f.message)); +seite.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); }); +seite.on("response", (a) => { if (a.status() >= 400) konsole.push(`${a.status()} ${a.url()}`); }); + +await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); +await seite.click('.rolle[data-rolle="admin"]'); +await seite.fill("#code", "CODE-DOGI-0001"); +await seite.click("button[type=submit]"); +await seite.waitForURL("**/start.html", { timeout: 25000 }); +await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); +await seite.waitForSelector(".kachel", { timeout: 10000 }); +await seite.waitForTimeout(600); + +/* Alles, was sich beim Hauswechsel NICHT aendern darf, wird vorher + abgeholt -- Form, Silhouette, Eckwinkel, Seitengrund. */ +async function formAufnehmen() { + return seite.evaluate(() => { + const k = document.querySelector(".kachel"); + const s = getComputedStyle(k); + const n = getComputedStyle(k, "::after"); + const v = getComputedStyle(document.documentElement); + /* Ein Hintergrund ist eine Liste, und in jeder Ebene stehen selbst + Kommas. Mit einem Muster kommt man da nicht weit -- Chromium + schreibt die Verlaeufe voll aus, Klammern in Klammern. Gezaehlt + wird deshalb, wie tief man gerade steht. */ + let tiefe = 0, ebenen = 1; + for (const z of n.backgroundImage) { + if (z === "(") tiefe++; + else if (z === ")") tiefe--; + else if (z === "," && tiefe === 0) ebenen++; + } + return { + fase: v.getPropertyValue("--fase").trim(), + silhouette: s.clipPath, + /* Die sechs Eckwinkel sind die ersten sechs Ebenen. Ihre Groessen + und Lagen SIND die Form. */ + winkelMass: n.backgroundSize.split(",").slice(0, 6).join(",").trim(), + winkelLage: n.backgroundPosition.split(",").slice(0, 6).join(",").trim(), + ebenen, + seitengrund: getComputedStyle(document.body).backgroundColor, + sternenfeld: v.getPropertyValue("--sternenfeld").trim(), + schleier: v.getPropertyValue("--modul-schleier").trim(), + hausname: v.getPropertyValue("--haus-name").trim(), + }; + }); +} +const vorher = await formAufnehmen(); + +melde(""); +melde("=== 2. Das Agenturhaus bleibt unberuehrt ==="); +ok(vorher.sternenfeld === "none", + `dort steht kein Sternenfeld (${vorher.sternenfeld})`); +ok(vorher.schleier === ".26", + `und der Schleier steht unveraendert auf .26 (${vorher.schleier})`); +ok(vorher.ebenen === 9, + `neun Ebenen: sechs Eckwinkel, zwei Rasterlinien, eine leere (${vorher.ebenen})`); + +/* Bildpunkte aus einem Bildschirmfoto -- derselbe Weg wie in + pruef-lesbarkeit und pruef-personen-kachel. Zurueck kommen der + Mittelwert und die SORTIERTE Helligkeitsliste; wer daraus einen + Anteil rechnen will, braucht die ganze Liste, nicht eine Kennzahl. */ +async function punkte(clip) { + const bild = await seite.screenshot({ clip }); + return seite.evaluate(async (b64) => { + const bild = new Image(); + await new Promise((r) => { bild.onload = r; bild.src = "data:image/png;base64," + b64; }); + const c = document.createElement("canvas"); + c.width = bild.width; c.height = bild.height; + const g = c.getContext("2d", { willReadFrequently: true }); + g.drawImage(bild, 0, 0); + const dd = g.getImageData(0, 0, c.width, c.height).data; + const hell = (p) => { + const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); }); + return .2126 * f[0] + .7152 * f[1] + .0722 * f[2]; + }; + const liste = []; + let sr = 0, sg = 0, sb = 0, hr = 0, hg = 0, hb = 0, best = -1; + for (let i = 0; i < dd.length; i += 4) { + sr += dd[i]; sg += dd[i + 1]; sb += dd[i + 2]; + const h = hell([dd[i], dd[i + 1], dd[i + 2]]); + liste.push(h); + if (h > best) { best = h; hr = dd[i]; hg = dd[i + 1]; hb = dd[i + 2]; } + } + const n = liste.length; + liste.sort((a, b) => a - b); + return { + mittel: [Math.round(sr / n), Math.round(sg / n), Math.round(sb / n)], + hellsterPunkt: [hr, hg, hb], + hell: liste, anzahl: n, + }; + }, bild.toString("base64")); +} + +/* WAS HINTER DEM TEXT LIEGT, IST NICHT DER TEXT -- UND AUCH NICHT SEIN + NACHBAR. Fuer die Dauer der Messung wird der INHALT der Kachel + unsichtbar, die Kachel selbst nicht: Hintergrund, Kantenlicht, + Raster, Eckwinkel und Sternenfeld gehoeren ihr, nicht ihren Kindern. */ +const KARTEN = ".kachel, .t-kachel, .tperson, .tl, .eintrag-karte, .person, .liste-kachel"; +async function inhaltAus(waehler) { + await seite.evaluate(([w, karten]) => { + const e = document.querySelector(w); + const karte = e && (e.closest(karten) || e.parentElement); + if (!karte) return; + karte.dataset.grundmessung = "ja"; + karte.querySelectorAll("*").forEach((x) => { + x.dataset.warSichtbar = x.style.visibility || ""; + x.style.visibility = "hidden"; + }); + }, [waehler, KARTEN]); +} +async function inhaltAn() { + await seite.evaluate(() => { + const karte = document.querySelector('[data-grundmessung="ja"]'); + if (!karte) return; + karte.querySelectorAll("*").forEach((x) => { + x.style.visibility = x.dataset.warSichtbar || ""; + delete x.dataset.warSichtbar; + }); + delete karte.dataset.grundmessung; + }); +} + +/* DER AUSSCHNITT LIEGT IN DER MITTE DER KACHEL. Am Rand sitzt das + Kantenlicht -- eine 1,6 px schmale Linie in fast vollem Kachelton, + und damit der hellste Punkt der ganzen Kachel. Wer den mitmisst, + misst die Form statt des Himmels: Der erste Anlauf meldete deshalb + "keine neuen hellen Punkte", obwohl das Sternenfeld laengst da war. */ +const kachelRahmen = await seite.evaluate(() => { + const k = document.querySelector(".kachel"); + k.scrollIntoView({ block: "center" }); + const r = k.getBoundingClientRect(); + return { x: Math.round(r.x + r.width * .22), y: Math.round(r.y + r.height * .22), + width: Math.round(r.width * .56), height: Math.round(r.height * .56) }; +}); +await inhaltAus(".kachel"); +const grundAgentur = await punkte(kachelRahmen); +await inhaltAn(); + +/* ---- Jetzt dasselbe Fenster, aber im Haus von Team Dogi ---- */ +await seite.addStyleTag({ url: CREW_CSS_ADRESSE }); +await seite.waitForTimeout(400); +const nachher = await formAufnehmen(); + +melde(""); +melde("=== 3. Die Form bleibt gleich, der Grund nicht ==="); +ok(nachher.hausname === '"crew"', + `das Haus von Team Dogi ist angekommen (${nachher.hausname})`); +ok(nachher.fase === vorher.fase, `die Fase ist unveraendert (${nachher.fase})`); +ok(nachher.silhouette === vorher.silhouette, "die Silhouette der Kachel ist Zeichen fuer Zeichen dieselbe"); +ok(nachher.winkelMass === vorher.winkelMass, "die drei Eckwinkel haben dieselbe Groesse"); +ok(nachher.winkelLage === vorher.winkelLage, "und dieselbe Lage"); +ok(nachher.ebenen === 13, + `aus der leeren Ebene sind fuenf geworden: drei Sternebenen, zwei Nebel (${nachher.ebenen})`); +ok(nachher.schleier === ".30", `der Schleier laesst etwas mehr durch (${nachher.schleier})`); +ok(nachher.sternenfeld.includes("radial-gradient"), + "und das Sternenfeld steht als Bild da, nicht als none"); + +await inhaltAus(".kachel"); +const grundCrew = await punkte(kachelRahmen); +await inhaltAn(); +{ + const abstand = Math.max(...grundCrew.mittel.map((x, i) => Math.abs(x - grundAgentur.mittel[i]))); + ok(abstand >= 4, + `der Kachelgrund ist messbar ein anderer geworden (${grundAgentur.mittel} -> ${grundCrew.mittel})`); + ok(grundCrew.hell[grundCrew.anzahl - 1] > grundAgentur.hell[grundAgentur.anzahl - 1] * 1.5, + `und es gibt jetzt helle Punkte, die es vorher nicht gab (${grundAgentur.hellsterPunkt} -> ${grundCrew.hellsterPunkt})`); +} + +/* GEGENPROBE ZUM MESSGERAET. Nimmt man das Sternenfeld wieder weg, muss + der Grund zurueckspringen. Ohne diese Probe koennte die Messung oben + auch etwas ganz anderes gesehen haben -- die Farben haben sich beim + Hauswechsel ja ohnehin geaendert. */ +{ + await seite.addStyleTag({ content: ":root { --sternenfeld: none; }" }); + await seite.waitForTimeout(250); + await inhaltAus(".kachel"); + const ohne = await punkte(kachelRahmen); + await inhaltAn(); + ok(ohne.hell[ohne.anzahl - 1] < grundCrew.hell[grundCrew.anzahl - 1] * .8, + `ohne Sternenfeld verschwinden die hellen Punkte wieder (${grundCrew.hellsterPunkt} -> ${ohne.hellsterPunkt})`); + await seite.evaluate(() => { + const letzte = [...document.querySelectorAll("style")].pop(); + if (letzte && letzte.textContent.includes("--sternenfeld: none")) letzte.remove(); + }); + await seite.waitForTimeout(250); +} + +/* ======================================================================= + 4. NUR DIE KACHELN -- der Grund der Seite bleibt + ======================================================================= */ +melde(""); +melde("=== 4. Nur die Kacheln ==="); +{ + const zwischenraum = await seite.evaluate(() => { + /* Ein Fleck ZWISCHEN zwei Kacheln. Er gehoert der Seite, nicht + einer Kachel -- dort darf kein Stern stehen. */ + const kacheln = [...document.querySelectorAll(".kachel")]; + if (kacheln.length < 2) return null; + const a = kacheln[0].getBoundingClientRect(); + const b = kacheln[1].getBoundingClientRect(); + if (b.x - (a.x + a.width) < 6) return null; + return { x: Math.round(a.x + a.width) + 1, y: Math.round(a.y) + 20, + width: Math.max(2, Math.round(b.x - (a.x + a.width)) - 2), height: 40 }; + }); + ok(zwischenraum !== null, "zwischen zwei Kacheln ist Platz zum Messen"); + if (zwischenraum) { + const p = await punkte(zwischenraum); + const spanne = Math.max(...p.hellsterPunkt.map((x, i) => x - p.mittel[i])); + ok(spanne <= 12, + `der Grund zwischen den Kacheln ist ruhig, dort steht kein Stern (Spanne ${spanne})`); + } +} + +/* ======================================================================= + 5. MAN ERKENNT ALLES NOCH GUT + ======================================================================= */ +function kontrast(a, lb) { + const l = (p) => { + const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); }); + return .2126 * f[0] + .7152 * f[1] + .0722 * f[2]; + }; + const x = typeof a === "number" ? a : l(a); + return (Math.max(x, lb) + .05) / (Math.min(x, lb) + .05); +} +function leuchtkraft(p) { + const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); }); + return .2126 * f[0] + .7152 * f[1] + .0722 * f[2]; +} + +async function messen(waehler) { + const stelle = await seite.evaluate((w) => { + const e = document.querySelector(w); + if (!e) return null; + e.scrollIntoView({ block: "center" }); + const r = e.getBoundingClientRect(); + if (r.width < 8 || r.height < 6) return null; + /* Die Schriftfarbe wird GEZEICHNET, nicht gelesen: color-mix() + liefert getComputedStyle als "color(srgb …)", und daraus Ziffern + zu klauben ergibt Unsinn. Ein Canvas versteht jede Schreibweise. */ + const c = document.createElement("canvas"); + c.width = 1; c.height = 1; + const g = c.getContext("2d", { willReadFrequently: true }); + g.fillStyle = "#000000"; + g.fillStyle = getComputedStyle(e).color; + g.fillRect(0, 0, 1, 1); + const px = g.getImageData(0, 0, 1, 1).data; + e.dataset.messen = "ja"; + e.style.color = "transparent"; + return { x: Math.round(r.x), y: Math.round(r.y), + w: Math.max(8, Math.round(r.width)), h: Math.round(Math.min(r.height, 40)), + farbe: [px[0], px[1], px[2]] }; + }, waehler); + if (!stelle) return null; + await inhaltAus(waehler); + const hinten = await punkte({ x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h }); + await inhaltAn(); + await seite.evaluate(() => { + const e = document.querySelector('[data-messen="ja"]'); + if (e) { e.style.color = ""; delete e.dataset.messen; } + }); + const lt = leuchtkraft(stelle.farbe); + const unter = hinten.hell.filter((h) => kontrast(lt, h) < 4.5).length; + return { + farbe: stelle.farbe, + mittel: kontrast(lt, leuchtkraft(hinten.mittel)), + hellster: kontrast(lt, hinten.hell[hinten.anzahl - 1]), + anteil: 100 * unter / hinten.anzahl, + punkte: hinten.anzahl, + }; +} + +async function lesbar(name, waehler) { + const m = await messen(waehler); + if (m === null) { ok(false, `${name}: nicht gefunden (${waehler})`); return; } + ok(m.punkte > 200 && m.mittel >= 4.5 && m.anteil <= 1.5 && m.hellster >= 3.4, + `${name}: Mittel ${m.mittel.toFixed(2)} · unter 4,5 liegen ${m.anteil.toFixed(3)} % · hellster Punkt ${m.hellster.toFixed(2)} (${m.punkte} Bildpunkte)`); +} + +melde(""); +melde("=== 5. Lesbarkeit auf der Startseite ==="); +for (const [name, w] of [ + ["Kachelname", ".kachel .kachel__name"], + ["Kachelzeile", ".kachel .kachel__unter"], + ["kleine Kachel, Name", '.kachel:not([data-gross="ja"]) .kachel__name'], + ["kleine Kachel, Zeile", '.kachel:not([data-gross="ja"]) .kachel__unter'], +]) await lesbar(name, w); + +/* GEGENPROBE: ein absichtlich zu dunkler Text MUSS durchfallen. Ohne + sie waere nicht bewiesen, dass diese Messung ueberhaupt "nicht in + Ordnung" sagen kann. */ +{ + await seite.evaluate(() => { + document.querySelector(".kachel .kachel__name").style.color = "#171326"; + }); + const schlecht = await messen(".kachel .kachel__name"); + ok(schlecht !== null && schlecht.mittel < 4.5 && schlecht.anteil > 50, + `absichtlich zu dunkler Text faellt durch (${schlecht?.mittel.toFixed(2)}:1, ${schlecht?.anteil.toFixed(1)} % unter der Grenze)`); + await seite.evaluate(() => { + document.querySelector(".kachel .kachel__name").style.color = ""; + }); +} + +/* GEGENPROBE ZUM ANTEIL: ein zu heller Nebel MUSS auffallen, auch wenn + der Mittelwert noch durchgeht. Das ist der Fall, fuer den diese Zahl + ueberhaupt gebaut wurde. */ +{ + await seite.addStyleTag({ content: ":root { --sternenfeld:" + + " radial-gradient(ellipse 90% 90% at 50% 50%, rgba(255,255,255,.95) 0%, transparent 85%);" + + " --sternenfeld-mass: 100% 100%; --sternenfeld-lage: 0 0; }" }); + await seite.waitForTimeout(300); + const grell = await messen(".kachel .kachel__unter"); + ok(grell !== null && grell.anteil > 1.5, + `ein zu heller Nebel faellt durch (${grell?.anteil.toFixed(1)} % unter der Grenze)`); + await seite.evaluate(() => { + const letzte = [...document.querySelectorAll("style")].pop(); + if (letzte && letzte.textContent.includes("ellipse 90% 90%")) letzte.remove(); + }); + await seite.waitForTimeout(300); +} + +melde(""); +melde("=== 6. Lesbarkeit auf der Teamseite ==="); +await seite.goto(BASIS + "/workspace/team.html", { waitUntil: "networkidle" }); +await seite.addStyleTag({ url: CREW_CSS_ADRESSE }); +await seite.waitForSelector(".t-kachel", { timeout: 15000 }); +await seite.waitForTimeout(900); +/* Zugeklappte Abschnitte messen nichts -- erst aufklappen, dann messen. */ +await seite.evaluate(() => { + document.querySelectorAll("details:not([open])").forEach((x) => { x.open = true; }); +}); +await seite.waitForTimeout(500); +{ + const da = await seite.evaluate(() => ({ + kachel: !!document.querySelector(".t-kachel"), + person: !!document.querySelector(".tperson"), + titel: !!document.querySelector(".t-kopf__titel"), + haus: getComputedStyle(document.documentElement).getPropertyValue("--haus-name").trim(), + })); + ok(da.kachel && da.titel && da.person && da.haus === '"crew"', + `die Teamseite ist gezeichnet und traegt das richtige Haus (${JSON.stringify(da)})`); +} +for (const [name, w] of [ + ["Abschnittstitel", ".t-kachel .t-kopf__titel"], + ["Personenname", ".tperson .tperson__name"], + ["Personenzeile (leisester Text im Haus)", ".tperson .tperson__unter"], +]) await lesbar(name, w); + +melde(""); +melde("=== 7. Nichts abgestuerzt ==="); +ok(konsole.length === 0, `keine Fehler im Browser (${konsole.slice(0, 3).join(" | ") || "keine"})`); + +await ctx.close(); +await browser.close(); +/* Der Server haelt die Datenbankdatei offen -- unter Windows laesst sie + sich dann nicht loeschen. Das ist kein Befund, sondern ein + Aufraeumrest im Temp-Ordner, und darf die Pruefung nicht rot machen. */ +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + +console.log(""); +console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); +console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); +process.exit(fehler === 0 ? 0 : 1); diff --git a/server/pruef-sicht.mjs b/server/pruef-sicht.mjs index a8f523bb..f09df86a 100644 --- a/server/pruef-sicht.mjs +++ b/server/pruef-sicht.mjs @@ -257,16 +257,26 @@ for (const [pfad, name, feld] of [ ok(a.status === 200 && b.status === 200, `${name}: beide Abfragen gehen durch`); } -/* 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. - Dass die Regel nicht einfach ALLES ausblendet, zeigt der Umschalter: - Als Patrick kommen beide seiner Eintraege an. Ohne diesen Nachweis - waere ein Kalender, der gar nichts mehr liefert, die sauberste - Pruefung von allen. */ +/* DER KALENDER IST PRIVAT -- AUCH FUER DEN UMSCHALTER (11.09.2026). + + Bis heute stand hier das Gegenteil: "Fremde Verabredungen sieht er + nicht mehr in seinem eigenen Kalender; dafuer gibt es den + Umschalter." Filipe hat die Regel selbst nachgezogen, als er die + Sicht vervollstaendigen liess: + + "oben bei meine sicht soll ich auch die sicht von allen jeden + moment sehen koennen und das genau genau so wie sie es sehen + alles. AUSSER die kalender daten oder chat daten wo ich nicht mit + drin bin.. also private daten." + + Damit ist der Kalender die AUSNAHME von der Sicht, nicht ihr + Paradebeispiel. Ein Termin traegt Titel, Ort und Gegenueber; wer ihn + liest, liest mit, wo jemand war und mit wem. + + DIE GEGENPROBE BLEIBT DIESELBE FRAGE, NUR ANDERSHERUM GESTELLT. Ein + Kalender, der gar nichts mehr liefert, waere die sauberste Pruefung + von allen und die wertloseste -- deshalb muss jemand die Eintraege + sehen. Das ist jetzt Patrick selbst, angemeldet als er. */ { const d2 = new DatabaseSync(process.env.WORKSPACE_DB); const rein = d2.prepare( @@ -280,6 +290,14 @@ for (const [pfad, name, feld] of [ Cigdems Kalender gesehen. Der hier gehoert also Patrick, und genau das wird unten geprueft. */ rein.run("PATRICKS NOTIZ", "termin", heute + "T14:00", 30, null, jetzt, idPat); + /* UND EINER FUER MARA. Ohne ihn stand die Managerin in der Pruefung + unten mit einer LEEREN Call-Liste da -- und eine leere Liste ist + zufaellig genau dasselbe wie DogFathers leere Liste. Die Pruefung + "bleibt privat" waere damit nicht bestanden, sondern gar nicht + durchfuehrbar gewesen. Sie hat das gemeldet, statt es zu + verschweigen; die Antwort darauf sind Daten, keine weichere + Bedingung. */ + rein.run("MARA-CALL", "call", heute + "T16:00", 30, null, jetzt, idMgr); d2.close(); const j = async (k, s) => { @@ -292,14 +310,113 @@ for (const [pfad, name, feld] of [ `DogFather sieht fremde Verabredungen nicht mehr (${alleT.join(", ") || "nichts"})`); ok(!alleT.includes("PATRICKS NOTIZ"), "auch einen Termin OHNE Teilnehmer nicht -- der gehoert dem, der ihn anlegt"); - ok(patT.includes("PATRICK-TERMIN") && patT.includes("PATRICKS NOTIZ") + ok(!patT.includes("PATRICK-TERMIN") && !patT.includes("PATRICKS NOTIZ") && !patT.includes("BEN-TERMIN"), - `mit dem Umschalter auf Patrick genau dessen, beide (${patT.join(", ")})`); + `auch mit dem Umschalter auf Patrick nicht -- der Kalender ist privat (${patT.join(", ") || "nichts"})`); + /* UND JETZT DER BEWEIS, DASS DIE PRUEFUNG DARUEBER ETWAS MISST. + Patrick selbst muss beide Eintraege bekommen; sonst hiesse + "DogFather sieht sie nicht" nur, dass niemand sie sieht. */ + const patSelbst = await j(kPat); + ok(patSelbst.includes("PATRICK-TERMIN") && patSelbst.includes("PATRICKS NOTIZ") + && !patSelbst.includes("BEN-TERMIN"), + `Patrick selbst sieht genau seine beiden (${patSelbst.join(", ")})`); } /* ================================================================ 6. IM BROWSER: der Umschalter steht da -- und nur bei DogFather ================================================================ */ +/* ======================================================================= + DIE SICHT IST VOLLSTAENDIG -- UND SIE HAT GENAU ZWEI AUSNAHMEN + ======================================================================= + + Filipe, 11.09.2026: "oben bei meine sicht soll ich auch die sicht von + allen jeden moment sehen koennen und das genau genau so wie sie es + sehen alles. ausser die kalender daten oder chat daten wo ich nicht + mit drin bin.. also private daten." + + "GENAU SO WIE SIE ES SEHEN" IST EINE GLEICHUNG, ALSO WIRD SIE ALS + GLEICHUNG GEPRUEFT. Verglichen wird nicht "mit Umschalter gegen ohne" + -- das ist bei duenner Datenlage ueberall gleich und beweist nichts, + genau die Sorte gruener Haken, die nichts gruent. Verglichen wird die + Antwort, die DogFather mit gesetztem Umschalter bekommt, mit der + Antwort, die die Person SELBST bekommt, angemeldet als sie. + + So ist dieser Abschnitt entstanden: als Wegwerf-Messung ueber alle + Wege des Hauses. Sie hat sechs Stellen gefunden, an denen die Sicht + ins Leere lief -- darunter eine, an der seit dem 01.09. ein + ausfuehrlicher Kommentar beschrieb, was der Code daneben nie getan + hat (`req.person.sicht`, ein Feld, das es nicht gibt). Ein Tippfehler + in einem Variablennamen macht nichts kaputt; er tut nur nichts. Genau + deshalb steht die Messung jetzt hier fest drin. + + DIE AUSNAHMEN WERDEN GENAUSO SCHARF GEPRUEFT wie die Regel: Kalender, + Calls, Wiederholungen und Chat MUESSEN abweichen. Waeren sie gleich, + waere Filipes Grenze gefallen, ohne dass es jemandem auffiele. */ +console.log("\n=== Die Sicht zeigt, was die andere Person sieht"); +{ + /* Was hier NICHT steht und warum: `/workspace/api/ich` (es meldet + absichtlich weiter, WER angemeldet ist -- sonst stuende im Kopf der + falsche Name), `/workspace/api/personen` (das ist die Liste, aus + der der Umschalter selbst gebaut wird; folgte sie der Sicht, kaeme + man aus einer fremden Sicht nicht mehr heraus) und die Listen, aus + denen man beim ANLEGEN waehlt (Kategorien, Empfaenger) -- die + gehoeren zum Schreiben, und geschrieben wird immer als man selbst. */ + const GLEICH = [ + ["die Zentrale", "/workspace/api/zentrale"], + ["die Leistungszahlen", "/workspace/api/leistung"], + ["die Creator-Liste im Profil", "/workspace/api/profil"], + ["die Schulungen", "/workspace/api/schulung"], + ["die Fruehwarnung", "/workspace/api/fruehwarnung"], + ["der Report", "/workspace/api/report"], + ["die Uebersicht", "/workspace/api/uebersicht"], + ]; + const PRIVAT = [ + ["der Kalender", "/workspace/api/termine"], + ["die Calls", "/workspace/api/calls"], + ["die Gespraeche", "/workspace/api/chat/raeume"], + ]; + + const text = async (pfad, keks) => { + const a = await holen(pfad, keks); + return { status: a.status, text: await a.text() }; + }; + + for (const [wer, id, keks] of [ + ["Manager", idMgr, kMgr], ["Scout", idPat, kPat], ["Creator", idTili, kTili], + ]) { + for (const [name, pfad] of GLEICH) { + const durch = await text(pfad + "?sicht=" + id, kDogi); + const selbst = await text(pfad, keks); + /* DRITTER AUSGANG: Kann die Person den Weg gar nicht aufrufen, + ist hier nichts zu vergleichen -- und das ist ein Fehler, kein + Ueberspringen. Sonst waere diese Schleife gruen, sobald eine + Rolle irgendwann 404 bekommt. */ + ok(durch.status === 200 && selbst.status === 200 + && durch.text === selbst.text, + `${wer}: ${name} ist in der Sicht dieselbe Antwort` + + ` (${durch.status}/${selbst.status}, ${durch.text.length}/${selbst.text.length} Zeichen)`); + } + for (const [name, pfad] of PRIVAT) { + const durch = await text(pfad + "?sicht=" + id, kDogi); + const selbst = await text(pfad, keks); + const eigen = await text(pfad, kDogi); + ok(durch.status === 200 && durch.text === eigen.text && durch.text !== selbst.text, + `${wer}: ${name} bleibt privat -- der Umschalter aendert daran nichts` + + ` (${durch.text.length} eigene / ${selbst.text.length} ihre)`); + } + } + + /* GEGENPROBE ZUR GLEICHUNG SELBST. Wenn zwei Antworten, die + verschieden sein MUESSEN, hier als gleich durchgingen, waere der + ganze Abschnitt darueber wertlos. Also einmal absichtlich die + falsche Person einsetzen: Die Zentrale des Managers und die des + Scouts duerfen nicht dieselbe Antwort sein. */ + const zMgr = await (await holen("/workspace/api/zentrale?sicht=" + idMgr, kDogi)).text(); + const zPat = await (await holen("/workspace/api/zentrale?sicht=" + idPat, kDogi)).text(); + ok(zMgr !== zPat, + "Gegenprobe: zwei verschiedene Personen ergeben auch zwei verschiedene Antworten"); +} + console.log("\n=== Der Umschalter im Seitenkopf"); const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index e48011c6..d57f0dbd 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -10,7 +10,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, betreuteIds, + db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, MODI_KATEGORIEN, kategorienFuer, kategoriePersonen, ohneTeamDogi, ohneAgentur, siehtModis, TEAM_DOGI_ROLLEN, @@ -278,6 +278,11 @@ aufgabenRouter.get("/workspace/api/aufgaben", (req, res) => { /* Fuer wessen Aufgaben sie gilt -- als Nummern, nicht als Rollenname. Im Browser darf nichts stehen, woraus sich auf eine verborgene Rolle schliessen laesst. */ + /* Die Kategorien bleiben beim Angemeldeten: Sie entscheiden, + WOHIN geschrieben werden darf, nicht was zu sehen ist. Wer in + fremder Sicht eine Aufgabe anlegt, legt sie als er selbst an -- + eine Auswahl aus fremden Kategorien waere ein Angebot, das der + Server danach ablehnt. */ kategorie_fuer: kategoriePersonen(req.person), }); } catch (fehler) { @@ -366,8 +371,22 @@ aufgabenRouter.get("/workspace/api/personen", (req, res) => { und beim naechsten Umzug des Ordners waere er an sechs Stellen falsch. Wer kein Bild hat, bekommt `null`; die Oberflaeche zeigt dann wie bisher den Anfangsbuchstaben. */ + /* DIE UEBERSCHRIFT KOMMT MIT (11.09.2026, dritter Fall derselben + Luecke). + + Der Browser gruppiert Personenlisten nach der Rollenfolge aus + bereiche.js -- und wer dort nicht steht, wird nicht gezeichnet. + Das ist jetzt zum dritten Mal aufgefallen: in der Chat-Auswahl, + in der Personenliste, und hier im Sicht-Umschalter. DogFather + konnte die Sicht seines Teams nicht waehlen, weil das Team in + der Auswahl gar nicht vorkam. + + Die Rollennamen duerfen dort nicht stehen (sie gehoeren in keine + Datei, die jeder herunterlaedt) -- also schickt der Server einen + TEXT mit, und der Browser zeichnet, was ankommt. */ const mitBild = (z) => ({ id: z.id, name: z.name, rolle: z.rolle, + gruppe: ROLLEN_GRUPPE[z.rolle] || "Weitere", bild: z.bild ? `/workspace/api/steckbrief/bild/${z.bild}` : null, }); const ids = sichtbarePersonenIds(req.person); @@ -379,7 +398,9 @@ aufgabenRouter.get("/workspace/api/personen", (req, res) => { }); } if (!ids.length) { - return res.json({ personen: [{ id: req.person.id, name: req.person.name, rolle: req.person.rolle, bild: null }] }); + return res.json({ personen: [{ id: req.person.id, name: req.person.name, + rolle: req.person.rolle, gruppe: ROLLEN_GRUPPE[req.person.rolle] || "Weitere", + bild: null }] }); } res.json({ personen: db().prepare( diff --git a/server/workspace-calls.js b/server/workspace-calls.js index 772c8cea..0c25243e 100644 --- a/server/workspace-calls.js +++ b/server/workspace-calls.js @@ -108,7 +108,10 @@ function aufgabenZu(protokollIds) { callRouter.get("/workspace/api/calls", (req, res) => { try { - const { wo, werte } = sichtbar(req.sicht || req.person); + /* Calls & Protokolle haengen an derselben Regel wie die Termine -- + und sind damit ebenso privat. Wer hier die fremde Sicht laesst, + hat die Ausnahme nur halb gebaut. */ + const { wo, werte } = sichtbar(req.person); const alle = db().prepare(` SELECT ${SPALTEN} ${VERBUND} WHERE ${wo} AND t.art IN ('call','review') diff --git a/server/workspace-fruehwarnung.js b/server/workspace-fruehwarnung.js index b8615586..7d505bbc 100644 --- a/server/workspace-fruehwarnung.js +++ b/server/workspace-fruehwarnung.js @@ -239,9 +239,25 @@ fruehwarnungRouter.get("/workspace/api/fruehwarnung", (req, res) => { lesen, dass das System ihn für abwanderungsgefährdet hält -- das ist eine Arbeitsgrundlage für die Betreuung, keine Mitteilung an die betroffene Person. */ - if (req.person.rolle === "creator") return res.json({ creator: [], gilt_nicht: true }); + /* DURCH DIE AUGEN DER ANGESEHENEN PERSON (11.09.2026). - const ids = sichtbareCreatorIds(req.person); + Filipe: "oben bei meine sicht soll ich auch die sicht von allen + jeden moment sehen koennen und das genau genau so wie sie es + sehen alles." + + Nur LESEN wird umgestellt, nie ein Recht: `req.person` bleibt + ueberall dort stehen, wo geschrieben oder geprueft wird. Und + weil DogFather ohnehin alles sehen darf, kann diese Umstellung + nichts oeffnen -- sie kann nur WENIGER zeigen, naemlich genau + das, was die andere Person sieht. + Und weil die Fruehwarnung ausdruecklich NICHT fuer die + betroffene Person gedacht ist, muss auch dieser Riegel der Sicht + folgen: In der Sicht auf einen Creator gehoert dort `gilt_nicht` + zu stehen, nicht DogFathers eigene Liste. */ + const zeigt = req.sicht || req.person; + if (zeigt.rolle === "creator") return res.json({ creator: [], gilt_nicht: true }); + + const ids = sichtbareCreatorIds(zeigt); const wo = ids === null ? "rolle = 'creator' AND aktiv = 1" : ids.length ? `id IN (${ids.map(() => "?").join(",")}) AND aktiv = 1` : "0=1"; const leute = db().prepare(`SELECT id, name FROM personen WHERE ${wo} ORDER BY name`) diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index e0d24661..71266de8 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -283,7 +283,24 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { Der Nachfüller bremst sich selbst (höchstens alle fünf Minuten) und legt nur an, was noch fehlt. */ nachfuellenAlle(); - const { wo, werte } = sichtbar(req.sicht || req.person); + /* DER KALENDER BLEIBT IMMER DER EIGENE (11.09.2026). + + Filipe: "oben bei meine sicht soll ich auch die sicht von allen + jeden moment sehen koennen und das genau genau so wie sie es + sehen alles. AUSSER die kalender daten oder chat daten wo ich + nicht mit drin bin.. also private daten." + + Hier stand `req.sicht || req.person`. Damit zeigte die fremde + Sicht die Termine der anderen Person -- und ein Termin traegt + Titel, Ort und Gegenueber. Das ist genau die Sorte Daten, die er + ausnimmt. + + Es steht `req.person` und nicht eine Bedingung: Eine Ausnahme + ("ausser bei privaten Terminen") muesste jeder Termin einzeln + beantworten, und die Antwort waere raten. Der Kalender ist als + Ganzes privat -- so wie der Chat, der die Sicht ohnehin noch nie + benutzt hat. */ + const { wo, werte } = sichtbar(req.person); /* Zeitraum. Ohne Angabe: ab heute 00:00, 90 Tage nach vorn -- das deckt den 90-Tage-Plan aus dem Konzept ab. */ @@ -322,7 +339,8 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { Jetzt gilt dieselbe Regel wie fuer die Termine daneben: eigene plus die der zugeteilten Creator. */ - const sicht = req.sicht || req.person; + /* Auch hier die eigene Person -- siehe oben. */ + const sicht = req.person; const eigenA = "(a.creator_id = ? OR a.verantwortlich_id = ? OR a.erstellt_von = ?)"; const werteA = [sicht.id, sicht.id, sicht.id]; const bA = betreutWo(sicht, "a.creator_id"); diff --git a/server/workspace-leistung.js b/server/workspace-leistung.js index 434ab293..240b42d8 100644 --- a/server/workspace-leistung.js +++ b/server/workspace-leistung.js @@ -404,7 +404,21 @@ leistungRouter.delete("/workspace/api/leistung/:creatorId/:tag", gleicheHerkunft * Eine Abfrage statt einer je Creator. */ leistungRouter.get("/workspace/api/leistung", (req, res) => { try { - const ids = sichtbareCreatorIds(req.person); + /* DURCH DIE AUGEN DER ANGESEHENEN PERSON (11.09.2026). + + Filipe: "oben bei meine sicht soll ich auch die sicht von allen + jeden moment sehen koennen und das genau genau so wie sie es + sehen alles." + + Nur LESEN wird umgestellt, nie ein Recht: `req.person` bleibt + ueberall dort stehen, wo geschrieben oder geprueft wird. Und + weil DogFather ohnehin alles sehen darf, kann diese Umstellung + nichts oeffnen -- sie kann nur WENIGER zeigen, naemlich genau + das, was die andere Person sieht. + Gemessen: In der Sicht auf einen Modi kamen hier DogFathers + eigene Creator zurueck -- 131 Zeichen statt 14. */ + const zeigt = req.sicht || req.person; + const ids = sichtbareCreatorIds(zeigt); const wo = ids === null ? "rolle = 'creator' AND aktiv = 1" : ids.length ? `id IN (${ids.map(() => "?").join(",")}) AND aktiv = 1` : "0=1"; diff --git a/server/workspace-profil.js b/server/workspace-profil.js index 545da26e..0d9aa814 100644 --- a/server/workspace-profil.js +++ b/server/workspace-profil.js @@ -115,8 +115,21 @@ profilRouter.get("/workspace/api/profil/:id", (req, res) => { /* Liste aller Creator -- fürs Management, zur Auswahl. */ profilRouter.get("/workspace/api/profil", (req, res) => { try { - if (req.person.rolle === "creator") { - return res.json({ creator: [{ id: req.person.id, name: req.person.name }], eigen: true }); + /* DURCH DIE AUGEN DER ANGESEHENEN PERSON (11.09.2026). + + Filipe: "oben bei meine sicht soll ich auch die sicht von allen + jeden moment sehen koennen und das genau genau so wie sie es + sehen alles." + + Nur LESEN wird umgestellt, nie ein Recht: `req.person` bleibt + ueberall dort stehen, wo geschrieben oder geprueft wird. Und + weil DogFather ohnehin alles sehen darf, kann diese Umstellung + nichts oeffnen -- sie kann nur WENIGER zeigen, naemlich genau + das, was die andere Person sieht. + */ + const zeigt = req.sicht || req.person; + if (zeigt.rolle === "creator") { + return res.json({ creator: [{ id: zeigt.id, name: zeigt.name }], eigen: true }); } /* Ein Scout bekommt genau die Creator, die er betreut -- und ohne Zuteilung eine leere Liste statt einer Fehlermeldung. */ @@ -125,10 +138,10 @@ profilRouter.get("/workspace/api/profil", (req, res) => { die ihrer Scouts. */ let nur = ""; let werte = []; - const erlaubt = sichtbareCreatorIds(req.person); + const erlaubt = sichtbareCreatorIds(zeigt); if (erlaubt !== null) { if (!erlaubt.length) { - return res.json({ creator: [], eigen: false, betreuend: req.person.rolle !== "creator" }); + return res.json({ creator: [], eigen: false, betreuend: zeigt.rolle !== "creator" }); } nur = ` AND p.id IN (${erlaubt.map(() => "?").join(",")})`; werte = erlaubt; @@ -145,7 +158,7 @@ profilRouter.get("/workspace/api/profil", (req, res) => { FROM personen p WHERE p.rolle = 'creator'${nur} ORDER BY p.aktiv DESC, p.name`).all(...werte), eigen: false, - betreuend: req.person.rolle === "scout", + betreuend: zeigt.rolle === "scout", }); } catch (fehler) { console.error("[workspace] Creator-Liste:", fehler?.message); diff --git a/server/workspace-reports.js b/server/workspace-reports.js index 84d6d09c..b9b499d0 100644 --- a/server/workspace-reports.js +++ b/server/workspace-reports.js @@ -59,7 +59,16 @@ reportRouter.use("/workspace/api/report", angemeldet); (oder alle sehen), ein Creator immer nur sich selbst -- unabhaengig davon, was im Aufruf steht. */ function zielCreator(req) { - if (req.person.rolle === "creator") return req.person.id; + /* DURCH DIE AUGEN DER ANGESEHENEN PERSON (11.09.2026). + + Der Report ist reine Auswertung -- es wird nichts geschrieben und + nichts freigegeben. Und weil DogFather ohnehin ueber jeden Creator + auswerten darf, kann die Sicht hier nur WENIGER zeigen: genau den + Ausschnitt, den die andere Person sieht. Gemessen wurde vorher, dass + in der Sicht auf einen Modi DogFathers eigene Auswertung + zurueckkam. */ + const zeigt = req.sicht || req.person; + if (zeigt.rolle === "creator") return zeigt.id; const w = req.query.creator; const gewuenscht = w && w !== "alle" && Number.isInteger(Number(w)) && Number(w) > 0 @@ -69,13 +78,13 @@ function zielCreator(req) { bei ihm "alle MEINE". Vorher lieferte gewuenscht=null eine Auswertung ueber den gesamten Bestand -- die Zahlen darin stammten zum Teil von Creators, die ihm nie zugeteilt waren. (03.09.2026) */ - if (siehtAlles(req.person)) return gewuenscht; + if (siehtAlles(zeigt)) return gewuenscht; /* Scout: darf nur ueber die eigenen Creator einen Report ziehen. Ein fremder Wunsch wird nicht abgelehnt, sondern still auf den ersten eigenen zurueckgesetzt -- so kann man den Parameter nicht dazu benutzen, die Zuteilung anderer abzufragen. */ - const meine = betreuteIds(req.person); + const meine = betreuteIds(zeigt); if (gewuenscht && meine.includes(gewuenscht)) return gewuenscht; return meine[0] ?? -1; } @@ -84,7 +93,7 @@ reportRouter.get("/workspace/api/report", (req, res) => { try { /* Ohne zugeteilten Creator gibt es nichts auszuwerten. Das ist eine Auskunft, kein Fehler -- die Seite baut daraus ihren Hinweis. */ - if (ohneZuteilung(req.person)) { + if (ohneZuteilung(req.sicht || req.person)) { return res.json({ leer: true, grund: "keine_zuteilung", creator: null }); } const tage = Math.min(Math.max(Number(req.query.tage) || 30, 7), 180); diff --git a/server/workspace-schulung.js b/server/workspace-schulung.js index 27755094..e03e20a7 100644 --- a/server/workspace-schulung.js +++ b/server/workspace-schulung.js @@ -188,15 +188,28 @@ schulungRouter.get("/workspace/api/schulung", (req, res) => { /* Fuer welche Creator wird der Stand gebraucht? */ let creatorIds; - if (req.person.rolle === "creator") creatorIds = [req.person.id]; - else if (istLeitung(req.person)) { + /* DURCH DIE AUGEN DER ANGESEHENEN PERSON (11.09.2026). + + Filipe: "oben bei meine sicht soll ich auch die sicht von allen + jeden moment sehen koennen und das genau genau so wie sie es + sehen alles." + + Nur LESEN wird umgestellt, nie ein Recht: `req.person` bleibt + ueberall dort stehen, wo geschrieben oder geprueft wird. Und + weil DogFather ohnehin alles sehen darf, kann diese Umstellung + nichts oeffnen -- sie kann nur WENIGER zeigen, naemlich genau + das, was die andere Person sieht. + */ + const zeigt = req.sicht || req.person; + if (zeigt.rolle === "creator") creatorIds = [zeigt.id]; + else if (istLeitung(zeigt)) { creatorIds = db().prepare( "SELECT id FROM personen WHERE rolle = 'creator' AND aktiv = 1").all().map((z) => z.id); /* Zuteilung statt Rolle (03.09.2026) -- eine Managerin sah sonst den Schulungsstand aller Creator, auch fremder. */ - const erlaubtC = sichtbareCreatorIds(req.person); + const erlaubtC = sichtbareCreatorIds(zeigt); if (erlaubtC !== null) creatorIds = creatorIds.filter((x) => erlaubtC.includes(x)); - } else creatorIds = betreuteIds(req.person); + } else creatorIds = betreuteIds(zeigt); const stand = creatorIds.length ? db().prepare(` @@ -224,8 +237,13 @@ schulungRouter.get("/workspace/api/schulung", (req, res) => { res.json({ schulungen, stand, creatoren, - ich: { id: req.person.id, rolle: req.person.rolle }, - darf_anlegen: istLeitung(req.person), + /* Auch die Knoepfe gehoeren zur Sicht: Wer sich den + Arbeitsplatz eines anderen ansieht, soll ihn sehen, wie er + wirklich aussieht -- mit denselben Knoepfen. Geschrieben wird + trotzdem immer als der Angemeldete; das entscheidet der Server + beim Speichern, nicht diese Zeile. */ + ich: { id: zeigt.id, rolle: zeigt.rolle }, + darf_anlegen: istLeitung(zeigt), }); } catch (fehler) { console.error("[workspace] Schulungen lesen:", fehler?.message); diff --git a/server/workspace-serien.js b/server/workspace-serien.js index 57335820..f9298f97 100644 --- a/server/workspace-serien.js +++ b/server/workspace-serien.js @@ -531,7 +531,9 @@ const darfAendern = (person, serie) => istLeitung(person) || serie.erstellt_von serienRouter.get("/workspace/api/serien", (req, res) => { try { nachfuellenAlle(); - const person = req.sicht || req.person; + /* Wiederholungen sind Termine. Dieselbe Regel, dieselbe + Begruendung -- und dies ist die Stelle, die man vergisst. */ + const person = req.person; const { wo, werte } = termineSichtbar(person, "s"); const roh = db().prepare(` SELECT s.*, diff --git a/server/workspace-teamlage.js b/server/workspace-teamlage.js index bcd08134..df99ce9a 100644 --- a/server/workspace-teamlage.js +++ b/server/workspace-teamlage.js @@ -162,11 +162,19 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => { `id <> ?` LAESST DEN LESER SELBST HERAUS. Ohne das saehe die rechte Hand eine Karte ueber sich selbst -- mit ihrer eigenen Ampel, ihrer eigenen Stufe und ihren eigenen offenen Punkten. - Das ist kein Ueberblick ueber das Team, das ist ein Spiegel. */ + Das ist kein Ueberblick ueber das Team, das ist ein Spiegel. + + UND ZWAR DEN, DURCH DESSEN AUGEN GESEHEN WIRD (11.09.2026). + Hier stand `person.id`, also immer der Angemeldete. In der Sicht + auf die rechte Hand stand sie deshalb selbst mit auf ihrer + eigenen Seite -- genau der Spiegel, den der Absatz darueber + verhindern soll. Der Riegel eine Zeile hoeher (`darfEingang`) + bleibt beim Angemeldeten: Das ist ein Recht, keine Anzeige. */ + const zeigt = req.sicht || person; const modis = db().prepare( `SELECT id, name, bild, letzter_login, aktiv, erstellt, stufe FROM personen WHERE rolle IN ('hand','modi') AND id <> ? - ORDER BY aktiv DESC, name`).all(person.id); + ORDER BY aktiv DESC, name`).all(zeigt.id); if (!modis.length) { /* `eingang` auch hier -- die Oberflaeche darf nicht raten diff --git a/server/workspace-zentrale.js b/server/workspace-zentrale.js index e868709a..058092c1 100644 --- a/server/workspace-zentrale.js +++ b/server/workspace-zentrale.js @@ -77,14 +77,28 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { try { const d = db(); const heute = heuteLokal(); - const ich = req.person; + /* WESSEN LAGE WIRD GEZEIGT? -- EINE VARIABLE, NICHT ZWEI. - /* Wessen Lage wird gezeigt? Bei aktiver Fremdsicht die der Person, - in die man hineinsieht -- sonst zeigte die Kachel die eigene Lage - und alles darunter eine fremde. Genau diese Uneinheitlichkeit hat - auf der Startseite schon einmal zu "ist das jetzt meins oder - ihrs?" gefuehrt. */ - const zeigt = req.person.sicht || req.person; + Hier standen bis zum 11.09.2026 ZWEI: `ich` (immer der + Angemeldete) fuer den Ring, und `zeigt` fuer die drei Zahlen + darunter. Gedacht war, dass beide der fremden Sicht folgen; im + Code stand dafuer `req.person.sicht` -- ein Feld, das es nicht + gibt. Der Ausdruck war also immer `undefined || req.person`. + + Ergebnis: Sah DogFather sich den Arbeitsplatz eines Modis an, + zeigte der Ring seine eigene Lage ("Team versorgt", alle Creator + des Hauses), waehrend darunter die Zahlen der anderen Person + standen. Zwei Wahrheiten in einer Kachel -- und weil ein Tippfehler + in einer Variablen nichts kaputt macht, sondern nur nichts tut, + faellt so etwas nie von selbst auf. Gefunden, indem die Antwort in + fremder Sicht mit der verglichen wurde, die die Person SELBST + bekommt. + + Deshalb jetzt genau ein Name fuer genau eine Frage. Alles in + dieser Route ist Anzeige, kein Recht: Es gibt nichts zu schreiben + und nichts zu pruefen. Und weil DogFather ohnehin alles sehen + darf, kann die Sicht hier nur WENIGER zeigen, nie mehr. */ + const zeigt = req.sicht || req.person; /* ================================================================ WESSEN TEAM? JEDE ROLLE SIEHT IHR EIGENES (08.09.2026) @@ -113,7 +127,7 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { Leitung und sahen deshalb den Stundenring des eigenen Tages -- aber ein Scout hat ein Team, naemlich seine Creator. Genau danach hat Filipe gefragt. */ - const istTeamsicht = istLeitung(ich) || ich.rolle === "scout"; + const istTeamsicht = istLeitung(zeigt) || zeigt.rolle === "scout"; if (istTeamsicht) { /* ---------- DAS TEAM ---------- @@ -128,7 +142,7 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { -> das ist das, was liegen bleibt */ let leute; - if (ich.rolle === "admin" || ich.rolle === "spicy") { + if (zeigt.rolle === "admin" || zeigt.rolle === "spicy") { /* Das ganze Haus -- und zwar ohne sich selbst: Wer den Ring ansieht, ist die Person, die ihn liest; sich selbst als Segment im eigenen Team zu zaehlen, verschiebt jede Prozent- @@ -143,7 +157,7 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { gemeldet; ohne die Pruefung waere es nicht aufgefallen, denn im Ring steht nur ein farbiges Segment mit einem Namen in der Sprechblase. */ - const weg = verborgeneIds(ich); + const weg = verborgeneIds(zeigt); const zusatz = weg.length ? ` AND id NOT IN (${weg.map(() => "?").join(",")})` : ""; /* UND AUF DER TEAM-ADRESSE NUR DAS TEAM (10.09.2026). @@ -156,15 +170,15 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { Bedingung kommt deshalb aus derselben Funktion. */ leute = d.prepare( `SELECT id, name, rolle FROM personen - WHERE aktiv = 1 AND id <> ?${zusatz}${hausBedingung(ich)} ORDER BY rolle, name`) - .all(ich.id, ...weg); + WHERE aktiv = 1 AND id <> ?${zusatz}${hausBedingung(zeigt)} ORDER BY rolle, name`) + .all(zeigt.id, ...weg); } else { /* Manager: seine Scouts UND die Creator (eigene wie die seiner Scouts). Scout: nur seine Creator. */ const ids = [...new Set([ - ...(ich.rolle === "manager" ? scoutsVon(ich.id) : []), - ...betreuteIds(ich), - ])].filter((id) => id !== ich.id); + ...(zeigt.rolle === "manager" ? scoutsVon(zeigt.id) : []), + ...betreuteIds(zeigt), + ])].filter((id) => id !== zeigt.id); leute = ids.length ? d.prepare( @@ -202,7 +216,7 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { /* Ein Scout fuehrt Creator, kein "Team" im Sinne des Hauses. Das Wort auf dem Ring sagt deshalb, WEN er zaehlt -- sonst liest ein Scout "Team versorgt" und sucht die anderen vier Rollen darin. */ - const nurCreator = ich.rolle === "scout"; + const nurCreator = zeigt.rolle === "scout"; return res.json({ art: "team", diff --git a/server/workspace.js b/server/workspace.js index e6e8a2b2..cf00770b 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -4408,7 +4408,32 @@ export function sichtPerson(req) { /* Nicht gefunden oder abgeschaltet: zurueck auf die eigene Sicht. Eine leere Seite waere hier die schlechtere Antwort -- sie sieht aus wie ein Fehler, obwohl nur die Auswahl veraltet ist. */ - return z || ich; + if (!z) return ich; + + /* IN WELCHEM HAUS STEHT DIE ANGESEHENE PERSON? (11.09.2026) + + Ohne diese vier Zeilen fehlte der angesehenen Person das Feld + `haus` -- und `nurHaus()` und `hausBedingung()` fangen beide mit + `person?.haus !== "crew"` an. Eine Sicht ohne Haus war damit + automatisch eine Agentursicht: DogFather sah in der Sicht auf + einen Modi die Dateien, die Personen und die Berichte des + ANDEREN Hauses -- also mehr, als die Person selbst je sieht, und + nicht das, was sie sieht. + + Das Haus haengt hier an der ROLLE, nicht an der Adresse. Eine + rechte Hand und ein Modi gibt es nur im Haus von Team Dogi; eine + Managerin, eine Scout und eine Creatorin nur im Agenturhaus. + Wuerde stattdessen die Adresse entscheiden, saehe dieselbe + Person je nach aufgerufener Wand etwas anderes -- und genau das + soll die Sicht ja gerade nicht. + + DogFather selbst ist in beiden Haeusern zu Hause. Sieht er sich + durch die Augen eines zweiten Kontos mit seiner Rolle an, gilt + das Haus, in dem er gerade steht. */ + const haus = TEAM_DOGI_ROLLEN.has(z.rolle) ? "crew" + : z.rolle === "admin" ? (ich.haus || "agentur") + : "agentur"; + return { ...z, haus }; } catch { return ich; /* im Zweifel die eigene Sicht, nie eine fremde */ } diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 7ca6afbe..37422f0a 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609110109", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609110209", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609110109", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609110209", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609110109", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609110209", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609110109", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609110209", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/crew-haus.css b/workspace/assets/css/crew-haus.css index 3831e20a..b3134503 100644 --- a/workspace/assets/css/crew-haus.css +++ b/workspace/assets/css/crew-haus.css @@ -446,3 +446,139 @@ 2px 2px 16px -11px rgba(143, 216, 255, .5), 0 8px 20px -12px rgba(0, 0, 0, .9); } + + +/* ===================================================================== + DER GRUND JEDER KACHEL: EIN STERNENFELD + ===================================================================== + + Filipe: "veraender die kacheln und den hintergrund, ich will dass + die hintergrunde von den kacheln immer unviersum artig ist, es muss + richtig geil sein aber immer so dass man alles noch gut erkkent. + und das IN DER GANZEN WEBSITE VON TEAM DOGI. nur die kacheln. [...] + wichtig ist die form der kacheln soll gleich bleiben." + + --------------------------------------------------------------------- + WARUM HIER NUR VIER WERTE STEHEN UND KEINE EINZIGE KACHELREGEL + + Das Haus hat eine kanonische KACHELLISTE -- 48 Klassen, von der + grossen Startkachel bis zur Personenzeile. Sie steht in module.css + SIEBENMAL, und pruef-css-klassen vergleicht die sieben Vorkommen + miteinander. Eine achte Abschrift an dieser Stelle waere die Sorte + Fehler, die nicht auffaellt: Sie ginge heute auf und waere bei der + naechsten neuen Kachel unvollstaendig -- lautlos, denn niemand + vermisst einen Hintergrund, den er nie gesehen hat. + + Deshalb faerbt diese Datei keine Kachel. Sie setzt die Werte um, aus + denen module.css den Grund JEDER Kachel baut. Was danach dazukommt, + bekommt das Sternenfeld von selbst. pruef-kachel-universum haelt + fest, dass hier keine Kachelklasse auftaucht. + + DIE FORM BLEIBT UNANGETASTET. Fase, Kantenlicht und die drei + Eckwinkel stehen an anderen Stellen und werden hier nicht erwaehnt; + die Pruefung vergleicht Silhouette, Fase und Eckwinkel vor und nach + dem Hauswechsel Zeichen fuer Zeichen. Diese Datei aendert Farbe, + nicht Geometrie. + + --------------------------------------------------------------------- + WARUM DIE NEBEL OBEN RECHTS UND UNTEN LINKS STEHEN + + Das ist die wichtigste Zeile dieses Abschnitts, und sie stammt nicht + aus dem Geschmack, sondern aus einer Messung. + + Zuerst standen sie oben links -- die naheliegende Ecke, weil dort in + jedem Bild der Blick anfaengt. Gemessen wurde danach etwas anderes: + Hinter der leisesten Textzeile einer Personenkarte lagen + ZWEI PROZENT aller Bildpunkte unter der Lesbarkeitsgrenze. Im + Agenturhaus sind es an derselben Stelle 0,1 Prozent. + + Der Grund: JEDE Kachel dieses Hauses hat bereits einen eigenen + Lichtverlauf, und der laeuft bei allen aus derselben Richtung -- + 155 Grad bei den Startkacheln, 150 bei den Teamkacheln, 158 beim + Kantenlicht. Oben links ist damit ueberall schon die hellste Stelle + der Kachel, und genau dort stehen ueberall die Ueberschriften. Ein + Nebel obendrauf addiert Licht auf Licht, ausgerechnet unter dem Text. + + Nach dem Umzug in die beiden GEGENUEBERLIEGENDEN Ecken: 0,22 Prozent. + Derselbe Nebel, an einer anderen Stelle -- und er durfte dabei sogar + KRAEFTIGER werden (.80 statt .62), weil er nicht mehr auf dem + hellsten Punkt der Kachel liegt. Besser lesbar UND deutlicher zu + sehen; das ist selten und war hier umsonst zu haben. + + --------------------------------------------------------------------- + WARUM ES LESBAR BLEIBT -- die Zahlen dahinter + + Alles unten ist an echten Bildpunkten gemessen, nicht gerechnet, und + gegen dieselbe Stelle im Agenturhaus gehalten. Drei Regeln haben die + Werte bestimmt: + + 1. DIE FLAECHE ZAEHLT, NICHT DER PUNKT. Ein Sternenfeld ist kein + Hintergrund, sondern viele. Gemessen wird deshalb, welcher ANTEIL + der Punkte hinter einer Textzeile unter 4,5:1 faellt. Im + Agenturhaus sind das im schlechtesten Fall 0,115 Prozent, hier + 0,253 Prozent -- beides ein paar hundert Punkte von 17 000, und + es sind die Sternkerne. Der Mittelwert, also das, was ein + Buchstabe wirklich sieht, liegt ueberall bei 6,4:1 oder besser. + + 2. STERNE SIND PUNKTE UND BLEIBEN LEISE. Ihre Deckkraft ist die + einzige Zahl, die den hellsten Punkt hinter einem Buchstaben + bestimmt. Bei .58 mal dem Schleier von .30 kommt der Kern auf gut + ein Sechstel Weiss; auf dem dunklen Grund dieses Hauses steht die + Schrift selbst DORT noch bei 3,6:1. Ein Versuch mit kleineren, + dafuer helleren Kernen war der falsche Weg: Der hellste Punkt + blieb fast gleich, der Stern wurde nur unschaerfer. + + 3. KEIN KARO. Die Sternebenen wiederholen sich in Abstaenden ohne + gemeinsamen Teiler (89/77, 59/51, 167/139). Gleiche Zahlen + ergaeben ein Muster, und ein Muster sieht man sofort als Tapete. + Ungleiche ergeben ein Feld, das sich erst nach Hunderten von + Pixeln wiederholt -- laenger, als eine Kachel breit ist. + + Der Nebel nimmt zu knapp einem Fuenftel den Ton der jeweiligen + Kachel auf. Dadurch hat jede Kategorie ihren eigenen Himmel, ohne + dass eine Kachel aus der Mischung faellt: Vier Fuenftel bleiben + Lila beziehungsweise Babyblau. */ +:root { + --sternenfeld: + /* feine weisse Sterne */ + radial-gradient(circle at 22% 28%, rgba(255, 255, 255, .58) 0 .9px, transparent 2px), + /* babyblaue Sterne, dichter gesaet */ + radial-gradient(circle at 68% 71%, rgba(142, 201, 255, .52) 0 .8px, transparent 1.8px), + /* wenige groessere in Lila */ + radial-gradient(circle at 47% 15%, rgba(169, 139, 255, .60) 0 1.3px, transparent 3px), + /* Nebel oben RECHTS, Lila -- die Ecke, an der keine Kachel schon + von sich aus hell ist (siehe oben) */ + radial-gradient(ellipse 74% 64% at 90% 16%, + color-mix(in srgb, var(--ton, var(--violett)) 18%, rgba(125, 92, 224, .80)) 0%, + transparent 68%), + /* Nebel unten LINKS, Babyblau -- die gegenueberliegende Ecke, + damit der Verlauf quer durch die Kachel laeuft */ + radial-gradient(ellipse 66% 62% at 12% 88%, + color-mix(in srgb, var(--ton, var(--akzent)) 18%, rgba(90, 162, 232, .70)) 0%, + transparent 70%); + + --sternenfeld-mass: + 89px 77px, + 59px 51px, + 167px 139px, + 100% 100%, + 100% 100%; + + /* Die Versaetze sorgen dafuer, dass die drei Sternebenen nicht in + derselben Ecke anfangen -- sonst stuenden an jedem Rasterpunkt + drei Sterne uebereinander und dazwischen keiner. */ + --sternenfeld-lage: + 0 0, + 11px 19px, + 5px 7px, + 0 0, + 0 0; + + /* Der Schleier daempft Raster, Eckwinkel UND Sternenfeld gemeinsam + -- eine Ebene, eine Deckkraft. Im Agenturhaus steht er auf .26, + dort traegt er nur ein Raster. Hier traegt er einen Himmel; .30 + ist der Wert, bei dem die Sternkerne die Messung oben halten. Die + Eckwinkel werden dadurch um einen Hauch mitgehoben, was recht ist: + Sie sind die Form, und die soll vor dem Himmel nicht verschwinden. */ + --modul-schleier: .30; +} diff --git a/workspace/assets/css/module.css b/workspace/assets/css/module.css index 56301f4f..b4c3fe0e 100644 --- a/workspace/assets/css/module.css +++ b/workspace/assets/css/module.css @@ -106,6 +106,32 @@ --raster: repeating-linear-gradient(0deg, rgba(150, 186, 220, .09) 0 1px, transparent 1px 26px), repeating-linear-gradient(90deg, rgba(150, 186, 220, .09) 0 1px, transparent 1px 26px); + + /* DAS STERNENFELD -- die Stelle, an der ein Haus dem Grund jeder + Kachel etwas Eigenes geben kann, OHNE die Kachelliste ein achtes + Mal abzuschreiben. + + Hier steht `none`: Im Agenturhaus aendert sich dadurch kein + einziges Pixel -- eine Ebene ohne Bild zeichnet nichts. Wer ein + Haus baut, setzt diese drei Werte auf `:root` um und hat damit + jede Kachel der ganzen Adresse erwischt, auch die, die es noch + gar nicht gibt. + + DREI WERTE, WEIL EIN HINTERGRUND DREI LISTEN HAT. Bild, Groesse + und Lage werden Ebene fuer Ebene einander zugeordnet. Wer nur das + Bild austauscht, bekommt die Groesse der Ebene davor -- deshalb + gehoeren sie zusammen und werden zusammen gesetzt. + + Die FORM bleibt davon unberuehrt: Fase, Kantenlicht und Eckwinkel + stehen woanders. Ein Sternenfeld faerbt, es schneidet nicht. */ + --sternenfeld: none; + --sternenfeld-mass: auto; + --sternenfeld-lage: 0 0; + + /* Wie stark die ganze Ebene aus Raster, Eckwinkeln und Sternenfeld + durchkommt. Steht als Wert da, damit ein Haus mit eigenem Grund + ihn heben kann -- .26 bleibt der Wert des Agenturhauses. */ + --modul-schleier: .26; } /* MODULLISTE */ @@ -185,21 +211,24 @@ var(--winkel), var(--winkel), /* unten rechts */ var(--winkel), var(--winkel), /* oben rechts */ var(--winkel), var(--winkel), /* unten links */ - var(--raster); + var(--raster), + var(--sternenfeld, none); background-size: 18px 1.5px, 1.5px 18px, 18px 1.5px, 1.5px 18px, 18px 1.5px, 1.5px 18px, - 26px 26px, 26px 26px; + 26px 26px, 26px 26px, + var(--sternenfeld-mass, auto); background-position: right 10px bottom 10px, right 10px bottom 10px, right 10px top 10px, right 10px top 10px, left 10px bottom 10px, left 10px bottom 10px, - 0 0, 0 0; + 0 0, 0 0, + var(--sternenfeld-lage, 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; + opacity: var(--modul-schleier, .26); transform: none; } diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index cd283491..7f881fc6 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -1305,7 +1305,9 @@ nichts und machte den Knopf so breit, dass am Handy nichts anderes mehr danebenpasste. */ s.append(new Option('Meine Sicht', '')); + const gezeichnet = new Set(); for (const [rolle, name] of rollenGruppen()) { + gezeichnet.add(rolle); const drin = personen.filter((p) => p.rolle === rolle && p.id !== ich.id && p.aktiv !== false); if (!drin.length) continue; const g = document.createElement('optgroup'); @@ -1313,6 +1315,35 @@ for (const p of drin) g.append(new Option(p.name, String(p.id))); s.append(g); } + + /* UND ALLE UEBRIGEN (11.09.2026). + + Filipe: "oben bei meine sicht soll ich auch die sicht von allen + jeden moment sehen koennen." + + Er konnte es nicht: `rollenGruppen()` kommt aus bereiche.js und + kennt fuenf Rollen. Wer eine sechste hat, stand nicht in der + Auswahl -- kein Fehler, keine Luecke, kein Hinweis. Das ist + DIESELBE Lücke wie in der Chat-Auswahl und in der Personenliste, + zum dritten Mal an einer dritten Stelle. + + Die Ueberschrift kommt deshalb vom Server (`p.gruppe`), und was + hier ankommt, wird gezeichnet -- auch eine Rolle, deren Namen + diese Datei gar nicht kennen darf. */ + const uebrig = personen.filter( + (p) => !gezeichnet.has(p.rolle) && p.id !== ich.id && p.aktiv !== false); + const nachTitel = new Map(); + for (const p of uebrig) { + const titel = p.gruppe || 'Weitere'; + if (!nachTitel.has(titel)) nachTitel.set(titel, []); + nachTitel.get(titel).push(p); + } + for (const [titel, leute] of nachTitel) { + const g = document.createElement('optgroup'); + g.label = titel; + for (const p of leute) g.append(new Option(p.name, String(p.id))); + s.append(g); + } if (aktiv) s.value = String(aktiv); /* Stand die gemerkte Person nicht mehr in der Liste (geloescht oder abgeschaltet), faellt die Auswahl zurueck -- sonst zeigte das Feld diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 3e57b703..b2433c65 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -252,10 +252,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 2614808e..13f3089c 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -147,10 +147,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 21ae53b4..9bbaaf48 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -181,14 +181,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 159a20df..ef95185d 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index f52266fc..a284f4ad 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -289,10 +289,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 51a1b76f..c2fa5bf1 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index fe51f41a..136395ff 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -30,15 +30,15 @@ - - - + + + - + @@ -252,6 +252,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 21e9108c..55ddea3e 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609110109", + "src": "/assets/img/app-symbole/crew-192.png?v=202609110209", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609110109", + "src": "/assets/img/app-symbole/crew-512.png?v=202609110209", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609110109", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609110209", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609110109", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609110209", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 5595ae0d..b451381c 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index 723580bd..c1a41ce3 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 0c939357..9d9eabfb 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index e47ea56b..3b25c3e2 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -224,10 +224,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 80347b10..20ee0b55 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -396,10 +396,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 2f9d35e0..3a9195f1 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index 888478d6..463ea79b 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 18b24233..615544dc 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -139,10 +139,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 6d8f1696..a200d82c 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index c3a6bfb9..7d99f70f 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 3234d14f..a7ac1273 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -126,10 +126,10 @@ - - - - - + + + + + diff --git a/workspace/team.html b/workspace/team.html index 72f4e88c..993f9fba 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 3ab0ccd4..1042da46 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 18a18096..36f0775f 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index a66db8f4..e0319632 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +