diff --git a/assets/img/app-symbole/crew-180.png b/assets/img/app-symbole/crew-180.png new file mode 100644 index 00000000..5d91b7dc Binary files /dev/null and b/assets/img/app-symbole/crew-180.png differ diff --git a/assets/img/app-symbole/crew-192-maskable.png b/assets/img/app-symbole/crew-192-maskable.png new file mode 100644 index 00000000..e5a2e5d3 Binary files /dev/null and b/assets/img/app-symbole/crew-192-maskable.png differ diff --git a/assets/img/app-symbole/crew-192.png b/assets/img/app-symbole/crew-192.png new file mode 100644 index 00000000..2f5d4dbc Binary files /dev/null and b/assets/img/app-symbole/crew-192.png differ diff --git a/assets/img/app-symbole/crew-32.png b/assets/img/app-symbole/crew-32.png new file mode 100644 index 00000000..43d77c51 Binary files /dev/null and b/assets/img/app-symbole/crew-32.png differ diff --git a/assets/img/app-symbole/crew-512-maskable.png b/assets/img/app-symbole/crew-512-maskable.png new file mode 100644 index 00000000..8d45f42b Binary files /dev/null and b/assets/img/app-symbole/crew-512-maskable.png differ diff --git a/assets/img/app-symbole/crew-512.png b/assets/img/app-symbole/crew-512.png new file mode 100644 index 00000000..44d363dd Binary files /dev/null and b/assets/img/app-symbole/crew-512.png differ diff --git a/server/crew-adresse.js b/server/crew-adresse.js index 6453dea8..3450782d 100644 --- a/server/crew-adresse.js +++ b/server/crew-adresse.js @@ -157,6 +157,28 @@ export function crewWeiche(req, res, next) { res.setHeader("X-Robots-Tag", "noindex, nofollow"); + /* DAS MANIFEST MACHT DIE APP (10.09.2026). + + Beide Adressen liefern dieselben HTML-Dateien aus -- in jeder steht + . Auf der + Modi-Adresse soll dahinter aber ein anderes Manifest stehen: eigener + Name ("Team Dogi"), eigenes Symbol. Sonst hiesse die App auf dem + Startbildschirm eines Modis "Creator Workspace" und traege das + Zeichen mit der Chili -- die zu Spicy Media gehoert, nicht zu ihnen. + + WARUM DIE ADRESSE UMGEBOGEN WIRD, STATT IN 30 HTML-DATEIEN EINE + ZWEITE ZEILE ZU SETZEN: Eine zweite Zeile muesste jemand bei jeder + neuen Seite mitschreiben, und wer sie vergisst, bekommt eine Seite, + die still zur falschen App gehoert. Hier steht es einmal. + + ZWISCHENSPEICHER: Cloudflare nimmt den Hostnamen in den Schluessel + auf, die beiden Antworten koennen sich also nicht vermischen. Der + Browser ohnehin nicht -- fuer ihn sind es zwei Ursprunge. */ + if (req.path === "/workspace/app.webmanifest") { + req.url = "/workspace/crew.webmanifest"; + return next(); + } + if (istWorkspacePfad(req.path) || req.path.startsWith("/assets/")) return next(); return res.redirect(302, "/workspace/"); } diff --git a/server/index.js b/server/index.js index f5a904bc..276eabdf 100644 --- a/server/index.js +++ b/server/index.js @@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url"; import { gateMiddleware } from "./gate.js"; import { workspaceUmzug } from "./workspace-umzug.js"; +import { crewWeiche } from "./crew-adresse.js"; import { webdesignGate } from "./webdesign-gate.js"; import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js"; import { workspaceRouter, sichtSetzen } from "./workspace.js"; @@ -112,6 +113,20 @@ app.use((req, res, next) => { unter der alten Adresse nicht mehr erscheint. */ app.use(workspaceUmzug); +/* Die eigene Adresse der Modi-App (10.09.2026): crew.dogfather-universe.com. + Begruendung und die drei Fallstricke stehen in crew-adresse.js -- sie sind + dort als eigene Funktionen pruefbar (pruef-crew-adresse.mjs). + + Steht direkt neben dem Workspace-Umzug und aus demselben Grund FRUEH: Der + Kopf "nicht indizieren" soll auch fuer Manifest, Symbole und die + Zugangswand gelten, nicht erst ab der ersten Schranke. + + WICHTIG: Diese Weiche vergibt KEINE Rechte und nimmt keine. Wer auf + welcher Adresse angemeldet sein darf, entscheidet sitzungLesen() in + workspace.js -- an genau einer Stelle, durch die alle 26 Fachmodule + gehen. Zwei Orte fuer dieselbe Regel waeren einer zu viel. */ +app.use(crewWeiche); + /* Inhaltsrichtlinie (26.08.2026). Steht bewusst HIER -- vor jeder Schranke und vor express.static: Auch die Zugangswand und die 404-Seite sollen sie bekommen, denn gerade dort werden Eingaben diff --git a/server/pruef-crew-adresse.mjs b/server/pruef-crew-adresse.mjs new file mode 100644 index 00000000..d42e0960 --- /dev/null +++ b/server/pruef-crew-adresse.mjs @@ -0,0 +1,335 @@ +/* ===================================================================== + DIE EIGENE ADRESSE DER MODI-APP + + Geprueft wird nicht "die neue Adresse geht", sondern die vier Fragen, + an denen dieser Umbau scheitern kann: + + 1. KOMMT EIN MODI AUF crew. HEREIN -- und sonst niemand? + 2. IST DER WORKSPACE UNVERAENDERT? Derselbe Dienst bedient beide + Adressen. Wer nicht auf den Hostnamen prueft, sperrt mit einer + einzigen Zeile alle Creator, Scouts und Manager aus, sofort nach + dem Neustart und ohne Vorwarnung. + 3. BLEIBEN DIE PRUEFADRESSEN UNBERUEHRT? Waere die Regel "alles + ausser crew", dann wuerden ab sofort sieben andere Pruefdateien + messen, dass ein Modi nirgends hereinkommt -- gruen, weil sie + nichts mehr finden. Ein Lauf, der nichts mehr prueft, ist + schlimmer als ein roter. + 4. HEISST DIE APP AUF DEM HANDY DES MODIS ANDERS? Sonst war der + ganze Umzug umsonst. + + MIT GEGENPROBE. Ganz unten bekommt der Modi in der Datenbank eine + andere Rolle -- danach MUSS ihn dieselbe Sitzung auf crew. nicht mehr + hereinlassen. Ohne diesen Teil waere nicht bewiesen, dass die + Pruefung ueberhaupt anschlagen kann. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request } from "node:http"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-crew-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4371"; +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"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-crew-adresse"); +await new Promise((r) => setTimeout(r, 700)); + +let fehler = 0; +let geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +let offen = 0; +const unklar = (t) => { offen++; console.log(" -- " + t); }; + +/* --------------------------------------------------------------------- + Ein Aufruf mit frei waehlbarem Host-Kopf. + + Bewusst node:http und nicht fetch: `Host` gilt in der Fetch-Norm als + verbotener Kopf, und ob undici das durchlaesst, ist eine Annahme, auf + die diese ganze Pruefung nicht gebaut sein soll. Hier wird der Kopf + nachweislich gesetzt. + ------------------------------------------------------------------ */ +function hole(pfad, { host, methode = "GET", rumpf = null, keks = "" } = {}) { + return new Promise((fertig, schief) => { + const kopf = { Host: host }; + if (rumpf) { kopf["Content-Type"] = "application/json"; kopf["Content-Length"] = Buffer.byteLength(rumpf); } + if (keks) kopf.Cookie = keks; + const a = request({ host: "127.0.0.1", port: 4371, path: pfad, method: methode, headers: kopf }, + (antwort) => { + let text = ""; + antwort.setEncoding("utf8"); + antwort.on("data", (t) => { text += t; }); + antwort.on("end", () => fertig({ + status: antwort.statusCode, + kopf: antwort.headers, + text, + keks: (antwort.headers["set-cookie"] || []) + .map((z) => z.split(";")[0]).join("; "), + })); + }); + a.on("error", schief); + a.setTimeout(15_000, () => { a.destroy(new Error("Zeitueberschreitung")); }); + if (rumpf) a.write(rumpf); + a.end(); + }); +} + +const anmelden = (host, rolle, code) => + hole("/workspace/api/anmelden", { host, methode: "POST", rumpf: JSON.stringify({ rolle, code }) }); + +/* ---------- Personen anlegen ---------------------------------------- */ +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); +const kennung = (code) => createHmac("sha256", schluessel).update(code).digest("hex"); + +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, kennung(code), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} + +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("Schulle", "manager", "CODE-MAN-0001"); +const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); + +const CREW = "crew.dogfather-universe.com"; +const WS = "workspace.dogfather-universe.com"; +const HAUPT = "dogfather-universe.com"; +const LOKAL = "127.0.0.1:4371"; + +/* ===================================================================== + TEIL A -- DIE REGELN, OHNE SERVER + + Sie stehen als eigene Funktionen in crew-adresse.js, damit genau das + hier moeglich ist: die Entscheidung pruefen, nicht ihre Auswirkung. + ===================================================================== */ +console.log("\n--- Die Regeln selbst ---"); +const { istCrewAdresse, istOhneModiAdresse, istWorkspacePfad, sitzungPasstZurAdresse, CREW_ADRESSE } + = await import("./crew-adresse.js"); + +ok(istCrewAdresse(CREW) === true, "crew.dogfather-universe.com ist die Modi-Adresse"); +ok(istCrewAdresse("CREW.Dogfather-Universe.COM") === true, "Gross/klein macht keinen Unterschied"); +ok(istCrewAdresse(CREW + ":443") === true, "ein Port angehaengt macht keinen Unterschied"); +ok(istCrewAdresse(WS) === false, "workspace. ist NICHT die Modi-Adresse"); +ok(istCrewAdresse(HAUPT) === false, "die Hauptseite ist NICHT die Modi-Adresse"); +ok(istCrewAdresse("crew.dogfather-universe.com.boese.de") === false, + "eine fremde Domain mit dem Namen darin zaehlt nicht"); +ok(istCrewAdresse("") === false && istCrewAdresse(null) === false, + "leer und fehlend sind nicht die Modi-Adresse"); + +ok(istOhneModiAdresse(WS) === true, "auf workspace. haben Modis nichts verloren"); +ok(istOhneModiAdresse(HAUPT) === true, "auf der Hauptseite ebenfalls nicht"); +ok(istOhneModiAdresse("www." + HAUPT) === true, "auf www. ebenfalls nicht"); +ok(istOhneModiAdresse(CREW) === false, "crew. gehoert NICHT dazu (Falle 1)"); +ok(istOhneModiAdresse(LOKAL) === false, "127.0.0.1 gehoert NICHT dazu (Falle 2)"); +ok(istOhneModiAdresse("localhost:4100") === false, "localhost gehoert NICHT dazu (Falle 2)"); + +/* Die beiden Mengen duerfen sich nirgends beruehren. Ein Muster statt + einer Liste haette hier sofort angeschlagen. */ +const alleAdressen = [CREW, WS, HAUPT, "www." + HAUPT, LOKAL, "localhost:4100", "git." + HAUPT]; +ok(alleAdressen.every((h) => !(istCrewAdresse(h) && istOhneModiAdresse(h))), + "keine Adresse liegt in beiden Mengen"); + +ok(istWorkspacePfad("/workspace") === true, "/workspace gehoert dazu"); +ok(istWorkspacePfad("/workspace/start.html") === true, "/workspace/start.html gehoert dazu"); +ok(istWorkspacePfad("/workspaceXYZ") === false, "/workspaceXYZ gehoert NICHT dazu (Falle 3)"); +ok(istWorkspacePfad("/workspace-alt") === false, "/workspace-alt gehoert NICHT dazu (Falle 3)"); +ok(istWorkspacePfad("/assets/img/x.png") === false, "/assets liegt ausserhalb"); + +const ROLLEN = ["admin", "spicy", "manager", "scout", "creator", "modi"]; +let passtWahr = 0, passtFalsch = 0; +for (const r of ROLLEN) { + const aufCrew = sitzungPasstZurAdresse(r, CREW); + const aufWs = sitzungPasstZurAdresse(r, WS); + const aufLokal = sitzungPasstZurAdresse(r, LOKAL); + ok(aufCrew === (r === "modi"), `crew.: ${r} ${r === "modi" ? "darf" : "darf nicht"}`); + ok(aufWs === (r !== "modi"), `workspace.: ${r} ${r !== "modi" ? "darf" : "darf nicht"}`); + ok(aufLokal === true, `127.0.0.1: ${r} unveraendert erlaubt`); + for (const w of [aufCrew, aufWs, aufLokal]) { if (w) passtWahr++; else passtFalsch++; } +} +/* Die Gegenprobe zur Regel selbst: Eine Funktion, die immer `true` + liefert, haette alle 18 Zeilen darueber ebenfalls teilweise bestanden. + Beide Antworten muessen vorkommen. */ +ok(passtWahr > 0 && passtFalsch > 0, + `die Regel unterscheidet wirklich (${passtWahr}x ja, ${passtFalsch}x nein)`); + +/* ===================================================================== + TEIL B -- AM LAUFENDEN SERVER + ===================================================================== */ +console.log("\n--- Anmeldung je Adresse ---"); + +const modiAufCrew = await anmelden(CREW, "modi", "CODE-MODI-0001"); +ok(modiAufCrew.status === 200, "crew.: der Modi kommt herein"); +let zielModi = {}; +try { zielModi = JSON.parse(modiAufCrew.text); } catch { /* bleibt leer */ } +ok(zielModi.weiter === "/workspace/start.html", "crew.: er landet auf seiner Startseite"); +ok(modiAufCrew.keks.includes("dfw_sitzung"), "crew.: er bekommt eine Sitzung"); + +/* Der Code des Modis wird bewusst mit der Kachel 'creator' geschickt -- + genau so kommt er auch aus der Zugangswand, denn eine Modi-Kachel + gibt es nicht. */ +const modiOhneKachel = await anmelden(CREW, "creator", "CODE-MODI-0001"); +ok(modiOhneKachel.status === 200, "crew.: die gewaehlte Kachel ist egal (stiller Zugang)"); + +const managerAufCrew = await anmelden(CREW, "manager", "CODE-MAN-0001"); +ok(managerAufCrew.status === 401, "crew.: ein Manager mit gueltigem Code kommt NICHT herein"); +ok(/ungueltig/.test(managerAufCrew.text), + "crew.: er bekommt dieselbe Antwort wie bei einem Tippfehler"); +ok(!managerAufCrew.keks.includes("dfw_sitzung"), "crew.: und keine Sitzung"); + +const adminAufCrew = await anmelden(CREW, "admin", "CODE-DOGI-0001"); +ok(adminAufCrew.status === 401, "crew.: auch DogFather selbst kommt dort nicht herein"); + +const modiAufWs = await anmelden(WS, "creator", "CODE-MODI-0001"); +ok(modiAufWs.status === 200, "workspace.: der Modi bekommt eine Antwort"); +let zielWs = {}; +try { zielWs = JSON.parse(modiAufWs.text); } catch { /* bleibt leer */ } +ok(zielWs.weiter === `${CREW_ADRESSE}/workspace/start.html`, + "workspace.: sie zeigt ihm die neue Adresse"); +ok(!modiAufWs.keks.includes("dfw_sitzung"), + "workspace.: aber KEINE Sitzung -- er meldet sich nur noch auf crew. an"); + +/* DER WICHTIGSTE PUNKT DIESER DATEI. Derselbe Dienst bedient beide + Adressen; ein Denkfehler hier sperrt das ganze Team aus. */ +const managerAufWs = await anmelden(WS, "manager", "CODE-MAN-0001"); +ok(managerAufWs.status === 200 && managerAufWs.keks.includes("dfw_sitzung"), + "workspace.: der Manager kommt herein wie immer -- der Workspace ist unberuehrt"); + +const managerLokal = await anmelden(LOKAL, "manager", "CODE-MAN-0001"); +const modiLokal = await anmelden(LOKAL, "creator", "CODE-MODI-0001"); +ok(managerLokal.status === 200 && managerLokal.keks.includes("dfw_sitzung"), + "127.0.0.1: der Manager kommt herein (Pruefadressen unberuehrt)"); +ok(modiLokal.status === 200 && modiLokal.keks.includes("dfw_sitzung"), + "127.0.0.1: der Modi ebenfalls -- die sieben anderen Modi-Pruefungen messen weiter"); + +/* --------------------------------------------------------------------- + Eine Sitzung gehoert zu genau einer Adresse. + ------------------------------------------------------------------ */ +console.log("\n--- Eine Sitzung, eine Adresse ---"); +const keksModi = modiAufCrew.keks; +const keksManager = managerAufWs.keks; + +const modiDaheim = await hole("/workspace/api/ich", { host: CREW, keks: keksModi }); +ok(modiDaheim.status === 200, "crew.: der angemeldete Modi wird erkannt"); +ok(/Marina/.test(modiDaheim.text), "crew.: und zwar als er selbst"); + +const modiFremd = await hole("/workspace/api/ich", { host: WS, keks: keksModi }); +ok(modiFremd.status === 401, "workspace.: dieselbe Sitzung zaehlt dort nicht"); + +const managerDaheim = await hole("/workspace/api/ich", { host: WS, keks: keksManager }); +ok(managerDaheim.status === 200, "workspace.: der Manager wird erkannt"); + +const managerFremd = await hole("/workspace/api/ich", { host: CREW, keks: keksManager }); +ok(managerFremd.status === 401, "crew.: seine Sitzung zaehlt dort nicht"); + +/* Auch die Seitenschranke, nicht nur die Schnittstelle: Eine Seite, die + ausgeliefert wird, waere ein Loch, selbst wenn die Daten dahinter + leer blieben. */ +const seiteFremd = await hole("/workspace/start.html", { host: CREW, keks: keksManager }); +ok(seiteFremd.status === 302, "crew.: die Startseite schickt den Manager zur Zugangswand"); +const seiteDaheim = await hole("/workspace/start.html", { host: CREW, keks: keksModi }); +ok(seiteDaheim.status === 200, "crew.: dem Modi liefert sie die Seite"); + +/* --------------------------------------------------------------------- + Die Weiche: was es auf crew. ueberhaupt gibt. + ------------------------------------------------------------------ */ +console.log("\n--- Die Weiche ---"); +const wurzel = await hole("/", { host: CREW }); +ok(wurzel.status === 302 && wurzel.kopf.location === "/workspace/", + "crew.: die Hauptseite gibt es dort nicht, es geht zur Zugangswand"); +const wurzelWs = await hole("/", { host: HAUPT }); +ok(wurzelWs.status === 200, "die Hauptseite selbst ist unveraendert erreichbar"); + +const fastWorkspace = await hole("/workspaceXYZ", { host: CREW }); +ok(fastWorkspace.status === 302, "crew.: /workspaceXYZ gehoert nicht dazu (Falle 3)"); + +const robots = await hole("/workspace/", { host: CREW }); +ok(String(robots.kopf["x-robots-tag"] || "").includes("noindex"), + "crew.: nicht indizieren -- die Adresse soll in keiner Suchmaschine stehen"); +const robotsWs = await hole("/workspace/", { host: WS }); +ok(!robotsWs.kopf["x-robots-tag"], + "workspace.: dort steht der Kopf NICHT (Gegenprobe zur Weiche)"); + +/* --------------------------------------------------------------------- + Die App heisst anders. + ------------------------------------------------------------------ */ +console.log("\n--- Zwei Apps, zwei Namen ---"); +const manifestCrew = await hole("/workspace/app.webmanifest", { host: CREW }); +const manifestWs = await hole("/workspace/app.webmanifest", { host: WS }); +let mc = {}, mw = {}; +try { mc = JSON.parse(manifestCrew.text); } catch { /* bleibt leer */ } +try { mw = JSON.parse(manifestWs.text); } catch { /* bleibt leer */ } +ok(manifestCrew.status === 200 && manifestWs.status === 200, + "beide Adressen liefern ein Manifest"); +ok(mc.name === "Team Dogi", `crew.: die App heisst "Team Dogi" (gelesen: ${mc.name || "?"})`); +ok(mw.name === "Creator Workspace — Spicy & Dogi", + `workspace.: dort heisst sie unveraendert (gelesen: ${mw.name || "?"})`); +ok(Array.isArray(mc.icons) && mc.icons.length > 0 + && mc.icons.every((s) => /crew-/.test(s.src)), + `crew.: alle ${(mc.icons || []).length} Symbole sind die eigenen`); +ok(Array.isArray(mw.icons) && mw.icons.length > 0 + && mw.icons.every((s) => /workspace-/.test(s.src)), + "workspace.: dort weiterhin die alten"); +ok(mc.start_url === "/workspace/start.html" && mc.scope === "/workspace/", + "crew.: Bereich und Startseite stimmen"); + +/* Die Symboldateien muessen es auch wirklich geben -- ein Manifest, das + auf nichts zeigt, faellt sonst erst auf dem Startbildschirm auf. */ +let symboleDa = 0; +for (const s of mc.icons || []) { + const pfad = String(s.src).split("?")[0]; + const a = await hole(pfad, { host: CREW }); + if (a.status === 200) symboleDa++; +} +ok((mc.icons || []).length > 0 && symboleDa === (mc.icons || []).length, + `crew.: alle ${symboleDa} Symbole werden auch ausgeliefert`); + +/* ===================================================================== + DIE GEGENPROBE + + Bis hierhin koennte alles auch dann gruen sein, wenn die Regel gar + nicht auf die Rolle sieht. Deshalb wird sie jetzt ausgehebelt: Der + Modi bekommt in der Datenbank die Rolle 'manager'. Seine Sitzung + bleibt dieselbe -- aber auf crew. darf sie ab jetzt NICHT mehr + gelten. + ===================================================================== */ +console.log("\n--- Gegenprobe: die Regel ausgehebelt ---"); +d.prepare("UPDATE personen SET rolle = 'manager' WHERE id = ?").run(idModi); + +const nachher = await hole("/workspace/api/ich", { host: CREW, keks: keksModi }); +ok(nachher.status === 401, + "aus dem Modi wurde ein Manager -- seine Sitzung gilt auf crew. nicht mehr"); +const nachherWs = await hole("/workspace/api/ich", { host: WS, keks: keksModi }); +ok(nachherWs.status === 200, + "und dieselbe Sitzung gilt jetzt auf workspace. -- die Regel sieht wirklich auf die Rolle"); + +d.prepare("UPDATE personen SET rolle = 'modi' WHERE id = ?").run(idModi); +const zurueck = await hole("/workspace/api/ich", { host: CREW, keks: keksModi }); +ok(zurueck.status === 200, "zurueckgedreht gilt sie wieder"); + +/* ---------- Ende ------------------------------------------------------ */ +d.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt die Datei manchmal */ } + +console.log(`\n${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}` + + ` -- ${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`); +process.exit(fehler === 0 ? 0 : 1); diff --git a/server/workspace.js b/server/workspace.js index 90019855..81ad7625 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -27,6 +27,8 @@ import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { mkdirSync } from "node:fs"; import { createRequire } from "node:module"; +import { sitzungPasstZurAdresse, CREW_ADRESSE, istOhneModiAdresse, istCrewAdresse } + from "./crew-adresse.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -2397,6 +2399,24 @@ export function sitzungLesen(req) { db().prepare("DELETE FROM sitzungen WHERE token_hash = ?").run(tokenHash(token)); return null; } + /* DIE ADRESSE ENTSCHEIDET MIT (10.09.2026). + + Seit die Modi-App unter crew.dogfather-universe.com laeuft, gehoert + jede Sitzung zu genau EINER Adresse: Modis dorthin, alle anderen auf + den Workspace. Die Regel steht in crew-adresse.js und ist dort ohne + laufenden Server pruefbar. + + WARUM HIER UND NICHT IN DEN MODULEN: Durch diese Funktion geht jedes + der 26 Fachmodule und jede Seitenschranke. Eine Middleware daneben + koennte man in einem neuen Modul vergessen -- und ein vergessener + Rechteschutz faellt nicht auf, weil dann alles geht. + + `null` heisst hier dasselbe wie bei einer abgelaufenen Sitzung: + nicht angemeldet. Die Seitenschranke schickt zur Zugangswand, die + Schnittstellen antworten mit 401. Beides ist genau richtig -- auf + dieser Adresse ist diese Person schlicht niemand. */ + if (!sitzungPasstZurAdresse(reihe.rolle, req.get?.("host"))) return null; + return { id: reihe.id, name: reihe.name, rolle: reihe.rolle }; } catch { return null; } } @@ -2697,6 +2717,33 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => { WEN man pruefen soll -- danach entscheidet scrypt wie ueberall. */ const still = codeBrauchbar ? stillerZugang(code) : null; if (still) { + /* AUF DER ALTEN ADRESSE GIBT ES KEINE SITZUNG MEHR (10.09.2026). + + Die Modi-App hat seit heute eine eigene Adresse. Wer seinen Code + noch auf workspace.dogfather-universe.com eintippt, bekommt hier + den Weg dorthin -- und ausdruecklich KEINE Anmeldung: zwei + Sitzungen auf zwei Adressen waeren zwei Apps auf dem + Startbildschirm und eine zweite Spur genau dort, wo alle anderen + arbeiten. + + DASS DIE ANTWORT DIE NEUE ADRESSE NENNT, IST KEIN LECK. Sie + kommt nur nach Eingabe eines gueltigen Modi-Codes; wer den hat, + ist Modi. Fuer jeden anderen -- auch fuer einen Manager mit + gueltigem eigenen Code -- veraendert sich hier gar nichts. + + Kein Eintrag in `versuche`: Der Code war richtig. Ein Modi, der + die alte Verknuepfung auf dem Handy hat, wuerde sich sonst mit + jedem Versuch selbst aussperren. */ + if (istOhneModiAdresse(req.get("host"))) { + protokolliere("anmeldung_alte_adresse", { + personId: still.id, rolle: still.rolle, ip, detail: still.name, + }); + return res.json({ + weiter: `${CREW_ADRESSE}/workspace/start.html`, + name: still.name, rolle: still.rolle, umgezogen: true, + }); + } + db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip); db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), still.id); sitzungSetzen(res, still, req); @@ -2711,7 +2758,24 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => { /* Ab hier der gewohnte Weg -- unveraendert. 'modi' ist keine Kachel-Rolle und faellt deshalb in dieselbe Antwort wie eine erfundene. */ - if (!ROLLEN_KACHEL.has(rolle) || !codeBrauchbar) { + /* AUF DER MODI-ADRESSE ENDET DER GEWOHNTE WEG HIER (10.09.2026). + + crew.dogfather-universe.com ist nicht die zweite Tuer in den + Workspace. Wer sie errät und seinen eigenen Code eintippt, soll + genau das erleben, was er bei einem Tippfehler erlebt: Die + Zugangswand sieht aus wie immer, der Code stimmt nicht. Wuerde er + stattdessen hereinkommen, wuesste er im selben Moment, dass es + hier noch etwas gibt. + + BEWUSST IN DIESELBE BEDINGUNG GESCHRIEBEN und nicht als eigener + Block davor: So ist es nicht nur dieselbe ANTWORT, sondern + derselbe Weg -- ein Eintrag in `versuche`, kein scrypt-Durchlauf, + 401 "ungueltig". Ein eigener Block waere messbar schneller oder + langsamer gewesen als der Weg daneben, und genau daran erkennt + man eine Sonderbehandlung. */ + const fremdAufCrew = istCrewAdresse(req.get("host")); + + if (!ROLLEN_KACHEL.has(rolle) || !codeBrauchbar || fremdAufCrew) { db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt()); return res.status(401).json({ fehler: "ungueltig" }); } diff --git a/tools/crew-symbol.mjs b/tools/crew-symbol.mjs new file mode 100644 index 00000000..e68ac009 --- /dev/null +++ b/tools/crew-symbol.mjs @@ -0,0 +1,260 @@ +/* =================================================================== + Das App-Symbol der Modi-App ("Team Dogi", 10.09.2026). + + DER HUSKY IST DERSELBE. Filipe am 09.09.2026 zum Rollenbild: + "das modi symbol soll das gleiche sein wie bei dogfather." Das gilt + hier genauso -- es ist dieselbe Vorlage, dieselbe Technik. + + DIE CHILI IST ES NICHT, UND ZWAR ABSICHTLICH. Sie steht fuer Spicy + Media. Die Modis gehoeren nicht dorthin; ihre Marke heisst "Team + Dogi". Eine Chili auf ihrem Startbildschirm waere kein Schmuck, + sondern eine falsche Aussage. An ihre Stelle tritt der Ton, der auch + ihre Kacheln traegt: das gedeckte Stahlblau #5f8a9f. + + WARUM EINE EIGENE DATEI UND KEIN SCHALTER IN workspace-symbol.mjs: + Jenes Werkzeug PRUEFT, dass die Chili in jeder Fassung sichtbar ist. + Ein Schalter haette diese Pruefung fuer den halben Aufruf abschalten + muessen -- und eine Pruefung, die man abschalten kann, ist bald + abgeschaltet. Zwei Bilder mit zwei Regeln bekommen zwei Werkzeuge, + die jeweils ihre eigenen Regeln durchsetzen. + + DIE DRITTE GEGENPROBE, DIE ES DORT NICHT GIBT: Am Ende wird das + erzeugte Symbol gegen das des Workspace gehalten. Waeren beide + gleich, haette der ganze Umzug seinen Sinn verloren -- zwei Apps mit + demselben Zeichen sind auf einem Startbildschirm dasselbe Symbol + zweimal. Gemessen wird das, nicht behauptet. + + Aufrufen mit: node tools/crew-symbol.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 HUSKY = join(WURZEL, "workspace", "assets", "img", "marke-husky.webp"); +/* Umlenkbar wie beim Schwesterwerkzeug: Ein Symbol beurteilt man, indem + man es ansieht -- und dafuer darf man die echten Dateien nicht bei + jedem Versuch ueberschreiben. */ +const ZIEL = process.env.SYMBOL_ZIEL || join(WURZEL, "assets", "img", "app-symbole"); +/* Zum Vergleichen -- immer die echten Symbole, auch wenn das Ziel + umgelenkt ist. Fehlt eines, ist die dritte Gegenprobe NICHT + durchfuehrbar, und das ist ein Fehler, kein Ueberspringen. */ +const VERGLEICH = join(WURZEL, "assets", "img", "app-symbole"); + +if (!existsSync(HUSKY)) { console.error(`Fehlt: ${HUSKY}`); process.exit(1); } +const alsDaten = (p) => "data:image/webp;base64," + readFileSync(p).toString("base64"); + +const GROESSEN = [ + { n: 512, endung: "512" }, + { n: 192, endung: "192" }, + { n: 512, endung: "512-maskable", maskable: true }, + { n: 192, endung: "192-maskable", maskable: true }, + { n: 180, endung: "180" }, + { n: 32, endung: "32" }, +]; + +function html(n, maskable, husky, weglassen = "") { + /* KLEIN = alles unter 64 px. Dort faellt die Gesichtszeichnung weg: + achtfach vergroessert verglichen ist der Kopf mit Zeichnung bei + 32 px ein dunkler Fleck, als reine Silhouette ein Hund. Dieselbe + Messung wie beim Workspace-Symbol, dasselbe Ergebnis. */ + const klein = n < 64; + const rand = Math.round(n * 0.22); + /* ALLES IN EINER GRUPPE, damit fuer die maskable-Fassung genau EINE + Zahl kleiner wird und nicht mehrere auseinanderlaufen. 0,74 haelt + alles im 80-%-Sicherheitskreis, den Android als einziges garantiert. + Ohne Chili darf der Kopf etwas groesser stehen -- er ist hier allein + und muss die Flaeche selbst tragen. */ + const g = Math.round(n * (maskable ? 0.74 : 0.96)); + const ecke = maskable ? "" : `border-radius:${rand}px;`; + + return `
+ +
`; +} + +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(); +if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true }); + +const husky = alsDaten(HUSKY); +let gebaut = 0; +const maengel = []; + +async function aufnehmen(n, maskable, weglassen = "") { + await seite.setViewportSize({ width: n, height: n }); + await seite.setContent(html(n, maskable, husky, weglassen), { waitUntil: "networkidle" }); + return await seite.screenshot({ omitBackground: false }); +} + +/* Streuung der Helligkeit. Ein leeres Feld hat keine -- das beweist, + dass ueberhaupt etwas draufsteht, mehr nicht. Die Bytezahl beweist + auch das nicht: ein einfarbiges PNG ist genauso gross wie ein volles. */ +async function streuungVon(bild) { + return await seite.evaluate(async (b64) => { + const b = new Image(); b.src = "data:image/png;base64," + b64; await b.decode(); + const c = document.createElement("canvas"); + c.width = b.width; c.height = b.height; + const g2 = c.getContext("2d"); g2.drawImage(b, 0, 0); + const d = g2.getImageData(0, 0, c.width, c.height).data; + let s = 0, s2 = 0, m = 0; + for (let i = 0; i < d.length; i += 4) { + const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]; + s += h; s2 += h * h; m++; + } + return Math.sqrt(s2 / m - (s / m) ** 2); + }, bild.toString("base64")); +} + +/* Anteil der Bildpunkte, die sich sichtbar unterscheiden. Acht Stufen + Abstand, damit Kantenglaettung allein den Wert nicht hochtreibt. + -1 heisst "nicht vergleichbar" (verschiedene Groessen) und ist + ausdruecklich etwas anderes als 0. */ +async function unterschied(a, b) { + return await seite.evaluate(async ([x, y]) => { + const laden = async (b64) => { + const b = new Image(); b.src = "data:image/png;base64," + b64; await b.decode(); + const c = document.createElement("canvas"); + c.width = b.width; c.height = b.height; + c.getContext("2d").drawImage(b, 0, 0); + return c.getContext("2d").getImageData(0, 0, c.width, c.height).data; + }; + const [p, q] = [await laden(x), await laden(y)]; + if (p.length !== q.length) return -1; + let anders = 0, m = 0; + for (let i = 0; i < p.length; i += 4) { + 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]), Math.abs(p[i + 3] - q[i + 3])); + if (w > 8) anders++; + m++; + } + return anders / m; + }, [a.toString("base64"), b.toString("base64")]); +} + +let verglichen = 0; + +for (const g of GROESSEN) { + const bild = await aufnehmen(g.n, !!g.maskable); + writeFileSync(join(ZIEL, `crew-${g.endung}.png`), bild); + gebaut++; + + const streuung = await streuungVon(bild); + if (streuung < 12) maengel.push(`${g.endung}: kaum Zeichnung (Streuung ${streuung.toFixed(1)})`); + + const klein = g.n < 64; + const ohneZeichnung = await unterschied(bild, await aufnehmen(g.n, !!g.maskable, "zeichnung")); + + if (klein) { + /* Unter 64 px ist die schlichte Silhouette Absicht. Geprueft wird, + dass sie es auch bleibt -- sonst waere die Regel bloss ein + Kommentar, der beim naechsten Umbau stillschweigend kippt. */ + if (ohneZeichnung > 0.001) maengel.push(`${g.endung}: klein soll ohne Gesichtszeichnung sein`); + } else if (ohneZeichnung < 0.04) { + maengel.push(`${g.endung}: das Gesicht kommt nicht durch (${(ohneZeichnung * 100).toFixed(1)} %)`); + } + + /* DRITTE GEGENPROBE: anders als das Workspace-Symbol. */ + const gegen = join(VERGLEICH, `workspace-${g.endung}.png`); + let anders = -2; + if (!existsSync(gegen)) { + maengel.push(`${g.endung}: Vergleichsbild workspace-${g.endung}.png fehlt ` + + `-- nicht nachsehbar, ob sich die Symbole unterscheiden`); + } else { + anders = await unterschied(bild, readFileSync(gegen)); + verglichen++; + if (anders < 0) { + maengel.push(`${g.endung}: Vergleichsbild hat eine andere Groesse -- nicht vergleichbar`); + } else if (anders < 0.10) { + maengel.push(`${g.endung}: kaum vom Workspace-Symbol zu unterscheiden ` + + `(${(anders * 100).toFixed(1)} %)`); + } + } + + console.log(` crew-${g.endung}.png`.padEnd(30) + + `${(bild.length / 1024).toFixed(1)} KB · Streuung ${streuung.toFixed(1)}` + + ` · Gesicht ${(ohneZeichnung * 100).toFixed(1)} %` + + ` · anders als Workspace ${anders < 0 ? "?" : (anders * 100).toFixed(1) + " %"}`); +} +await browser.close(); + +/* Die Zahlen gehoeren in die Bedingung, nicht nur in den Bericht. */ +if (gebaut !== GROESSEN.length) { + console.error(`\nFEHLER: ${gebaut} von ${GROESSEN.length} erzeugt.`); process.exit(1); +} +if (verglichen !== GROESSEN.length) { + console.error(`\nFEHLER: nur ${verglichen} von ${GROESSEN.length} gegen das ` + + `Workspace-Symbol gehalten.`); process.exit(1); +} +if (maengel.length) { + console.error("\nFEHLER:\n " + maengel.join("\n ")); process.exit(1); +} +console.log(`\n${gebaut} Symbole erzeugt. Der Husky ist derselbe wie bei DogFather,` + + ` das Gesicht ist in jeder grossen Fassung nachweisbar sichtbar, und alle` + + ` ${verglichen} unterscheiden sich messbar vom Workspace-Symbol.`); diff --git a/tools/workspace-stempel.mjs b/tools/workspace-stempel.mjs index 631f135f..8d0beb94 100644 --- a/tools/workspace-stempel.mjs +++ b/tools/workspace-stempel.mjs @@ -20,7 +20,7 @@ Gehoert vor jeden Deploy, der JS oder CSS im Workspace anfasst. ===================================================================== */ -import { readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -88,11 +88,24 @@ for (const name of readdirSync(ORDNER)) { Derselbe Stempel wie in den Seiten -- `pruef-zwischenspeicher` verlangt im ganzen Haus genau eine Zahl und wuerde eine zweite sofort melden. */ -const MANIFEST = join(ORDNER, "app.webmanifest"); +/* ES SIND ZWEI MANIFESTE (10.09.2026). Seit die Modi-App unter ihrer + eigenen Adresse laeuft, gibt es neben app.webmanifest auch + crew.webmanifest -- dasselbe Haus, anderer Name, andere Symbole. + + ALS LISTE UND NICHT ALS ZWEITER BLOCK: Ein kopierter Block waere beim + dritten Manifest ein dritter, und irgendwann bleibt einer stehen. Ein + fehlendes Manifest ist hier ausserdem ein FEHLER und kein + Ueberspringen -- sonst faellt das Ausbleiben nicht auf. */ +const MANIFESTE = ["app.webmanifest", "crew.webmanifest"]; const MUSTER_MANIFEST = /("src":\s*")(\/assets\/img\/app-symbole\/[A-Za-z0-9._-]+)(?:\?v=\d+)?(")/g; -{ - const alt = readFileSync(MANIFEST, "utf8"); +for (const name of MANIFESTE) { + const pfad = join(ORDNER, name); + if (!existsSync(pfad)) { + console.error(`FEHLER: ${name} fehlt -- erwartet in ${ORDNER}.`); + process.exit(1); + } + const alt = readFileSync(pfad, "utf8"); let n = 0; const neu = alt.replace(MUSTER_MANIFEST, (_, vor, datei, nach) => { n += 1; @@ -102,11 +115,11 @@ const MUSTER_MANIFEST = Dann hat jemand das Manifest umgebaut, und ab da laeuft das Symbol still auf dem alten Stand weiter. */ if (n === 0) { - console.error("FEHLER: im Manifest keine Symboladresse gefunden -- Muster pruefen."); + console.error(`FEHLER: in ${name} keine Symboladresse gefunden -- Muster pruefen.`); process.exit(1); } - if (neu !== alt) { writeFileSync(MANIFEST, neu); dateien += 1; stellen += n; } - console.log(` app.webmanifest ${n} Verweise`); + if (neu !== alt) { writeFileSync(pfad, neu); dateien += 1; stellen += n; } + console.log(` ${name.padEnd(20)} ${n} Verweise`); } console.log(`\nStempel ${STEMPEL} auf ${stellen} Verweise in ${dateien} Dateien gesetzt.`); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 993efac8..83597fce 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=202609100327", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609101301", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609100327", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609101301", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100327", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609101301", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100327", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609101301", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 9ae75548..53e47ca8 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 e1de7a1c..e7ce98a2 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 6952e80b..3e292d55 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 fc06079c..dbf7ed81 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 aa51b106..bad51077 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 ad347581..b6f72e22 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.webmanifest b/workspace/crew.webmanifest new file mode 100644 index 00000000..cc8ec26f --- /dev/null +++ b/workspace/crew.webmanifest @@ -0,0 +1,40 @@ +{ + "name": "Team Dogi", + "short_name": "Team Dogi", + "description": "Aufgaben, Termine, Ideen und Angebote an einem Ort. Nur mit persönlichem Zugangscode.", + "start_url": "/workspace/start.html", + "id": "/workspace/start.html", + "scope": "/workspace/", + "display": "standalone", + "orientation": "any", + "lang": "de", + "dir": "ltr", + "background_color": "#0a121e", + "theme_color": "#06090f", + "icons": [ + { + "src": "/assets/img/app-symbole/crew-192.png?v=202609101301", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/img/app-symbole/crew-512.png?v=202609101301", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609101301", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609101301", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/workspace/dateien.html b/workspace/dateien.html index 885b1408..468653dd 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 e19260c1..296f9d04 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 98522088..1a7b78e4 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 2750702e..3ec456b1 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 c8b65afa..13cdc5ed 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 1a685cf2..7ad0cb5a 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 59375e4f..f5f089a6 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 45766ee4..23462aed 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 fd372b12..cc86eeb4 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 57e205d7..ae6f8cd5 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 9d707f84..7bc535f5 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 703c4260..e7b9e50d 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 4444d62c..fad33088 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -66,8 +66,8 @@ - - - + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 86ede86b..5055b58d 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 849eb9f0..fe61217f 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 @@ - - - - - + + + + +