Drei Dinge, die zusammengehoeren, weil sie alle denselben Satz aus
Kapitel 01 des Bauplans beantworten: Wer neu ist, soll nicht alles
gleich wichtig vorgesetzt bekommen.
1) DIE KACHEL STEHT BEIM TEAM OBEN (Kapitel 03)
Gemessen stand "Willkommen & Anleitung" bei einem Modi in der Gruppe
"Community" -- der FUENFTEN von sechs, hinter allen Brettern, die er
moderiert. Der Plan sagt dazu "zusaetzlich als erste Kachel in 'Fuer
dich', weil sie bei ihnen sonst weit unten steht".
Ich habe sie VERSCHOBEN statt verdoppelt, und das ist eine bewusste
Abweichung vom Wortlaut: Zwei Kacheln mit demselben Namen und
demselben Ziel hatte dieses Haus am 19.09. schon einmal (zweimal
"Chat"), und der Satz, der daraus wurde, steht seitdem im Quelltext --
"Zwei gleich benannte Wege zum selben Ort sind schlimmer als ein
fehlender". Das Ziel des Plans ist so erreicht, der Nebeneffekt bleibt
aus. Fuer die Community aendert sich nichts: Bei ihr ist "Community"
die erste Gruppe, dort steht sie schon vorne und breit.
Entschieden wird das an AUSSEN_ROLLEN, nicht an Rollennamen.
2) "NEU FUER DICH" BEIM AUFSTIEG (Kapitel 10, Etappe 6)
Erreicht ein Mitglied "Dabei" oder "Stamm", steht auf der Zentrale
einmal eine Karte mit dem, was jetzt geht, und einem Sprung auf die
passende Karte der Anleitung.
WAS JETZT GEHT, IST ABGELEITET -- aus TREFF_SCHREIBEN, derselben
Tafel, die es auch ENTSCHEIDET. Eine Liste im Code haette beim
naechsten neuen Brett entweder etwas versprochen, das die Person gar
nicht darf, oder verschwiegen, was sie duerfte.
Der Schluessel ist (Person, STUFE) und nicht (Person): Wer von Neu auf
Dabei steigt, bekommt die Karte, und Wochen spaeter beim Sprung auf
Stamm noch einmal, mit anderem Inhalt. Eine einzelne Spalte "schon
gesehen" haette den zweiten Aufstieg verschluckt -- und das waere
niemandem aufgefallen, es fehlt ja nur etwas.
Gemerkt wird beim ZEIGEN, nicht beim Wegklicken. Dieselbe Entscheidung
wie bei der Begruessung, aus demselben gemessenen Grund.
Dazu springt `anleitung.html?karte=<schluessel>` jetzt auf eine
bestimmte Karte. Erst beim Messen fiel auf, dass der Sprung ins Leere
ging, obwohl Karte und Abschnitt richtig waren: Unter den Karten
werden danach noch drei Abschnitte eingehaengt, und das sanfte
Scrollen zielte auf eine Hoehe, die sich dabei verschob. Der Sprung
steht jetzt ganz am Ende des Aufbaus.
3) DAS ABGELOESTE WILLKOMMEN IST WEG
willkommen.html ist seit heute frueh eine Weiterleitung. Der Unterbau
lief aber weiter: eine Schnittstelle, die jede Kachel einer Rolle
gleich ausfuehrlich erklaerte (genau das, was Kapitel 01 als Problem
beschreibt), dazu ein Skript und ein Stilblatt, die keine Seite mehr
lud. Entfernt: server/workspace-willkommen.js, assets/js/willkommen.js,
assets/css/willkommen.css und der Router in index.js.
Aufgefallen ist es, weil pruef-willkommen rot wurde und danach in ihre
Notbremse lief -- eine Pruefung, die eine zurueckgenommene Regel
verteidigt. Sie ist mitgegangen: Die inhaltlichen Fragen beantwortet
jetzt pruef-anleitung; hier bleibt die eine, die sonst niemand stellt
("verliert die alte Adresse jemanden?") plus der Nachweis, dass das
Alte wirklich fort ist. 15 Pruefungen statt der alten Fassung, und der
Rueckgang ist damit erklaert.
Nebenbefund: tools/kachel-regenbogen.mjs verwies auf pruef-willkommen
als Wache ueber den gerechneten CSS-Block. Die Datei hat ihn nie
angesehen -- die Wache ist pruef-kachelfarben. Zeiger berichtigt.
AUSSERDEM: .zeileneditor__weg war 34x34 und das einzige ungedeckte
Tippziel des Hauses (pruef-tippziele war deshalb rot). Er wirft eine
Zeile aus einem Eintrag. Auf Fingergeraeten jetzt 44x44, das Kreuz
mitgerechnet statt geraten. Der Knopf stammt aus einer anderen
Sitzung; ich habe ihn Filipe gemeldet und keine Antwort bekommen --
eine rote Pruefung, die liegen bleibt, wird nach dem zweiten Mal
weggeklickt, deshalb jetzt behoben statt weiter gemeldet.
GEPRUEFT
pruef-anleitung 137 -> 166 (neu: Kachelplatz je Rolle mit
Gegenprobe aufs Agenturhaus, der ganze
Aufstiegsweg inkl. ZWEITEM Aufstieg, und die
Abnahme-Checkliste aus Kapitel 13 -- 14/29/29/34
Karten MIT ihrer Aufteilung 5/4/5, 7/7/15,
7/7/15, 8/7/19 und "Nicht deins")
pruef-willkommen neu gefasst, 15, gruen
pruef-sackgassen 14, 145 Kacheln, 0 ins Leere
pruef-start-ansicht gruen, keine Konsolenfehler
pruef-rollen 454, gruen (das eine "nicht nachsehbar" ist das
Haus-Wechsel-Feld und war immer so)
pruef-tippziele 13, jetzt 0 Fehlschlaege
Gegenproben gefahren: Umsortierung ausgebaut -> drei FEHL (und die
Meldung zeigte den alten Zustand, Gruppe 5 von 6). Datenbank vor der
Schemaaenderung gesichert (sicherungen/vor-aufstieg-20261006-172614.db,
integrity_check ok).
Im Browser nachgemessen (echte Anmeldung, 390 px, HTTPS-Vorbau):
Karte da, Satz richtig, drei Wege, 0 zu kleine Tippziele, Sprung
landet auf der richtigen Karte, Abschnitt aufgeklappt, im Bild -- und
beim zweiten Aufruf ist sie weg.
Co-Authored-By: Claude Opus 5 <[email protected]>
628 lines
30 KiB
JavaScript
628 lines
30 KiB
JavaScript
/* =====================================================================
|
||
index.js — Haupteinstiegspunkt für DogFather Universe. Liefert die statische Website aus
|
||
(../ ist der Ordner mit allen .html-Dateien) plus die Zugangsschranke (gate.js), 1:1 wie
|
||
vorher der Cloudflare Worker (gate-worker.js).
|
||
===================================================================== */
|
||
|
||
import "dotenv/config";
|
||
import express from "express";
|
||
import cookieParser from "cookie-parser";
|
||
import { dirname, join } from "node:path";
|
||
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";
|
||
import { aufgabenRouter } from "./workspace-aufgaben.js";
|
||
import { zuteilungRouter } from "./workspace-zuteilung.js";
|
||
import { materialRouter } from "./workspace-material.js";
|
||
import { personenRouter } from "./workspace-personen.js";
|
||
import { profilRouter } from "./workspace-profil.js";
|
||
import { kalenderRouter } from "./workspace-kalender.js";
|
||
import { serienRouter, serienStarten } from "./workspace-serien.js";
|
||
import { dateienRouter } from "./workspace-dateien.js";
|
||
import { bereicheRouter } from "./workspace-bereiche.js";
|
||
import { treffRouter } from "./workspace-treff.js";
|
||
import { entwicklungRouter } from "./workspace-entwicklung.js";
|
||
/* Die Auswertung UEBER den Katalog -- eigene Datei, eigener Gegenstand.
|
||
Sie liest nur; geschrieben wird die Spur in workspace-entwicklung.js. */
|
||
import { werdegangRouter } from "./workspace-werdegang.js";
|
||
import { rechteRouter, tafelLaden } from "./workspace-rechte.js";
|
||
import { aufbewahrungRouter, aufbewahrungStarten } from "./workspace-aufbewahrung.js";
|
||
import { auskunftRouter } from "./workspace-auskunft.js";
|
||
import { uebernahmeRouter } from "./workspace-uebernahme.js";
|
||
import { befindenRouter } from "./workspace-befinden.js";
|
||
import { hilfeRouter } from "./workspace-hilfe.js";
|
||
import { supportRouter } from "./workspace-support.js";
|
||
import { notizenRouter } from "./workspace-notizen.js";
|
||
import { reaktionRouter } from "./workspace-reaktion.js";
|
||
import { spendenRouter } from "./workspace-spenden.js";
|
||
import { buehneRouter } from "./workspace-buehne.js";
|
||
import { unterstuetzungRouter } from "./workspace-unterstuetzung.js";
|
||
import { reportRouter } from "./workspace-reports.js";
|
||
import { scoutRouter } from "./workspace-scouts.js";
|
||
import { callRouter } from "./workspace-calls.js";
|
||
import { hinweisRouter } from "./workspace-hinweise.js";
|
||
import { zentraleRouter } from "./workspace-zentrale.js";
|
||
import { sucheRouter } from "./workspace-suche.js";
|
||
import { startcheckRouter } from "./workspace-startcheck.js";
|
||
import { wissenRouter } from "./workspace-wissen.js";
|
||
import { kiRouter } from "./workspace-ki.js";
|
||
import { contentRouter } from "./workspace-content.js";
|
||
import { sicherungRouter, sicherungStarten } from "./workspace-sicherung.js";
|
||
import { steckbriefRouter } from "./workspace-steckbrief.js";
|
||
import { draussenRouter } from "./workspace-draussen.js";
|
||
import { vorlagenRouter } from "./workspace-vorlagen.js";
|
||
/* Die Arbeitslage von Managern und Scouts -- nur fuer Spicy Media und
|
||
DogFather. Sie ruehrt Termine, Chats und Dateien nicht an; die
|
||
Begruendung steht im Kopf der Datei. */
|
||
import { teamRouter } from "./workspace-team.js";
|
||
import { schulungRouter } from "./workspace-schulung.js";
|
||
import { ampelRouter } from "./workspace-ampel.js";
|
||
import { checklisteRouter } from "./workspace-checkliste.js";
|
||
import { teamlageRouter } from "./workspace-teamlage.js";
|
||
import { pushRouter, pushStarten } from "./workspace-push.js";
|
||
import { chatRouter, toeneNachruestenStarten } from "./workspace-chat.js";
|
||
import { anrufRouter } from "./workspace-anruf.js";
|
||
import { videoRouter, videoWacheStarten } from "./workspace-video.js";
|
||
import { bewerbungRouter } from "./workspace-bewerbung.js";
|
||
import { leistungRouter } from "./workspace-leistung.js";
|
||
import { managerZieleRouter } from "./workspace-manager-ziele.js";
|
||
import { anleitungRouter } from "./workspace-anleitung.js";
|
||
import { fruehwarnungRouter } from "./workspace-fruehwarnung.js";
|
||
import { icsRouter } from "./workspace-ics.js";
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const SITE_DIR = join(__dirname, "..");
|
||
const PORT = Number(process.env.PORT || 4100);
|
||
|
||
const app = express();
|
||
app.set("trust proxy", 1);
|
||
app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack
|
||
|
||
/* Sicherheits-Header, ergänzt 05.08.2026. Vorher standen diese in der Datei `_headers` — die
|
||
funktioniert aber NUR bei Cloudflare Pages/Netlify und wurde auf dem eigenen Server schlicht
|
||
ignoriert, d.h. die Seite lief seit dem Umzug komplett ohne diese Schutzmaßnahmen (nachgeprüft
|
||
an den Live-Antwortheadern). */
|
||
app.use((req, res, next) => {
|
||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||
/* MIKROFON UND KAMERA SIND GESPERRT -- AUSSER IM CHAT (18.09.2026).
|
||
|
||
Diese Sperre ist kein Versehen, sie ist der Schutz: Ohne sie
|
||
koennte jedes eingeschleuste Skript auf JEDER Seite das Mikrofon
|
||
anschalten. Sie bleibt deshalb ueberall stehen und wird an genau
|
||
EINER Stelle geoeffnet -- dort, wo telefoniert wird.
|
||
|
||
GEFUNDEN HAT DAS DIE PRUEFUNG, nicht das Lesen: Der Anrufkasten
|
||
meldete "Der Browser laesst das Mikrofon nicht zu", und in der
|
||
Konsole stand "microphone is not allowed in this document". Ohne
|
||
diesen Lauf waere der Anruf live gegangen und haette bei jedem
|
||
versagt -- mit einer Meldung, die auf die falsche Faehrte fuehrt
|
||
(man sucht dann an den Browsereinstellungen).
|
||
|
||
`(self)` und nicht `*`: Nur diese Seite selbst, keine
|
||
eingebetteten Fremdinhalte. */
|
||
/* DIE ZWEITE STELLE KAM AM 28.09.2026 DAZU: die Reaction. Sie
|
||
zeigt die Kamera des Hosts und bis zu zwei Gaeste, ueber
|
||
denselben Weg wie der Anruf im Chat.
|
||
|
||
Und sie kam als MENGE dazu, nicht als zweiter Sonderfall: Beim
|
||
Chat stand die Bedingung als Einzelfall im Code, und ein zweiter
|
||
Einzelfall daneben ist die Bauweise, bei der der dritte
|
||
vergessen wird. Vergessen heisst hier: Die Sendung startet, alle
|
||
sind da, und die Kamera geht nicht an -- mit einer Meldung, die
|
||
auf die falsche Faehrte fuehrt.
|
||
|
||
pruef-kamera-richtlinie.mjs haelt die Menge gegen den Quelltext:
|
||
Jede Seite, deren Skripte `getUserMedia` aufrufen, muss hier
|
||
stehen -- und jede andere darf es nicht. */
|
||
const MIT_KAMERA = [/^\/workspace\/chat\.html/, /^\/workspace\/reaktion\.html/];
|
||
const darfKamera = MIT_KAMERA.some((m) => m.test(req.path));
|
||
res.setHeader("Permissions-Policy",
|
||
darfKamera
|
||
? "geolocation=(), microphone=(self), camera=(self), payment=()"
|
||
: "geolocation=(), microphone=(), camera=(), payment=()");
|
||
|
||
/* HSTS NUR ÜBER EINE GESICHERTE VERBINDUNG (Befund 04.09.2026, Audit).
|
||
|
||
RFC 6797, Abschnitt 7.2, ist an dieser Stelle unmissverständlich:
|
||
"An HSTS Host MUST NOT include the STS header field in HTTP
|
||
responses conveyed over non-secure transport."
|
||
|
||
Vorher ging der Header bei JEDER Antwort mit, auch über http. Live
|
||
fällt das nicht auf -- hinter Cloudflare und Caddy kommt ohnehin
|
||
alles über https an, und `trust proxy` macht `req.secure` dort zu
|
||
`true`. Aufgefallen ist es beim Browsertest: WebKit (Safari) nimmt
|
||
den Header auch von einer http-Adresse an, merkt sich 127.0.0.1 als
|
||
"nur noch https" -- und ab da scheitert jede Verbindung zum lokalen
|
||
Testserver mit "SSL connect error". Chromium und Firefox machen für
|
||
localhost eine Ausnahme, WebKit nicht.
|
||
|
||
Der Fehler war also nie in der Website zu sehen, hätte aber jede
|
||
künftige Safari-Prüfung unmöglich gemacht -- und das ist genau der
|
||
Browser, den jedes iPhone benutzt.
|
||
|
||
BEWUSST NICHT NUR `req.secure`. Das wäre die reine Lehre, hängt
|
||
aber daran, dass Caddy X-Forwarded-Proto wirklich setzt. Sollte das
|
||
einmal nicht so sein, fiele HSTS auf der echten Domain STILL weg --
|
||
eine Sicherheitsverschlechterung, die niemandem auffällt. Ein
|
||
Audit-Fix darf nicht die Möglichkeit schaffen, dass er selbst
|
||
Schaden anrichtet.
|
||
|
||
Deshalb die umgekehrte Bedingung: Weggelassen wird der Header nur
|
||
dort, wo er nachweislich nicht hingehört -- auf einer unsicheren
|
||
Verbindung zu einem LOKALEN Namen. Auf der echten Domain geht er
|
||
immer mit, egal was der Proxy meldet. */
|
||
const host = (req.get("host") || "").toLowerCase();
|
||
const lokal = /^(localhost|127\.\d+\.\d+\.\d+|\[::1\]|0\.0\.0\.0)(:\d+)?$/.test(host);
|
||
if (req.secure || !lokal) {
|
||
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
||
}
|
||
next();
|
||
});
|
||
|
||
/* Umzug des Creator Workspace auf eine eigene Adresse (06.09.2026).
|
||
Begruendung und die beiden Fallstricke stehen in workspace-umzug.js —
|
||
sie sind dort als eigene Funktion pruefbar (pruef-workspace-umzug.mjs).
|
||
Steht bewusst FRUEH: vor jeder Schranke, damit auch die Anmeldeseite
|
||
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
|
||
entgegengenommen.
|
||
|
||
Warum die Pruefsummen nicht fest im Code stehen, ist im Kopf von
|
||
inhaltsrichtlinie.js begruendet -- kurz: Statische Dateien gehen hier
|
||
ohne Neustart live, feste Summen waeren nach der naechsten
|
||
Textaenderung falsch und wuerden die Seite lahmlegen. */
|
||
app.use(inhaltsrichtlinie(SITE_DIR));
|
||
|
||
app.use(cookieParser());
|
||
app.use(express.json({ limit: "1mb" }));
|
||
app.use(gateMiddleware);
|
||
|
||
/* Zugangsschutz für /webdesign (22.08.2026). Muss NACH gateMiddleware und
|
||
VOR express.static stehen. Eine eigene Schranke ist zwingend: gate.js
|
||
lässt seit dem öffentlichen Start am 21.08.2026 jeden durch
|
||
(SITE_PUBLIC_LAUNCH_AT liegt in der Vergangenheit) — der ausdrücklich
|
||
nicht-öffentliche Webdesign-Bereich wäre darüber sofort für alle
|
||
lesbar. Ausführliche Begründung im Kopf von webdesign-gate.js. */
|
||
app.use(webdesignGate);
|
||
|
||
/* Creator Workspace (27.08.2026): Anmeldung, Sitzungen und Audit-Log für
|
||
/workspace. Muss VOR express.static stehen, weil der Router auch die
|
||
angemeldeten Seiten schützt -- stünde er dahinter, würde express.static
|
||
/workspace/start.html vorher ungeschützt ausliefern.
|
||
|
||
Das Modul ist so gebaut, dass es diese Website nicht mitreißen kann:
|
||
Es öffnet beim Laden keine Datenbank, und jede Route fängt ihre Fehler
|
||
selbst ab (Begründung im Kopf von workspace.js). */
|
||
app.use(workspaceRouter);
|
||
|
||
/* DIE SICHT EINES ANDEREN (01.09.2026) -- setzt req.sicht.
|
||
|
||
Steht GLOBAL und vor allen Fachmodulen, damit sie ueberall zur
|
||
Verfuegung steht, ohne dass zwoelf Module dieselbe Zeile bekommen.
|
||
Sie prueft die Rolle selbst (nur DogFather) und faellt sonst auf die
|
||
eigene Sicht zurueck -- eine Middleware, die man vergessen kann, waere
|
||
ein Rechteloch mit Ansage.
|
||
|
||
Nur fuer /workspace/api: Statische Dateien haben keine Sicht. */
|
||
app.use("/workspace/api", sichtSetzen);
|
||
|
||
/* MUSS vor aufgabenRouter stehen: Der haengt eine Schranke ueber ALLE
|
||
Pfade unter /workspace/api. Die Rueckmeldung der Sicherungskopie kommt
|
||
aber von einem Skript ohne Sitzung und wuerde dort abgewiesen, bevor
|
||
ihr Schluessel ueberhaupt geprueft wird. */
|
||
/* EBENFALLS VOR aufgabenRouter, aus demselben Grund: Die zwei
|
||
Buehnenquellen fuer OBS kommen OHNE Sitzung -- geprueft wird ein
|
||
Schluessel in der Adresse. Weiter unten eingehaengt wuerden sie mit
|
||
401 abgewiesen, bevor der Schluessel ueberhaupt angesehen wird.
|
||
Gemessen: genau das ist beim ersten Versuch passiert. */
|
||
app.use(buehneRouter);
|
||
app.use(sicherungRouter);
|
||
/* Ebenfalls VOR aufgabenRouter: Das Profilbild wird von <img src="..."/>
|
||
geholt. Der Browser schickt dabei den Sitzungs-Cookie zwar mit, aber
|
||
die Schranke des Aufgaben-Routers antwortet mit JSON -- ein 401 als
|
||
JSON in einem <img> ergibt nur ein kaputtes Bild ohne jeden Hinweis.
|
||
Der Steckbrief bringt seine eigene Schranke mit. */
|
||
app.use(steckbriefRouter);
|
||
app.use(draussenRouter);
|
||
/* Ebenfalls VOR aufgabenRouter: Der Service Worker holt den
|
||
oeffentlichen Schluessel, bevor irgendetwas anderes laeuft, und die
|
||
Push-Wege bringen ihre eigene Schranke mit. */
|
||
app.use(pushRouter);
|
||
/* CHAT (06.09.2026). Steht neben pushRouter, weil beide zusammenwirken:
|
||
Wer eine Nachricht bekommt und nicht zusieht, wird ueber Push
|
||
benachrichtigt.
|
||
|
||
ZUR VORGESCHICHTE, damit sie sich nicht wiederholt: Diese Zeile und
|
||
ihr Import wurden am selben Tag schon einmal entfernt (09a4cfd,
|
||
"AUSFALL BEHOBEN"). Zu Recht -- sie waren aus einem unfertigen Stand
|
||
mitcommittet worden, waehrend workspace-chat.js noch gar nicht im
|
||
Repo lag. Der Server fand das Modul nicht und stuerzte in einer
|
||
Schleife ab: 502 fuer alle.
|
||
|
||
Jetzt ist die Datei da (4637aa2). Wer diese Zeile kuenftig anfasst,
|
||
pruefe beides zusammen -- Import UND Einhaengung UND Datei. Fehlt die
|
||
Datei, faellt der ganze Dienst aus; fehlt nur die Einhaengung, geben
|
||
alle Chat-Wege still 404 und niemand sieht warum. Der zweite Fall ist
|
||
der unangenehmere: Die Seite laedt, der Chat bleibt leer. */
|
||
app.use(chatRouter);
|
||
/* Der Anruf nutzt den Ereigniskanal des Chats -- deshalb direkt
|
||
dahinter, damit die Reihenfolge die Abhaengigkeit zeigt. */
|
||
app.use(anrufRouter);
|
||
/* DER VIDEO-WEG (17.09.2026), Stufe A des Video-Plans. Er liegt VOR
|
||
dem Bereichs-Router: Beide horchen auf `/workspace/api/bereich/...`,
|
||
und `/:bereich/:id` wuerde "video" sonst fuer eine Nummer halten --
|
||
derselbe Fehler, der heute frueh beim Antwort-Weg drei Anlaeufe
|
||
gekostet hat. */
|
||
app.use(videoRouter);
|
||
/* LEISTUNG und FRUEHWARNUNG (06.09.2026, Stufe 1+2 des Plans). Die
|
||
Fruehwarnung liest aus `leistung` -- ohne Zahlen hat sie nichts zu
|
||
melden, deshalb stehen sie beieinander. */
|
||
app.use(leistungRouter);
|
||
app.use(fruehwarnungRouter);
|
||
/* KALENDER ZUM ABONNIEREN (06.09.2026, Stufe 3.2 des Plans).
|
||
|
||
Steht VOR aufgabenRouter, und das ist keine Stilfrage: Der haengt
|
||
eine Schranke ueber ALLE Pfade unter /workspace/api. Der ICS-Weg
|
||
selbst liegt zwar unter /workspace/kalender/, aber die Verwaltung
|
||
des eigenen Links unter /workspace/api/ics -- und die bringt ihre
|
||
eigene Schranke mit.
|
||
|
||
Der Kalender-Weg selbst MUSS ohne Anmeldung erreichbar sein: Google,
|
||
Apple und Outlook holen ihn im Hintergrund ab, ohne Browser und ohne
|
||
Sitzung. Ein Kalender, der eine Anmeldung verlangt, laesst sich
|
||
schlicht nicht abonnieren. Der geheime Schluessel in der Adresse IST
|
||
deshalb der Zugang -- 32 Byte Zufall, jederzeit widerrufbar. */
|
||
app.use(icsRouter);
|
||
app.use(vorlagenRouter);
|
||
app.use(teamRouter);
|
||
app.use(schulungRouter);
|
||
app.use(ampelRouter);
|
||
app.use(checklisteRouter);
|
||
app.use(teamlageRouter);
|
||
/* VOR aufgabenRouter: Der faengt "/workspace/api/aufgaben/:id" ab und
|
||
wuerde "/workspace/api/aufgaben/resuemee" als Aufgabe mit der Nummer
|
||
"resuemee" behandeln. Die Reihenfolge ist hier keine Stilfrage. */
|
||
app.use(materialRouter);
|
||
app.use(zuteilungRouter);
|
||
app.use(aufgabenRouter);
|
||
app.use(personenRouter);
|
||
app.use(profilRouter);
|
||
app.use(kalenderRouter);
|
||
app.use(serienRouter);
|
||
app.use(dateienRouter);
|
||
app.use(bereicheRouter);
|
||
/* Der Treff (11.09.2026): Stimmen, Anheften, Freigeben, Melden,
|
||
Massnahmen, Stufen. Die sieben Bretter selbst laufen ueber
|
||
bereicheRouter -- es sind gewoehnliche Eintraege, und ein zweiter
|
||
Weg zu denselben Zeilen waere ein zweiter Ort fuer dieselbe
|
||
Rechtepruefung. */
|
||
app.use(treffRouter);
|
||
/* Entwicklung & Nachwuchs (11.09.2026): die zwei Kataloge zum Anklicken.
|
||
Die Bretter mit den freien Notizen laufen weiter ueber bereicheRouter
|
||
-- beides nebeneinander ist Absicht: Der Katalog ist das, was man
|
||
regelmaessig durchgeht, die Notiz das, was dazwischen auffaellt. */
|
||
app.use(entwicklungRouter);
|
||
app.use(werdegangRouter);
|
||
/* Die Rechtetafel -- ansehen und umstellen (11.09.2026). */
|
||
app.use(rechteRouter);
|
||
app.use(aufbewahrungRouter);
|
||
app.use(auskunftRouter);
|
||
app.use(uebernahmeRouter);
|
||
app.use(befindenRouter);
|
||
app.use(hilfeRouter);
|
||
app.use(supportRouter);
|
||
/* Der Notizblock (26.09.2026): vier Rollen des Teamhauses, und jeder
|
||
sieht nur seinen eigenen -- auch DogFather. Begruendung im Kopf von
|
||
notiz-tabellen.js. */
|
||
app.use(notizenRouter);
|
||
app.use(reaktionRouter);
|
||
app.use(spendenRouter);
|
||
/* Unterstützen (24.09.2026): die Wege, auf denen die Community etwas
|
||
beitragen kann – lesen alle, ändern nur DogFather. */
|
||
app.use(unterstuetzungRouter);
|
||
app.use(bewerbungRouter);
|
||
app.use(reportRouter);
|
||
app.use(scoutRouter);
|
||
/* Manager-Ziele (02.10.2026): die vier festen Monatsaufgaben. Steht
|
||
hinter dem Scout-CRM, weil es dessen Pipeline mitbenutzt -- die
|
||
Reihenfolge der Router entscheidet hier zwar nichts (die Wege
|
||
ueberschneiden sich nicht), aber wer suchen muss, sucht sie
|
||
nebeneinander. */
|
||
app.use(managerZieleRouter);
|
||
app.use(anleitungRouter);
|
||
app.use(callRouter);
|
||
app.use(hinweisRouter);
|
||
app.use(zentraleRouter);
|
||
app.use(sucheRouter);
|
||
app.use(startcheckRouter);
|
||
app.use(wissenRouter);
|
||
app.use(kiRouter);
|
||
app.use(contentRouter);
|
||
|
||
/* Naechtliche Sicherung. Startet einen eigenen Takt im Prozess -- siehe
|
||
workspace-sicherung.js, dort steht auch, warum sie NICHT als eigener
|
||
Systemdienst laeuft und warum eine Dateikopie hier falsch waere. */
|
||
sicherungStarten();
|
||
aufbewahrungStarten();
|
||
/* Sprachnachrichten in eine Fassung bringen, die jedes Geraet
|
||
abspielt (Miss im Support, Meldung #12). Der Lauf meldet sich
|
||
nur, wenn er etwas getan hat. */
|
||
toeneNachruestenStarten();
|
||
/* Sieht taeglich nach, ob die verlinkten Videos bei TikTok noch da
|
||
sind. Ein Knopf, der ins Leere fuehrt, ist schlimmer als kein
|
||
Knopf -- man klickt ihn ja. */
|
||
videoWacheStarten();
|
||
|
||
/* Wiederkehrende Termine einmal auffuellen. Kein Takt, kein Dienst --
|
||
siehe workspace-serien.js: Der Nachfueller legt nur an, was fehlt, und
|
||
laeuft ausserdem bei jedem Oeffnen des Kalenders. Der Lauf beim Start
|
||
sorgt dafuer, dass die Termine auch dann bereitliegen, wenn an einem
|
||
Tag niemand den Kalender oeffnet, aber jemand die Startseite, die
|
||
Call-Liste oder die Suche benutzt -- die lesen dieselbe Tabelle. */
|
||
serienStarten();
|
||
|
||
/* Benachrichtigungen. Eigener Takt im Prozess -- wie die Sicherung und
|
||
der Serien-Nachfueller: kein Systemdienst, kein Cron, eine Sache
|
||
weniger, die stillschweigend ausfallen kann. Begruendung im Kopf von
|
||
workspace-push.js. */
|
||
pushStarten();
|
||
|
||
/* Sicherheits-Fix 20.08.2026 (Audit vor dem geplanten öffentlichen Start morgen).
|
||
SITE_DIR ist der GESAMTE Repo-Ordner (join(__dirname, "..")) — express.static liefert
|
||
ohne diese Sperre daher nicht nur die eigentliche Website aus, sondern auch den Server-
|
||
Quellcode (server/, server-internal/, cloudflare-worker/) und das komplette
|
||
.git-Verzeichnis samt Commit-Historie.
|
||
Live nachgewiesen (mit gültigem Zugangscode, der morgen für alle wegfällt):
|
||
/server/gate.js und /.git/config lieferten HTTP 200. Ursache: serve-static blockt per
|
||
Voreinstellung nur Dateien, deren EIGENER Name mit einem Punkt beginnt (server/.env ->
|
||
404 durch diese Regel) — aber NICHT rekursiv. Eine Datei wie .git/config wird trotzdem
|
||
ausgeliefert, weil "config" selbst nicht mit einem Punkt beginnt, nur der Ordnername davor.
|
||
Diese Sperre läuft daher bewusst unabhängig von gateMiddleware (bleibt auch nach dem
|
||
morgigen Entfernen der Zugangsschranke wirksam) und blockt ganze Ordner/Muster statt
|
||
einzelner Dateien, damit neue Dateien darin automatisch mitgeschützt sind. */
|
||
const GESPERRTE_ORDNER = ["/server", "/server-internal", "/cloudflare-worker"];
|
||
const GESPERRTE_DATEIEN = [
|
||
"/CLAUDE.md", "/DEPLOY.md", "/wrangler.toml", "/netlify.toml",
|
||
"/gate-worker.js", "/_headers", "/.gitignore", "/.nojekyll", "/.assetsignore",
|
||
];
|
||
app.use((req, res, next) => {
|
||
const pfad = req.path;
|
||
const hatVerstecktenOrdner = pfad
|
||
.split("/")
|
||
.some((teil) => teil.startsWith(".") && teil.length > 1 && teil !== ".well-known");
|
||
const inGesperrtemOrdner = GESPERRTE_ORDNER.some((p) => pfad === p || pfad.startsWith(p + "/"));
|
||
const istGesperrteDatei = GESPERRTE_DATEIEN.includes(pfad);
|
||
const istBackupDatei = /\.bak(-|\.|$)/i.test(pfad);
|
||
if (hatVerstecktenOrdner || inGesperrtemOrdner || istGesperrteDatei || istBackupDatei) {
|
||
return res.status(404).sendFile(join(SITE_DIR, "404.html"), (err) => {
|
||
if (err) res.status(404).send("Nicht gefunden.");
|
||
});
|
||
}
|
||
next();
|
||
});
|
||
|
||
/* Bugfix 20.08.2026 (Nutzer-Report: "ich kann sie immer noch nicht runter
|
||
laden und die leiste oben ist immer noch so groß", NACHDEM der jeweilige
|
||
Fix schon deployed war): Express selbst schickt standardmäßig GAR KEIN
|
||
Cache-Control mit (nur ETag/Last-Modified). Weil dogfather-universe.com
|
||
hinter Cloudflare liegt (orange-cloud-Proxy), springt Cloudflare bei
|
||
fehlendem Origin-Cache-Control mit seinem EIGENEN Standardwert ein
|
||
("Browser Cache TTL", per curl bestätigt: 4 Stunden = max-age=14400) —
|
||
ein frischer Deploy war dadurch für Besucher:innen bis zu 4 Std lang im
|
||
eigenen Browser-Cache unsichtbar, obwohl der Server längst die neue
|
||
Version ausliefert. Cloudflare respektiert laut Doku ein vom Origin
|
||
gesetztes Cache-Control statt seinen Standardwert zu benutzen — deshalb
|
||
hier explizit "no-cache" (erzwingt bei jedem Laden eine Revalidierung
|
||
per ETag/If-None-Match, liefert bei unverändertem Inhalt ein schnelles
|
||
304 statt die ganze Datei erneut zu senden — kein "gar nicht cachen",
|
||
nur "nie blind auf einen alten Stand vertrauen"). */
|
||
app.use((req, res, next) => {
|
||
res.setHeader("Cache-Control", "no-cache");
|
||
next();
|
||
});
|
||
|
||
/* =====================================================================
|
||
HTML WIRD IMMER NACHGEFRAGT, ALLES ANDERE DARF LIEGENBLEIBEN
|
||
(09.09.2026)
|
||
|
||
DER FALL, DER ES GEZEIGT HAT: Filipe meldete am Nachmittag, die
|
||
Kopfleiste auf dem Handy sei "nicht gemacht" -- gemessen war sie auf
|
||
allen 18 Seiten, in allen fuenf Rollen und bei drei Breiten
|
||
einzeilig, und die neuen Dateien lagen nachweislich auf dem Server.
|
||
Er sah trotzdem den alten Stand.
|
||
|
||
Die Ursache ist eine Luecke, die man nur bemerkt, wenn man sie sucht:
|
||
Die HTML-Seiten gingen OHNE JEDE Cache-Angabe hinaus. Ohne
|
||
`Cache-Control` und ohne `Expires` darf ein Browser heuristisch
|
||
zwischenspeichern -- ueblicherweise ein Zehntel des Alters seit der
|
||
letzten Aenderung. Bei einer Datei, die vor zwei Wochen zuletzt
|
||
angefasst wurde, sind das anderthalb Tage.
|
||
|
||
Und das ist schlimmer als es klingt: In der HTML stehen die
|
||
VERSIONSSTEMPEL der Stilvorlagen (`start.css?v=...`). Eine alte HTML
|
||
verweist auf alte Stempel -- der Browser holt sich dann brav die
|
||
ALTEN CSS- und JS-Dateien, obwohl die neuen bereitliegen. Der ganze
|
||
Stempelmechanismus haengt an einer Datei, die selbst
|
||
zwischengespeichert werden durfte.
|
||
|
||
Deshalb: HTML immer nachfragen (`no-cache` heisst NICHT "nicht
|
||
speichern", sondern "vor jeder Benutzung rueckfragen" -- die Antwort
|
||
ist meistens ein 304 ohne Rumpf und kostet fast nichts). Alles mit
|
||
Stempel darf dagegen ein Jahr liegenbleiben; sein Name aendert sich
|
||
ja, sobald sich der Inhalt aendert.
|
||
|
||
Bilder und Schriften ohne Stempel bekommen einen Tag -- lang genug,
|
||
um jeden Seitenaufruf zu tragen, kurz genug, um nach einem Austausch
|
||
nicht ewig alt zu bleiben. */
|
||
app.use(express.static(SITE_DIR, {
|
||
index: "index.html",
|
||
setHeaders(res, pfad) {
|
||
if (pfad.endsWith(".html")) {
|
||
res.setHeader("Cache-Control", "no-cache");
|
||
return;
|
||
}
|
||
/* "UNVERAENDERLICH" NUR, WENN DIE ADRESSE DAS AUCH HERGIBT
|
||
(berichtigt 21.09.2026).
|
||
|
||
Hier stand: "`originalUrl` steht hier nicht zur Verfuegung --
|
||
geprueft wird deshalb die Dateiart." Nachgemessen stimmt das
|
||
nicht: `res.req.originalUrl` ist da und liefert
|
||
"/start.html?v=123". Die Annahme war nie geprueft worden, und
|
||
aus ihr folgte eine Zusage, die das Haus nicht halten kann.
|
||
|
||
WAS DARAUS WURDE: Jede CSS- und JS-Datei ging mit
|
||
`max-age=31536000, immutable` hinaus -- auch unter ihrer
|
||
Adresse OHNE Stempel. "immutable" heisst woertlich: Der Inhalt
|
||
unter dieser Adresse aendert sich nie. Fuer `gate.css?v=...`
|
||
stimmt das; fuer `gate.css` ist es falsch, denn genau dort
|
||
aendert er sich bei jeder Auslieferung.
|
||
|
||
UND ES WAR NICHT THEORETISCH. Am 21.09. wurde crew-haus.css auf
|
||
die zweite Adresse begrenzt und die Crew-Adresse aus gate.css
|
||
entfernt. Der Ursprung lieferte danach korrekt 404 und eine
|
||
bereinigte Datei -- Cloudflare aber weiterhin die ALTE Fassung
|
||
aus seinem Zwischenspeicher, 27 806 Byte, `cf-cache-status:
|
||
HIT`. Mit einem Jahr Zusage haette sie dort ein Jahr gelegen.
|
||
Ein Zwischenspeicher, der etwas Falsches zeigt, ist schlimmer
|
||
als gar keiner -- man glaubt ihm ja.
|
||
|
||
Mit Stempel bleibt alles wie bisher: Die Seiten rufen ohnehin
|
||
immer `?v=...` auf, dort kostet die Aenderung nichts. Ohne
|
||
Stempel wird nachgefragt (`no-cache` heisst nicht "nicht
|
||
speichern", sondern "vorher rueckfragen"). */
|
||
const adresse = String(res.req?.originalUrl || "");
|
||
const mitStempel = /[?&]v=/.test(adresse);
|
||
/* AUSNAHME: sw.js. Der Service Worker wird OHNE Stempel geladen
|
||
(glocke.js: register('/workspace/sw.js')). Ein Jahr unveraenderlich
|
||
hiesse: Ein Fehler darin bliebe ein Jahr stehen. Browser
|
||
begrenzen das zwar selbst auf 24 Stunden -- sich darauf zu
|
||
verlassen waere aber genau die Sorte Annahme, die still bricht. */
|
||
if (pfad.endsWith("sw.js")) {
|
||
res.setHeader("Cache-Control", "no-cache");
|
||
return;
|
||
}
|
||
if (/\.(css|js|mjs)$/.test(pfad)) {
|
||
res.setHeader("Cache-Control", mitStempel
|
||
? "public, max-age=31536000, immutable"
|
||
: "no-cache");
|
||
return;
|
||
}
|
||
if (/\.(webp|png|jpe?g|svg|gif|ico|woff2?)$/.test(pfad)) {
|
||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||
}
|
||
},
|
||
}));
|
||
|
||
app.use((req, res) => {
|
||
res.status(404).sendFile(join(SITE_DIR, "404.html"), (err) => {
|
||
if (err) res.status(404).send("Nicht gefunden.");
|
||
});
|
||
});
|
||
|
||
/* Fehler-Handler (muss NACH allen Routen stehen), ergänzt 05.08.2026: antwortet mit einer
|
||
schlichten Meldung statt einer Express-Standardseite mit komplettem Stacktrace und Dateipfaden. */
|
||
// eslint-disable-next-line no-unused-vars
|
||
app.use((err, req, res, next) => {
|
||
/* EIN KAPUTTER RUMPF IST KEIN SERVERFEHLER (Befund 04.09.2026, Audit).
|
||
Vorher wurde JEDER Fehler zu einer 500 in Text/HTML. Gemessen mit
|
||
abgeschnittenem JSON ('{"titel": "ab'), wie es bei einer
|
||
abgebrochenen Verbindung entsteht:
|
||
|
||
-> 500 "Es ist ein Fehler aufgetreten. Bitte später erneut versuchen."
|
||
|
||
Drei Dinge waren daran falsch. Erstens die Nummer: Der Rumpf kam vom
|
||
Client, das ist eine 400. Zweitens die ANTWORTART -- die Oberflaeche
|
||
ruft ueberall `await a.json()` auf; auf Text laeuft sie in einen
|
||
zweiten Fehler, und der Knopf haengt ohne jede Meldung. Drittens der
|
||
Rat "spaeter erneut versuchen": Spaeter hilft nie, der Rumpf bleibt
|
||
kaputt. Dazu stand jedes Mal ein voller Stacktrace im Log, in dem
|
||
ein echter Fehler untergeht.
|
||
|
||
body-parser kennzeichnet diese Faelle selbst (`err.type`), es muss
|
||
also nichts geraten werden. */
|
||
const NUTZERFEHLER = new Set([
|
||
"entity.parse.failed", // kaputtes JSON
|
||
"entity.verify.failed",
|
||
"request.aborted", // Verbindung mittendrin weg
|
||
"request.size.invalid",
|
||
"encoding.unsupported",
|
||
"charset.unsupported",
|
||
]);
|
||
const istJson = req.path.startsWith("/workspace/api/");
|
||
|
||
if (NUTZERFEHLER.has(err?.type)) {
|
||
/* Bewusst NUR eine Zeile ins Log, kein Stacktrace: Das passiert bei
|
||
jeder abgebrochenen Verbindung und darf das Protokoll nicht
|
||
zumuellen -- sonst uebersieht man darin die echten Fehler. */
|
||
console.warn(`[rumpf] ${req.method} ${req.originalUrl}: ${err.type}`);
|
||
if (res.headersSent) return;
|
||
return istJson
|
||
? res.status(400).json({ fehler: "Die Anfrage kam unvollständig oder fehlerhaft an." })
|
||
: res.status(400).send("Die Anfrage kam unvollständig oder fehlerhaft an.");
|
||
}
|
||
|
||
if (err?.type === "entity.too.large") {
|
||
console.warn(`[rumpf] ${req.method} ${req.originalUrl}: zu gross`);
|
||
if (res.headersSent) return;
|
||
return istJson
|
||
? res.status(413).json({ fehler: "Die Daten sind zu groß." })
|
||
: res.status(413).send("Die Daten sind zu groß.");
|
||
}
|
||
|
||
console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err);
|
||
if (res.headersSent) return;
|
||
/* Auch der echte Serverfehler kommt unter /workspace/api als JSON --
|
||
sonst bricht die Oberflaeche schon am Auswerten der Antwort ab und
|
||
zeigt gar nichts an, statt "da ging etwas schief". */
|
||
if (istJson) return res.status(500).json({ fehler: "nicht_verfuegbar" });
|
||
res.status(500).send("Es ist ein Fehler aufgetreten. Bitte später erneut versuchen.");
|
||
});
|
||
|
||
/* Letztes Netz: Ein unbehandelter Fehler darf die Website nicht offline nehmen. */
|
||
process.on("unhandledRejection", (grund) => console.error("[unhandledRejection]", grund));
|
||
process.on("uncaughtException", (fehler) => console.error("[uncaughtException]", fehler?.stack || fehler));
|
||
|
||
app.listen(PORT, "127.0.0.1", () => {
|
||
console.log(`DogFather Universe – Server läuft auf http://127.0.0.1:${PORT}`);
|
||
});
|
||
|
||
/* =====================================================================
|
||
DIE ANWENDUNG NACH AUSSEN (26.09.2026)
|
||
|
||
Nur fuer Pruefungen. `pruef-notizen` geht damit den Routenbaum von
|
||
express durch und sucht Wege, die zweimal vergeben sind -- genau so
|
||
ein Fall hat beim Bau des Notizblocks eine Stunde gekostet
|
||
(DELETE /workspace/api/notizen/:id gab es schon in
|
||
workspace-aufgaben.js, und express nimmt schweigend die erste).
|
||
|
||
Der Export aendert am Betrieb nichts: Wer diese Datei importiert,
|
||
startet den Server ohnehin -- das tut sie beim Laden. Er macht nur
|
||
sichtbar, was vorher nur im Speicher stand. */
|
||
export default app;
|