diff --git a/server/crew-adresse.js b/server/crew-adresse.js index 3450782d..83ad4a43 100644 --- a/server/crew-adresse.js +++ b/server/crew-adresse.js @@ -153,7 +153,35 @@ export function sitzungPasstZurAdresse(rolle, hostKopfzeile) { * Serverumbau lautlos weg. */ export function crewWeiche(req, res, next) { - if (!istCrewAdresse(req.get("host"))) return next(); + if (!istCrewAdresse(req.get("host"))) { + /* DIE ANDERE RICHTUNG -- und sie war ein echtes Loch. + + Die zweite Zugangswand liegt als Datei im selben Ordner wie alles + andere und wird von express.static ausgeliefert. Ohne diese Zeile + kann sie JEDER unter ihrem Dateinamen aufrufen, auch auf + workspace.dogfather-universe.com -- und saehe dort eine + Anmeldeseite, die es dort gar nicht geben soll. + + Gefunden hat das nicht ein Gedanke, sondern eine Pruefung: Der + Wortleck-Test durchsucht ALLE ausgelieferten Dateien, und dass er + diese eine ueberhaupt las, war die Antwort auf eine Frage, die + ich mir nicht gestellt hatte -- naemlich wer sie sonst noch + abrufen kann. + + 404 UND KEINE WEITERLEITUNG: "Gibt es nicht" ist die Antwort, die + nichts verraet. Eine Weiterleitung wuerde sagen, dass es die + Datei gibt, nur woanders. + + NUR AUF DEN DREI ECHTEN ADRESSEN, nicht "ueberall ausser crew": + Sonst waere sie auch auf localhost gesperrt, und die Pruefung, + die die Wand im Browser ansieht, koennte sie nicht mehr oeffnen + -- sie wuerde gruen bleiben und nichts mehr messen. */ + if (istOhneModiAdresse(req.get("host")) + && req.path.toLowerCase().replace(/\/{2,}/g, "/") === "/workspace/crew-index.html") { + return res.status(404).type("html").send("Nicht gefunden."); + } + return next(); + } res.setHeader("X-Robots-Tag", "noindex, nofollow"); @@ -179,6 +207,32 @@ export function crewWeiche(req, res, next) { return next(); } + /* DIE ZUGANGSWAND GEHOERT DOGI UND DEN MODIS (10.09.2026). + + Filipe: "alles soll auf dogfather und die modis perfektionniert + werden auf der neuen modi seite." + + Bis dahin stand hier die gewohnte Wand: Spicy-Media-Logo, "Creator + Workspace", fuenf Rollenkacheln -- fuer einen Modi war davon nichts + richtig. Jetzt liefert crew. eine eigene Seite mit eigener Buehne, + eigenem Namen und ohne Kachelreihe. + + DAS KEHRT EINE FRUEHERE ENTSCHEIDUNG UM, und das gehoert + hierhergeschrieben: Vorher war die Wand absichtlich identisch -- + wer die Adresse errat, sollte etwas Vertrautes sehen und nichts + ueber Modis erfahren. Filipe hat anders entschieden. Der Tausch + ist damit bewusst: Ein Ratender liest jetzt "Team Dogi". Alles + andere bleibt, wie es war -- sein Code funktioniert dort nicht. + + ZWEI ADRESSEN, EINE DATEI: `/workspace/` und + `/workspace/index.html` fuehren beide zur Wand, also muessen beide + umgebogen werden. Die zweite zu vergessen waere nicht aufgefallen, + denn im Alltag tippt sie niemand. */ + if (req.path === "/workspace/" || req.path === "/workspace/index.html") { + req.url = "/workspace/crew-index.html"; + return next(); + } + if (istWorkspacePfad(req.path) || req.path.startsWith("/assets/")) return next(); return res.redirect(302, "/workspace/"); } diff --git a/server/pruef-crew-adresse.mjs b/server/pruef-crew-adresse.mjs index d42e0960..051d524e 100644 --- a/server/pruef-crew-adresse.mjs +++ b/server/pruef-crew-adresse.mjs @@ -303,6 +303,121 @@ for (const s of mc.icons || []) { ok((mc.icons || []).length > 0 && symboleDa === (mc.icons || []).length, `crew.: alle ${symboleDa} Symbole werden auch ausgeliefert`); +/* ===================================================================== + DIE ZUGANGSWAND + + Filipe am 10.09.2026: "alles soll auf dogfather und die modis + perfektionniert werden auf der neuen modi seite." Bis dahin stand auf + crew. die gewohnte Wand -- Spicy-Media-Logo, "Creator Workspace", + fuenf Rollenkacheln. Fuer einen Modi war davon nichts richtig. + + Zwei Dinge werden hier gemessen, und das zweite ist das wichtigere: + dass die ALTE Wand unangetastet blieb. + ===================================================================== */ +console.log(""); +console.log("--- Zwei Waende ---"); + +const wandCrew = await hole("/workspace/", { host: CREW }); +const wandWs = await hole("/workspace/", { host: WS }); + +ok(wandCrew.status === 200, "crew.: die Zugangswand kommt (keine Weiterleitungsschleife)"); +ok(wandWs.status === 200, "workspace.: die Zugangswand kommt"); + +ok(/Team Dogi|Team Dogi/.test(wandCrew.text), "crew.: dort steht Team Dogi"); +ok(/DogFather/.test(wandCrew.text), "crew.: und DogFather daneben"); +ok(!/Spicy/i.test(wandCrew.text), "crew.: von Spicy Media steht dort nichts mehr"); +ok(!/Creator/i.test(wandCrew.text), "crew.: und vom Creator Workspace auch nicht"); +ok(/die Modis/.test(wandCrew.text), "crew.: der Satz nennt DogFather und die Modis"); + +/* Die Kachelreihe ist weg -- auf einer Adresse mit genau einer Sorte + Zugang waere sie eine Bedienhuerde, keine Auswahl. */ +const kachelnCrew = (wandCrew.text.match(/class="rolle"/g) || []).length; +const kachelnWs = (wandWs.text.match(/class="rolle"/g) || []).length; +ok(kachelnCrew === 0, `crew.: keine Rollenkacheln (gezaehlt: ${kachelnCrew})`); +ok(kachelnWs === 5, `workspace.: dort weiterhin fuenf (gezaehlt: ${kachelnWs})`); +ok(/SpicyMedia/.test(wandWs.text), "workspace.: die alte Wand ist unveraendert"); + +/* Auch der ausgeschriebene Weg. Ihn zu vergessen waere nicht + aufgefallen -- im Alltag tippt ihn niemand. */ +const wandCrewLang = await hole("/workspace/index.html", { host: CREW }); +ok(wandCrewLang.status === 200 && /Team Dogi|Team Dogi/.test(wandCrewLang.text), + "crew.: auch /workspace/index.html fuehrt zur neuen Wand"); + +/* --------------------------------------------------------------------- + DIE ZWEITE WAND GIBT ES NUR AUF crew. + + Sie liegt als Datei im selben Ordner wie alles andere. Ohne Sperre + koennte sie jeder unter ihrem Dateinamen aufrufen -- auch auf + workspace. -- und saehe dort eine Anmeldeseite, die es dort nicht + geben soll. + ------------------------------------------------------------------ */ +/* ZWEI NUMMERN, EINE AUSSAGE. Auf workspace. antwortet die Sperre mit + 404 "gibt es nicht". Auf der Hauptseite und www. kommt schon der + Umzugs-Zwischenschritt vom 06.09.2026 davor und beantwortet den + GANZEN Pfad /workspace mit 410 "gab es, ist absichtlich weg". + + Erwartet werden deshalb beide -- was zaehlt, ist: kein Inhalt und + keine 200. Auf eine der zwei Nummern zu bestehen hiesse, eine + fremde, aeltere Entscheidung nachtraeglich zu einem Fehler zu + erklaeren. */ +for (const [name, host] of [["workspace.", WS], ["die Hauptseite", HAUPT], ["www.", "www." + HAUPT]]) { + const a = await hole("/workspace/crew-index.html", { host }); + ok(a.status === 404 || a.status === 410, + `${name}: die zweite Wand gibt es dort nicht (${a.status})`); + ok(!/Team Dogi/.test(a.text), `${name}: und ihr Inhalt kommt auch nicht durch`); +} +/* Doppelte Schraegstriche haben diese Schranke im Haus schon einmal + umgangen (04.09.2026, //workspace/start.html kam mit 200). */ +const schraeg = await hole("/workspace//crew-index.html", { host: WS }); +ok(schraeg.status === 404 || !/Team Dogi/.test(schraeg.text), + `workspace.: auch mit doppeltem Schraegstrich nicht (${schraeg.status})`); + +/* GEGENPROBE: Auf crew. und auf den Pruefadressen bleibt sie erreichbar + -- sonst haette ich sie fuer alle gesperrt, auch fuer die Pruefung, + die sie im Browser ansieht. Die waere dann gruen und wertlos. */ +const dortSchon = await hole("/workspace/crew-index.html", { host: CREW }); +ok(dortSchon.status === 200, "crew.: dort gibt es sie natuerlich"); +const lokalSchon = await hole("/workspace/crew-index.html", { host: LOKAL }); +ok(lokalSchon.status === 200, "127.0.0.1: und auf den Pruefadressen auch"); + +/* --------------------------------------------------------------------- + WAS gate.js AUS DEM HTML BRAUCHT -- auf BEIDEN Seiten. + + Es gibt jetzt zwei Wanddateien und ein gemeinsames Skript. Wer nur + eine anfasst, macht die andere kaputt, und zwar lautlos: Ein + fehlendes Element ist im Browser ein `null`, der Fehler steht in der + Konsole und sonst nirgends. Deshalb wird beides gegen dieselbe Liste + gehalten. + ------------------------------------------------------------------ */ +const GEBRAUCHT = ["formular", "code", "auge", "fehler", "hinweis", "knopf", "fuer"]; +for (const [name, wand] of [["crew.", wandCrew], ["workspace.", wandWs]]) { + const fehlend = GEBRAUCHT.filter((id) => !wand.text.includes(`id="${id}"`)); + ok(fehlend.length === 0, + `${name} die Wand hat alle ${GEBRAUCHT.length} Elemente, die gate.js sucht` + + (fehlend.length ? ` -- es fehlen: ${fehlend.join(", ")}` : "")); + ok(wand.text.includes("knopf__text") && wand.text.includes("buehne__bild"), + `${name} auch die beiden Klassen, die gate.js anspricht`); +} + +/* --------------------------------------------------------------------- + Die Buehne. + ------------------------------------------------------------------ */ +ok(/crew-gate-\d+\.webp/.test(wandCrew.text), "crew.: die eigene Buehne ist eingebunden"); +ok(!/buehne\/gate-\d+\.webp/.test(wandCrew.text), "crew.: die alte Buehne nicht mehr"); +ok(/buehne\/gate-\d+\.webp/.test(wandWs.text), "workspace.: dort weiterhin die alte"); +ok(/crew-32\.png/.test(wandCrew.text) && /crew-180\.png/.test(wandCrew.text), + "crew.: eigenes Zeichen im Reiter und auf dem Startbildschirm"); + +/* Ein Verweis beweist nicht, dass die Datei da ist. */ +const buehnen = [...new Set(wandCrew.text.match(/assets\/img\/buehne\/crew-gate-[a-z0-9-]+\.webp/g) || [])]; +let buehnenDa = 0; +for (const b of buehnen) { + const a = await hole("/workspace/" + b, { host: CREW }); + if (a.status === 200) buehnenDa++; +} +ok(buehnen.length >= 8 && buehnenDa === buehnen.length, + `crew.: alle ${buehnenDa} von ${buehnen.length} Buehnenbildern werden ausgeliefert`); + /* ===================================================================== DIE GEGENPROBE diff --git a/server/pruef-crew-wand-bild.mjs b/server/pruef-crew-wand-bild.mjs new file mode 100644 index 00000000..f61f4025 --- /dev/null +++ b/server/pruef-crew-wand-bild.mjs @@ -0,0 +1,297 @@ +/* ===================================================================== + Die Zugangswand der Modi-App ANSEHEN, nicht nur zaehlen. + + pruef-crew-adresse.mjs misst, was im HTML steht. Das beantwortet + nicht, ob die Karte in der gemalten Tafel sitzt, ob Dogi vom Text + verdeckt wird oder ob auf dem Handy etwas ueber den Rand haengt -- + und genau diese Fehler sieht man nur. + + Zwei Bildschirmfotos, ein Rechner und ein Handy, plus zwei Messungen, + die man dem Bild nicht ansieht: + + * Die Karte muss INNERHALB der gemalten Tafel liegen. gate.css + rechnet dafuer die Cover-Geometrie des Bildes nach; sitzt das + Motiv anders als das alte, laege sie daneben. Toleranz: zwei + Prozent der Tafelbreite. + * Kein Element darf ueber den rechten Rand hinausragen -- der + haeufigste Fehler auf schmalen Geraeten, und er faellt im + Bildschirmfoto erst auf, wenn man scrollt. + + Aufrufen mit: node server/pruef-crew-wand-bild.mjs + Die Bilder landen als server/pruef-crew-wand-*.png. + ===================================================================== */ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-crewbild-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4373"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-crew-wand-bild"); +await new Promise((r) => setTimeout(r, 700)); + +/* Die Bilder landen als pruef-*.png neben den anderen Pruefdateien. + + NICHT in einem eigenen Ordner: .gitignore kennt `server/pruef-*.png` + seit dem 22.08.2026, mit ausfuehrlicher Begruendung -- ein eigener + Ordner braeuchte eine zweite Regel fuer dieselbe Sache, und die + vergisst beim naechsten Mal jemand. Dann liegen Bildschirmfotos im + Repo und tauchen bei jedem Commit als Aenderung auf. */ +const HIER = dirname(fileURLToPath(import.meta.url)); + +let fehler = 0, geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +/* Die vier Zahlen aus gate.css -- die Tafel im Bild. */ +const TAFEL = { x: 0.5545, y: 0.1315, b: 0.3865, h: 0.6385 }; +const BILD = { b: 1647, h: 955 }; + +/* Die alte Wand laeuft in denselben Messungen mit. + + Ohne sie schriebe ich eine Regel fest, von der ich nicht weiss, ob + die vorhandene Seite sie ueberhaupt einhaelt -- und beim ersten + Fehlschlag waere unklar, ob die neue Seite schlecht ist oder die + Regel zu streng. Mit ihr ist die Antwort sofort da. */ +for (const geraet of [ + { name: "rechner", breite: 1440, hoehe: 900 }, + { name: "handy", breite: 412, hoehe: 915 }, + { name: "alt-rechner", breite: 1440, hoehe: 900, alt: true }, + { name: "alt-handy", breite: 412, hoehe: 915, alt: true }, +]) { + const ctx = await browser.newContext({ + viewport: { width: geraet.breite, height: geraet.hoehe }, + deviceScaleFactor: 1, + }); + const seite = await ctx.newPage(); + const meldungen = []; + seite.on("console", (m) => { if (m.type() === "error") meldungen.push(m.text()); }); + + const adresse = geraet.alt + ? "http://127.0.0.1:4373/workspace/index.html" + : "http://127.0.0.1:4373/workspace/crew-index.html"; + await seite.goto(adresse, { waitUntil: "networkidle" }); + await seite.waitForTimeout(400); + + const datei = join(HIER, `pruef-crew-wand-${geraet.name}.png`); + if (!geraet.alt) await seite.screenshot({ path: datei }); + console.log(`\n--- ${geraet.name} (${geraet.breite}x${geraet.hoehe}) -> ${datei}`); + + /* Kein Skriptfehler. Ohne diese Zeile faellt ein kaputtes gate.js + erst auf, wenn jemand den Knopf drueckt. */ + ok(meldungen.length === 0, + `${geraet.name}: keine Fehler im Skript` + (meldungen.length ? ` -- ${meldungen[0]}` : "")); + + /* Ist ueberhaupt die richtige Seite da? */ + const titel = await seite.title(); + const kacheln = await seite.locator(".rolle").count(); + if (geraet.alt) { + ok(kacheln === 5, `${geraet.name}: die alte Wand hat ihre fuenf Kacheln (${kacheln})`); + } else { + ok(/Team Dogi/.test(titel), `${geraet.name}: der Reiter heisst "${titel}"`); + ok(kacheln === 0, `${geraet.name}: keine Rollenkacheln (${kacheln})`); + } + + /* Die Karte muss sichtbar und bedienbar sein. */ + const karte = await seite.locator("#tafel").boundingBox(); + const feld = await seite.locator("#code").boundingBox(); + const knopf = await seite.locator("#knopf").boundingBox(); + ok(!!karte && karte.width > 200, `${geraet.name}: die Karte steht (${Math.round(karte?.width || 0)} px breit)`); + ok(!!feld && feld.height >= 40, `${geraet.name}: das Codefeld ist gross genug zum Tippen`); + ok(!!knopf && knopf.height >= 40, `${geraet.name}: der Anmeldeknopf ist gross genug`); + + /* HAENGT ETWAS HERAUS? Die Frage muss genauer gestellt werden. + + Erster Entwurf ging ueber ALLE Elemente und meldete zwei Treffer: + `buehne__bild` und `tafel-anker`. Beide sind ABSICHTLICH breiter + als das Fenster -- so arbeitet `object-fit: cover`, und der Anker + ist bloss eine Schablone ohne Klicks. Die Pruefung war rot, die + Seite in Ordnung. + + Gemeint sind zwei Dinge, und beide lassen sich sauber messen: + 1. Laesst sich die Seite seitlich schieben? Das ist der Fehler, + den man als Nutzer merkt. + 2. Liegt etwas BEDIENBARES ausserhalb? Das ist der Fehler, den + man nicht merkt -- man kommt nur nicht daran. */ + const ueberstand = await seite.evaluate(() => { + const w = document.documentElement; + const schiebt = w.scrollWidth - w.clientWidth; + let schlimmster = 0, wer = ""; + for (const e of document.querySelectorAll( + "a, button, input, textarea, select, label, h1, .unterzeile, .marke")) { + const r = e.getBoundingClientRect(); + if (r.width === 0 && r.height === 0) continue; + const ueber = Math.max(r.right - w.clientWidth, -r.left); + if (ueber > schlimmster) { schlimmster = ueber; wer = e.className || e.tagName; } + } + return { schiebt: Math.round(schiebt), schlimmster: Math.round(schlimmster), + wer: String(wer).slice(0, 40) }; + }); + ok(ueberstand.schiebt <= 1, + `${geraet.name}: die Seite laesst sich nicht seitlich schieben` + + (ueberstand.schiebt > 1 ? ` -- ${ueberstand.schiebt} px zu breit` : "")); + ok(ueberstand.schlimmster <= 2, + `${geraet.name}: alles Bedienbare liegt im Fenster` + + (ueberstand.schlimmster > 2 ? ` -- ${ueberstand.wer} um ${ueberstand.schlimmster} px` : "")); + + /* IST DIE SCHRIFT AUF DEM ANMELDEKNOPF LESBAR? + + Der Knopf traegt einen VERLAUF. Man kann seine Lesbarkeit deshalb + nicht aus einer einzelnen Farbe im Stylesheet ableiten -- die + hellste Stelle entscheidet, und die steht dort nirgends als Zahl. + Also wird das fertige Bild ausgemessen. + + Die Schrift ist weiss; weisse Bildpunkte sind damit die Schrift und + werden ausgenommen. Vom Rest zaehlt das obere Zehntel: die hellste + Stelle des Verlaufs, auf der noch Schrift liegen kann. + + 4,5 zu 1 ist die Grenze aus WCAG AA fuer normale Schrift. Der Knopf + ist zwar fett, aber mit rund 16 px unter den 18,66 px, ab denen + 3 zu 1 genuegen wuerde -- also gilt die strengere Zahl. */ + const knopfBild = await seite.locator("#knopf").screenshot(); + const kontrast = await seite.evaluate(async (b64) => { + const i = new Image(); i.src = "data:image/png;base64," + b64; await i.decode(); + const c = document.createElement("canvas"); + c.width = i.width; c.height = i.height; + const g = c.getContext("2d"); + g.drawImage(i, 0, 0); + const d = g.getImageData(0, 0, c.width, c.height).data; + const linear = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }; + const hell = []; + for (let k = 0; k < d.length; k += 4) { + const L = 0.2126 * linear(d[k]) + 0.7152 * linear(d[k + 1]) + 0.0722 * linear(d[k + 2]); + if (L > 0.6) continue; // das ist die Schrift selbst + hell.push(L); + } + if (!hell.length) return null; + hell.sort((x, y) => x - y); + const oben = hell[Math.floor(hell.length * 0.9)]; + return { hellste: oben, verhaeltnis: 1.05 / (oben + 0.05), punkte: hell.length }; + }, knopfBild.toString("base64")); + + ok(kontrast && kontrast.punkte > 500 && kontrast.verhaeltnis >= 4.5, + `${geraet.name}: weisse Schrift auf dem Knopf haelt ` + + `${kontrast ? kontrast.verhaeltnis.toFixed(2) : "?"} zu 1 (mindestens 4,5)`); + + /* NUR AM RECHNER: Sitzt die Karte in der gemalten Tafel? + Auf dem Handy liegt sie bewusst frei vor dem Bild -- dort gaebe es + nichts zu vergleichen, und eine Pruefung, die dort etwas behauptet, + behauptete es ohne Grundlage. */ + if (geraet.breite >= 1100 && geraet.breite / geraet.hoehe > 1.4) { + const soll = await seite.evaluate(([t, bild]) => { + /* Dieselbe Cover-Rechnung wie im CSS. */ + const vw = document.documentElement.clientWidth; + const vh = window.innerHeight; + const b = Math.max(vw, vh * bild.b / bild.h) * 1.03; + const h = Math.max(vh, vw * bild.h / bild.b) * 1.03; + const links = (vw - b) / 2, oben = (vh - h) / 2; + return { + x: links + b * t.x, y: oben + h * t.y, + b: b * t.b, h: h * t.h, + }; + }, [TAFEL, BILD]); + + const abx = Math.abs(karte.x - soll.x); + const aby = Math.abs(karte.y - soll.y); + const grenze = soll.b * 0.02; + ok(abx <= grenze && aby <= grenze, + `${geraet.name}: die Karte sitzt in der gemalten Tafel ` + + `(${Math.round(abx)} px seitlich, ${Math.round(aby)} px hoch, erlaubt ${Math.round(grenze)})`); + } + + await ctx.close(); +} + +/* ===================================================================== + NACH DER ANMELDUNG: heisst die Seite auch im Reiter anders? + + In jeder der 30 Seiten steht fest ... · Spicy & Dogi + und daneben das Zeichen des Workspace. Fuer einen Modi ist beides + falsch -- und beides sieht man als Erstes: im Browserreiter und, wenn + die Seite als App laeuft, im Fensterrahmen. + + kopf.js zieht das aus `ich.marke` nach. Das laeuft erst IM BROWSER, + laesst sich also nur hier messen und nicht in pruef-crew-adresse.mjs, + die rohes HTTP spricht. + + DIE GEGENPROBE STEHT DANEBEN: Dieselben zwei Werte werden fuer einen + Manager gelesen. Bei ihm MUESSEN sie unveraendert bleiben -- sonst + haette ich die Kopfleiste fuer alle umgebaut, und das waere niemandem + aufgefallen, weil es ja "auch geht". + ===================================================================== */ +console.log(""); +console.log("--- Nach der Anmeldung ---"); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const schluessel = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", schluessel, jetzt); + +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, + createHmac("sha256", schluessel).update(code).digest("hex"), jetzt); +} +anlegen("Marina", "modi", "CODE-MODI-0001"); +anlegen("Schulle", "manager", "CODE-MAN-0001"); + +const STEMPEL = new RegExp("\\?v=\\d+"); + +for (const wer of [ + { name: "Modi", rolle: "creator", code: "CODE-MODI-0001", eigen: true }, + { name: "Manager", rolle: "manager", code: "CODE-MAN-0001", eigen: false }, +]) { + const ctx = await browser.newContext({ viewport: { width: 1280, height: 860 } }); + const antwort = await ctx.request.post("http://127.0.0.1:4373/workspace/api/anmelden", { + data: { rolle: wer.rolle, code: wer.code }, + }); + ok(antwort.ok(), wer.name + ": die Anmeldung geht durch"); + + const seite = await ctx.newPage(); + await seite.goto("http://127.0.0.1:4373/workspace/aufgaben.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(500); + + const titel = await seite.title(); + const zeichen = await seite.evaluate(() => + [...document.querySelectorAll('link[rel="icon"], link[rel="apple-touch-icon"]')] + .map((l) => l.getAttribute("href") || "")); + + if (wer.eigen) { + ok(/Team Dogi/.test(titel) && !/Spicy/.test(titel), + wer.name + ': der Reiter heisst "' + titel + '"'); + ok(zeichen.length > 0 && zeichen.every((h) => h.includes("/crew-")), + wer.name + ": alle " + zeichen.length + " Zeichen sind die eigenen"); + ok(zeichen.every((h) => STEMPEL.test(h)), + wer.name + ": der Versionsstempel bleibt am Zeichen haengen"); + } else { + ok(/Spicy/.test(titel) && !/Team Dogi/.test(titel), + wer.name + ': der Reiter ist unveraendert ("' + titel + '")'); + ok(zeichen.length > 0 && zeichen.every((h) => h.includes("/workspace-")), + wer.name + ": sein Zeichen ist unveraendert (Gegenprobe)"); + } + await ctx.close(); +} +d.close(); + +await browser.close(); +console.log(`\n${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"} -- ${geprueft} geprueft, ${fehler} Fehler`); +process.exit(fehler === 0 ? 0 : 1); diff --git a/server/pruef-modi-checkliste.mjs b/server/pruef-modi-checkliste.mjs index 3fd79f7e..c67b4364 100644 --- a/server/pruef-modi-checkliste.mjs +++ b/server/pruef-modi-checkliste.mjs @@ -137,8 +137,12 @@ for (const [name, an] of [["DogFather", dogiAn], ["VanVan", vanAn]]) { method: "PUT", headers: jsonKopf, body: JSON.stringify({ stufe: "gut", creator_id: idModi }), }); - ok(b.status === 403 && /Team-Lage/.test(b.daten?.fehler || ""), - `${name.padEnd(10)} und wird auf die Team-Lage verwiesen (${b.status})`); + /* Der Name in der Meldung wird MITGEPRUEFT, nicht nur die Nummer. + Am 10.09.2026 wurde die Kachel von "Team-Lage" in "Eingang" + umbenannt, weil es zwei gleichnamige gab -- ein Hinweistext, der + danach auf die alte fuehrt, schickt jeden auf die falsche Seite. */ + ok(b.status === 403 && /Eingang/.test(b.daten?.fehler || ""), + `${name.padEnd(10)} und wird auf den Eingang verwiesen (${b.status})`); } /* ---------- 3. Er meldet etwas --------------------------------------- */ @@ -260,7 +264,7 @@ console.log("\n=== 6. Spicy Media bekommt nichts davon ===\n"); const b = await hole(spicyAn.keks, `/workspace/api/checkliste/live?creator_id=${idModi}`); ok(b.status === 404, `und über die Adresse ebenfalls nicht (${b.status})`); const c = await hole(spicyAn.keks, "/workspace/api/teamlage"); - ok(c.status === 404, `die Team-Lage bekommt sie nicht (${c.status})`); + ok(c.status === 404, `der Eingang bekommt sie nicht (${c.status})`); const e = await hole(spicyAn.keks, "/workspace/api/teamlage/behandeln", { method: "POST", headers: jsonKopf, body: JSON.stringify({ bereich: "live", schluessel: ersterPunkt, person_id: idModi, art: "aufgabe" }), @@ -300,7 +304,7 @@ console.log("\n=== 8. Ein Angebot an DogFather ===\n"); Teil waere. Ohne das muesste er entscheiden, ohne ihn zu kennen. */ const b = await hole(dogiAn.keks, "/workspace/api/teamlage"); const ang = (b.daten?.angebote || [])[0]; - ok(!!ang, `es steht in der Team-Lage (${(b.daten?.angebote || []).length})`); + ok(!!ang, `es steht im Eingang (${(b.daten?.angebote || []).length})`); ok(ang?.einsatz === "Im Live ansagen und die Regeln vorlesen", `mit seinem Teil dabei ("${ang?.einsatz || ""}")`); ok(ang?.bewertung === 4 && ang?.dringlichkeit === "niedrig", diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index 7468ff5d..f892329a 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -208,9 +208,25 @@ for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) { Gezaehlt wird deshalb, was diese Rolle laut bereiche.js sehen SOLL. Fehlt eine auf dem Bildschirm, faellt es weiterhin auf -- und eine neue kostet keinen Handgriff mehr. */ - const sollKacheln = await seite.evaluate(() => window.Bereiche.GRUPPEN - .flatMap((g) => g.bereiche).filter((b) => b.rollen.includes("admin")).length); - ok(sollKacheln > 15, `bereiche.js kennt ${sollKacheln} Kacheln fuer DogFather`); + /* NACHTRAG 10.09.2026: Es gibt eine ZWEITE Quelle. + + Seit dem 09.09. haengt der Server einzelnen Rollen Kacheln an, die + gar nicht in bereiche.js stehen (`bereiche_zusatz`) -- gerade damit + sie nicht in einer Datei stehen, die jeder herunterladen kann. Die + Zaehlung oben kannte nur die Datei und meldete deshalb "alle 23 + Kacheln stehen auf der Seite (24)": ein Fehler, den es nicht gab. + + Das ist genau derselbe Fall wie am 06.09., nur eine Ebene weiter -- + und dieselbe Antwort: nicht die Zahl anpassen, sondern die zweite + Quelle mitzaehlen. Gefragt wird jetzt BEIDE. */ + const sollKacheln = await seite.evaluate(async () => { + const ausDatei = window.Bereiche.GRUPPEN + .flatMap((g) => g.bereiche).filter((b) => b.rollen.includes("admin")).length; + const a = await fetch("/workspace/api/ich", { credentials: "same-origin" }); + const ich = a.ok ? await a.json() : {}; + return ausDatei + (ich.bereiche_zusatz || []).length; + }); + ok(sollKacheln > 15, `Datei und Server nennen zusammen ${sollKacheln} Kacheln fuer DogFather`); ok(r.kacheln.length === sollKacheln, `alle ${sollKacheln} Kacheln stehen auf der Seite (${r.kacheln.length})`); ok(/^(Guten Morgen|Guten Mittag|Guten Tag|Guten Abend|Späten Abend|Noch wach), Filipe$/ diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index e243f229..aba1271d 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -179,7 +179,7 @@ export const BEREICHE = { Ein Modi schlaegt etwas vor -- eine Aktion, ein Format, eine Regel -- und schreibt dazu, was DogFather dafuer tun muesste. DogFather oder VanVan nimmt an oder lehnt ab; bei Annahme entstehen die - Aufgaben. Entschieden wird in der Team-Lage, an derselben Stelle + Aufgaben. Entschieden wird im Eingang, an derselben Stelle wie die uebrigen Rueckmeldungen: EIN Eingang, nicht zwei. DIE ZWEI ZAHLEN SIND UMBENANNT, nicht neu erfunden: `bewertung` diff --git a/server/workspace-checkliste.js b/server/workspace-checkliste.js index 28dc16c7..baebcc9c 100644 --- a/server/workspace-checkliste.js +++ b/server/workspace-checkliste.js @@ -401,9 +401,9 @@ checklisteRouter.get("/workspace/api/checkliste/:bereich", (req, res) => { sein Bericht, sondern eine Abstimmung darueber, was er gesehen haben soll. - DogFather und VanVan LESEN ihn und handeln in der Team-Lage: - Aus "Verbessern" wird dort mit einem Klick eine Aufgabe -- oder - eine begruendete Absage, die beim Modi ankommt. */ + DogFather und VanVan LESEN ihn und handeln im Eingang: Aus + "Verbessern" wird dort mit einem Klick eine Aufgabe -- oder eine + begruendete Absage, die beim Verfasser ankommt. */ darf_bewerten: zielRolle === "modi" ? req.person.id === creatorId : (req.person.rolle !== "creator" && req.person.rolle !== "modi"), @@ -431,10 +431,17 @@ checklisteRouter.put("/workspace/api/checkliste/:bereich/:schluessel", gleicheHe const eigenerBericht = zielRolle === "modi" && req.person.id === zielId; if (zielRolle === "modi" && !eigenerBericht) { /* Auch DogFather nicht: Es ist der Bericht des Modis. Er - handelt in der Team-Lage, nicht hier. */ + handelt im Eingang, nicht hier. + + DER NAME IN DER MELDUNG MUSS STIMMEN (10.09.2026). Hier stand + "Team-Lage" -- so hiess die Kachel, als das gebaut wurde. Sie + heisst jetzt "Eingang", und "Team-Lage" fuehrt seither + eindeutig zu einer ANDEREN Seite. Ein Hinweis, der auf die + falsche Seite zeigt, ist schlimmer als keiner: Man geht hin, + findet nichts und glaubt, die Funktion sei kaputt. */ return res.status(403).json({ fehler: "Das ist die Rückmeldung des Modis. Sie wird nicht überschrieben, " - + "sondern in der Team-Lage behandelt.", + + "sondern im Eingang behandelt.", }); } if (!eigenerBericht diff --git a/server/workspace.js b/server/workspace.js index 81ad7625..915505c1 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -477,11 +477,35 @@ export function markeFuer(person) { Kachel, die nur bei einer einzigen Rolle erscheint, wirft die Frage auf, fuer wen sie ist. Genau die soll niemand stellen. ===================================================================== */ +/* ZWEIMAL "TEAM-LAGE" -- KORRIGIERT AM 10.09.2026. + + Hier stand: Gruppe "Rund um das Team", Name "Team-Lage", Ton 22. + In bereiche.js gibt es aber SCHON eine Kachel "Team-Lage" mit Ton 22 + und demselben Zeichen, die nach team.html fuehrt. DogFather hatte + damit zwei Kacheln, die gleich heissen, gleich aussehen, gleich + gefaerbt sind, nebeneinander in derselben Gruppe stehen -- und zu + verschiedenen Seiten fuehren. + + Gefunden hat das nicht das Auge, sondern pruef-start-ansicht mit dem + Satz "jede Kachel hat ihre eigene Farbe (23 Farben auf 24 Kacheln)". + Ohne diese Zeile waere es stehen geblieben; zwei gleiche Kacheln + fallen niemandem auf, der weiss, welche er meint. + + Jetzt drei Unterschiede statt keinem: eigener Name, eigene Gruppe, + eigene Farbe. Das Zeichen bleibt geteilt -- nachgemessen sind alle + 22 Zeichen des Hauses bei DogFather bereits in Gebrauch, und ein + geteiltes Zeichen bei verschiedenem Namen, Ort und Ton ist keine + Verwechslungsgefahr (profil steht ohnehin schon zweimal). + + "Eingang" und nicht "Rueckmeldungen": Die Kachel steht bei den + taeglichen Dingen, weil dort etwas WARTET -- Vorschlaege, ueber die + entschieden werden will. Das ist eine Aufgabe fuer heute, keine + Uebersicht fuer zwischendurch. */ const ZUSATZ_BEREICHE = { admin: [{ - gruppe: "Rund um das Team", - name: "Team-Lage", unter: "Was noch nicht besprochen ist", - zeichen: "teamlage", ton: 22, ziel: "teamlage.html", szene: "halle", + gruppe: "Täglich", + name: "Eingang", unter: "Vorschläge und Angebote, die auf dich warten", + zeichen: "teamlage", ton: 24, ziel: "teamlage.html", szene: "halle", }], }; @@ -2579,7 +2603,16 @@ const GESCHUETZT = { /* Die einzige Seite unter /workspace, die offen sein MUSS -- man kann sich schlecht anmelden, wenn die Anmeldeseite eine Anmeldung verlangt. */ -const OFFEN = new Set(["/workspace/index.html"]); +/* DIE ZUGANGSWAENDE. Zwei, seit die Modi-App ihre eigene Adresse hat: + crew-index.html ist dieselbe Wand in Dogis Farben, ohne Kachelreihe. + + SIE MUSS HIER STEHEN, sonst dreht sich die Seite im Kreis: crewWeiche + biegt /workspace/ auf /workspace/crew-index.html um; stuende die + Datei nicht als offen, schickte die Schranke sie zurueck nach + /workspace/ -- und von dort wieder hierher. Der Browser bricht das + nach ein paar Runden mit "zu viele Weiterleitungen" ab, und man + sucht den Fehler ueberall, nur nicht in dieser Zeile. */ +const OFFEN = new Set(["/workspace/index.html", "/workspace/crew-index.html"]); /** Der Pfad, wie ihn die Schranke ansehen muss. * diff --git a/tools/crew-buehne.mjs b/tools/crew-buehne.mjs new file mode 100644 index 00000000..ea3f7a51 --- /dev/null +++ b/tools/crew-buehne.mjs @@ -0,0 +1,523 @@ +/* =================================================================== + Die Buehne der Modi-App (crew.dogfather-universe.com, 10.09.2026). + + Filipe: "alles soll auf dogfather und die modis perfektionniert + werden auf der neuen modi seite." + + --------------------------------------------------------------------- + WARUM AUS DEM VORHANDENEN MOTIV UND NICHT NEU + + Die Anmeldeseite ist kein Bild mit Text darauf, sondern eine + MECHANIK: Rechts steht im Motiv eine leere Tafel mit Neonrahmen, und + gate.css setzt die Anmeldekarte in genau dieses Rechteck -- + 55,45 % / 13,15 % / 38,65 % / 63,85 %, gemessen von gate-bauen.mjs. + Ein frei erfundenes Bild haette diese vier Zahlen mitgenommen, und + die Karte laege irgendwo. + + Also dasselbe Motiv, dieselbe Geometrie -- nur gehoert es jetzt Dogi: + + 1. BLAU STATT ROT. Die Szene wird ueber den Mischmodus "color" + umgefaerbt: Helligkeit und Zeichnung bleiben Bildpunkt fuer + Bildpunkt erhalten, nur Farbton und Saettigung kommen neu. Aus + dem roten Neon wird blaues, aus dem roten Ahorn blaues Laub. + Ein Filter wie hue-rotate haette Rot nach Cyan und Blau nach + Gelb gedreht -- also die eine Haelfte repariert und die andere + kaputtgemacht. + + 2. DOGI STATT DES SPICY-MEDAILLONS. Wo das Chili-Logo im Kreis + stand, steht jetzt der Husky aus assets/img/marke -- weich + eingeblendet, damit sein dunkler Grund in der Szene verschwindet + statt als Rechteck darauf zu liegen. + + 3. "TEAM DOGI" darunter, in Metall wie im uebrigen Markenbild. + + --------------------------------------------------------------------- + DREI GEGENPROBEN, DIE ALLE DREI SCHRITTE NACHWEISEN + + Ein Bild, das entstanden ist, beweist nicht, dass darauf steht, was + draufstehen soll. Deshalb wird gemessen: + + * ROT IST WEG. Der Anteil deutlich roter Bildpunkte muss von ueber + zehn Prozent im Original auf nahe null fallen. Faellt er nicht, + hat die Umfaerbung nicht gegriffen. + * DIE TAFEL IST FREI. Das Rechteck, in dem die Karte sitzt, muss + dunkel und ruhig bleiben. Male ich versehentlich hinein, sitzt + spaeter Text auf Muster. + * DOGI IST WIRKLICH DA. Dasselbe Bild wird ein zweites Mal OHNE + ihn gebaut und verglichen. Sind beide gleich, war er nie zu + sehen -- genau der Fehler, der beim App-Symbol am 09.09. eine + Fassung lang unbemerkt blieb. + + Aufrufen mit: node tools/crew-buehne.mjs + =================================================================== */ + +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const WURZEL = join(dirname(fileURLToPath(import.meta.url)), ".."); +const QUELLE = join(WURZEL, "assets", "img", "marke"); +const SZENE = join(QUELLE, "gate-spicy-dogfather.jpeg"); +const DOGI = join(QUELLE, "dogfather-husky.png"); +const ZIEL = process.env.BUEHNE_ZIEL || join(WURZEL, "workspace", "assets", "img", "buehne"); + +for (const q of [SZENE, DOGI]) { + if (!existsSync(q)) { console.error(`Fehlt: ${q}`); process.exit(1); } +} +const alsDaten = (p) => { + const art = p.endsWith(".png") ? "png" : "jpeg"; + return `data:image/${art};base64,` + readFileSync(p).toString("base64"); +}; + +/* Die Tafel, in der die Anmeldekarte sitzt. Wortgleich mit gate.css -- + hier nur, um sie FREIZUHALTEN, nicht um sie zu zeichnen. */ +const TAFEL = { x: 0.5545, y: 0.1315, b: 0.3865, h: 0.6385 }; + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); +const seite = await (await browser.newContext({ deviceScaleFactor: 1 })).newPage(); +await seite.setViewportSize({ width: 400, height: 300 }); +if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true }); + +const szeneDaten = alsDaten(SZENE); +const dogiDaten = alsDaten(DOGI); + +/** + * Baut eine Fassung und gibt sie als WebP-Puffer zurueck. + * `ohneDogi` ist nur fuer die Gegenprobe da. + */ +async function bauen({ breite, ausschnitt = null, guete = 0.92, dunkel = 0.08, + vignette = 0.26, ohneDogi = false }) { + const roh = await seite.evaluate(async (a) => { + const laden = async (d) => { const i = new Image(); i.src = d; await i.decode(); return i; }; + const szene = await laden(a.szene); + const dogi = await laden(a.dogi); + + const qx = a.ausschnitt ? a.ausschnitt[0] * szene.width : 0; + const qy = a.ausschnitt ? a.ausschnitt[1] * szene.height : 0; + const qb = a.ausschnitt ? a.ausschnitt[2] * szene.width : szene.width; + const qh = a.ausschnitt ? a.ausschnitt[3] * szene.height : szene.height; + + const zb = a.breite; + const zh = Math.round(zb * (qh / qb)); + + const c = document.createElement("canvas"); + c.width = zb; c.height = zh; + const g = c.getContext("2d"); + g.imageSmoothingQuality = "high"; + + /* ---- 1. Die Szene, etwas kraeftiger gezeichnet ---------------- */ + g.filter = "contrast(1.12) saturate(1.10)"; + g.drawImage(szene, qx, qy, qb, qh, 0, 0, zb, zh); + g.filter = "none"; + + /* ---- 2. UMFAERBEN -------------------------------------------- + "color" nimmt Farbton und Saettigung von der Farbe und die + Helligkeit vom Bild darunter. Deshalb bleibt jede Kante, jeder + Nebelschwaden und der ganze Glanz erhalten -- es wechselt nur + die Farbe. Zwei Lagen: unten ein tiefes Marineblau fuer die + Flaeche, darueber ein Verlauf, damit oben das kalte Licht und + unten das waermere Wasserblau steht und die Szene nicht flach + wird. */ + g.globalCompositeOperation = "color"; + g.fillStyle = "#1d4f80"; + g.fillRect(0, 0, zb, zh); + + const farbe = g.createLinearGradient(0, 0, zb * 0.35, zh); + farbe.addColorStop(0, "#2f7fc4"); + farbe.addColorStop(0.55, "#1d4f80"); + farbe.addColorStop(1, "#123a63"); + g.globalAlpha = 0.65; + g.fillStyle = farbe; + g.fillRect(0, 0, zb, zh); + g.globalAlpha = 1; + g.globalCompositeOperation = "source-over"; + + /* ---- 3. Das alte Medaillon zudecken -------------------------- + Ein weicher, dunkler Kreis genau dort, wo das Chili-Logo stand. + Weich, nicht hart: Eine scharfe Kante saehe man sofort als + Flicken. */ + const mx = zb * 0.295, my = zh * 0.375, mr = zh * 0.62; + /* GANZ DECKEND BIS 0,66 -- nicht 0,90. + + Beim ersten Versuch stand hier 0,97 aussen abfallend. Zehn + Prozent Rest genuegen: Im Bildschirmfoto las man "SP...DIA" + quer durch Dogi hindurch und darunter den alten + DOGFATHER-Schriftzug ueber seinem Hoodie. Hoher Kontrast + braucht fast nichts, um lesbar zu bleiben. + + Deshalb bis weit hinter beide Schriftzuege voll deckend und + erst dann weich auslaufen. Der Kreis wird dadurch gross und + dunkel -- was darin fehlt, bringen Lichtschein und Dogi + zurueck. */ + const deckel = g.createRadialGradient(mx, my, 0, mx, my, mr); + deckel.addColorStop(0, "rgba(4,10,18,1)"); + deckel.addColorStop(0.66, "rgba(4,10,18,1)"); + deckel.addColorStop(0.84, "rgba(4,10,18,0.58)"); + deckel.addColorStop(1, "rgba(4,10,18,0)"); + g.fillStyle = deckel; + g.fillRect(0, 0, zb, zh); + + /* Ein kalter Lichtschein an derselben Stelle -- sonst ist dort + jetzt bloss ein Loch. */ + const schein = g.createRadialGradient(mx, my * 1.05, 0, mx, my * 1.05, mr * 0.92); + schein.addColorStop(0, "rgba(84,168,235,0.30)"); + schein.addColorStop(0.45, "rgba(52,120,190,0.15)"); + schein.addColorStop(1, "rgba(24,60,110,0)"); + g.fillStyle = schein; + g.fillRect(0, 0, zb, zh); + + /* ---- 4. DOGI --------------------------------------------------- + Er steht auf dem Podest, das im Motiv ohnehin da ist. Sein + eigener Grund ist dunkel; damit daraus kein Rechteck wird, wird + er durch eine weiche Ellipse maskiert und mit "lighter" + aufgetragen -- Dunkles bleibt dadurch unsichtbar, Helles + (Fell, Hoodie, Augen) kommt heraus. */ + let dogiKasten = null; + if (!a.ohneDogi) { + const dh = zh * 0.84; + const db = dh * ((dogi.width * 0.82) / (dogi.height * 0.91)); + const dx = zb * 0.295 - db / 2; + const dy = zh * 0.90 - dh; + dogiKasten = [dx, dy, db, dh]; + + const h = document.createElement("canvas"); + h.width = Math.max(1, Math.round(db)); h.height = Math.max(1, Math.round(dh)); + const gh = h.getContext("2d"); + gh.imageSmoothingQuality = "high"; + /* SEIN GRUND MUSS AUF SCHWARZ, bevor er aufgetragen wird. + + "lighter" ADDIERT. Schwarz addiert nichts und bleibt deshalb + unsichtbar -- sein Grund ist aber nicht schwarz, sondern ein + warmer brauner Schein. Der wurde mitaddiert, und genau das war + der helle Kasten um ihn herum im ersten Versuch. + + Kontrast druckt den Schein nach unten, ohne ihn wegzuschneiden; + eine harte Schwelle haette sein schwarzes Fell gleich mit + erwischt. Dazu etwas Blau ueber den Mischmodus "color", damit + sein warmes Streiflicht zur kalten Szene passt -- nur zu einem + Drittel, sonst verliert das Fell seine Zeichnung. */ + gh.filter = "contrast(1.42) brightness(1.04) saturate(1.12)"; + /* NUR SEINE SILHOUETTE, nicht das ganze Blatt. In der Vorlage + steht er mit reichlich Luft ringsum, und in dieser Luft liegt + der warme Schein, der als Kasten sichtbar wurde. Gemessen an + der Vorlage steht er zwischen 11 und 93 Prozent der Breite und + zwischen 8 und 99 Prozent der Hoehe (seine Rute reicht weiter + nach rechts, als es aussieht -- beim ersten Zuschnitt war sie + ab und hinterliess eine senkrechte Kante) -- alles ausserhalb ist + nur Grund und wird gar nicht erst mitgenommen. */ + gh.drawImage(dogi, + dogi.width * 0.11, dogi.height * 0.08, + dogi.width * 0.82, dogi.height * 0.91, + 0, 0, h.width, h.height); + gh.filter = "none"; + gh.globalCompositeOperation = "color"; + gh.globalAlpha = 0.34; + gh.fillStyle = "#2a6ea8"; + gh.fillRect(0, 0, h.width, h.height); + gh.globalAlpha = 1; + gh.globalCompositeOperation = "source-over"; + + /* IHN UEBER DIE HELLIGKEIT FREISTELLEN. + + Kontrast allein hat den Kasten nicht wegbekommen: Der warme + Dunst um seine Pfoten liegt bei einer Helligkeit von 40 bis 70 + -- "lighter" addiert das sichtbar dazu, und im Bildschirmfoto + stand ein hellerer Rahmen um ihn. + + Also bekommt jeder Bildpunkt seine Deckkraft aus seiner + HELLIGKEIT: unter 42 ganz weg, ab 88 ganz da, dazwischen ein + weicher Uebergang (die uebliche glatte Kurve, damit keine + Stufenkante entsteht). 26 war der erste Versuch und zu tief -- + sein Streiflicht liegt bei 50 bis 75 und blieb stehen. + + DASS SEIN SCHWARZES FELL DABEI DURCHSICHTIG WIRD, IST KEIN + VERLUST: Unter "lighter" traegt Schwarz ohnehin nichts bei -- + es ist mit und ohne Deckkraft gleich unsichtbar. Und hinter + ihm liegt der deckende Kreis, also ist dort ohnehin Nacht. + Sein Fell liest sich deshalb weiter als Fell, nicht als Loch. */ + const bd = gh.getImageData(0, 0, h.width, h.height); + const px = bd.data; + for (let i = 0; i < px.length; i += 4) { + const L = 0.2126 * px[i] + 0.7152 * px[i + 1] + 0.0722 * px[i + 2]; + let t = (L - 42) / 46; + t = t < 0 ? 0 : t > 1 ? 1 : t; + px[i + 3] = Math.round(px[i + 3] * (t * t * (3 - 2 * t))); + } + gh.putImageData(bd, 0, 0); + + /* Die Maske bleibt als Netz darunter -- sie faengt, was die + Helligkeit stehen laesst (etwa eine helle Ecke im Grund). */ + /* SIE MUSS EINE ELLIPSE SEIN, KEIN KREIS -- und das war der + eigentliche Fehler. + + Hier stand ein Kreis mit dem Radius `max(Breite, Hoehe) * 0.56`. + Seine Bildflaeche ist aber hochkant (rund 1 zu 2): Der Radius + richtete sich also nach der HOEHE und reichte seitlich weit + ueber den Rand hinaus. Links und rechts wurde damit ueberhaupt + nie ausgeblendet -- genau dort blieb im Bildschirmfoto der + helle Kasten stehen, und zwar durch alle Nachbesserungen an der + Schwelle hindurch. Man sieht einer solchen Zeile nicht an, dass + sie eine ganze Achse auslaesst. + + Ueber `scale` wird aus dem Kreis eine Ellipse, die zur Flaeche + passt: waagerecht 0,52 der Breite, senkrecht 0,52 der Hoehe. + Beide Achsen blenden dadurch vor dem Rand aus. */ + gh.globalCompositeOperation = "destination-in"; + gh.save(); + /* MITTIG UND KNAPP UNTER DER HALBEN BREITE. + + Vorher: Mitte bei 0,52 der Hoehe, Radius 0,52 -- unten reichte + die Ellipse damit bis 1,04 und blendete am unteren Rand NIE + aus. Die Randprobe hat genau das gemeldet (4,1 % statt unter + 3 %), bevor es jemand gesehen hat. Mittig und 0,50 laesst beide + Kanten sauber auslaufen; dass seine Pfoten unten einen Hauch + weicher werden, ist kein Verlust -- dort steht im Motiv ohnehin + Nebel ueber dem Wasser. */ + gh.translate(h.width / 2, h.height * 0.50); + gh.scale(1, h.height / h.width); + const r = h.width * 0.50; + const m = gh.createRadialGradient(0, 0, 0, 0, 0, r); + m.addColorStop(0, "rgba(0,0,0,1)"); + m.addColorStop(0.62, "rgba(0,0,0,1)"); + m.addColorStop(0.85, "rgba(0,0,0,0.45)"); + m.addColorStop(1, "rgba(0,0,0,0)"); + gh.fillStyle = m; + gh.fillRect(-h.width, -h.height, h.width * 2, h.height * 2); + gh.restore(); + + g.save(); + g.globalCompositeOperation = "lighter"; + g.globalAlpha = 1; + g.filter = "saturate(1.15) contrast(1.06)"; + g.drawImage(h, dx, dy, db, dh); + g.restore(); + } + + /* ---- 5. "TEAM DOGI" ------------------------------------------- */ + const schriftHoehe = Math.round(zh * 0.072); + g.save(); + g.textAlign = "center"; + g.textBaseline = "alphabetic"; + g.font = `800 ${schriftHoehe}px "Segoe UI", system-ui, sans-serif`; + /* 0,955 lag halb unter dem Bildrand -- im Bildschirmfoto war von + "TEAM DOGI" die untere Haelfte abgeschnitten. Jetzt so weit + herauf, dass die Unterlaengen noch auf dem Wasser stehen. */ + const ty = zh * 0.915; + /* Erst ein kalter Schein darunter, dann die Schrift in Metall -- + so liest sie sich vor dem dunklen Wasser, ohne zu blenden. */ + g.shadowColor = "rgba(70,150,220,0.85)"; + g.shadowBlur = schriftHoehe * 0.9; + g.fillStyle = "rgba(10,25,45,0.9)"; + g.fillText("TEAM DOGI", mx, ty); + g.shadowBlur = 0; + const metall = g.createLinearGradient(0, ty - schriftHoehe, 0, ty); + metall.addColorStop(0, "#ffffff"); + metall.addColorStop(0.45, "#cfe2f2"); + metall.addColorStop(0.55, "#7ea6c6"); + metall.addColorStop(1, "#e8f2fa"); + g.fillStyle = metall; + g.fillText("TEAM DOGI", mx, ty); + g.restore(); + + /* ---- 6. Vignette und ein Hauch Dunkel ------------------------- */ + if (a.vignette > 0) { + const v = g.createRadialGradient(zb / 2, zh / 2, Math.min(zb, zh) * 0.28, + zb / 2, zh / 2, Math.max(zb, zh) * 0.72); + v.addColorStop(0, "rgba(0,0,0,0)"); + v.addColorStop(0.55, `rgba(0,0,0,${(a.vignette * 0.22).toFixed(3)})`); + v.addColorStop(0.8, `rgba(0,0,0,${(a.vignette * 0.62).toFixed(3)})`); + v.addColorStop(1, `rgba(0,0,0,${a.vignette.toFixed(3)})`); + g.fillStyle = v; + g.fillRect(0, 0, zb, zh); + } + if (a.dunkel > 0) { + g.fillStyle = `rgba(0,0,0,${a.dunkel})`; + g.fillRect(0, 0, zb, zh); + } + + /* ---- Messwerte gleich mit zurueckgeben ------------------------ */ + const d = g.getImageData(0, 0, zb, zh).data; + let rot = 0, alle = 0; + for (let i = 0; i < d.length; i += 4) { + alle++; + if (d[i] > d[i + 2] + 25 && d[i] > 60) rot++; + } + /* Die Tafel: dunkel und ruhig? */ + const tx = Math.round(a.tafel.x * zb), ty2 = Math.round(a.tafel.y * zh); + const tb = Math.round(a.tafel.b * zb), th = Math.round(a.tafel.h * zh); + const t = g.getImageData(tx, ty2, tb, th).data; + let s = 0, s2 = 0, m2 = 0; + for (let i = 0; i < t.length; i += 4) { + const hh = 0.2126 * t[i] + 0.7152 * t[i + 1] + 0.0722 * t[i + 2]; + s += hh; s2 += hh * hh; m2++; + } + const mittel = s / m2; + + return { + bild: c.toDataURL("image/webp", a.guete), + rotAnteil: rot / alle, + tafelMittel: mittel, + tafelStreuung: Math.sqrt(s2 / m2 - mittel * mittel), + breite: zb, hoehe: zh, + dogiKasten, + }; + }, { szene: szeneDaten, dogi: dogiDaten, breite, ausschnitt, guete, dunkel, vignette, + ohneDogi, tafel: TAFEL }); + + return { ...roh, puffer: Buffer.from(roh.bild.split(",")[1], "base64") }; +} + +/* Wie rot ist das ORIGINAL? Ohne diesen Wert waere "wenig Rot" eine + Behauptung ohne Bezug. */ +const rotVorher = await seite.evaluate(async (d) => { + const i = new Image(); i.src = d; await i.decode(); + const c = document.createElement("canvas"); + c.width = 800; c.height = Math.round(800 * i.height / i.width); + const g = c.getContext("2d"); + g.drawImage(i, 0, 0, c.width, c.height); + const p = g.getImageData(0, 0, c.width, c.height).data; + let rot = 0, alle = 0; + for (let k = 0; k < p.length; k += 4) { + alle++; + if (p[k] > p[k + 2] + 25 && p[k] > 60) rot++; + } + return rot / alle; +}, szeneDaten); + +console.log(`\nRot im Original: ${(rotVorher * 100).toFixed(1)} %`); + +/* Anteil sichtbar verschiedener Bildpunkte zwischen zwei Fassungen. + * + * `rahmen` schraenkt den Vergleich auf den RANDSTREIFEN eines + * Rechtecks ein -- damit wird der Kasten messbar, der zwei + * Nachbesserungen lang sichtbar blieb, weil die Maske ein Kreis statt + * einer Ellipse war. Ein Blick auf das ganze Bild sagt nur "Dogi ist + * da"; erst der Blick auf seinen Rand sagt, ob man SEINE FLAECHE sieht + * statt ihn. + */ +async function unterschied(a, b, rahmen = null) { + return await seite.evaluate(async ([x, y, rr]) => { + const laden = async (b64) => { + const i = new Image(); i.src = "data:image/webp;base64," + b64; await i.decode(); + const c = document.createElement("canvas"); + c.width = i.width; c.height = i.height; + c.getContext("2d").drawImage(i, 0, 0); + return c.getContext("2d").getImageData(0, 0, c.width, c.height).data; + }; + const breiteVon = async (b64) => { + const i = new Image(); i.src = "data:image/webp;base64," + b64; await i.decode(); + return i.width; + }; + const [p, q, breite] = [await laden(x), await laden(y), await breiteVon(x)]; + if (p.length !== q.length) return -1; + let anders = 0, m = 0; + for (let i = 0; i < p.length; i += 4) { + if (rr) { + const px = (i / 4) % breite, py = Math.floor((i / 4) / breite); + const [bx, by, bb, bh] = rr; + const drin = px >= bx && px < bx + bb && py >= by && py < by + bh; + if (!drin) continue; + /* Nur der Randstreifen: innerhalb der Flaeche, aber ausserhalb + ihrer um 8 Prozent eingerueckten Mitte. */ + const ix = bx + bb * 0.08, iy = by + bh * 0.08; + if (px >= ix && px < bx + bb * 0.92 && py >= iy && py < by + bh * 0.92) continue; + } + const w = Math.max(Math.abs(p[i] - q[i]), Math.abs(p[i + 1] - q[i + 1]), + Math.abs(p[i + 2] - q[i + 2])); + if (w > 8) anders++; + m++; + } + return m === 0 ? -1 : anders / m; + }, [a.toString("base64"), b.toString("base64"), rahmen]); +} + +const maengel = []; +let gebaut = 0; + +console.log("\n--- Quer ---"); +for (const b of [960, 1280, 1600, 1920, 2560]) { + const r = await bauen({ breite: b, guete: 0.93, dunkel: 0.08, vignette: 0.26 }); + writeFileSync(join(ZIEL, `crew-gate-${b}.webp`), r.puffer); + gebaut++; + + if (r.rotAnteil > 0.01) maengel.push( + `crew-gate-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte -- Umfaerbung hat nicht gegriffen`); + if (r.tafelMittel > 46) maengel.push( + `crew-gate-${b}: die Tafel ist zu hell (${r.tafelMittel.toFixed(1)}) -- die Karte braucht einen ruhigen Grund`); + if (r.tafelStreuung > 34) maengel.push( + `crew-gate-${b}: in der Tafel ist zu viel Muster (Streuung ${r.tafelStreuung.toFixed(1)})`); + + console.log(` crew-gate-${b}.webp`.padEnd(30) + + `${(r.puffer.length / 1024).toFixed(0)} KB · ${r.breite}x${r.hoehe}` + + ` · Rot ${(r.rotAnteil * 100).toFixed(2)} %` + + ` · Tafel ${r.tafelMittel.toFixed(1)}/${r.tafelStreuung.toFixed(1)}`); +} + +/* HOCHKANT: der Ausschnitt um DOGI, nicht um die Tafel -- die ist leer, + und auf dem Handy liegt die Karte ohnehin davor. Genau derselbe + Gedanke wie beim Original, nur ist die linke Seite jetzt er. */ +console.log("\n--- Hoch ---"); +for (const b of [440, 580, 900]) { + const r = await bauen({ + breite: b, /* HOCHKANT UND UM DOGI HERUM. Erster Versuch war + [0.03, 0.06, 0.50, 0.94] -- fast quadratisch. Auf einem Handy + (412 zu 915) schneidet `cover` davon die Seiten weg, und im + Bildschirmfoto stand Dogi halb ausserhalb, dafuer der leere + Deckkreis mitten im Bild. Jetzt ein hochkantiger Streifen, dessen + Mitte auf seiner Mitte liegt (0,13 bis 0,45 -- Mitte 0,29, er + steht bei 0,295). */ + ausschnitt: [0.13, 0.02, 0.32, 0.98], guete: 0.88, + dunkel: 0.22, vignette: 0.30, + }); + writeFileSync(join(ZIEL, `crew-gate-hoch-${b}.webp`), r.puffer); + gebaut++; + if (r.rotAnteil > 0.01) maengel.push( + `crew-gate-hoch-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte`); + console.log(` crew-gate-hoch-${b}.webp`.padEnd(30) + + `${(r.puffer.length / 1024).toFixed(0)} KB · ${r.breite}x${r.hoehe}` + + ` · Rot ${(r.rotAnteil * 100).toFixed(2)} %`); +} + +/* ---- DIE GEGENPROBE: ist Dogi wirklich zu sehen? ------------------- */ +console.log("\n--- Gegenprobe ---"); +const mit = await bauen({ breite: 1280 }); +const ohne = await bauen({ breite: 1280, ohneDogi: true }); +const anders = await unterschied(mit.puffer, ohne.puffer); +if (anders < 0) { + maengel.push("Gegenprobe nicht durchfuehrbar -- die beiden Fassungen haben verschiedene Groessen"); +} else if (anders < 0.03) { + maengel.push(`Dogi ist nicht zu sehen: mit und ohne unterscheiden sich nur um ` + + `${(anders * 100).toFixed(2)} %`); +} +console.log(` mit Dogi gegen ohne Dogi: ${(anders * 100).toFixed(2)} % der Bildpunkte anders`); + +/* SIEHT MAN IHN ODER SEINE FLAECHE? Am Rand seines Bildbereichs darf + fast nichts passieren -- sonst liegt dort eine sichtbare Kante. */ +const randAnders = await unterschied(mit.puffer, ohne.puffer, mit.dogiKasten); +if (randAnders < 0) { + maengel.push("Randprobe nicht durchfuehrbar -- Dogis Flaeche ist nicht bekannt"); +} else if (randAnders > 0.03) { + maengel.push(`Am Rand von Dogis Flaeche sind ${(randAnders * 100).toFixed(1)} % der ` + + `Bildpunkte anders -- man sieht seinen Kasten statt ihn`); +} +console.log(` davon am Rand seiner Flaeche: ${(randAnders * 100).toFixed(2)} %` + + ` (ueber 3 % waere ein sichtbarer Kasten)`); +console.log(` Rot vorher ${(rotVorher * 100).toFixed(1)} % -> nachher ` + + `${(mit.rotAnteil * 100).toFixed(2)} %`); + +await browser.close(); + +if (gebaut !== 8) { + console.error(`\nFEHLER: ${gebaut} von 8 Fassungen erzeugt.`); process.exit(1); +} +if (rotVorher < 0.05) { + console.error(`\nFEHLER: Das Original ist gar nicht rot (${(rotVorher * 100).toFixed(1)} %) -- ` + + `dann misst die Rot-Probe nichts. Quelle pruefen.`); process.exit(1); +} +if (maengel.length) { + console.error("\nFEHLER:\n " + maengel.join("\n ")); process.exit(1); +} +console.log(`\n${gebaut} Fassungen erzeugt. Das Rot ist weg, die Tafel bleibt frei,` + + ` und Dogi ist nachweisbar zu sehen.`); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 83597fce..6a4983d6 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609101301", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609101351", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609101301", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609101351", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609101301", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609101351", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609101301", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609101351", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css index 4fbfcd2a..c0595c7a 100644 --- a/workspace/assets/css/gate.css +++ b/workspace/assets/css/gate.css @@ -2273,6 +2273,20 @@ body.gate { color: #a9dcff; text-shadow: 0 1px 10px rgba(99, 184, 238, .45); } +/* TEAM DOGI (10.09.2026, nur auf crew.dogfather-universe.com). + + Bewusst NICHT .marke__spicy mitbenutzt, obwohl es dieselbe Stelle im + Satz ist: Die Klasse ist rosarot, weil dort "SpicyMedia" steht. Auf + dieser Seite waere das der einzige warme Fleck auf einer durchweg + blauen Seite -- und er saesse ausgerechnet auf dem Namen, der dort + NICHT hingehoert. + + Silbrig wie der Schriftzug im Hintergrundbild, damit beide dasselbe + sagen. */ +.marke--paar .marke__crew { + color: #dce9f3; + text-shadow: 0 1px 10px rgba(150, 190, 220, .45); +} .marke--paar .marke__x { /* Kleiner, leiser, und mit etwas Luft -- es verbindet, es benennt nicht. */ @@ -2696,3 +2710,76 @@ body.gate { radial-gradient(1.1px 1.1px at 63% 72%, rgba(226, 238, 250, .38), transparent), radial-gradient(1.6px 1.6px at 84% 26%, rgba(255, 255, 255, .3), transparent); } + + +/* ===================================================================== + DIE ZWEITE ZUGANGSWAND (10.09.2026) + + Nur crew-index.html traegt `gate--crew`. Hier stehen die wenigen + Stellen, an denen die gemeinsame Gestaltung nicht passt -- und zwar + NUR die. + + ES GEHT NICHT UM GESCHMACK, SONDERN UM DAS BILD DAHINTER. + Die Karte holt sich absichtlich die Farben des gemalten Rahmens + herein: rot oben links, blau unten rechts, "das, was die Karte mit + dem Bild verbindet, statt sie davorzusetzen" (Begruendung weiter + oben). Auf der zweiten Buehne ist der Rahmen aber RINGSUM blau -- die + Szene wurde umgefaerbt. Das Rot verbindet dort also nichts mehr, + sondern widerspricht dem Bild. + + Dieselbe Ueberlegung beim Anmeldeknopf: Sein Verlauf lief von Rot + nach Blau, weil er die beiden Marken aufnahm. Auf dieser Seite gibt + es nur eine. + ===================================================================== */ + +/* Der Knopf: von tiefem zu hellerem Blau statt von Rot nach Blau. + + DIE HELLIGKEIT IST NACHGERECHNET, NICHT GEWAEHLT. Der erste Entwurf + endete bei #4aa4cf -- huebsch, und weisse Schrift darauf haette nur + 2,8 zu 1 gehabt. Der rot-blaue Verlauf, den er ersetzt, haelt ueber + seine GANZE Laenge 4,65 zu 1; er war offenbar genau darauf gebaut. + Ein hellerer Knopf sieht kraeftiger aus und ist schlechter zu lesen + -- das ist kein Tausch, den man nebenbei macht. + + Jede Stufe hier liegt bei mindestens 5 zu 1 gegen Weiss. Gemessen + wird das in pruef-crew-wand-bild.mjs am fertigen Bildschirmfoto, denn + ein Verlauf laesst sich nicht aus einer einzelnen Farbe beurteilen. */ +body.gate--crew #knopf { + background: + linear-gradient(100deg, #17527f 0%, #205f8c 34%, #2f6f9a 74%, #3a7699 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .28), + inset 0 -2px 0 rgba(0, 0, 0, .22), + 0 10px 26px -14px rgba(64, 146, 200, .9), + 0 10px 26px -16px rgba(126, 200, 242, .8); +} +body.gate--crew #knopf:hover:not([disabled]) { + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .34), + inset 0 -2px 0 rgba(0, 0, 0, .22), + 0 14px 30px -12px rgba(64, 146, 200, .95), + 0 14px 30px -14px rgba(126, 200, 242, .85); +} + +/* Die Karte in der gemalten Tafel. WORTGLEICH dieselbe Bedingung wie + oben -- nicht global: Auf schmalen Geraeten hat die Karte einen + anderen Aufbau, und ein globales `background` haette den ueberschrieben. + Nur die zwei roten Werte werden blau, alles andere bleibt Zeile fuer + Zeile wie es war. */ +@media (min-width: 1100px) and (min-height: 620px) and (min-aspect-ratio: 7/5) { + body.gate--crew .tafel { + background: + radial-gradient(135% 100% at 0% 0%, rgba(96, 168, 224, .24), transparent 55%), + radial-gradient(135% 100% at 100% 100%, rgba(56, 158, 246, .30), transparent 58%), + linear-gradient(176deg, + rgba(16, 23, 36, .93) 0%, + rgba(9, 13, 21, .95) 46%, + rgba(5, 7, 13, .96) 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, .11), + inset 0 -1px 0 rgba(0, 0, 0, .5), + inset 2px 2px 0 -1px rgba(120, 190, 240, .32), + inset -2px -2px 0 -1px rgba(56, 158, 246, .38), + inset 0 0 54px -20px rgba(0, 0, 0, .92); + } +} diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index 35d36c78..91cf4c65 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -949,6 +949,19 @@ body.start > main { position: relative; z-index: 1; } sieht abgeschaltet aus. Gedeckt statt grell, wie alles hier. */ [data-ton="23"] { --ton: #5f8a9f; } /* Ideen-Board */ +/* Ton 24 (10.09.2026, Eingang). Wieder gerechnet statt gewaehlt, und bei + 23 belegten Toenen wird es eng: Der Abstand im CIELAB-Raum zum + naechsten Nachbarn betraegt 38,5 -- die weichen Violett-Toene, die + zuerst naheliegend schienen, lagen alle zwischen 13 und 26, weil + Ton 19 (#796ce0) und Ton 8 (#c06ad0) diesen Bereich schon besetzen. + + Reines Blau haette mit 52 den groessten Abstand gehabt und wurde + verworfen: stark gesaettigtes Blau ist auf dunklem Grund am + schlechtesten zu fokussieren. Kraeftiges Gruen kam auf 39,5, hat aber + L* 81 und wuerde leuchten. Dieser Ton hat eine Leuchtdichte von 0,109 + -- dunkel genug, um neben den anderen ruhig zu bleiben. */ +[data-ton="24"] { --ton: #8a20cf; } /* Eingang */ + /* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat. Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der Regeln als NICHT vorhanden und die Nummer oben gewinnt immer. Ohne diff --git a/workspace/assets/img/buehne/crew-gate-1280.webp b/workspace/assets/img/buehne/crew-gate-1280.webp new file mode 100644 index 00000000..12c4a3bb Binary files /dev/null and b/workspace/assets/img/buehne/crew-gate-1280.webp differ diff --git a/workspace/assets/img/buehne/crew-gate-1600.webp b/workspace/assets/img/buehne/crew-gate-1600.webp new file mode 100644 index 00000000..67bc4c84 Binary files /dev/null and b/workspace/assets/img/buehne/crew-gate-1600.webp differ diff --git a/workspace/assets/img/buehne/crew-gate-1920.webp b/workspace/assets/img/buehne/crew-gate-1920.webp new file mode 100644 index 00000000..e4df8fdb Binary files /dev/null and b/workspace/assets/img/buehne/crew-gate-1920.webp differ diff --git a/workspace/assets/img/buehne/crew-gate-2560.webp b/workspace/assets/img/buehne/crew-gate-2560.webp new file mode 100644 index 00000000..a0b0a972 Binary files /dev/null and b/workspace/assets/img/buehne/crew-gate-2560.webp differ diff --git a/workspace/assets/img/buehne/crew-gate-960.webp b/workspace/assets/img/buehne/crew-gate-960.webp new file mode 100644 index 00000000..e9ba0782 Binary files /dev/null and b/workspace/assets/img/buehne/crew-gate-960.webp differ diff --git a/workspace/assets/img/buehne/crew-gate-hoch-440.webp b/workspace/assets/img/buehne/crew-gate-hoch-440.webp new file mode 100644 index 00000000..3359ba52 Binary files /dev/null and b/workspace/assets/img/buehne/crew-gate-hoch-440.webp differ diff --git a/workspace/assets/img/buehne/crew-gate-hoch-580.webp b/workspace/assets/img/buehne/crew-gate-hoch-580.webp new file mode 100644 index 00000000..6e36d70f Binary files /dev/null and b/workspace/assets/img/buehne/crew-gate-hoch-580.webp differ diff --git a/workspace/assets/img/buehne/crew-gate-hoch-900.webp b/workspace/assets/img/buehne/crew-gate-hoch-900.webp new file mode 100644 index 00000000..dffe1caf Binary files /dev/null and b/workspace/assets/img/buehne/crew-gate-hoch-900.webp differ diff --git a/workspace/assets/js/gate.js b/workspace/assets/js/gate.js index 3b0488d7..8128ad6d 100644 --- a/workspace/assets/js/gate.js +++ b/workspace/assets/js/gate.js @@ -31,6 +31,19 @@ const SPEICHER = 'dfw.rolle'; let gewaehlt = null; + /* AUF DER ZWEITEN ADRESSE GIBT ES KEINE KACHELN (10.09.2026). + + Dort fuehrt genau eine Sorte Zugang hinein, also ist nichts zu + waehlen -- jene Wand liefert die Reihe erst gar nicht mit. + + Geprueft wird deshalb, OB es Kacheln gibt, statt sie + vorauszusetzen. Ohne diese Zeile bliebe `gewaehlt` dort ewig null, + und der Anmeldeknopf antwortete auf jeden Druck mit "Bitte zuerst + auswaehlen, womit du dich anmeldest" -- vor einer Seite, auf der es + nichts auszuwaehlen gibt. Die Anmeldung waere unbenutzbar gewesen, + und zwar ohne Fehlermeldung im Protokoll. */ + const ohneKacheln = rollen.length === 0; + /* ---------- Rolle wählen ------------------------------------------ */ function waehle(rolle, fokus = false) { @@ -99,7 +112,7 @@ e.preventDefault(); const wert = code.value.trim(); - if (!gewaehlt) { melde('Bitte zuerst auswählen, womit du dich anmeldest.'); rollen[0].focus(); return; } + if (!ohneKacheln && !gewaehlt) { melde('Bitte zuerst auswählen, womit du dich anmeldest.'); rollen[0].focus(); return; } if (!wert) { melde('Bitte den Zugangscode eingeben.'); code.focus(); return; } melde(''); @@ -109,7 +122,11 @@ const antwort = await fetch('/workspace/api/anmelden', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ rolle: gewaehlt, code: wert }), + /* Ohne Kacheln geht ein leerer Rollenname mit. Der Server + braucht ihn dort nicht: Er prueft den verborgenen Zugang + VOR dem gewohnten Weg, und ein leerer Name faellt danach in + dieselbe Antwort wie ein erfundener. */ + body: JSON.stringify({ rolle: gewaehlt || '', code: wert }), credentials: 'same-origin', }); diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index ed1a6be7..3beeb479 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -57,6 +57,42 @@ for (const verweis of document.querySelectorAll('.kopfleiste .marke__text .zurueck')) { verweis.textContent = String(ich.marke).replace(/ /g, ' '); } + + /* DER REITER UND DAS ZEICHEN GEHOEREN DAZU (10.09.2026). + + Die Kopfleiste allein reicht nicht. In jeder Seite steht fest + Aufgaben · Spicy & Dogi und daneben das Zeichen + des Workspace -- beides sieht man als Erstes, im Browserreiter + und, wenn die Seite als App laeuft, im Fensterrahmen und beim + Umschalten zwischen Apps. + + WARUM HIER UND NICHT IN 30 HTML-DATEIEN: Der Name kommt vom + Server und haengt an der Person. Er kann also gar nicht fest im + HTML stehen -- und 30 Dateien zu pflegen hiesse, die 31. zu + vergessen. + + Der Seitenname bleibt stehen, nur der Teil hinter dem Punkt + wechselt. Auf der Startseite steht dort der Name des Bereichs + selbst; dann bleibt nur die Marke uebrig, statt sie doppelt zu + lesen. */ + const marke = String(ich.marke); + const stueck = document.title.split(' · '); + const seitenname = stueck.slice(0, -1).join(' · ').trim(); + document.title = (!seitenname || seitenname === 'Creator Workspace') + ? marke + : `${seitenname} · ${marke}`; + + /* Das Zeichen: derselbe Stempel, anderer Name. Ihn aus der + vorhandenen Adresse zu ersetzen statt ihn neu zu bauen haelt + die Versionsnummer daran -- sonst laedt der Browser ab hier ein + Bild ohne Stempel und behaelt es ein Jahr. */ + for (const zeichen of document.querySelectorAll( + 'link[rel="icon"], link[rel="apple-touch-icon"]')) { + const alt = zeichen.getAttribute('href') || ''; + if (alt.includes('/workspace-')) { + zeichen.setAttribute('href', alt.replace('/workspace-', '/crew-')); + } + } } /* Das eigene Bild, wenn es eines gibt -- sonst der Anfangsbuchstabe. @@ -134,7 +170,9 @@ k.type = 'button'; k.id = 'teilen'; k.className = 'teilen'; - k.title = 'Adresse des Workspace weitergeben'; + /* Ohne den Namen des Workspace: Auf der zweiten Adresse heisst der + Bereich anders, und "Adresse dieser Seite" stimmt ueberall. */ + k.title = 'Adresse dieser Seite weitergeben'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 'teilen__zeichen'); @@ -161,9 +199,14 @@ laeuft = true; try { if (kannTeilen) { + /* Der Name kommt aus derselben Quelle wie die Kopfleiste. + Steht dort nichts, bleibt es beim gewohnten Namen. */ + const wie = document.querySelector('.kopfleiste .marke__text .zurueck'); + const name = (wie?.textContent || '').replace(/ /g, ' ').trim() + || 'Creator Workspace'; await navigator.share({ - title: 'Creator Workspace', - text: 'Der Creator Workspace von DogFather Universe', + title: name, + text: `${name} · DogFather Universe`, url: ZIEL, }); } else { diff --git a/workspace/assets/js/teamlage.js b/workspace/assets/js/teamlage.js index 2b96b12c..ae5bcfd8 100644 --- a/workspace/assets/js/teamlage.js +++ b/workspace/assets/js/teamlage.js @@ -121,7 +121,7 @@ braucht. Die Zahlen darunter kann man sich ansehen; hier wartet jemand. - ZWEI ANTWORTEN, und beide kommen beim Modi an. Eine Meldung, auf + ZWEI ANTWORTEN, und beide kommen bei ihm an. Eine Meldung, auf die nichts folgt, sieht fuer ihn aus wie eine, die niemand gelesen hat -- und beim dritten Mal meldet er nichts mehr. ===================================================================== */ diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 53e47ca8..40c3c961 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,12 +12,12 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + @@ -245,10 +245,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index e7ce98a2..9aee69ae 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -140,10 +140,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 3e292d55..9c541579 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -157,14 +157,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index dbf7ed81..d74a7020 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -82,11 +82,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index bad51077..c9ae4f7f 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + @@ -247,10 +247,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index b6f72e22..94142a26 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -172,12 +172,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html new file mode 100644 index 00000000..5dfba821 --- /dev/null +++ b/workspace/crew-index.html @@ -0,0 +1,174 @@ + + + + + +Anmeldung · Team Dogi + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+

+ DogFather + × + Team Dogi +

+

Team Dogi

+

Privater Bereich für DogFather und die Modis.

+
+ + + +
+ + +
+ + +
+ + + + + +
+ +

+ Verschlüsselte Verbindung · Zugriffe werden protokolliert +

+
+
+ + + + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index cc8ec26f..df3c1262 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609101301", + "src": "/assets/img/app-symbole/crew-192.png?v=202609101351", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609101301", + "src": "/assets/img/app-symbole/crew-512.png?v=202609101351", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609101301", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609101351", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609101301", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609101351", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 468653dd..e637bc7f 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -101,10 +101,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index 296f9d04..593b39f1 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 1a7b78e4..a18489db 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -370,11 +370,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 3ec456b1..50882b90 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -217,10 +217,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 13cdc5ed..e6cb61e3 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -335,10 +335,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 7ad0cb5a..9899e5b5 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -207,11 +207,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index f5f089a6..eb5b6020 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -99,11 +99,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 23462aed..a18f18ad 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -132,10 +132,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index cc86eeb4..4719a654 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,14 +12,14 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + @@ -667,10 +667,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index ae6f8cd5..11d5d897 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -78,11 +78,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 7bc535f5..fdd9f6f2 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -119,10 +119,10 @@ - - - - - + + + + + diff --git a/workspace/team.html b/workspace/team.html index e7b9e50d..c3f44157 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -103,10 +103,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index fad33088..182bdff8 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -3,17 +3,17 @@ -Team-Lage · Spicy & Dogi +Eingang · Spicy & Dogi - - - - - - - + + + + + + + @@ -27,7 +27,7 @@ Zurück -

Creator Workspace · Team-Lage

+

Creator Workspace · Eingang

… @@ -50,7 +50,7 @@

Zusammen

-

Die Lage im Team

+

Was auf dich wartet

…

Was hier steht, ist eine Merkliste für euch beide – keine Bewertung @@ -66,8 +66,8 @@ - - - + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 5055b58d..83785e7c 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,15 +12,15 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + @@ -142,11 +142,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index fe61217f..01457b6a 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,13 +12,13 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + @@ -183,10 +183,10 @@ - - - - - + + + + +