diff --git a/server/pruef-verborgen.mjs b/server/pruef-verborgen.mjs new file mode 100644 index 00000000..1ee01f92 --- /dev/null +++ b/server/pruef-verborgen.mjs @@ -0,0 +1,162 @@ +/* Browserpruefung der Startseite: Gruppen, Zeichen, Farbtoene, Zahlen auf + den Kacheln -- und was jede Rolle davon zu sehen bekommt. */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-start-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4186"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; +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"); +/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort + zwei Auffangnetze ein, die einen Absturz nur protokollieren -- + richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese + Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der + Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ +notbremse(480_000, "pruef-start-ansicht"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4186"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); + +/* ORTSZEIT, nicht UTC. Die Anwendung rechnet "heute" nach der Uhr des + Benutzers; toISOString() liefert dagegen UTC und damit zwischen + Mitternacht und 2 Uhr den VORTAG. In diesem Fenster schlug diese + Pruefung fehl, ohne dass etwas kaputt war. */ +const tagText = (d = new Date()) => + `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` + + `-${String(d.getDate()).padStart(2, "0")}`; + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +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"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const heute = tagText(); +const gestern = tagText(new Date(Date.now() - 86400_000)); +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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001"); +const idAnna = anlegen("Tili", "creator", "CODE-ANNA-0001"); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idAnna, idScout, jetzt); + +/* Filipe, Patrick und Tili legt die Vorlage oben schon an. Hier kommen + die drei dazu, die diese Pruefung braucht -- VanVan als ZWEITER + Admin-Zugang (also mit hoeherer Nummer als Filipe, das ist der + ganze Unterschied), dazu Spicy Media und ein Manager. */ +const idVan = anlegen("VanVan", "admin", "CODE-VAN-0001"); +const idSpicy = anlegen("Cigdem", "spicy", "CODE-SPIC-0001"); +const idMan = anlegen("Schulle", "manager", "CODE-MAN-0001"); +d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idScout, idMan, jetzt); +d.close(); + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function anmelden(kontext, rolle, code) { + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click(`.rolle[data-rolle="${rolle}"]`); + await seite.fill("#code", code); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + return seite; +} + +console.log("\n=== Niemand sieht VanVan ausser DogFather ==="); +console.log(` (DogFather = id ${idDogi}, VanVan = id ${idVan})\n`); + +/* Jede Schnittstelle, die Personen ausgibt. Wer eine neue baut, sollte + sie hier eintragen -- die Liste ist die Landkarte dieser Regel. */ +const WEGE = [ + ["Chat-Partner", "/workspace/api/chat/partner", (d) => d.personen], + ["Personenverwaltung", "/workspace/api/verwaltung/personen", (d) => d.personen], + ["Betreuer-Auswahl", "/workspace/api/verwaltung/personen", (d) => d.betreuer], + ["Zentrale (Team)", "/workspace/api/zentrale", (d) => d.segmente], + /* NACHGESEHEN STATT GERATEN: Es gibt genau EINEN Endpunkt fuer die + Personenwahl -- `/workspace/api/personen` aus workspace-aufgaben.js. + Kalender, Aufgaben und Dateien benutzen alle denselben. Meine + ersten beiden Pfade waren erfunden und lieferten 404; eine + Pruefung, die auf einen 404 laeuft, prueft nichts. */ + ["Personenwahl", "/workspace/api/personen", (d) => d.personen], +]; + +for (const [rolle, code, name] of [ + ["admin", "CODE-DOGI-0001", "Filipe (DogFather)"], + ["admin", "CODE-VAN-0001", "VanVan selbst"], + ["spicy", "CODE-SPIC-0001", "Cigdem (Spicy Media)"], + ["manager", "CODE-MAN-0001", "Schulle (Manager)"], + ["scout", "CODE-SCOU-0001", "Patrick (Scout)"], + ["creator", "CODE-ANNA-0001", "Tili (Creator)"], +]) { + const k = await browser.newContext({ viewport: { width: 1200, height: 900 } }); + const s = await anmelden(k, rolle, code); + /* DARF diese Person VanVan sehen? Nur DogFather selbst -- und + VanVan sich selbst. */ + const darf = code === "CODE-DOGI-0001" || code === "CODE-VAN-0001"; + for (const [was, pfad, ziehen] of WEGE) { + const antwort = await s.evaluate(async (u) => { + const a = await fetch(u); + if (!a.ok) return { code: a.status }; + return { code: 200, daten: await a.json() }; + }, pfad); + if (antwort.code !== 200) { + /* Kein Zugriff ist kein Befund -- aber es wird GESAGT, damit + niemand glaubt, hier sei etwas geprueft worden. */ + console.log(` -- ${name.padEnd(22)} ${was.padEnd(20)} kein Zugriff (${antwort.code})`); + continue; + } + let liste = []; + try { liste = ziehen(antwort.daten) || []; } catch { liste = []; } + const drin = liste.some((x) => x && (x.id === idVan || x.person_id === idVan + || String(x.name || "").includes("VanVan"))); + if (darf) { + ok(true, `${name.padEnd(22)} ${was.padEnd(20)} ${drin ? "sieht VanVan" : "(nicht in dieser Liste)"}`); + } else { + ok(!drin, `${name.padEnd(22)} ${was.padEnd(20)} ${drin ? "SIEHT VANVAN — FEHLER" : "sieht VanVan nicht"}`); + } + } + await k.close(); +} + +/* ---- GEGENPROBE: Kann diese Pruefung ueberhaupt anschlagen? -------- + Ohne sie waere "sieht VanVan nicht" auch dann gruen, wenn die + Listen leer zurueckkaemen oder die Namen anders hiessen. Also + einmal ohne Regel nachsehen: Ein Manager MUSS DogFather sehen -- + dieselbe Liste, dieselbe Rolle, nur eine Person weiter. */ +{ + const k = await browser.newContext({ viewport: { width: 1200, height: 900 } }); + const s = await anmelden(k, "manager", "CODE-MAN-0001"); + const daten = await s.evaluate(async () => + (await (await fetch("/workspace/api/chat/partner")).json())); + const sichtDogi = (daten.personen || []).some((x) => x.id === idDogi); + ok(sichtDogi, `Gegenprobe: der Manager SIEHT DogFather in derselben Liste ` + + `(${(daten.personen || []).length} Personen) — die Pruefung misst also wirklich etwas`); + await k.close(); +} + +await browser.close(); +console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler === 0 ? 0 : 1); diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index 73684d1c..98434fb1 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -14,7 +14,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, siehtAlles, betreutWo, termineSichtbar, heuteLokal, - externPruefen, externSql, betreuteIds, einladbareIds, ROLLEN_SORTIERUNG, + externPruefen, externSql, betreuteIds, einladbareIds, verborgeneIds, ROLLEN_SORTIERUNG, } from "./workspace.js"; import { nachfuellenAlle, serienPruefen, serieAnlegen, zuordnungErzwingen, @@ -133,7 +133,26 @@ const VERBUND = ` * mich führt, dazu DogFather. Die Begründung, warum das keine * Aufweichung der Sichtbarkeit ist, steht dort. */ function darfEintragen(person) { - if (istLeitung(person)) return null; // null = alle + /* AUCH DIE LEITUNG DARF NICHT JEDEN EINTRAGEN (09.09.2026). + + Hier stand `if (istLeitung(person)) return null;` GANZ oben -- + also vor jeder Liste. Damit haette Spicy Media einen verborgenen + Zugang als Teilnehmer eines Termins eintragen koennen; er waere in + der Teilnehmerliste des Termins aufgetaucht, obwohl er in keiner + Personenliste steht. Eine Sichtbarkeitsregel, die nur beim LESEN + gilt und nicht beim SCHREIBEN, hat ein Loch in der Mitte. + + Gibt es nichts zu verbergen, bleibt alles wie vorher (`null` = + alle). */ + const weg = verborgeneIds(person); + if (istLeitung(person)) { + if (!weg.length) return null; + const alle = new Set(db().prepare("SELECT id FROM personen WHERE aktiv = 1") + .all().map((z) => z.id)); + for (const id of weg) alle.delete(id); + alle.add(person.id); + return alle; + } const ids = einladbareIds(person); if (ids === null) return null; const erlaubt = new Set(ids); diff --git a/server/workspace-personen.js b/server/workspace-personen.js index e22ca408..c81c5734 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -15,7 +15,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, + db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, siehtAlles, ROLLEN_SORTIERUNG, ROLLEN_REIHE, istSpicy, verborgeneIds, } from "./workspace.js"; import { sicherungJetzt } from "./workspace-sicherung.js"; @@ -159,10 +159,19 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => { in die Tabelle -- ein `haupt`-Feld. Solange es genau zwei Admin-Zugaenge gibt und der erste DogFather ist, ist die Nummer die ehrlichste Antwort ohne Datenbankumbau. */ - const ohne = req.ohneDogFather === true; - const hauptAdmin = ohne - ? (db().prepare("SELECT MIN(id) AS id FROM personen WHERE rolle = 'admin'").get()?.id || 0) - : 0; + /* SEIT DEM 09.09.2026 GILT DIE REGEL FUER ALLE, NICHT NUR FUER + SPICY MEDIA. + + Filipe: "keiner soll vanvan sehen ausser ich, ueberall soll + keiner vanvan sehen ausser dogfather." + + Hier stand eine eigene Fassung dieser Regel -- nur fuer Spicy + Media, nur an dieser Stelle. Sie ist ersetzt durch die zentrale + `verborgeneIds()` aus workspace.js, die ueberall dieselbe ist. + Zwei Fassungen derselben Regel waeren zwei Gelegenheiten, dass + eine nachgezogen wird und die andere nicht -- und bei einer + Sichtbarkeitsregel heisst das, dass jemand etwas sieht. */ + const weg = new Set(verborgeneIds(req.person)); res.json({ personen: db().prepare(` SELECT p.id, p.name, p.rolle, p.aktiv, p.erstellt, p.letzter_login, @@ -185,7 +194,7 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => { LEFT JOIN betreuung b ON b.creator_id = p.id LEFT JOIN scout_zuteilung sz ON sz.scout_id = p.id ORDER BY ${ROLLEN_SORTIERUNG.replace("rolle", "p.rolle")}, p.aktiv DESC, p.name`) - .all().filter((z) => !(ohne && z.rolle === "admin" && z.id !== hauptAdmin)), + .all().filter((z) => !weg.has(z.id)), /* Wer ueberhaupt als zustaendig eingetragen werden kann. Bis zum 31.08.2026 waren das nur Scouts. In der Auswahl stand @@ -196,10 +205,12 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => { Jetzt kann jede betreuende Rolle eingetragen werden -- DogFather und Manager genauso wie Scouts. In der ueblichen Reihenfolge. */ + /* Auch die Auswahl "wer betreut" -- ein verborgener Zugang darf + dort nicht als Vorschlag auftauchen. */ betreuer: db().prepare( `SELECT id, name, rolle FROM personen WHERE rolle IN ('admin', 'manager', 'scout') AND aktiv = 1 - ORDER BY ${ROLLEN_SORTIERUNG}, name`).all(), + ORDER BY ${ROLLEN_SORTIERUNG}, name`).all().filter((z) => !weg.has(z.id)), }); } catch (fehler) { console.error("[workspace] Personen lesen:", fehler?.message); diff --git a/server/workspace-zentrale.js b/server/workspace-zentrale.js index d647b249..1cd97e7d 100644 --- a/server/workspace-zentrale.js +++ b/server/workspace-zentrale.js @@ -50,7 +50,7 @@ ===================================================================== */ import express from "express"; -import { db, sitzungLesen, istLeitung, betreuteIds, scoutsVon } from "./workspace.js"; +import { db, sitzungLesen, istLeitung, betreuteIds, scoutsVon, verborgeneIds } from "./workspace.js"; export const zentraleRouter = express.Router(); @@ -132,9 +132,21 @@ zentraleRouter.get("/workspace/api/zentrale", (req, res) => { ansieht, ist die Person, die ihn liest; sich selbst als Segment im eigenen Team zu zaehlen, verschiebt jede Prozent- angabe um einen Platz. */ + /* AUCH HIER GILT DIE VERBERGUNGSREGEL (09.09.2026). + + Diese Abfrage geht als einzige NICHT durch die zentralen + Listenfunktionen -- sie holt sich das Haus selbst. Genau + deshalb war sie die eine Stelle, an der Spicy Media VanVan + noch gesehen hat, nachdem alle anderen Wege zu waren. + `server/pruef-verborgen.mjs` hat es beim ersten Lauf + 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 zusatz = weg.length ? ` AND id NOT IN (${weg.map(() => "?").join(",")})` : ""; leute = d.prepare( `SELECT id, name, rolle FROM personen - WHERE aktiv = 1 AND id <> ? ORDER BY rolle, name`).all(ich.id); + WHERE aktiv = 1 AND id <> ?${zusatz} ORDER BY rolle, name`).all(ich.id, ...weg); } else { /* Manager: seine Scouts UND die Creator (eigene wie die seiner Scouts). Scout: nur seine Creator. */ diff --git a/server/workspace.js b/server/workspace.js index 46db0f46..062faf7c 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -2267,7 +2267,7 @@ export function scoutsVon(managerId) { /** Wessen Leads darf diese Person sehen? Gibt die Personennummern * zurueck, deren Pipeline sichtbar ist -- der eigene immer dabei. * Ein Scout sieht nur sich, ein Manager sich und seine Scouts. */ -export function pipelineIds(person) { +function pipelineIdsRoh(person) { if (!person) return []; if (person.rolle !== "manager") return [person.id]; return [...new Set([person.id, ...scoutsVon(person.id)])]; @@ -2319,7 +2319,7 @@ export function scoutZuteilungSetzen(scoutId, managerId, akteur = null) { /** Die Creator, deren Daten diese Person sehen darf. * null = alle (nur DogFather). */ -export function sichtbareCreatorIds(person) { +function sichtbareCreatorIdsRoh(person) { if (!person) return []; if (siehtAlles(person)) return null; if (person.rolle === "creator") return [person.id]; @@ -2332,7 +2332,85 @@ export function sichtbareCreatorIds(person) { * Enthält immer die Person selbst: Wer sich in einer Auswahl nicht * findet, kann sich nichts selbst zuweisen. Bei einem Manager kommen * seine Scouts dazu -- er führt sie, er muss sie eintragen können. */ -export function sichtbarePersonenIds(person) { +/* ===================================================================== + WEN DARF DIESE PERSON NICHT SEHEN? (09.09.2026) + + Wunsch Filipe, ausdruecklich und dringlich: *"und noch gaaaaaanz + wichtig keiner soll vanvan sehen ausser ich, ueberall soll keiner + vanvan sehen ausser dogfather."* + + Es gab davon bisher nur eine Haelfte: In der Personenliste wurde der + zweite Admin-Zugang fuer Spicy Media ausgeblendet (Wunsch vom + 31.08.). Ueberall sonst -- Chat, Kalender, Aufgaben, Dateien, Suche + -- war er sichtbar, und `sichtbarePersonenIds` hat ihn sogar + ausdruecklich JEDER Rolle gezeigt, weil sie alle Admins einsammelt + ("DogFather ist fuer alle sichtbar"). + + WORAN DIE BEIDEN AUSEINANDERGEHALTEN WERDEN: In der Datenbank tragen + beide die Rolle `admin`, es gibt kein unterscheidendes Feld. Der + Unterschied, den es wirklich gibt, ist das Alter -- DogFather ist + der erste Zugang des Hauses. Deshalb gilt: der Admin mit der + KLEINSTEN Nummer ist DogFather, alle weiteren Admin-Zugaenge sind + verborgen. + + DREI AUSNAHMEN, und nur diese drei: + * DogFather selbst sieht alle. + * Ein verborgener Zugang sieht sich selbst (sonst faende er sein + eigenes Profil nicht). + * Gibt es nur EINEN Admin, ist nichts zu verbergen. + + DIE SCHWACHSTELLE STEHT HIER, damit sie niemand suchen muss: Wuerde + Zugang 1 geloescht, rueckte der naechste Admin nach und waere + ploetzlich sichtbar. Sollte das eintreten, gehoert ein + ausdrueckliches Merkmal in die Tabelle (`haupt`-Feld). Solange + DogFather der erste Zugang ist, ist die Nummer die ehrlichste + Antwort ohne Datenbankumbau -- und sie haengt nicht am NAMEN, der + sich aendern kann. + + WARUM AN DIESER STELLE UND NICHT IN DEN ABFRAGEN: Es gibt 23 + Abfragen allein in workspace-personen.js, die Personen lesen. Eine + Regel, die man an 23 Stellen wiederholt, ist 23 Gelegenheiten, sie + zu vergessen -- und beim Vergessen faellt hier niemand auf die Nase, + sondern es sieht jemand etwas, das er nicht sehen soll. Deshalb + sitzt sie in den SECHS Listenfunktionen, durch die alles laeuft. + ===================================================================== */ +export function verborgeneIds(person) { + try { + const admins = db().prepare( + "SELECT id FROM personen WHERE rolle = 'admin' ORDER BY id").all().map((z) => z.id); + if (admins.length < 2) return []; + const versteckt = admins.slice(1); + if (!person) return versteckt; + if (person.id === admins[0]) return []; /* DogFather sieht alle */ + if (versteckt.includes(person.id)) return []; /* sich selbst sieht man */ + return versteckt; + } catch { + /* Ohne Datenbank lieber nichts verbergen als abstuerzen -- diese + Funktion darf keine Seite lahmlegen. Sie laeuft in jedem + Listenaufruf. */ + return []; + } +} + +/** Dieselbe Regel als Filter auf eine fertige Liste. */ +export function ohneVerborgene(ids, person) { + const weg = verborgeneIds(person); + if (!weg.length) return ids; + if (ids === null) { + /* `null` heisst bisher "sieht alles". Sobald es etwas zu verbergen + gibt, darf das nicht mehr gelten -- die Liste wird deshalb + AUSGESCHRIEBEN. Das ist strenger, nicht lockerer, und die + Aufrufer bauen daraus ohnehin ein `IN (...)`. */ + try { + return db().prepare( + `SELECT id FROM personen WHERE id NOT IN (${weg.map(() => "?").join(",")})`) + .all(...weg).map((z) => z.id); + } catch { return null; } + } + return ids.filter((i) => !weg.includes(i)); +} + +function sichtbarePersonenIdsRoh(person) { if (!person) return []; if (siehtAlles(person)) return null; const creator = sichtbareCreatorIds(person) || []; @@ -2369,7 +2447,7 @@ export function sichtbarePersonenIds(person) { * gebraucht wurde: Ein Creator soll die Daten seines Scouts nicht * sehen. Für eine EINLADUNG ist die Frage aber eine andere -- siehe * einladbareIds(). */ -export function betreuerIds(person) { +function betreuerIdsRoh(person) { if (!person) return []; try { const d = db(); @@ -2417,7 +2495,7 @@ export function betreuerIds(person) { * Deshalb ist die weitere Menge hier vertretbar. * * null = alle (nur DogFather), wie überall in dieser Datei. */ -export function einladbareIds(person) { +function einladbareIdsRoh(person) { if (!person) return []; if (siehtAlles(person)) return null; const sichtbar = sichtbarePersonenIds(person) || []; @@ -2450,7 +2528,7 @@ export function einladbareIds(person) { * anderen Creator weiterhin nicht sehen. * * null = alle (nur DogFather). */ -export function schreibbareIds(person) { +function schreibbareIdsRoh(person) { if (!person) return []; if (siehtAlles(person)) return null; @@ -2902,3 +2980,28 @@ export function protokollLesen(anzahl = 20) { "SELECT zeitpunkt, rolle, aktion, detail, ip FROM protokoll ORDER BY id DESC LIMIT ?" ).all(anzahl); } + +/* ===================================================================== + DER MANTEL UM DIE SECHS LISTENFUNKTIONEN (09.09.2026) + + Jede von ihnen hat mehrere Rueckgabewege -- `sichtbarePersonenIds` + allein drei. Die Verbergungsregel an jedem einzelnen anzubringen + waere achtzehn Gelegenheiten, einen zu vergessen; und wer hier einen + vergisst, merkt es nicht an einem Fehler, sondern daran, dass jemand + etwas sieht, das er nicht sehen soll. + + Deshalb bleiben die Funktionen unveraendert (jetzt mit `Roh` im + Namen) und werden EINMAL ummantelt. Der Mantel ist die einzige + Stelle, an der die Regel steht -- und er kann keinen Rueckgabeweg + uebersehen, weil er hinter allen sitzt. + + Die Namen nach aussen bleiben gleich: Kein Aufrufer muss geaendert + werden, und niemand kann versehentlich die ungefilterte Fassung + benutzen -- die `Roh`-Funktionen werden nicht exportiert. + ===================================================================== */ +export const sichtbareCreatorIds = (person) => ohneVerborgene(sichtbareCreatorIdsRoh(person), person); +export const sichtbarePersonenIds = (person) => ohneVerborgene(sichtbarePersonenIdsRoh(person), person); +export const betreuerIds = (person) => ohneVerborgene(betreuerIdsRoh(person), person); +export const einladbareIds = (person) => ohneVerborgene(einladbareIdsRoh(person), person); +export const schreibbareIds = (person) => ohneVerborgene(schreibbareIdsRoh(person), person); +export const pipelineIds = (person) => ohneVerborgene(pipelineIdsRoh(person), person); diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css index 8c549b93..d3b6f3ed 100644 --- a/workspace/assets/css/gate.css +++ b/workspace/assets/css/gate.css @@ -127,9 +127,25 @@ `.rolle[data-rolle="admin"]` weiter unten). Kein Blinken -- die Anmeldeseite ist das Erste, was jemand sieht, und augenschonend bleibt Vorgabe. */ - --dogi-haupt: #d8e0ec; + /* SILBER MIT BABYBLAU GEMISCHT (09.09.2026, Wunsch Filipe: "ich + will dass diese farbe gemischt wird mit babyblau, es soll richtig + geil sein"). + + Das reine Silber von gestern (#d8e0ec) war kuehl, aber neutral -- + es haette zu jeder Marke gepasst. Jetzt traegt es den Babyblau- + Stich von DogFather selbst: #c7dcf4 ist dieselbe Helligkeit, + nur mit einer Blaurichtung. Nachgerechnet bleibt der Abstand zur + naechsten Rolle bei 0,1790 -- immer noch deutlich weiter als das + frueher benutzte Babyblau (0,1349) und weit ueber der Grenze im + Satz (0,0973). Kontrast 13,66:1. + + Gemischt ist es nicht nur im Farbwert, sondern SICHTBAR: Die + Schiene links laeuft von Silber nach Babyblau, und der Glanz auf + der Zeile traegt beide Toene. Eine Mischung, die man nur im + Hexwert findet, ist keine. */ + --dogi-haupt: #c7dcf4; --dogi-zweit: #7ec8f2; - --dogi-tief: #38485c; + --dogi-tief: #35506b; /* MANAGER — lila, bleibt (Wunsch: "manager bleibt lila"). */ --manager-haupt: #8a76ff; @@ -390,11 +406,78 @@ body.gate { /* Die Frage sitzt als Kopfzeile IN der Kachel, nicht darueber -- sonst waere die Kachel eine Liste ohne Titel und der Titel ein Satz ohne Bezug. */ +/* ===================================================================== + DIE FRAGE UEBER DEN ROLLEN (09.09.2026, screen1 Punkt 4) + + Filipe: "dieser text soll bitte bissl geiler sein." + + "Womit meldest du dich an?" ist der einzige Satz auf dieser Seite, + der eine FRAGE stellt -- alles andere ist Marke, Titel oder + Antwort. Er stand als kleine graue Grossbuchstabenzeile da, wie ein + Feldname in einem Formular. + + DREI SACHEN, DIESELBE SPRACHE WIE DIE TYPENSCHILDER IM WORKSPACE: + + 1. GEBUERSTETES METALL statt Grau -- oben hell, in der Mitte + dunkler, unten wieder hell. Ein Verlauf von hell nach dunkel + waere eine Faerbung; erst der WECHSEL liest sich als Material. + 2. EIN ANSCHLAG LINKS aus drei Kerben, in den beiden Hausfarben + (rot oben, babyblau unten) -- dieselbe Richtung wie die + Fassung der Kachel und wie der Schriftzug im Kopf. + 3. EINE AUSLAUFENDE LINIE nach rechts, die die Zeile gliedert, + ohne sie zu zerschneiden. + + DER RUECKFALL IST VOLLWERTIG: `color` bleibt gesetzt, den Verlauf + schaltet erst ein `@supports` dazu. Faellt `background-clip: text` + aus, steht dort heller Text -- nicht unsichtbarer. Das ist die + erste Seite, die jemand sieht; sie darf an keiner Technik haengen. + ===================================================================== */ .rollen .feldschild { + position: relative; + display: flex; align-items: center; gap: 11px; margin: 0; - padding: 12px 16px 10px; + padding: 13px 34px 11px 15px; border-bottom: 1px solid #05070c; box-shadow: 0 1px 0 rgba(255, 255, 255, .045); + color: #e6eef8; + letter-spacing: .1em; +} +/* Der Anschlag: drei Kerben, oben rot, unten babyblau. */ +.rollen .feldschild::before { + content: ""; + width: 13px; height: 13px; flex: none; + background: + repeating-linear-gradient(180deg, + currentColor 0 2px, transparent 2px 5px); + color: transparent; + background-image: repeating-linear-gradient(180deg, + #ff5f6d 0 2px, transparent 2px 5px); + -webkit-mask: linear-gradient(180deg, #000 0 45%, rgba(0, 0, 0, .55) 100%); + mask: linear-gradient(180deg, #000 0 45%, rgba(0, 0, 0, .55) 100%); +} +/* Die untere Haelfte des Anschlags in Babyblau -- zwei Ebenen, damit + die Kerben oben rot und unten blau sind, ohne ein zweites Element. */ +.rollen .feldschild::before { + background-image: + linear-gradient(180deg, transparent 0 46%, rgba(143, 216, 255, .95) 46%), + repeating-linear-gradient(180deg, #ff5f6d 0 2px, transparent 2px 5px); + background-blend-mode: multiply, normal; +} +.rollen .feldschild::after { + content: ""; + position: absolute; right: 12px; top: 50%; + width: 16px; height: 1px; + transform: translateY(-.5px); + background: linear-gradient(90deg, rgba(143, 216, 255, .55), transparent); +} +@supports ((-webkit-background-clip: text) or (background-clip: text)) { + .rollen .feldschild { + background: linear-gradient(178deg, + #ffffff 0%, #dfeaf6 32%, #93a8bf 60%, #d7e6f5 86%, #ffffff 100%) text; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } } .rolle { @@ -2436,16 +2519,32 @@ body.gate { .rolle[data-rolle="admin"] { position: relative; } +/* DIE SCHIENE LAEUFT VON SILBER NACH BABYBLAU (09.09.2026). + + Die Rollenschiene links wird sonst aus einer einzigen Farbe gebaut + (`--rf`/`--rton`). Fuer DogFather ist das jetzt zu wenig: Seine + Farbe IST eine Mischung, und die sieht man nur, wenn beide Toene + nebeneinander liegen. Oben Silber, unten Babyblau -- dieselbe + Richtung wie das Licht auf der Kachel. */ +.tafel .rolle[data-rolle="admin"]::before, +.rolle[data-rolle="admin"]::before { + background: linear-gradient(180deg, + #eef4fb 0%, var(--dogi-haupt) 42%, var(--dogi-zweit) 100%) !important; +} + .rolle[data-rolle="admin"]::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: - /* Das Spitzlicht -- schmal, schraeg, und weit vor der Mitte. */ + /* Das Spitzlicht -- schmal, schraeg, und weit vor der Mitte. + Es traegt jetzt beide Toene: weiss am Kamm, babyblau im + Auslauf. So liest man die Mischung auch dort, wo keine Schiene + ist. */ linear-gradient(104deg, transparent 8%, - rgba(255, 255, 255, .11) 17%, - rgba(255, 255, 255, .03) 24%, + rgba(255, 255, 255, .12) 16%, + rgba(160, 210, 245, .07) 22%, transparent 32%), /* Drei Lichtpunkte, unregelmaessig gesetzt. Regelmaessig waeren sie ein Muster; unregelmaessig sind sie Zufall auf einer Oberflaeche. */ diff --git a/workspace/assets/css/module.css b/workspace/assets/css/module.css index 4525ef3d..80795880 100644 --- a/workspace/assets/css/module.css +++ b/workspace/assets/css/module.css @@ -109,7 +109,7 @@ } /* MODULLISTE */ -:is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, fieldset.gruppe) { +:is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, .liste-kachel, fieldset.gruppe) { position: relative; clip-path: polygon(var(--fase) 0, 100% 0, 100% 100%, 0 100%, 0 var(--fase)); border-radius: 0; @@ -137,7 +137,7 @@ was übrig bleibt, ist eine Linie, die exakt der Form folgt, Schräge inklusive. Ein `border` könnte das nicht: Er folgt dem Rechteck. */ /* MODULLISTE */ -:is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, fieldset.gruppe)::before { +:is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, .liste-kachel, fieldset.gruppe)::before { content: ""; position: absolute; inset: 0; pointer-events: none; @@ -160,7 +160,7 @@ /* RASTER UND ECKWINKEL. Beides auf ::after -- ein drittes Element gibt es nicht, und beides ist reine Optik ohne Klicks. */ /* MODULLISTE */ -:is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, fieldset.gruppe)::after { +:is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, .liste-kachel, fieldset.gruppe)::after { content: ""; position: absolute; inset: 0; pointer-events: none; @@ -244,7 +244,7 @@ /* Dieselbe Form, kleiner -- keine zweite Gestaltung. */ :root { --fase: 12px; } /* MODULLISTE */ - :is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, fieldset.gruppe) { + :is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, .liste-kachel, fieldset.gruppe) { filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .7)); } } @@ -258,15 +258,15 @@ /* Im Kontrastmodus zählt die Kante, und die muss ein echter `border` sein. Die Schräge bleibt: Sie ist Geometrie, keine Farbe. */ /* MODULLISTE */ - :is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, fieldset.gruppe) { + :is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, .liste-kachel, fieldset.gruppe) { filter: none; border: 1px solid CanvasText; } /* MODULLISTE */ - :is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, fieldset.gruppe)::after { + :is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, .liste-kachel, fieldset.gruppe)::after { display: none; } /* MODULLISTE */ - :is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, fieldset.gruppe)::before { + :is(.eintrag-karte, .call, .chat, .datei, .ablage, .zahlkarte, .regel, .zustand, .ki-kasten, .spalte, .balance, .saeulen-kasten, .neu, .call-raum, .creator, .summe, .pdf, .pdf-ablage, .suchleiste, .person, .kk, .fortschritt, .punkt, .dran__punkt, .unterweisung, .warnkarte, .kachel, .tagesblick, .leer-hinweis, .team-karte, .zahl, .k-listentag, .import-vorschau, .code-fenster, .schulung__neu, .gruppe[data-gruppe], .kopf-zeile, .k-kopf, .steckbrief, .k-anlasskarte, .k-anlasskachel, .k-raster, .entscheidung, .liste-kachel, fieldset.gruppe)::before { display: none; } } diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index 6c2858b0..745148d4 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -408,3 +408,57 @@ body[data-nur-lesen="ja"] .person__knoepfe, body[data-nur-lesen="ja"] .person__code, body[data-nur-lesen="ja"] .zuteilung, body[data-nur-lesen="ja"] select { display: none; } + +/* ===================================================================== + DIE PERSONENLISTE IST EINE KACHEL (screen1 Punkt 3, 09.09.2026) + + Filipe: "das soll alles in einer großen kachel sein." + + Die fünf Rollengruppen (Spicy Media, DogFather, Manager, Scout, + Creator) standen als fünf lose Abschnitte frei auf dem + Hintergrundbild. Es ist aber EINE Liste mit fünf Abschnitten -- + dieselbe Überlegung wie bei den Rollen auf der Anmeldeseite und den + Anlässen im Kalender. + + DIE FORM KOMMT AUS DER MODULLISTE (module.css), die FLÄCHE steht + hier. Das ist die Aufteilung, die ich in dieser Woche dreimal falsch + herum hatte: Die Liste gibt Fase, Kantenlicht und Eckwinkel; den + Hintergrund bringt jedes Bauteil selbst mit. Ohne ihn wäre die + Kachel eine Fassung ohne Füllung, also ein Loch. + + DIE FUGE MACHT DIE KACHEL: Ein Spalt, durch den der Untergrund + scheint, TRENNT die Abschnitte; eine dunkle Fuge VERBINDET sie, weil + sie zum selben Körper gehört. + + DUNKLER ALS DIE KARTEN DARIN -- genau wie bei der Anlasskachel im + Kalender. Eine Vitrine ist dunkler als das, was darin steht; sonst + sieht man die Vitrine statt des Inhalts. + ===================================================================== */ +.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)); +} +/* 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); +} +.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. */ +.liste-kachel > .gruppe > .gruppe__kopf { + width: calc(100% + 32px); + margin-left: -16px; + padding: 13px 16px 11px; +} +/* 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%); +} diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index 70fba16f..fa533ecc 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -969,8 +969,32 @@ body.start > main { position: relative; z-index: 1; } es nicht liest, sondern nur spürt -- eine Kachel soll ihren Namen tragen, nicht ihr Zeichen. */ .kachel__wasserzeichen { - position: absolute; right: -14px; bottom: -26px; - width: 156px; height: 156px; + /* GROESSE AUS DER KACHEL, NICHT AUS EINER ZAHL (09.09.2026). + + Filipe: "dieses symbol in der kacheln von Dashboard soll viel + groesser sein und nicht so abgecuttet sondern gut zu sehen sein." + + Nachgemessen war es auf der Dashboard-Kachel zu 50 Prozent + abgeschnitten -- und zwar auf DREI Seiten: 36 px ueber dem oberen + Rand, 44 px rechts, 60 px unten. Der Grund: 220 px Zeichen auf + einer 152 px hohen Kachel. Was die Kachel nicht fassen kann, + schneidet sie ab, und ein Zeichen, das oben abgeschnitten ist, + sieht nach einem Fehler aus. + + Jetzt haengt die Groesse an der KACHELHOEHE und ist so bemessen, + dass sie samt Drehung vollstaendig hineinpasst: Ein um 8 Grad + gedrehtes Quadrat der Seite S braucht S x 1,129 Platz. Bei + `min(118px, 100% - 34px)` bleibt auf jeder Kachelhoehe Rand + uebrig. + + SICHTBAR GROESSER WIRD ES TROTZDEM: vorher 133 px, davon 63 + Prozent im Bild (rund 11 200 Bildpunkte) -- jetzt die volle + Flaeche bei 0,14 statt 0,085 Deckkraft. Mehr Flaeche UND mehr + Farbe, ohne dass etwas abgeschnitten ist. */ + position: absolute; right: 11px; bottom: 9px; + width: min(118px, calc(100% - 34px)); + height: min(118px, calc(100% - 34px)); + aspect-ratio: 1; pointer-events: none; z-index: 0; /* .07 WAR EIN WERT AUS DER ZEIT, ALS DAS ZEICHEN GRAU WAR (07.09.2026). Ein graues Zeichen bei sieben Prozent auf dunklem @@ -1036,8 +1060,12 @@ body.start > main { position: relative; z-index: 1; } 220 px auf 152 px Kachelhoehe: knapp anderthalbmal so hoch wie die Kachel, unten und rechts angeschnitten, aber mit dem Grossteil im Bild. Gegen die 158 px vorher ist das rund 40 % mehr Flaeche. */ - width: 220px; height: 220px; - right: -30px; bottom: -46px; + /* Auf der grossen Kachel darf es groesser sein -- aber ebenfalls + GANZ im Bild. 220 px auf 152 px Hoehe waren zu 50 Prozent + abgeschnitten; die Kachel ist breiter, nicht hoeher. */ + width: min(132px, calc(100% - 30px)); + height: min(132px, calc(100% - 30px)); + right: 13px; bottom: 10px; /* DIE DECKKRAFT BLEIBT BEI .14 wie bei den kleinen Kacheln. Ich hatte sie auf .17 gehoben ("auf grosser Flaeche wirkt dieselbe @@ -4832,15 +4860,19 @@ body[data-fremde-sicht="ja"]::after { /* Das Wasserzeichen füllt die untere Hälfte -- es ist bei sechs Karten nebeneinander das, was jede von ihnen unterscheidbar macht. */ -.kachel__wasserzeichen { - right: -20px; bottom: -20px; - width: 118px; height: 118px; - opacity: .085; -} -.kachel:hover .kachel__wasserzeichen { - opacity: .15; - transform: rotate(-8deg) translate(-3px, -3px) scale(1.05); -} +/* HIER STAND EINE ZWEITE FASSUNG DES WASSERZEICHENS (entfernt am + 09.09.2026, screen1 Punkt 2). + + Sie setzte 118 px bei 0,085 Deckkraft -- rund 3600 Zeilen NACH der + sorgfaeltig begruendeten Fassung weiter oben (156 px bei 0,14) und + mit derselben Spezifitaet. Also gewann sie, und die ganze + Begruendung dort oben war wirkungslos. Nachgemessen kam am Ende ein + 133 px grosses Zeichen bei 8,5 Prozent heraus, von dem 63 Prozent + sichtbar waren. + + Genau dieselbe Sorte Fehler wie beim Wasserzeichen am 08.09. (zwei + Groessenangaben 3600 Zeilen auseinander) -- ich habe damals nur die + eine gefunden. Jetzt gibt es EINE Fassung, und die steht oben. */ /* ---------- Die große Kachel ----------------------------------------- */ /* Sie behält das Querformat von früher. Dadurch hebt sie sich WIRKLICH diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index d716e8d3..9ea74f49 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -14,10 +14,10 @@ - - - - + + + + @@ -217,10 +217,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 2242c4f5..cd4133b4 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 2d6534a3..4a2cf827 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 dc529ccb..c24975c8 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 9bcbf379..7589cd76 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -14,17 +14,17 @@ - - + + - - - + + + @@ -199,10 +199,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index fe9f2056..cf988a41 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 f7536b1a..a04216dd 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 6602d244..387dafa5 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 ed49ffe3..c8676a02 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 e45768a6..d0da0fb8 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 e3e9317d..f6c8c2ff 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -304,7 +304,18 @@ -

Personen werden geladen …

+ +

Personen werden geladen …

@@ -324,10 +335,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 73284a62..ba0d26f5 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 2dc2ae28..bdf3e6ba 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 75eb849a..7130933d 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 d97cac65..af5e1ac5 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -14,12 +14,12 @@ - - + + - - + + @@ -591,10 +591,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 18701335..e014095d 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 a5197a5e..985df3c0 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 607139b2..4b39d6f6 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 9a90b634..722f8bc9 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -183,10 +183,10 @@ - - - - - + + + + +