Aufbau steht (Ring links, Titel mittig, Uhr rechts), aber pruef-start-ansicht meldet 3 Befunde am MAUS-LICHT der Kacheln. Gemessen: alter Stand 0 Befunde, dieser Stand 3 — kommt also von hier. Kein JS-Fehler (pageerror/console sind still), also Zeitverhalten: Die zusaetzliche Abfrage /api/zentrale verzoegert vermutlich den Aufbau der Kacheln ueber den Zeitpunkt hinaus, an dem kopf.js lichtFolgen ruft. Ausserdem offen: 2 Schriftgroessen unter 11,5 px (pruef-css-klassen). NICHT ausliefern.
374 lines
18 KiB
JavaScript
374 lines
18 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 { webdesignGate } from "./webdesign-gate.js";
|
||
import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js";
|
||
import { workspaceRouter, sichtSetzen } from "./workspace.js";
|
||
import { aufgabenRouter } from "./workspace-aufgaben.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 { 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 { vorlagenRouter } from "./workspace-vorlagen.js";
|
||
import { schulungRouter } from "./workspace-schulung.js";
|
||
import { ampelRouter } from "./workspace-ampel.js";
|
||
import { checklisteRouter } from "./workspace-checkliste.js";
|
||
import { pushRouter, pushStarten } from "./workspace-push.js";
|
||
import { chatRouter } from "./workspace-chat.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");
|
||
res.setHeader("Permissions-Policy", "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);
|
||
|
||
/* 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);
|
||
/* 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);
|
||
/* 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(schulungRouter);
|
||
app.use(ampelRouter);
|
||
app.use(checklisteRouter);
|
||
app.use(aufgabenRouter);
|
||
app.use(personenRouter);
|
||
app.use(profilRouter);
|
||
app.use(kalenderRouter);
|
||
app.use(serienRouter);
|
||
app.use(dateienRouter);
|
||
app.use(bereicheRouter);
|
||
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();
|
||
|
||
/* 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();
|
||
});
|
||
|
||
app.use(express.static(SITE_DIR, { index: "index.html" }));
|
||
|
||
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}`);
|
||
});
|