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 @@
-
-
-
-
-
+
+
+
+
+