Files
dogfather-universe/server/index.js
T
DogFatherGitandClaude Opus 5 9f5dbb42e3 Support: melden mit Bild, und für die Leitung ein Eingang
Filipe: „da fehlt auch eine support kachel. die jeder sieht. jeder
benutzen kann. jeder kann da probleme von der seite melden und
hinweisen. fotos mit schicken mit einem text. … so einfach wie
möglich, will nichts kompliziertes oder zu viel. kurz und knapp …
bei dogfather und der rechten hand soll die kachel anders gebaut sein
weil die sind die die sich um die probleme kümmern."

WARUM DAS NICHT DER VORHANDENE MELDEWEG IST. Es gibt ihn schon
(hilfe.html) und er sieht aehnlich aus. Drei Unterschiede machen ihn
zu etwas anderem:
  1. Er ist NICHT fuer alle -- in rechte.js steht
     `["gast","hand","admin"]`. Modis, Creator, Scouts, Manager und
     die linke Hand hatten gar keinen Weg, einen kaputten Knopf zu
     melden.
  2. Er kann keine Bilder. Bei „der Knopf tut nichts" ist ein
     Bildschirmfoto die halbe Antwort.
  3. Er ist bewusst schwer: strikter Wechsel, hoechstens drei offene
     Faelle, ein Betreff. Richtig bei einem Vorfall zwischen
     Menschen, zu viel fuer „das Datum steht falsch da".

Deshalb eigen und sehr kurz: EIN Feld, EIN Bild, fertig. Kein
Betreff, keine Kategorie, keine Dringlichkeitsstufe -- wer ein
Formular mit fuenf Feldern sieht, meldet den kleinen Fehler nicht,
und genau die kleinen erfaehrt sonst niemand.

DIE LEITUNG SIEHT ETWAS ANDERES: einen Eingang mit Zahlenband
(neu / in Arbeit / erledigt), drei Filtern und zwei Handgriffen --
„Ich kuemmere mich" und „Erledigt …". Zwei, nicht fuenf; eine
Zuweisung an eine Person und Prioritaeten waeren ein Ticketsystem
fuer zwei Menschen, die nebeneinander sitzen. Das Melde-Feld bleibt
auch fuer sie da, rutscht aber unter den Eingang.

Wer zumacht, schreibt einen Satz dazu -- der Melder sieht nur diesen
Satz, und ohne ihn weiss er nicht, ob etwas behoben wurde oder ob
niemand Zeit hatte. Beide Seiten bekommen eine Benachrichtigung.

UEBERNOMMEN STATT NEU ERFUNDEN:
  * `dateiErkennen` aus workspace-chat.js -- EXPORTIERT, nicht
    abgeschrieben. An ihr haengt die ganze Sicherheit der Uploads
    (der Typ kommt aus den ersten Bytes, nicht aus Name oder
    Content-Type), und eine zweite Fassung waere die, die beim
    naechsten Dateiformat vergessen wird.
  * Die zweigeteilte Sicht und `meldungFuer()` (gibt den Datensatz
    oder null zurueck, nie true/false) aus dem vertraulichen
    Meldeweg.
  * Der Upload-Weg (express.raw, Text im Kopf, Ordner unter
    DATEN_ORDNER, damit die Sicherungspruefung ihn findet).

DIE KACHEL BEKOMMT JEDER -- an EINER Stelle angehaengt: `bereicheFuer`
haengt sie an jede Rollenliste, statt sie in fuenf Listen
einzutragen. Genau so ist heute der Benachrichtigungs-Knopf auf 14
Seiten verschwunden. Dazu der Eintrag in rechte.js (ohne ihn
verschwindet die Kachel lautlos, `nurOffeneKacheln` filtert dagegen)
und in der Browser-Kachelliste fuer die Agentur-Rollen.

TON 44 GERECHNET, NICHT GEWAEHLT -- und dabei zweimal danebengelegen:
  * Ich habe erst einen eigenen Modus in kachel-farben.mjs gebaut.
    `tools/kachel-farbe-einzeln.mjs` gibt es aber seit dem 22.09. fuer
    genau diesen Zweck. Meine Dopplung ist wieder weg.
  * Von Hand hatte ich #bcdbff eingetragen. pruef-kachelfarben lehnte
    es ab (Buntheit 0,060, verlangt sind 0,12) -- und das vorhandene
    Werkzeug verteidigte es trotzdem, weil sein Abstand gut war. Ein
    Werkzeug, das einen regelwidrigen Zustand haelt, weil er zufaellig
    gut misst, behebt genau den Fehler nicht, fuer den es da ist.
    Behoben: Zuerst die Regeln, dann der Abstand. Ergebnis #fd6401,
    Abstand 0,0914 (engstes vorhandenes Paar: 0,0154).

DREI MESSFEHLER VON MIR, die wie schwere Befunde aussahen: `fetch`
verwirft den Host-Kopf (die Community kommt nur auf crew. herein),
die Community bestaetigt ihr Alter (`alter_ok`), und die Startroute
heisst /api/ich. Alle drei stehen in pruef-treff.mjs richtig.

Ein ECHTER Fehler kam dazu: support.html lud bereiche.js nicht, und
kopf.js stuerzte mit „Cannot read properties of undefined (reading
'LEITUNG')" ab -- sichtbar nur in der Browserkonsole. Und
pruef-css-klassen fand sofort, dass auch wahl.js fehlte.

Gemessen: pruef-support 45 Pruefungen, 0 Fehler (alle neun Rollen
kommen herein, jede sieht die Kachel, als Bild getarntes HTML wird
abgelehnt, eine fremde Meldung bleibt fremd, erledigt bleibt
erledigt). Dazu 24 Messungen am Bild in drei Groessen. Gruen:
pruef-kachelfarben (22), pruef-css-klassen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-24 01:32:55 +02:00

568 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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 { willkommenRouter } from "./workspace-willkommen.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 { 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 } 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 { 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. */
const chatSeite = /^\/workspace\/chat\.html/.test(req.path);
res.setHeader("Permissions-Policy",
chatSeite
? "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. */
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(willkommenRouter);
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);
app.use(bewerbungRouter);
app.use(reportRouter);
app.use(scoutRouter);
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();
/* 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}`);
});