diff --git a/server/pruef-personen-kachel.mjs b/server/pruef-personen-kachel.mjs new file mode 100644 index 00000000..6769c739 --- /dev/null +++ b/server/pruef-personen-kachel.mjs @@ -0,0 +1,505 @@ +/* DIE PERSONENKACHEL — Aufbau, Farben, Kontrast (09.09.2026) + + Filipe, mit Bildschirmfoto: "mach die ganze kachel perfekter + detailliert schönes. der hintergrund soll auch bissl dunkler sein + von der kachel." + + Auf seinem Bild stand unter jeder zugeklappten Rolle ein leerer + Streifen, und die fünf Zeilen sahen fünfmal gleich aus. Gemessen + waren es 29 px Nichts je Zeile, fünfmal untereinander. + + WAS HIER GEMESSEN WIRD UND WARUM SO: + + Der Rollenstreifen wird an ECHTEN BILDPUNKTEN geprüft, nicht am + Stylesheet. Der Grund steht im Code: Im ersten Anlauf sagte + getComputedStyle "3 px breit, sichtbar, Deckkraft 0,55" -- und zu + sehen war nichts, weil das Kantenlicht der Kachel (module.css, + z-index 2) genau darüber lag. Eine Prüfung, die den berechneten Stil + liest, hätte grün gemeldet. + + Der dunklere Grund wird als Helligkeit gemessen UND gegen die Karten + darin gehalten: Eine Vitrine ist dunkler als das, was darin steht. + Eine feste Zahl allein wäre eine Rechnung von gestern. + + DREI AUSGÄNGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten. */ + +import { mkdtempSync, rmSync, appendFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-perskachel-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +import { notbremse } from "./helfer-notbremse.mjs"; +const { portMussFreiSein } = await import("./helfer-port.mjs"); +await portMussFreiSein(4329, "die Personenkachel-Pruefung"); + +process.env.PORT = "4329"; +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 }); }; +await import("./index.js"); +notbremse(420_000, "pruef-personen-kachel"); +await new Promise((r) => setTimeout(r, 900)); +const BASIS = "http://127.0.0.1:4329"; + +const PROTOKOLL = join(process.cwd(), "pruef-personen-kachel-lauf.txt"); +try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ } +const melde = (z) => { console.log(z); try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ } }; +let fehler = 0, geprueft = 0; +const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +/* ---- Bestand ------------------------------------------------------------ */ +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +for (let i = 0; i < 60; i++) { + try { + const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +function anlegen(name, rolle, code, aktiv = 1) { + 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, aktiv, erstellt) VALUES (?,?,?,?,?,?,?)") + .run(name, rolle, hash, salt, 32768, aktiv, jetzt); +} +anlegen("Dogfather", "admin", "CODE-DOGI-0001"); +anlegen("Spicy Media", "spicy", "CODE-SPIC-0001"); +anlegen("Modis", "manager", "CODE-MODI-0001"); +anlegen("BanaStift", "manager", "CODE-BSTF-0001", 0); +anlegen("BananaStift", "scout", "CODE-BANA-0001"); +anlegen("Patrick", "scout", "CODE-PATR-0001"); +/* SECHS Creator -- damit die Namenszeichen wirklich abgeschnitten + werden. Mit vier waere der "+n"-Fall nie geprueft. */ +for (const n of ["Tili", "SpongBob", "Luna", "Mika", "Noa", "Robin"]) { + anlegen(n, "creator", "CODE-" + n.slice(0, 4).toUpperCase().padEnd(4, "X") + "-0001"); +} +d.close(); + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +const konsole = []; +async function alsDogi(breite = 1440) { + const ctx = await browser.newContext({ viewport: { width: breite, height: 1100 } }); + const s = await ctx.newPage(); + s.on("pageerror", (f) => konsole.push("Absturz: " + f.message)); + s.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); }); + await s.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await s.click('.rolle[data-rolle="admin"]'); + await s.fill("#code", "CODE-DOGI-0001"); + await s.click("button[type=submit]"); + await s.waitForURL("**/start.html", { timeout: 20000 }); + await s.evaluate(() => localStorage.setItem("dfw_personen_offen", "[]")); + await s.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); + await s.waitForSelector(".gruppe", { timeout: 10000 }); + await s.waitForTimeout(700); + return { ctx, s }; +} +const gross = await alsDogi(); +const seite = gross.s; + +/* Punkte aus einem Bildschirmfoto lesen -- derselbe Weg wie in + pruef-lesbarkeit: das Bild in ein Canvas, dort jeden Punkt. */ +async function mittelFarbe(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; + let r = 0, gr = 0, bl = 0, n = 0; + for (let i = 0; i < dd.length; i += 4) { r += dd[i]; gr += dd[i + 1]; bl += dd[i + 2]; n++; } + return [Math.round(r / n), Math.round(gr / n), Math.round(bl / n)]; + }, bild.toString("base64")); +} +const leuchtkraft = ([r, g, b]) => { + const f = (w) => { const v = w / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; + return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); +}; +const kontrast = (a, b) => { + const l1 = leuchtkraft(a), l2 = leuchtkraft(b); + return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); +}; + +/* ======================================================================= + 1. AUFBAU: fünf Rollen, jede mit ihrem Merkmal + ======================================================================= */ +melde("\n=== 1. Aufbau ==="); +{ + const g = await seite.$$eval(".liste-kachel > .gruppe", (b) => b.map((x) => ({ + rolle: x.dataset.rolle, auf: x.dataset.auf, + name: x.querySelector(".gruppe__name")?.textContent, + zahl: x.querySelector(".gruppe__zahl")?.textContent, + }))); + ok(g.length === 5, `fünf Rollenabschnitte (${g.length})`); + ok(g.every((x) => x.rolle), `jeder trägt seine Rolle (${g.map((x) => x.rolle).join(", ")})`); + ok(g.every((x) => x.auf === "ja" || x.auf === "nein"), + "und seinen Zustand als Merkmal — nicht nur im aria-expanded"); + + /* GEGENPROBE: Zustand am Merkmal und im aria-expanded müssen + dasselbe sagen. Zwei Wahrheiten wären schlimmer als eine. */ + const einig = await seite.$$eval(".liste-kachel > .gruppe", (b) => b.every((x) => + (x.dataset.auf === "ja") === (x.querySelector(".gruppe__kopf").getAttribute("aria-expanded") === "true"))); + ok(einig, "Merkmal und aria-expanded stimmen überein"); +} + +/* ======================================================================= + 2. KEIN LEERES BAND MEHR + + Das war der sichtbare Mangel auf Filipes Bild: 74 px Abschnitt, + 45 px Zeile, 29 px Nichts. + ======================================================================= */ +melde("\n=== 2. Kein leeres Band ==="); +{ + const zu = await seite.$$eval('.liste-kachel > .gruppe[data-auf="nein"]', (b) => b.map((x) => { + const r = x.getBoundingClientRect(); + const k = x.querySelector(".gruppe__kopf").getBoundingClientRect(); + return { name: x.dataset.rolle, rest: Math.round(r.height - k.height), hoehe: Math.round(r.height) }; + })); + ok(zu.length >= 4, `${zu.length} zugeklappte Abschnitte zum Messen`); + const schlimm = zu.filter((x) => x.rest > 3); + ok(schlimm.length === 0, + schlimm.length ? `leeres Band unter: ${schlimm.map((x) => `${x.name} ${x.rest} px`).join(", ")}` + : `unter keiner zugeklappten Zeile steht Leerraum (höchstens ${Math.max(...zu.map((x) => x.rest))} px)`); + ok(zu.every((x) => x.hoehe >= 46 && x.hoehe <= 66), + `die Zeilen sind gleich hoch und gut greifbar (${zu.map((x) => x.hoehe).join("/")} px)`); + + /* GEGENPROBE: Ein OFFENER Abschnitt braucht Luft -- sonst hätte ich + das eine Übel gegen das andere getauscht. */ + await seite.click('.gruppe[data-rolle="manager"] .gruppe__kopf'); + await seite.waitForTimeout(400); + const auf = await seite.$eval('.gruppe[data-rolle="manager"]', (x) => { + const r = x.getBoundingClientRect(); + const k = x.querySelector(".gruppe__kopf").getBoundingClientRect(); + return { rest: Math.round(r.height - k.height), personen: x.querySelectorAll(".person").length }; + }); + ok(auf.personen === 2 && auf.rest > 100, + `offen stehen die Karten mit Abstand da (${auf.personen} Karten, ${auf.rest} px)`); +} + +/* ======================================================================= + 3. DER ROLLENSTREIFEN — an echten Bildpunkten + + getComputedStyle hilft hier nicht: Beim ersten Anlauf meldete es + tadellose Werte für einen Streifen, den das Kantenlicht der Kachel + vollständig verdeckte. + ======================================================================= */ +melde("\n=== 3. Der Rollenstreifen ==="); +{ + /* Erwartet wird der KANAL, der bei dieser Rollenfarbe führt -- + nicht ein genauer Wert. Ein genauer Wert wäre eine Rechnung von + gestern: Sobald jemand die Rollenfarbe leicht verschiebt, wäre die + Prüfung rot, ohne dass etwas kaputt ist. */ + const ERWARTET = { + spicy: "r", // #ef5f57 + admin: "b", // #9ed3f4 + manager: "b", // #8a76ff + scout: "g", // #5fc99a + creator: "r", // #c79a6d + }; + /* DIESELBE STELLE, EINMAL MIT UND EINMAL OHNE STREIFEN. + + Der erste Anlauf verglich den Streifen mit einem Punkt 40 px weiter + rechts -- und traf dort die Schrift. Der Unterschied war riesig und + sagte nichts über den Streifen. Zwei Stellen sind eben zwei + Stellen; es gibt keinen Grund, warum sie gleich aussehen sollten. + + Richtig ist der Vergleich DERSELBEN Stelle mit sich selbst: einmal + wie sie ist, einmal mit ausgeschaltetem Streifen. Was sich dabei + ändert, IST der Streifen -- und was sich nicht ändert, ist die + Gegenprobe, ohne dass man sie eigens erfinden müsste. */ + const AUS = "personen-kachel-probe"; + const streifenAus = async (an) => seite.evaluate(([id, an]) => { + let s = document.getElementById(id); + if (!s) { s = document.createElement("style"); s.id = id; document.head.append(s); } + s.textContent = an ? ".liste-kachel > .gruppe::after { opacity: 0 !important; }" : ""; + }, [AUS, an]); + + const stellen = {}; + for (const rolle of Object.keys(ERWARTET)) { + stellen[rolle] = await seite.$eval(`.gruppe[data-rolle="${rolle}"]`, (x) => { + const r = x.getBoundingClientRect(); + return { x: Math.round(r.x) + 3, y: Math.round(r.y) + 18, width: 3, height: 10 }; + }); + } + const mit = {}; + for (const [rolle, clip] of Object.entries(stellen)) mit[rolle] = await mittelFarbe(clip); + await streifenAus(true); + await seite.waitForTimeout(200); + const ohne = {}; + for (const [rolle, clip] of Object.entries(stellen)) ohne[rolle] = await mittelFarbe(clip); + await streifenAus(false); + await seite.waitForTimeout(200); + const nochmal = await mittelFarbe(stellen.scout); + + for (const [rolle, kanal] of Object.entries(ERWARTET)) { + const [r, g, b] = mit[rolle]; + const o = ohne[rolle]; + /* Welcher Kanal hat durch den Streifen am meisten GEWONNEN? Der + absolute Wert allein wäre irreführend: Auf einem bläulichen Grund + führt Blau auch dann, wenn der Streifen rot ist. */ + const zu = [r - o[0], g - o[1], b - o[2]]; + const fuehrt = zu[0] >= zu[1] && zu[0] >= zu[2] ? "r" + : zu[1] >= zu[0] && zu[1] >= zu[2] ? "g" : "b"; + const abstand = zu.reduce((s, x) => s + Math.abs(x), 0); + ok(abstand > 25 && fuehrt === kanal, + `${rolle}: mit rgb(${mit[rolle]}) / ohne rgb(${o}) — Zuwachs ${JSON.stringify(zu)}, führend ${fuehrt}`); + } + + /* GEGENPROBE: Ausgeschaltet und wieder eingeschaltet muss dieselbe + Stelle wieder aussehen wie am Anfang. Täte sie das nicht, hätte die + Messung etwas anderes gemessen als den Streifen. */ + const zurueck = nochmal.reduce((s, x, i) => s + Math.abs(x - mit.scout[i]), 0); + ok(zurueck <= 6, `nach dem Wiedereinschalten sieht es aus wie vorher (Abweichung ${zurueck})`); +} + +/* ======================================================================= + 4. DIE KACHEL IST DUNKLER — UND DUNKLER ALS IHR INHALT + ======================================================================= */ +melde("\n=== 4. Der Grund ==="); +{ + const lage = await seite.$eval('.gruppe[data-rolle="scout"]', (x) => { + const r = x.getBoundingClientRect(); + return { x: Math.round(r.x), y: Math.round(r.y) }; + }); + const kachel = await mittelFarbe({ x: lage.x + 300, y: lage.y + 6, width: 120, height: 12 }); + const hell = Math.round(leuchtkraft(kachel) * 1000) / 10; + ok(hell < 2.0, `der Kachelgrund ist dunkel (rgb(${kachel}), Leuchtkraft ${hell} %)`); + + /* Die Vitrine muss dunkler sein als das, was darin steht -- sonst + sieht man die Vitrine statt des Inhalts. Manager ist offen. */ + const karte = await seite.$eval('.gruppe[data-rolle="manager"] .person', (x) => { + const r = x.getBoundingClientRect(); + return { x: Math.round(r.x) + 200, y: Math.round(r.y) + 6 }; + }); + const kartenFarbe = await mittelFarbe({ x: karte.x, y: karte.y, width: 120, height: 10 }); + ok(leuchtkraft(kartenFarbe) > leuchtkraft(kachel), + `die Karten darin sind heller als die Kachel (Karte rgb(${kartenFarbe}) gegen Kachel rgb(${kachel}))`); +} + +/* ======================================================================= + 5. LESBARKEIT — jede neue Textstelle gemessen + ======================================================================= */ +melde("\n=== 5. Lesbarkeit ==="); +{ + 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 FARBE WIRD GEZEICHNET, NICHT GELESEN. + ------------------------------------------------------------------ + Hier stand `.color.match(/\d+/g).slice(0,3)`. Das geht gut, + solange getComputedStyle "rgb(148, 165, 187)" liefert. Sobald + eine Farbe aus `color-mix()` kommt -- und genau das tun hier + alle neuen Stellen --, antwortet Chromium mit + "color(srgb 0.36 0.51 0.42)". Aus dem Muster fielen dann die + Ziffernfolgen "0", "510588", "0" heraus, und die Prüfung meldete + 775929299:1. Grün, und vollkommen wertlos. + + Ein Canvas nimmt JEDE Schreibweise entgegen, die der Browser + versteht, und gibt drei Zahlen von 0 bis 255 zurück. Was der + Browser malt, ist per Definition das, was man sieht. */ + const c = document.createElement("canvas"); + c.width = 1; c.height = 1; + const gg = c.getContext("2d", { willReadFrequently: true }); + gg.fillStyle = "#000000"; + gg.fillStyle = getComputedStyle(e).color; + gg.fillRect(0, 0, 1, 1); + const px = gg.getImageData(0, 0, 1, 1).data; + const farbe = [px[0], px[1], px[2]]; + e.dataset.messen = "ja"; + e.style.color = "transparent"; + return { x: Math.round(r.x), y: Math.round(r.y), + w: Math.round(r.width), h: Math.round(Math.min(r.height, 40)), farbe }; + }, waehler); + if (!stelle) return null; + const hinten = await mittelFarbe({ x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h }); + await seite.evaluate(() => { + const e = document.querySelector('[data-messen="ja"]'); + if (e) { e.style.color = ""; delete e.dataset.messen; } + }); + return { farbe: stelle.farbe, hinten, wert: kontrast(stelle.farbe, hinten) }; + } + + /* GEGENPROBE ZUM MESSGERÄT, VOR DER ERSTEN MESSUNG. + + Der Umweg über das Canvas ist neu, weil der alte Weg falsche Zahlen + lieferte. Bevor ich ihm glaube: zwei Farben mit bekanntem Ergebnis + hindurchschicken -- eine in gewöhnlicher Schreibweise, eine aus + color-mix(). Kommt dabei nicht heraus, was herauskommen muss, ist + die ganze Lesbarkeitsprüfung darunter wertlos. */ + { + const probe = await seite.evaluate(() => { + const c = document.createElement("canvas"); + c.width = 1; c.height = 1; + const g = c.getContext("2d", { willReadFrequently: true }); + const lies = (farbe) => { + g.fillStyle = "#000000"; + g.fillStyle = farbe; + g.fillRect(0, 0, 1, 1); + const p = g.getImageData(0, 0, 1, 1).data; + return [p[0], p[1], p[2]]; + }; + return { klar: lies("rgb(148, 165, 187)"), + gemischt: lies("color-mix(in srgb, #ffffff 50%, #000000)") }; + }); + const nah = (a, b) => a.every((x, i) => Math.abs(x - b[i]) <= 3); + ok(nah(probe.klar, [148, 165, 187]) && nah(probe.gemischt, [128, 128, 128]), + `das Messgerät stimmt: rgb-Schreibweise ${probe.klar}, color-mix ${probe.gemischt}`); + } + + for (const [name, waehler] of [ + ["Rollenname", '.gruppe[data-rolle="scout"] .gruppe__name'], + ["Zusatztext", '.gruppe[data-rolle="scout"] .gruppe__unter'], + ["Anzahl", '.gruppe[data-rolle="scout"] .gruppe__zahl'], + ["Namenszeichen", '.gruppe[data-rolle="scout"] .gruppe__kopf-zeichen'], + ["Namenszeichen (Creator, hellste Rolle)", '.gruppe[data-rolle="creator"] .gruppe__kopf-zeichen'], + ["Anzahl (Creator)", '.gruppe[data-rolle="creator"] .gruppe__zahl'], + ["Name offen", '.gruppe[data-rolle="manager"] .gruppe__name'], + ]) { + const m = await messen(waehler); + ok(m !== null && m.wert >= 4.5, + `${name}: ${m ? m.wert.toFixed(2) + ":1 (" + m.farbe.join(",") + " auf " + m.hinten.join(",") + ")" : "nicht gefunden"}`); + } + + /* GEGENPROBE: ein absichtlich zu dunkler Text MUSS durchfallen. */ + await seite.evaluate(() => { + document.querySelector('.gruppe[data-rolle="scout"] .gruppe__name').style.color = "#141a22"; + }); + const schlecht = await messen('.gruppe[data-rolle="scout"] .gruppe__name'); + ok(schlecht !== null && schlecht.wert < 4.5, + `absichtlich zu dunkler Text fällt durch (${schlecht?.wert.toFixed(2)}:1)`); + await seite.evaluate(() => { + document.querySelector('.gruppe[data-rolle="scout"] .gruppe__name').style.color = ""; + }); +} + +/* ======================================================================= + 6. WER DRINSTEHT — die Namenszeichen + ======================================================================= */ +melde("\n=== 6. Namenszeichen ==="); +{ + const creator = await seite.$eval('.gruppe[data-rolle="creator"]', (x) => ({ + zahl: x.querySelector(".gruppe__zahl").textContent, + zeichen: [...x.querySelectorAll(".gruppe__kopf-zeichen")].map((z) => ({ + text: z.textContent, titel: z.title, mehr: z.dataset.mehr })), + versteckt: x.querySelector(".gruppe__leute").getAttribute("aria-hidden"), + })); + ok(creator.zahl === "6", `sechs Creator sind angelegt (${creator.zahl})`); + ok(creator.zeichen.length === 5, + `vier Zeichen plus eines für den Rest (${creator.zeichen.map((z) => z.text).join(" ")})`); + ok(creator.zeichen[4]?.mehr === "ja" && creator.zeichen[4]?.text === "+2", + `das letzte sagt, wie viele fehlen ("${creator.zeichen[4]?.text}")`); + /* WELCHE zwei fehlen, hängt an der Sortierung des Servers -- die + hier abzuschreiben hiesse, sie an zwei Stellen zu pflegen. Geprüft + wird die Eigenschaft: genau zwei Namen, und keiner davon steht + schon als eigenes Zeichen davor. */ + const genannt = (creator.zeichen[4]?.titel || "").split(", ").filter(Boolean); + const vorne = creator.zeichen.slice(0, 4).map((z) => z.titel); + ok(genannt.length === 2 && genannt.every((n) => !vorne.includes(n)), + `und nennt beim Darüberfahren genau die zwei, die fehlen ("${creator.zeichen[4]?.titel}")`); + ok(creator.versteckt === "true", + "für das Vorleseprogramm sind sie ausgeblendet — die Anzahl steht als Zahl daneben"); + + /* Zwei Wörter geben zwei Buchstaben, ein Wort nur einen. */ + const spicy = await seite.$eval('.gruppe[data-rolle="spicy"] .gruppe__kopf-zeichen', + (z) => ({ text: z.textContent, titel: z.title })); + ok(spicy.text === "SM", `"Spicy Media" wird SM (${spicy.text})`); + const scout = await seite.$$eval('.gruppe[data-rolle="scout"] .gruppe__kopf-zeichen', + (b) => b.map((z) => z.textContent)); + ok(scout.join("") === "BP", `"BananaStift" und "Patrick" geben B und P (${scout.join(" ")})`); + + /* Gesperrt wird gekennzeichnet -- und zwar leise. */ + const manager = await seite.$$eval('.gruppe[data-rolle="manager"] .gruppe__kopf-zeichen', + (b) => b.map((z) => ({ text: z.textContent, gesperrt: z.dataset.gesperrt, titel: z.title }))); + const dieGesperrte = manager.find((z) => z.gesperrt === "ja"); + ok(!!dieGesperrte && /gesperrt/.test(dieGesperrte.titel), + `die gesperrte Person ist gekennzeichnet ("${dieGesperrte?.titel}")`); + ok(manager.filter((z) => z.gesperrt === "ja").length === 1, + "und nur sie — die aktive nicht"); +} + +/* ======================================================================= + 7. AM HANDY + ======================================================================= */ +melde("\n=== 7. Am Handy (390 px) ==="); +{ + const handy = await alsDogi(390); + const m = await handy.s.evaluate(() => { + const k = document.getElementById("liste"); + const leute = document.querySelector(".gruppe__leute"); + return { + ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, + leuteSichtbar: leute ? getComputedStyle(leute).display !== "none" : null, + breiter: [...k.querySelectorAll(".gruppe__kopf")] + .filter((x) => x.getBoundingClientRect().right > 391).length, + zeilen: [...k.querySelectorAll(".gruppe__kopf")].map((x) => Math.round(x.getBoundingClientRect().height)), + }; + }); + ok(m.ueber <= 1, `kein seitlicher Überstand (${m.ueber} px)`); + ok(m.leuteSichtbar === false, + "die Namenszeichen treten am Handy zurück — die Zahl sagt es dort"); + ok(m.breiter === 0, `keine Zeile ragt hinaus (${m.breiter})`); + ok(m.zeilen.every((h) => h >= 44), `die Zeilen bleiben greifbar (${m.zeilen.join("/")} px)`); + + /* ZWEI ZEILEN, NICHT DREI. + + Der Zusatztext bekommt am Handy eine eigene Zeile -- richtig, er + passt sonst nicht. Der PFEIL bekam dadurch eine dritte und stand + mittig unter dem Text wie ein vergessenes Zeichen: 106 px je Rolle, + fünfmal untereinander. Gemessen wird deshalb beides: die Höhe und + die Lage des Pfeils. */ + ok(m.zeilen.every((h) => h <= 82), + `und werden nicht dreizeilig (${m.zeilen.join("/")} px)`); + const pfeil = await handy.s.evaluate(() => { + const k = document.querySelector(".gruppe__kopf"); + const p = k.querySelector(".klapp-pfeil").getBoundingClientRect(); + const n = k.querySelector(".gruppe__name").getBoundingClientRect(); + const kr = k.getBoundingClientRect(); + return { rechts: Math.round(kr.right - p.right), + mitte: Math.round((p.top + p.bottom) / 2 - (kr.top + kr.bottom) / 2), + nebenNamen: p.top < n.bottom + 4 }; + }); + ok(pfeil.rechts >= 0 && pfeil.rechts < 30, + `der Pfeil steht rechts (${pfeil.rechts} px vom Rand)`); + ok(Math.abs(pfeil.mitte) < 8, + `und mittig zwischen beiden Zeilen (${pfeil.mitte} px von der Mitte)`); + await handy.ctx.close(); +} + +/* ======================================================================= + 8. KEINE STILLEN FEHLER + ======================================================================= */ +melde("\n=== 8. Stille Fehler ==="); +{ + ok(konsole.length === 0, konsole.length ? "Fehler: " + konsole.slice(0, 3).join(" | ") + : "keine Skript- oder Konsolenfehler"); + await seite.evaluate(() => { setTimeout(() => { throw new Error("Absicht"); }, 0); }); + await seite.waitForTimeout(400); + ok(konsole.some((z) => /Absicht/.test(z)), `ein absichtlicher Fehler wird bemerkt (${konsole.length})`); +} + +await gross.ctx.close(); +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + +melde(`\n${geprueft} Prüfungen`); +melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`); +process.exit(fehler ? 1 : 0); diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index 7c825346..86472a72 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -434,29 +434,56 @@ body[data-nur-lesen="ja"] select { display: none; } Kalender. Eine Vitrine ist dunkler als das, was darin steht; sonst sieht man die Vitrine statt des Inhalts. ===================================================================== */ +/* DUNKLER, 09.09.2026. Filipe: "der hintergrund soll auch bissl + dunkler sein von der kachel." + + Von rgba(9,13,20,.96) auf rgba(5,8,13,.985). Der Hintergrund schien + vorher merklich durch -- bei einem Motiv mit hellen Stellen sass + hinter der Liste mal ein dunkler, mal ein heller Fleck, und die + Kachel wirkte dadurch fleckig statt ruhig. Die Schrift wird davon + nur besser lesbar: Der Grund wird dunkler, die Farben bleiben. */ .liste-kachel { padding: 4px 0 0; margin-bottom: 24px; background: var(--raster), - linear-gradient(178deg, rgba(9, 13, 20, .96), rgba(4, 7, 12, .98)); + linear-gradient(178deg, rgba(5, 8, 13, .985), rgba(2, 4, 8, .995)); } /* Die Abschnitte werden Felder in einem Körper: keine eigenen Ränder, keine Luft dazwischen, sondern eine Fuge. */ .liste-kachel > .gruppe { margin: 0; - padding: 0 16px 14px; - border-bottom: 1px solid #05070c; - box-shadow: 0 1px 0 rgba(255, 255, 255, .035); + padding: 0 16px; + border-bottom: 1px solid #04060a; + box-shadow: 0 1px 0 rgba(255, 255, 255, .04); } .liste-kachel > .gruppe:last-child { border-bottom: 0; box-shadow: none; } -/* Der Kopf jeder Gruppe füllt die Breite der Kachel -- so ist die - ganze Zeile das Ziel, nicht nur der Text. */ + +/* --------------------------------------------------------------------- + DAS LEERE BAND UNTER JEDER ZEILE (gemessen, 09.09.2026) + + Filipe: "mach die ganze kachel perfekter detailliert schoenes." + + Auf seinem Bild stand unter jeder zugeklappten Rolle ein leerer + Streifen. Nachgemessen: Der Abschnitt war 74 px hoch, die Zeile + darin 45 -- 29 px Nichts, fuenfmal untereinander. + + Sie kamen aus ZWEI Quellen, und nur eine davon stand hier: + * padding-bottom: 14px an .liste-kachel > .gruppe + * margin-bottom: 14px an .gruppe__kopf -- aus start.css, Zeile 765, + geschrieben fuer die freistehenden Abschnitte der Startseite, wo + unter der Ueberschrift wirklich gleich Karten kommen. + + Zugeklappt kommt hier aber gar nichts, und dann ist der Abstand + Abstand zu nichts. Beide werden deshalb an den ZUSTAND gebunden: + offen -> Luft, zugeklappt -> keine. */ .liste-kachel > .gruppe > .gruppe__kopf { width: calc(100% + 32px); - margin-left: -16px; - padding: 13px 16px 11px; + margin: 0 0 0 -16px; + padding: 15px 16px; } +.liste-kachel > .gruppe[data-auf="ja"] > .gruppe__kopf { margin-bottom: 13px; } +.liste-kachel > .gruppe[data-auf="ja"] { padding-bottom: 15px; } /* ===================================================================== DIE KOEPFE STEHEN IN SPALTEN, NICHT IN DER MITTE (screen3, 09.09.2026) @@ -522,7 +549,201 @@ body[data-nur-lesen="ja"] select { display: none; } .liste-kachel .gruppe__unter { grid-column: 1 / -1; } } /* Ein Abschnitt, der offen ist, hebt sich leicht ab -- so sieht man, - welcher gerade Platz einnimmt, ohne eine zweite Farbe. */ -.liste-kachel > .gruppe:has(> .gruppe__kopf[aria-expanded="true"]) { - background: linear-gradient(180deg, rgba(255, 255, 255, .028), transparent 60%); + welcher gerade Platz einnimmt, ohne eine zweite Farbe. Der Ton kommt + aus der Rollenfarbe, ist aber so leise, dass er nicht als Farbe + auffaellt, sondern als Licht. */ +.liste-kachel > .gruppe[data-auf="ja"] { + background: + linear-gradient(180deg, color-mix(in srgb, var(--r) 7%, transparent), transparent 62%); +} + +/* ===================================================================== + DIE KACHEL IM DETAIL (09.09.2026) + + Filipe, mit Bildschirmfoto: "mach die ganze kachel perfekter + detailliert schönes. der hintergrund soll auch bissl dunkler sein + von der kachel." + + Vorher waren die fünf Zeilen fünfmal dasselbe: Name, Zahl, Satz, + Pfeil, in Grau. Die Rollen haben im ganzen Haus eigene Farben -- + auf der Anmeldeseite, an den Marken, in den Bereichen. Ausgerechnet + in der Liste, in der es NUR um Rollen geht, hörten sie auf. + + Der Ton kommt deshalb aus derselben Quelle wie überall (--*-haupt in + gate.css) und wird an EINER Stelle je Rolle gesetzt. Alles Weitere + liest --r: Streifen, Zahl, Namenszeichen, Pfeil, das Licht beim + Aufklappen. Fünf Farbangaben je Rolle wären fünf Gelegenheiten, + dass eine nachgezogen wird und vier nicht. + + AUGENSCHONEND HEISST HIER: die Farbe trägt, sie leuchtet nicht. Der + Streifen hat 3 px, die Fläche darunter höchstens 8 % Farbe. Fünf + kräftig eingefärbte Zeilen untereinander wären ein Regenbogen -- + und einer, den man jeden Tag ansieht. + ===================================================================== */ +.liste-kachel > .gruppe { --r: var(--text-still); } +/* UND DIE KARTEN DARIN ERBEN SIE. + + `--ton` ist die Variable, aus der die Modulliste (module.css) das + Kantenlicht und die Eckwinkel jeder Kachel zieht. Sie hier zu setzen + heisst: Die Personenkarten in einem Abschnitt tragen die Farbe ihrer + Rolle, nicht die allgemeine Hausfarbe. Vorher standen in einem + lila Manager-Abschnitt orange gerahmte Karten -- zwei Farben fuer + dieselbe Sache, und die aeussere hatte recht. + + Eine Zeile, und der ganze Abschnitt wird ein Stueck. */ +.liste-kachel > .gruppe { --ton: var(--r); } +.liste-kachel > .gruppe[data-rolle="spicy"] { --r: var(--spicy-haupt); } +.liste-kachel > .gruppe[data-rolle="admin"] { --r: var(--dogi-haupt); } +.liste-kachel > .gruppe[data-rolle="manager"] { --r: var(--manager-haupt); } +.liste-kachel > .gruppe[data-rolle="scout"] { --r: var(--scout-haupt); } +.liste-kachel > .gruppe[data-rolle="creator"] { --r: var(--creator-haupt); } + +/* Der Streifen sitzt NEBEN dem Kantenlicht, nicht darunter. + + Erster Anlauf: `left: 0`, volle Höhe. Im Bild war nichts zu sehen -- + und die berechneten Werte sagten trotzdem "3 px breit, sichtbar, + Deckkraft 0,55". Der Grund steht in module.css: Jede Kachel trägt auf + ::before ein Kantenlicht mit `inset: 0` und `z-index: 2`, also eine + 1,6 px breite Linie ÜBER allen Kindern. Vom Streifen blieben 1,4 px, + und die lagen genau in der Kante. + + Er beginnt deshalb bei 3 px, ist gerundet und lässt oben und unten + Luft: So liest er sich als Marke der Zeile und nicht als zweiter + Rahmen neben dem ersten. Zugeklappt reicht er über die Zeile, offen + über den ganzen Abschnitt -- man sieht am Rand, wie weit ein + geöffneter Bereich reicht. */ +.liste-kachel > .gruppe { position: relative; } +.liste-kachel > .gruppe::after { + content: ""; position: absolute; left: 3px; top: 9px; bottom: 9px; + width: 3px; border-radius: 999px; + background: linear-gradient(180deg, + color-mix(in srgb, var(--r) 85%, transparent), + color-mix(in srgb, var(--r) 30%, transparent)); + opacity: .7; + transition: opacity var(--tempo); + pointer-events: none; +} +.liste-kachel > .gruppe[data-auf="ja"]::after { opacity: 1; } + +/* Die Zeile reagiert als GANZE, nicht durch Verblassen. `opacity: .82` + auf dem Kopf (start.css) macht beim Überfahren den Text blasser -- + das ist das Gegenteil von "hier bist du". Ein leiser Farbzug von + links sagt dasselbe, ohne etwas wegzunehmen. */ +.liste-kachel > .gruppe > .gruppe__kopf--klappbar { opacity: 1; } +.liste-kachel > .gruppe > .gruppe__kopf--klappbar:hover { + opacity: 1; + background: linear-gradient(90deg, + color-mix(in srgb, var(--r) 11%, transparent), transparent 58%); +} + +/* Die Anzahl trägt die Rollenfarbe. Sie ist der einzige Punkt in der + Zeile, der ohnehin schon ein Feld ist -- eine zweite eingefärbte + Fläche daneben wäre eine zu viel. */ +.liste-kachel .gruppe__zahl { + color: color-mix(in srgb, var(--r) 78%, #ffffff); + background: color-mix(in srgb, var(--r) 15%, transparent); + border: 1px solid color-mix(in srgb, var(--r) 32%, transparent); + padding: 1px 8px; +} +/* Und der Rollenname bekommt eine Spur mehr Gewicht als vorher -- er + ist die Überschrift der Zeile, nicht ihre Beschriftung. */ +.liste-kachel .gruppe__name { color: var(--text); letter-spacing: .11em; } +.liste-kachel > .gruppe[data-auf="ja"] .gruppe__name { + color: color-mix(in srgb, var(--r) 55%, var(--text)); +} + +/* ---- Wer drinsteht --------------------------------------------------- + Vier Namenszeichen, leicht überlappend wie ein Stapel Karten -- so + liest man sie als EINE Gruppe und nicht als vier Einzelteile. Sie + stehen rechts, kurz vor dem Pfeil: Der Blick geht von links (wer ist + das) nach rechts (wer ist drin, kann ich aufklappen). */ +.gruppe__leute { display: flex; align-items: center; justify-self: end; } +.gruppe__kopf-zeichen { + width: 26px; height: 26px; border-radius: 50%; + display: grid; place-items: center; + margin-left: -7px; + /* 0,72 rem und nicht 0,66: Darunter faellt die Hausgrenze von + 11,5 px, und zwei Grossbuchstaben in einem 26-px-Kreis haben den + Platz. Gemeldet von pruef-css-klassen, bevor es jemand lesen + musste. */ + font-size: .72rem; font-weight: 700; letter-spacing: .01em; + color: color-mix(in srgb, var(--r) 72%, #ffffff); + background: color-mix(in srgb, var(--r) 17%, #070a11); + /* Der Ring in der Farbe der Kachel schneidet die Zeichen voneinander + frei -- ohne ihn verschwimmt der Stapel zu einem Fleck. */ + border: 1.5px solid #05080d; + box-shadow: 0 0 0 1px color-mix(in srgb, var(--r) 26%, transparent); +} +.gruppe__kopf-zeichen:first-child { margin-left: 0; } +/* Gesperrt ist keine Farbe, sondern eine Abwesenheit von Farbe. Rot + wäre hier eine Warnung je Person; die Warnung steht schon einmal im + Zusatztext, und zweimal dasselbe zu rufen macht beides leiser. */ +.gruppe__kopf-zeichen[data-gesperrt="ja"] { + color: var(--text-still); + background: rgba(255, 255, 255, .05); + box-shadow: 0 0 0 1px rgba(255, 255, 255, .1); +} +.gruppe__kopf-zeichen[data-mehr="ja"] { + color: var(--text-leise); + background: rgba(255, 255, 255, .06); + box-shadow: 0 0 0 1px rgba(255, 255, 255, .12); + font-weight: 600; +} + +/* Der Pfeil bekommt die Rollenfarbe erst beim Überfahren -- fünf + farbige Pfeile untereinander wären fünfmal dieselbe Aussage. */ +.liste-kachel > .gruppe > .gruppe__kopf--klappbar:hover .klapp-pfeil { + color: color-mix(in srgb, var(--r) 70%, var(--text)); +} +.liste-kachel > .gruppe[data-auf="ja"] .klapp-pfeil { + color: color-mix(in srgb, var(--r) 60%, var(--text-leise)); +} + +/* Die Namenszeichen bekommen eine eigene Spalte im Raster (Name, Zahl, + Zusatztext, Leute, Pfeil). */ +.liste-kachel > .gruppe > .gruppe__kopf--klappbar { + grid-template-columns: var(--namen-spalte, max-content) auto minmax(0, 1fr) auto auto; + align-items: center; +} +/* Der Zusatztext steht auf der Schriftlinie des Namens -- er ist Text, + die anderen drei sind Formen. */ +.liste-kachel .gruppe__name, .liste-kachel .gruppe__unter { align-self: baseline; } + +@media (max-width: 900px) { + /* Eng wird zuerst das Entbehrliche weggelassen: Die Zahl daneben sagt + schon, wie viele es sind. */ + .gruppe__leute { display: none; } + .liste-kachel > .gruppe > .gruppe__kopf--klappbar { + grid-template-columns: var(--namen-spalte, max-content) auto minmax(0, 1fr) auto; + } +} +/* ---- Am Handy: zwei Zeilen, und der Pfeil bleibt rechts ------------- + Hier stand nur `.gruppe__unter { grid-column: 1 / -1 }` (weiter oben + in dieser Datei). Der Zusatztext bekam damit eine eigene Rasterzeile + -- und schob den Pfeil in eine DRITTE, wo er mittig unter dem Text + stand wie ein vergessenes Zeichen. Gemessen: 106 px je Zeile, fuenfmal + untereinander, auf einem 390 px breiten Bildschirm. + + Richtig ist: Der Pfeil bekommt seine eigene Spalte ganz rechts und + ueberspannt BEIDE Zeilen. Dann steht er dort, wo die Hand ihn sucht, + und die Zeile ist zwei Zeilen hoch statt drei. */ +@media (max-width: 620px) { + .liste-kachel > .gruppe > .gruppe__kopf--klappbar { + grid-template-columns: max-content auto minmax(0, 1fr) auto; + row-gap: 3px; + } + .liste-kachel .gruppe__name { grid-column: 1; grid-row: 1; } + .liste-kachel .gruppe__zahl { grid-column: 2; grid-row: 1; } + .liste-kachel .gruppe__unter { grid-column: 1 / 4; grid-row: 2; } + .liste-kachel > .gruppe > .gruppe__kopf--klappbar .klapp-pfeil { + grid-column: 4; grid-row: 1 / span 2; align-self: center; + } +} + +@media (prefers-reduced-motion: reduce) { + .liste-kachel > .gruppe::after { transition: none; } +} +@media (forced-colors: active) { + .liste-kachel > .gruppe::after { background: CanvasText; } + .gruppe__kopf-zeichen { border: 1px solid CanvasText; } } diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index ee8d5059..a5e428d6 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -204,6 +204,18 @@ let betreuerListe = []; + /** Ein oder zwei Buchstaben aus einem Namen. + * + * Zwei nur bei einem echten zweiten Wort: Aus "Spicy Media" wird SM, + * aus "BananaStift" ein B -- Binnenversalien als zweiten Namen zu + * lesen ergäbe "BS" für eine Person, die einen Namen hat. */ + function initialen(name) { + const teile = String(name || '').trim().split(/[\s._-]+/).filter(Boolean); + if (!teile.length) return '?'; + if (teile.length === 1) return teile[0].slice(0, 1).toUpperCase(); + return (teile[0][0] + teile[teile.length - 1][0]).toUpperCase(); + } + function zeile(p) { const k = el('div', 'person'); k.dataset.aktiv = p.aktiv ? 'ja' : 'nein'; @@ -599,6 +611,18 @@ const auf = offen.has(abschnitt.rolle); const block = document.createElement('section'); block.className = 'gruppe'; + /* DIE ROLLE STEHT AM ABSCHNITT (09.09.2026). + Ohne sie sehen alle fünf Zeilen gleich aus, und die Farben, die + das Haus für die Rollen ohnehin führt (Anmeldeseite, Marken, + Bereiche), hören ausgerechnet in der Personenliste auf. Das + Merkmal ist die einzige Zeile Skript, die dafür nötig ist -- + alles Weitere macht das CSS. */ + block.dataset.rolle = abschnitt.rolle; + /* UND DER ZUSTAND, an derselben Stelle. Das CSS fragte ihn bisher + ueber `:has(> .gruppe__kopf[aria-expanded="true"])` ab -- das + geht, ist aber ein zweiter Weg zu einer Auskunft, die hier + ohnehin vorliegt. Ein Merkmal, drei Regeln, kein Suchen. */ + block.dataset.auf = auf ? 'ja' : 'nein'; /* Die Überschrift IST der Knopf. Ein eigener kleiner Schalter daneben wäre ein zweites Ziel für dieselbe Absicht -- und ein @@ -635,7 +659,40 @@ pfad.setAttribute('d', 'm6 9.5 6 6 6-6'); pfeil.append(pfad); - kopf.append(h, zahl, unter, pfeil); + /* WER DRINSTEHT — SICHTBAR, OHNE AUFZUKLAPPEN. + + Zugeklappt sagte die Zeile bisher nur, WIE VIELE es sind. Wer + wissen wollte, ob Patrick dabei ist, musste aufklappen. Vier + Namenszeichen beantworten das im Vorbeigehen; ab dem fünften + steht "+n" dahinter, damit die Zeile nicht wächst. + + `aria-hidden`, weil es eine Wiederholung ist: Die Anzahl steht + als Zahl daneben, die Namen stehen beim Aufklappen vollständig + da. Ein Vorleseprogramm würde hier sonst Buchstabenpaare + vorlesen, die niemandem etwas sagen. Für die Maus steht der + volle Name im title. */ + const leute = document.createElement('span'); + leute.className = 'gruppe__leute'; + leute.setAttribute('aria-hidden', 'true'); + const ZEIGEN = 4; + for (const p of abschnitt.drin.slice(0, ZEIGEN)) { + const z = document.createElement('span'); + z.className = 'gruppe__kopf-zeichen'; + z.textContent = initialen(p.name); + z.title = p.aktiv ? p.name : `${p.name} (gesperrt)`; + if (!p.aktiv) z.dataset.gesperrt = 'ja'; + leute.append(z); + } + if (abschnitt.drin.length > ZEIGEN) { + const mehr = document.createElement('span'); + mehr.className = 'gruppe__kopf-zeichen'; + mehr.dataset.mehr = 'ja'; + mehr.textContent = `+${abschnitt.drin.length - ZEIGEN}`; + mehr.title = abschnitt.drin.slice(ZEIGEN).map((p) => p.name).join(', '); + leute.append(mehr); + } + + kopf.append(h, zahl, unter, leute, pfeil); kopf.addEventListener('click', () => { if (offen.has(abschnitt.rolle)) offen.delete(abschnitt.rolle); else offen.add(abschnitt.rolle); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index a4d7e23c..a573fc3c 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -14,10 +14,10 @@ - - - - + + + + @@ -231,10 +231,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index de193f3c..ba046b22 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -140,10 +140,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 86fd88d3..728c3a32 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -149,14 +149,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 8ed22162..293981a1 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -82,11 +82,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 6a2cc803..bc3f40ea 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -14,17 +14,17 @@ - - + + - - - + + + @@ -247,10 +247,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 91ee3229..4c171882 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -172,12 +172,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 6cf8a930..abc93700 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -101,10 +101,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index d57b1f7c..efb4da05 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -32,7 +32,7 @@ - + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 0a298878..b480561a 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -340,11 +340,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index f1192056..633ec7d4 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -217,10 +217,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index b9fe60aa..f401a227 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -335,10 +335,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index be95ed78..ae40179b 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -207,11 +207,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index ec97ceb6..fc0fd49b 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -99,11 +99,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index f24e4fbb..fdcf8725 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -132,10 +132,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index df112033..6809e404 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -14,12 +14,12 @@ - - + + - - + + @@ -667,10 +667,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 3b589396..fc6606ef 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -78,11 +78,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 820e5643..f10cf0bc 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -119,10 +119,10 @@ - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 8222c270..211ff5af 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -14,13 +14,13 @@ - - + + - - - + + + @@ -142,11 +142,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 200bbeba..7bbaa15f 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -183,10 +183,10 @@ - - - - - + + + + +