Filipe, screen1: "die leiste da und alles andere was noch rot ist auf
team dogi seite soll lila werden. richtig geiles lila und babyblau
mischung ueberall."
Und screen2: "ich will doch dass der eingang hier getrennt ist von der
workspace seite ... aber es soll trotzdem so bleiben dass ich die daten
hier und da sehe."
GETRENNT WIRD DAS AUSSEHEN, NICHT DER BESTAND. Dieselbe Datenbank,
dieselben Seiten, dasselbe Programm -- und zwei Haeuser, die man nicht
verwechseln kann. Filipe und die rechte Hand sehen ihre Zahlen
weiterhin auf beiden Adressen.
EINE ZEILE JE SEITE, EINE REGEL IM SERVER. Jede Seite laedt als LETZTE
Stilvorlage `haus.css`. Auf der Agenturadresse ist die leer -- das Haus
dort IST der Grundzustand. Auf crew.dogfather-universe.com biegt die
Weiche genau diesen Dateinamen auf `crew-haus.css` um.
Drei Wege habe ich dafuer verworfen, und jeder hat einen Grund:
* `data-haus` per JavaScript -> die Seite laedt erst im falschen Ton
und faerbt sich um. Sichtbar, auf jeder Seite, bei jedem Aufruf.
* dasselbe Attribut serverseitig in den HTML-Text schreiben -> jede
HTML-Antwort muesste durch einen Umschreiber statt als Datei
ausgeliefert zu werden, nur wegen einer Farbe.
* zwanzig zweite <link>-Zeilen -> die muesste jemand bei jeder neuen
Seite mitschreiben, und wer sie vergisst, bekommt eine Seite, die
still zum falschen Haus gehoert.
DAS HAUS SIND FAST NUR VARIABLEN. Wer zwei Dutzend Farbwerte tauscht,
tauscht jede Kachel, jeden Rand, jeden Knopf und jedes Leuchten auf
einmal -- auch an Stellen, die man beim Nachbauen uebersehen wuerde.
Eine Datei, die stattdessen Regel fuer Regel umfaerbt, waere beim
naechsten neuen Bauteil sofort unvollstaendig, ohne dass es auffiele.
WO KEINE VARIABLE STAND, HAT DAS MESSEN SIE GEFUNDEN. Ich habe nicht
im Quelltext gesucht, sondern am fertigen Bildschirm jedes Element nach
Farben abgefragt, bei denen der Rotkanal deutlich ueber den anderen
liegt -- ueber alle Farbquellen, nicht nur `color` und
`background-color`. Erst das brachte die eigentliche Stelle ans Licht:
DIE KOPFLEISTE HAT EINEN ROTEN VERLAUF. Genau die Leiste aus Filipes
Bildschirmfoto. Sie glimmt im Agenturhaus wie Feuer -- sein eigener
Wunsch von screen36, und dort bleibt das auch so. Hier schimmert sie
jetzt lila, in derselben Bauweise: unten waermer, nach oben dunkel,
in der Mitte kraeftiger, alles unter 30 % Deckkraft.
Dazu die Fassung der Zentrale (rot->lila, Silber und Babyblau
unberuehrt, Prozentzahlen auf den Punkt gleich -- sie sind am 08.09.
eigens nachgerechnet worden), die Uhr an vier Stellen, das Universum
dahinter, Glocke und Tagesruf, der Schriftzug, und auf der Zugangswand
Knopf, Kachelreihe, Innenglas und Karte.
ZWEI FEHLER, DIE ERST DER BILDSCHIRM ZEIGTE:
Der Schriftzug wurde zu einem ausgefuellten Balken. `background` ist
eine Kurzschreibweise und setzt `background-clip` mit zurueck -- und
genau darueber wird der Text in die Buchstaben ausgestanzt. Jetzt
`background-image`. Im Quelltext sah die Zeile voellig richtig aus.
Zwei Regeln wurden geladen und taten nichts: Die Originale stehen
unter `.kopfleiste .marke__haupt` und `.willkommen > .zuniversum`.
Wer nur die halbe Kette schreibt, verliert gegen zwei Klassen.
DIE MARKE HAENGT JETZT AN ZWEI DINGEN. `markeFuer()` kannte nur die
Rolle -- und DogFather gehoert nun einmal zur Agentur. Ueber der
Zentrale stand deshalb auch auf der zweiten Adresse gross "SPICY
MEDIA". Jetzt entscheidet auch der Hostname, an EINER Stelle.
WAS WARM BLEIBT, UND WARUM: Warnungen. Eine Warnung ist keine
Verzierung, sondern eine Bedeutung -- faerbt man sie ins Lila der
Seite, sieht "etwas stimmt nicht" aus wie alles andere. Sie wird nur
ins Rosa gezogen, damit sie neben Lila kein Fremdkoerper ist. Ebenso
bleiben die 24 Kachelfarben: dass jede Kachel ihre eigene hat, ist
gepruefte Absicht.
Weisse Schrift auf dem Anmeldeknopf haelt jetzt 6,23 zu 1 statt 5,1 --
an echten Bildpunkten gemessen, nicht an der Farbangabe.
pruef-crew-adresse 123 -> 129 · pruef-crew-wand-bild 45 ·
pruef-css-klassen gruen (prueft ab jetzt das PAAR module.css/haus.css,
nicht mehr eine Datei) · pruef-modi-wortleck 5 · pruef-rollen 274 ·
pruef-start-ansicht gruen · pruef-chat-kanaele 79.
Co-Authored-By: Claude Opus 5 <[email protected]>
632 lines
33 KiB
JavaScript
632 lines
33 KiB
JavaScript
/* =====================================================================
|
|
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");
|
|
anlegen("Rieke", "hand", "CODE-HAND-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 }
|
|
= 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");
|
|
|
|
/* SEIT DEM 10.09.2026 DREI ROLLEN AUF crew. -- Filipe: "3 rollen.
|
|
dogfather. rechte hand und modis."
|
|
|
|
DogFather steht damit auf BEIDEN Adressen. Das ist keine Luecke in
|
|
der Trennung, sondern ihr Sinn: Er ist die einzige Person, die in
|
|
beiden Welten arbeitet. Die rechte Hand und die Modis dagegen gibt
|
|
es nur hier -- und auf workspace. gar nicht. */
|
|
const ROLLEN = ["admin", "spicy", "manager", "scout", "creator", "hand", "modi"];
|
|
const TEAM = new Set(["hand", "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);
|
|
const darfCrew = TEAM.has(r) || r === "admin";
|
|
ok(aufCrew === darfCrew, `crew.: ${r} ${darfCrew ? "darf" : "darf nicht"}`);
|
|
ok(aufWs === !TEAM.has(r), `workspace.: ${r} ${TEAM.has(r) ? "darf nicht" : "darf"}`);
|
|
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 === 200 && adminAufCrew.keks.includes("dfw_sitzung"),
|
|
"crew.: DogFather kommt herein -- er arbeitet in beiden Welten");
|
|
|
|
const handAufCrew = await anmelden(CREW, "hand", "CODE-HAND-0001");
|
|
ok(handAufCrew.status === 200 && handAufCrew.keks.includes("dfw_sitzung"),
|
|
"crew.: die rechte Hand kommt herein");
|
|
|
|
/* Und sie kommt NICHT auf die andere Adresse -- genau wie ein Modi,
|
|
und mit genau derselben Antwort wie ein erfundener Code. */
|
|
const handAufWs = await anmelden(WS, "creator", "CODE-HAND-0001");
|
|
const erfunden2 = await anmelden(WS, "creator", "AUCH-DIESEN-NICHT-000");
|
|
ok(handAufWs.status === 401 && handAufWs.text === erfunden2.text,
|
|
`workspace.: die rechte Hand wird abgewiesen wie ein erfundener Code (${handAufWs.status})`);
|
|
ok(!handAufWs.keks.includes("dfw_sitzung"),
|
|
"workspace.: und bekommt dort keine Sitzung");
|
|
|
|
/* Eine Kachel, die es auf crew. nicht gibt, ist dort so gut wie
|
|
erfunden -- auch mit einem gueltigen Code dahinter. */
|
|
const spicyAufCrew = await anmelden(CREW, "spicy", "CODE-DOGI-0001");
|
|
ok(spicyAufCrew.status === 401, "crew.: eine fremde Kachel gibt es dort nicht");
|
|
|
|
/* =====================================================================
|
|
DIE ALTE WAND SCHWEIGT (Entscheidung Filipe, 10.09.2026)
|
|
|
|
Vorher zeigte sie einem Modi den Weg zur neuen Adresse. Seine
|
|
Entscheidung: "gar nichts -- Code stimmt nicht."
|
|
|
|
GEPRUEFT WIRD NICHT DER STATUSCODE, SONDERN DIE UNUNTERSCHEIDBARKEIT.
|
|
Eine 401 waere leicht zu erfuellen und truege trotzdem eine Spur, wenn
|
|
der Rumpf, die Koepfe oder die Dauer anders aussaehen als bei einem
|
|
erfundenen Code. Deshalb wird beides nebeneinandergelegt und
|
|
verglichen -- Zeichen fuer Zeichen. */
|
|
const erfunden = await anmelden(WS, "creator", "DIESEN-CODE-GIBT-ES-NICHT");
|
|
const modiAufWs = await anmelden(WS, "creator", "CODE-MODI-0001");
|
|
ok(modiAufWs.status === erfunden.status && modiAufWs.status === 401,
|
|
`workspace.: der Modi-Code wird abgewiesen wie ein erfundener (${modiAufWs.status})`);
|
|
ok(modiAufWs.text === erfunden.text,
|
|
`workspace.: und die Antwort ist Zeichen fuer Zeichen dieselbe (${modiAufWs.text.slice(0, 40)})`);
|
|
ok(!modiAufWs.keks.includes("dfw_sitzung"), "workspace.: keine Sitzung");
|
|
ok(!/crew\./.test(modiAufWs.text), "workspace.: die neue Adresse wird nicht genannt");
|
|
|
|
/* 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 ZUGANGSWAND
|
|
|
|
Filipe am 10.09.2026: "alles soll auf dogfather und die modis
|
|
perfektionniert werden auf der neuen modi seite." Bis dahin stand auf
|
|
crew. die gewohnte Wand -- Spicy-Media-Logo, "Creator Workspace",
|
|
fuenf Rollenkacheln. Fuer einen Modi war davon nichts richtig.
|
|
|
|
Zwei Dinge werden hier gemessen, und das zweite ist das wichtigere:
|
|
dass die ALTE Wand unangetastet blieb.
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Zwei Waende ---");
|
|
|
|
const wandCrew = await hole("/workspace/", { host: CREW });
|
|
const wandWs = await hole("/workspace/", { host: WS });
|
|
|
|
ok(wandCrew.status === 200, "crew.: die Zugangswand kommt (keine Weiterleitungsschleife)");
|
|
ok(wandWs.status === 200, "workspace.: die Zugangswand kommt");
|
|
|
|
ok(/Team Dogi|Team Dogi/.test(wandCrew.text), "crew.: dort steht Team Dogi");
|
|
ok(/DogFather/.test(wandCrew.text), "crew.: und DogFather daneben");
|
|
ok(!/Spicy/i.test(wandCrew.text), "crew.: von Spicy Media steht dort nichts mehr");
|
|
ok(!/Creator/i.test(wandCrew.text), "crew.: und vom Creator Workspace auch nicht");
|
|
ok(/die Modis/.test(wandCrew.text), "crew.: der Satz nennt DogFather und die Modis");
|
|
|
|
/* Die Kachelreihe ist weg -- auf einer Adresse mit genau einer Sorte
|
|
Zugang waere sie eine Bedienhuerde, keine Auswahl. */
|
|
const kachelnCrew = (wandCrew.text.match(/class="rolle"/g) || []).length;
|
|
const kachelnWs = (wandWs.text.match(/class="rolle"/g) || []).length;
|
|
ok(kachelnCrew === 3, `crew.: drei Rollenkacheln (gezaehlt: ${kachelnCrew})`);
|
|
for (const [rolle, name] of [["admin", "DogFather"], ["hand", "Rechte Hand"], ["modi", "Modi"]]) {
|
|
ok(wandCrew.text.includes(`data-rolle="${rolle}"`) && wandCrew.text.includes(`>${name}<`),
|
|
`crew.: die Kachel "${name}" steht da`);
|
|
}
|
|
/* WELCHES ZEICHEN AUF WELCHER KACHEL (Filipe, 10.09.2026: "bei rechte
|
|
hand soll der husky sein und bei modis soll die pfote sein").
|
|
|
|
Geprueft wird die ZUORDNUNG, nicht das Aussehen: DogFather und die
|
|
rechte Hand tragen dasselbe, der Modi ein anderes. So gelesen haelt
|
|
die Pruefung auch dann, wenn die Zeichen selbst eines Tages neu
|
|
gezeichnet werden -- gewollt ist die Gruppe, nicht der Husky. */
|
|
{
|
|
const zeichenVon = (rolle) => {
|
|
const stelle = wandCrew.text.indexOf(`data-rolle="${rolle}"`);
|
|
if (stelle < 0) return null;
|
|
return (/href="(#r-[a-z]+)"/.exec(wandCrew.text.slice(stelle, stelle + 400)) || [])[1] || null;
|
|
};
|
|
const [zAdmin, zHand, zModi] = ["admin", "hand", "modi"].map(zeichenVon);
|
|
ok(!!zAdmin && zHand === zAdmin,
|
|
`crew.: die Rechte Hand traegt dasselbe Zeichen wie DogFather (${zHand} / ${zAdmin})`);
|
|
ok(!!zModi && zModi !== zAdmin, `crew.: der Modi ein eigenes (${zModi})`);
|
|
}
|
|
|
|
/* Die Namen kommen aus den Kacheln, nicht aus einer Liste in gate.js --
|
|
sonst stuenden sie in einer Datei, die jeder bekommt. */
|
|
ok(!/Rechte Hand/.test(wandWs.text), "workspace.: dort steht die dritte Rolle nicht");
|
|
ok(kachelnWs === 5, `workspace.: dort weiterhin fuenf (gezaehlt: ${kachelnWs})`);
|
|
ok(/SpicyMedia/.test(wandWs.text), "workspace.: die alte Wand ist unveraendert");
|
|
|
|
/* Auch der ausgeschriebene Weg. Ihn zu vergessen waere nicht
|
|
aufgefallen -- im Alltag tippt ihn niemand. */
|
|
const wandCrewLang = await hole("/workspace/index.html", { host: CREW });
|
|
ok(wandCrewLang.status === 200 && /Team Dogi|Team Dogi/.test(wandCrewLang.text),
|
|
"crew.: auch /workspace/index.html fuehrt zur neuen Wand");
|
|
|
|
/* ---------------------------------------------------------------------
|
|
DIE ZWEITE WAND GIBT ES NUR AUF crew.
|
|
|
|
Sie liegt als Datei im selben Ordner wie alles andere. Ohne Sperre
|
|
koennte sie jeder unter ihrem Dateinamen aufrufen -- auch auf
|
|
workspace. -- und saehe dort eine Anmeldeseite, die es dort nicht
|
|
geben soll.
|
|
------------------------------------------------------------------ */
|
|
/* ZWEI NUMMERN, EINE AUSSAGE. Auf workspace. antwortet die Sperre mit
|
|
404 "gibt es nicht". Auf der Hauptseite und www. kommt schon der
|
|
Umzugs-Zwischenschritt vom 06.09.2026 davor und beantwortet den
|
|
GANZEN Pfad /workspace mit 410 "gab es, ist absichtlich weg".
|
|
|
|
Erwartet werden deshalb beide -- was zaehlt, ist: kein Inhalt und
|
|
keine 200. Auf eine der zwei Nummern zu bestehen hiesse, eine
|
|
fremde, aeltere Entscheidung nachtraeglich zu einem Fehler zu
|
|
erklaeren. */
|
|
for (const [name, host] of [["workspace.", WS], ["die Hauptseite", HAUPT], ["www.", "www." + HAUPT]]) {
|
|
const a = await hole("/workspace/crew-index.html", { host });
|
|
ok(a.status === 404 || a.status === 410,
|
|
`${name}: die zweite Wand gibt es dort nicht (${a.status})`);
|
|
ok(!/Team Dogi/.test(a.text), `${name}: und ihr Inhalt kommt auch nicht durch`);
|
|
}
|
|
/* Doppelte Schraegstriche haben diese Schranke im Haus schon einmal
|
|
umgangen (04.09.2026, //workspace/start.html kam mit 200). */
|
|
const schraeg = await hole("/workspace//crew-index.html", { host: WS });
|
|
ok(schraeg.status === 404 || !/Team Dogi/.test(schraeg.text),
|
|
`workspace.: auch mit doppeltem Schraegstrich nicht (${schraeg.status})`);
|
|
|
|
/* GEGENPROBE: Auf crew. und auf den Pruefadressen bleibt sie erreichbar
|
|
-- sonst haette ich sie fuer alle gesperrt, auch fuer die Pruefung,
|
|
die sie im Browser ansieht. Die waere dann gruen und wertlos. */
|
|
const dortSchon = await hole("/workspace/crew-index.html", { host: CREW });
|
|
ok(dortSchon.status === 200, "crew.: dort gibt es sie natuerlich");
|
|
const lokalSchon = await hole("/workspace/crew-index.html", { host: LOKAL });
|
|
ok(lokalSchon.status === 200, "127.0.0.1: und auf den Pruefadressen auch");
|
|
|
|
/* ---------------------------------------------------------------------
|
|
WAS gate.js AUS DEM HTML BRAUCHT -- auf BEIDEN Seiten.
|
|
|
|
Es gibt jetzt zwei Wanddateien und ein gemeinsames Skript. Wer nur
|
|
eine anfasst, macht die andere kaputt, und zwar lautlos: Ein
|
|
fehlendes Element ist im Browser ein `null`, der Fehler steht in der
|
|
Konsole und sonst nirgends. Deshalb wird beides gegen dieselbe Liste
|
|
gehalten.
|
|
------------------------------------------------------------------ */
|
|
const GEBRAUCHT = ["formular", "code", "auge", "fehler", "hinweis", "knopf", "fuer"];
|
|
for (const [name, wand] of [["crew.", wandCrew], ["workspace.", wandWs]]) {
|
|
const fehlend = GEBRAUCHT.filter((id) => !wand.text.includes(`id="${id}"`));
|
|
ok(fehlend.length === 0,
|
|
`${name} die Wand hat alle ${GEBRAUCHT.length} Elemente, die gate.js sucht`
|
|
+ (fehlend.length ? ` -- es fehlen: ${fehlend.join(", ")}` : ""));
|
|
ok(wand.text.includes("knopf__text") && wand.text.includes("buehne__bild"),
|
|
`${name} auch die beiden Klassen, die gate.js anspricht`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Die Buehne.
|
|
------------------------------------------------------------------ */
|
|
ok(/crew-gate-\d+\.webp/.test(wandCrew.text), "crew.: die eigene Buehne ist eingebunden");
|
|
ok(!/buehne\/gate-\d+\.webp/.test(wandCrew.text), "crew.: die alte Buehne nicht mehr");
|
|
ok(/buehne\/gate-\d+\.webp/.test(wandWs.text), "workspace.: dort weiterhin die alte");
|
|
ok(/crew-32\.png/.test(wandCrew.text) && /crew-180\.png/.test(wandCrew.text),
|
|
"crew.: eigenes Zeichen im Reiter und auf dem Startbildschirm");
|
|
|
|
/* Ein Verweis beweist nicht, dass die Datei da ist. */
|
|
const buehnen = [...new Set(wandCrew.text.match(/assets\/img\/buehne\/crew-gate-[a-z0-9-]+\.webp/g) || [])];
|
|
let buehnenDa = 0;
|
|
for (const b of buehnen) {
|
|
const a = await hole("/workspace/" + b, { host: CREW });
|
|
if (a.status === 200) buehnenDa++;
|
|
}
|
|
ok(buehnen.length >= 8 && buehnenDa === buehnen.length,
|
|
`crew.: alle ${buehnenDa} von ${buehnen.length} Buehnenbildern werden ausgeliefert`);
|
|
|
|
/* =====================================================================
|
|
DIE BUEHNEN INNEN (10.09.2026)
|
|
|
|
Filipe: "auch die hintergrund bilder vom crew espace sollen anderes
|
|
aussehen, der community angepasst wie die zugangscode seite".
|
|
|
|
DIE SZENENNAMEN WERDEN AUS start.css GELESEN, nicht hier
|
|
abgeschrieben. Eine Liste an zwei Stellen ist eine Liste, die
|
|
auseinanderlaeuft -- und dann faellt eine neue Szene ohne eigenes
|
|
Motiv niemandem auf: Auf der Modi-Seite stuende ploetzlich wieder
|
|
ein fremdes Logo im Hintergrund, und zwar genau auf der Seite, an
|
|
die niemand gedacht hat.
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Die Buehnen innen ---");
|
|
{
|
|
const { readFileSync } = await import("node:fs");
|
|
const { dirname, join } = await import("node:path");
|
|
const { fileURLToPath } = await import("node:url");
|
|
const css = readFileSync(
|
|
join(dirname(fileURLToPath(import.meta.url)), "..", "workspace", "assets", "css", "start.css"),
|
|
"utf8");
|
|
const namen = [...new Set((css.match(/buehne-[a-z]+-(?:uhd|breit|schmal)\.webp/g) || []))];
|
|
|
|
/* Die Zahl gehoert in die Bedingung: Faende das Muster nichts --
|
|
umbenannte Dateien, anderer Ordner --, waere alles darunter gruen,
|
|
ohne dass ein einziges Bild angesehen wurde. */
|
|
ok(namen.length >= 5, `${namen.length} Buehnenbilder in start.css gefunden`);
|
|
|
|
let eigene = 0, alte = 0;
|
|
const fehlend = [];
|
|
for (const name of namen) {
|
|
const aufCrew = await hole(`/workspace/assets/img/${name}`, { host: CREW });
|
|
const aufWs = await hole(`/workspace/assets/img/${name}`, { host: WS });
|
|
if (aufCrew.status === 200) eigene++; else fehlend.push(name);
|
|
if (aufWs.status === 200) alte++;
|
|
}
|
|
ok(eigene === namen.length,
|
|
`crew.: fuer alle ${namen.length} gibt es ein eigenes Motiv`
|
|
+ (fehlend.length ? ` -- es fehlt: ${fehlend.slice(0, 3).join(", ")}` : ""));
|
|
ok(alte === namen.length, `workspace.: dort liegen unveraendert die alten (${alte})`);
|
|
|
|
/* DAS ZEICHEN IN DER KOPFLEISTE. Es kommt aus dem CSS als
|
|
Hintergrundbild -- auf der Modi-Adresse stand dort die Chili von
|
|
Spicy Media, auf jeder Seite. */
|
|
const zeichenCrew = await hole("/workspace/assets/img/marke-spicy.webp", { host: CREW });
|
|
const zeichenWs = await hole("/workspace/assets/img/marke-spicy.webp", { host: WS });
|
|
const husky = await hole("/workspace/assets/img/marke-husky.webp", { host: WS });
|
|
ok(zeichenCrew.status === 200 && husky.status === 200
|
|
&& zeichenCrew.text === husky.text,
|
|
"crew.: oben links kommt der Husky, nicht die Chili");
|
|
ok(zeichenWs.status === 200 && zeichenWs.text !== husky.text,
|
|
"workspace.: dort bleibt es die Chili (Gegenprobe)");
|
|
|
|
/* UND SIE MUESSEN VERSCHIEDEN SEIN. Beide Adressen antworten mit 200
|
|
-- das sagt noch nicht, dass es zwei verschiedene Bilder sind. Ohne
|
|
diese Zeile waere die Pruefung auch dann gruen, wenn die Weiche gar
|
|
nicht greift. */
|
|
const probe = namen.find((n) => n.endsWith("-breit.webp")) || namen[0];
|
|
const a1 = await hole(`/workspace/assets/img/${probe}`, { host: CREW });
|
|
const a2 = await hole(`/workspace/assets/img/${probe}`, { host: WS });
|
|
ok(a1.text.length > 0 && a2.text.length > 0 && a1.text !== a2.text,
|
|
`und es sind zwei verschiedene Bilder (${probe}: `
|
|
+ `${Math.round(a1.text.length / 1024)} KB gegen ${Math.round(a2.text.length / 1024)} KB)`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
DAS HAUS (10.09.2026)
|
|
|
|
Filipe: "ich will doch dass der eingang hier getrennt ist von der
|
|
workspace seite … aber es soll trotzdem so bleiben dass ich die
|
|
daten hier und da sehe."
|
|
|
|
Getrennt wird das AUSSEHEN, nicht der Bestand. Jede Seite laedt als
|
|
letzte Stilvorlage `haus.css`; auf der Agenturadresse ist die leer,
|
|
auf crew. biegt die Weiche sie auf `crew-haus.css` um.
|
|
|
|
GEPRUEFT WIRD NICHT NUR "beide antworten mit 200". Zwei gueltige
|
|
Antworten sagen nichts darueber, ob es zwei VERSCHIEDENE Dateien
|
|
sind -- genau daran waere die Pruefung der Buehnen weiter oben schon
|
|
einmal fast vorbeigelaufen. Deshalb wird der Inhalt verglichen, und
|
|
zusaetzlich der Griff `--haus-name` gelesen: Er unterscheidet "die
|
|
leere Fassung ist angekommen" von "gar nichts ist angekommen".
|
|
===================================================================== */
|
|
console.log("");
|
|
console.log("--- Welches Haus kommt an? ---");
|
|
{
|
|
const hausCrew = await hole("/workspace/assets/css/haus.css", { host: CREW });
|
|
const hausWs = await hole("/workspace/assets/css/haus.css", { host: WS });
|
|
ok(hausCrew.status === 200 && hausWs.status === 200,
|
|
`beide Adressen liefern ein Haus (${hausCrew.status}/${hausWs.status})`);
|
|
ok(hausCrew.text !== hausWs.text,
|
|
`und es sind zwei verschiedene (${hausCrew.text.length} gegen ${hausWs.text.length} Zeichen)`);
|
|
ok(/--haus-name:\s*"crew"/.test(hausCrew.text),
|
|
"auf crew. steht das Haus von Team Dogi darin");
|
|
ok(/--haus-name:\s*"workspace"/.test(hausWs.text),
|
|
"auf workspace. das der Agentur (Gegenprobe)");
|
|
|
|
/* DIE FASSUNGSNUMMER MUSS MITREISEN. Jede Seite ruft die Datei mit
|
|
`?v=…` auf. Wer beim Umbiegen nur den Pfad setzt, wirft sie weg --
|
|
und der Browser behaelt die Datei dann laenger, als er soll.
|
|
Aufgefallen waere das erst beim naechsten Farbwechsel, Wochen
|
|
spaeter, bei einem einzelnen Menschen. */
|
|
const mitNummer = await hole("/workspace/assets/css/haus.css?v=202609102225", { host: CREW });
|
|
ok(mitNummer.status === 200 && /--haus-name:\s*"crew"/.test(mitNummer.text),
|
|
`mit Fassungsnummer kommt dasselbe an (${mitNummer.status})`);
|
|
|
|
/* Und die Datei des anderen Hauses ist auf beiden Adressen direkt
|
|
abrufbar -- sie ist kein Geheimnis, sie ist nur nicht die
|
|
Vorgabe. Das steht hier, damit niemand spaeter eine Sperre
|
|
einbaut, die die Pruefung oben blind machen wuerde. */
|
|
const direkt = await hole("/workspace/assets/css/crew-haus.css", { host: WS });
|
|
ok(direkt.status === 200, `crew-haus.css ist auch direkt abrufbar (${direkt.status})`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
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);
|