/* LEISTUNGSZAHLEN UND FRÜHWARNUNG. Stufe 1 und 2 des Plans vom 31.08.2026 -- das Fundament, auf dem jede Betreuungsentscheidung stehen soll. WARUM HIER BESONDERS GENAU GEPRÜFT WIRD: Eine falsche Zahl ist schlimmer als eine fehlende. Eine fehlende sieht man; eine falsche sieht aus wie eine Auskunft und führt zu einer Entscheidung. Wenn das Dashboard "−40 %" sagt, ruft jemand an. DIE FALLEN, die diese Prüfung stellt: 1. RECHNEN. Durchschnitte über Tage ohne Wert, Prozent von null, Wochengrenzen. Jede einzeln nachgerechnet. 2. IMPORT ZWEIMAL. Der häufigste Fehler bei Importen -- dieselbe Datei doppelt eingelesen darf nichts verdoppeln. 3. ZAHLENFORMATE. "1.234,5" (deutsch) und "1,234.5" (englisch) bedeuten dasselbe. Wer das verwechselt, liest 1234,5 als 1,2345. 4. RECHTE. Ein Creator trägt seine Zahlen nicht selbst ein, und niemand sieht fremde. 5. DIE FRÜHWARNUNG SCHLÄGT NICHT ZU FRÜH AN. Ein System, das jede Woche Alarm gibt, wird jede Woche weniger gelesen. GEGENPROBEN durchgehend. */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-leistung-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const { eigenerPort } = await import("./helfer-port.mjs"); const PORT = await eigenerPort(import.meta, "die Leistungspruefung"); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; 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 }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 1000)); const BASIS = `http://127.0.0.1:${PORT}`; let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; /* ---- Bestand ------------------------------------------------------------ */ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); for (let i = 0; i < 60; i++) { try { const p = new DatabaseSync(process.env.WORKSPACE_DB); try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } } catch { await new Promise((r) => setTimeout(r, 250)); } } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001"); const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001"); const idNora = anlegen("Nora", "creator", "CODE-CREA-0002"); d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt); /* ORTSZEIT -- nie toISOString(). Zwischen Mitternacht und zwei Uhr läge das Datum sonst einen Tag daneben, und "diese Woche" wäre verschoben. */ const tag = (v) => { const t = new Date(); t.setDate(t.getDate() + v); const p = (n) => String(n).padStart(2, "0"); return `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}`; }; async function anmelden(rolle, code) { const a = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle, code }) }); const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); return { ok: a.ok, keks }; } const rufe = (keks, weg, koerper, methode = "GET") => fetch(BASIS + weg, { method: methode, headers: { "Content-Type": "application/json", Cookie: keks }, body: koerper === undefined ? undefined : JSON.stringify(koerper), }); const holen = async (keks, weg) => (await rufe(keks, weg)).json(); const dogi = await anmelden("admin", "CODE-DOGI-0001"); const pat = await anmelden("scout", "CODE-SCOU-0001"); const luna = await anmelden("creator", "CODE-CREA-0001"); const nora = await anmelden("creator", "CODE-CREA-0002"); ok([dogi, pat, luna, nora].every((x) => x.ok && x.keks), "alle vier Rollen angemeldet"); /* ======================================================================= 1. Eintragen und wiederfinden ======================================================================= */ console.log("\n=== Einen Tag eintragen ==="); { const a = await rufe(dogi.keks, `/workspace/api/leistung/${idLuna}/${tag(-1)}`, { diamanten: 1200, dauer_min: 95, gueltiger_tag: true, zuschauer_avg: 48, zuschauer_max: 130, verweildauer_s: 210, schenker: 14, follower_neu: 23, notiz: "PK gegen Mia", }, "PUT"); ok(a.ok, `eingetragen (${a.status})`); const { tage } = await holen(dogi.keks, `/workspace/api/leistung/${idLuna}/tage`); const z = tage?.[0]; ok(z?.diamanten === 1200 && z?.verweildauer_s === 210 && z?.notiz === "PK gegen Mia", `alles gespeichert (${z?.diamanten} Diamanten, ${z?.verweildauer_s}s, "${z?.notiz}")`); ok(z?.quelle === "hand", `Quelle vermerkt: ${z?.quelle}`); /* DERSELBE TAG NOCHMAL überschreibt, statt zu verdoppeln. */ await rufe(dogi.keks, `/workspace/api/leistung/${idLuna}/${tag(-1)}`, { diamanten: 1500, gueltiger_tag: true }, "PUT"); const p = new DatabaseSync(process.env.WORKSPACE_DB); const n = p.prepare("SELECT COUNT(*) AS n FROM leistung WHERE creator_id = ? AND tag = ?") .get(idLuna, tag(-1)).n; const wert = p.prepare("SELECT diamanten FROM leistung WHERE creator_id = ? AND tag = ?") .get(idLuna, tag(-1)).diamanten; p.close(); ok(n === 1 && wert === 1500, `zweimal eingetragen: ${n} Zeile, Wert ${wert} (überschrieben)`); /* KEINE ZUKUNFT. Ein Tippfehler im Jahr legt sonst eine Zeile an, die nie wieder auffällt -- sie taucht in keiner Auswertung auf. */ const z2 = await rufe(dogi.keks, `/workspace/api/leistung/${idLuna}/${tag(3)}`, { diamanten: 999 }, "PUT"); ok(z2.status === 400, `ein Tag in der Zukunft: abgelehnt (${z2.status})`); } /* ======================================================================= 2. Das Rechnen — hier wird jede Zahl nachgerechnet ======================================================================= */ console.log("\n=== Die Rechnung stimmt ==="); { const p = new DatabaseSync(process.env.WORKSPACE_DB); p.prepare("DELETE FROM leistung WHERE creator_id = ?").run(idLuna); /* Diese Woche: 3 Tage mit Werten. Ein vierter Tag OHNE Zuschauerzahl -- er darf den Durchschnitt nicht verwässern. */ const setz = p.prepare(`INSERT INTO leistung (creator_id, tag, diamanten, dauer_min, gueltiger_tag, zuschauer_avg, verweildauer_s, erfasst, quelle) VALUES (?,?,?,?,?,?,?,?, 'hand')`); setz.run(idLuna, tag(-1), 100, 60, 1, 40, 200, jetzt); setz.run(idLuna, tag(-2), 200, 60, 1, 60, 300, jetzt); setz.run(idLuna, tag(-3), 300, 60, 1, 50, 250, jetzt); setz.run(idLuna, tag(-4), 0, 0, 0, null, null, jetzt); // Ruhetag /* Vorwoche: zusammen 300 -- damit der Vergleich +100 % ergibt. */ setz.run(idLuna, tag(-8), 300, 60, 1, 30, 100, jetzt); p.close(); const z = await holen(dogi.keks, `/workspace/api/leistung/${idLuna}`); ok(z.woche.diamanten.wert === 600, `Diamanten diese Woche: ${z.woche.diamanten.wert} (100+200+300+0 = 600)`); ok(z.woche.diamanten.vorher === 300, `Vorwoche: ${z.woche.diamanten.vorher}`); ok(z.woche.diamanten.wandel?.prozent === 100, `Veränderung: +${z.woche.diamanten.wandel?.prozent} % (300 → 600)`); /* DER DURCHSCHNITT ZÄHLT NUR TAGE MIT WERT. (40+60+50)/3 = 50. Würde der Ruhetag als 0 mitgerechnet, käme 37,5 heraus -- und jede Zahl würde kleingeredet, sobald jemand Urlaub hat. */ ok(z.woche.zuschauer_avg.wert === 50, `Ø Zuschauer: ${z.woche.zuschauer_avg.wert} — der Ruhetag ohne Wert zählt nicht mit`); /* Dasselbe für die Leitzahl. (200+300+250)/3 = 250 */ ok(z.woche.verweildauer_s.wert === 250, `Ø Verweildauer: ${z.woche.verweildauer_s.wert}s`); ok(z.woche.gueltige_tage.wert === 3, `gültige Tage: ${z.woche.gueltige_tage.wert} (der Ruhetag zählt nicht)`); /* Erfassungsgrad -- eine Zahl aus zwei von sieben Tagen ist etwas anderes als eine aus sieben. */ ok(z.erfassung.tage === 4, `${z.erfassung.tage} von 7 Tagen erfasst`); /* VERLAUF für die kleine Linie. */ ok(Array.isArray(z.verlauf) && z.verlauf.length === 8, `Verlauf über ${z.verlauf?.length} Wochen`); ok(z.verlauf.at(-1).diamanten === 600, `die letzte Woche im Verlauf stimmt mit der Wochenzahl überein`); } /* ======================================================================= 3. Prozent von null — die Falle ======================================================================= */ console.log("\n=== Von null aus gibt es keine Prozent ==="); { const p = new DatabaseSync(process.env.WORKSPACE_DB); p.prepare("DELETE FROM leistung WHERE creator_id = ?").run(idNora); p.prepare(`INSERT INTO leistung (creator_id, tag, diamanten, gueltiger_tag, erfasst, quelle) VALUES (?,?,?,1,?, 'hand')`).run(idNora, tag(-1), 500, jetzt); p.close(); const z = await holen(dogi.keks, `/workspace/api/leistung/${idNora}`); /* Vorwoche war 0. "+unendlich Prozent" wäre Unsinn auf dem Bildschirm -- stattdessen die Auskunft "vorher nichts". */ ok(z.woche.diamanten.wandel?.neu === true && z.woche.diamanten.wandel?.prozent === undefined, `von 0 auf 500: als "neu" gemeldet, nicht als Prozentwert`); /* Und wenn beide null sind, gibt es gar keine Meldung. */ ok(z.woche.schenker.wandel === null, `0 → 0 ergibt keine Veränderungsmeldung`); } /* ======================================================================= 4. Ziele als Fortschritt ======================================================================= */ console.log("\n=== Ziele ==="); { await rufe(dogi.keks, `/workspace/api/leistung/${idLuna}/ziele`, { diamanten_woche: 1200, tage_woche: 4, verweildauer_s: 300 }, "PUT"); const z = await holen(dogi.keks, `/workspace/api/leistung/${idLuna}`); ok(z.ziele?.diamanten?.anteil === 50, `600 von 1200 Diamanten = ${z.ziele?.diamanten?.anteil} %`); ok(z.ziele?.tage?.anteil === 75, `3 von 4 Tagen = ${z.ziele?.tage?.anteil} %`); /* Aus "mehr Reichweite" wird eine prüfbare Größe. */ ok(z.ziele?.verweildauer?.ist === 250 && z.ziele?.verweildauer?.ziel === 300, `Verweildauer ${z.ziele?.verweildauer?.ist}s von ${z.ziele?.verweildauer?.ziel}s`); } /* ======================================================================= 5. Rechte ======================================================================= */ console.log("\n=== Wer darf was ==="); { /* Ein Creator trägt seine Zahlen NICHT selbst ein. Nicht aus Misstrauen: Die Zahlen kommen aus dem offiziellen Export. */ const a = await rufe(luna.keks, `/workspace/api/leistung/${idLuna}/${tag(-1)}`, { diamanten: 99999 }, "PUT"); ok(a.status === 403, `Creator trägt eigene Zahlen ein: abgelehnt (${a.status})`); /* Aber SEHEN darf sie ihre eigenen. */ const b = await rufe(luna.keks, `/workspace/api/leistung/${idLuna}`); ok(b.ok, `ihre eigenen Zahlen sehen: geht (${b.status})`); /* Fremde nicht -- und zwar 404, nicht 403. */ const c = await rufe(luna.keks, `/workspace/api/leistung/${idNora}`); ok(c.status === 404, `fremde Zahlen: ${c.status} (404 — die Nummer verrät nichts)`); /* Der Scout darf bei SEINEM Creator eintragen. */ const e = await rufe(pat.keks, `/workspace/api/leistung/${idLuna}/${tag(-5)}`, { diamanten: 50, gueltiger_tag: true }, "PUT"); ok(e.ok, `Scout bei seinem Creator: geht (${e.status})`); /* Bei einem fremden nicht. */ const f = await rufe(pat.keks, `/workspace/api/leistung/${idNora}/${tag(-1)}`, { diamanten: 50 }, "PUT"); ok(f.status === 404, `Scout bei fremdem Creator: ${f.status}`); } /* ======================================================================= 6. Der CSV-Import ======================================================================= */ console.log("\n=== Import aus dem Export ==="); { const p = new DatabaseSync(process.env.WORKSPACE_DB); p.prepare("DELETE FROM leistung WHERE creator_id = ?").run(idNora); p.close(); /* Deutsches Format: Semikolon, Punkt als Tausendertrenner. */ const csv = [ "Datum;Diamanten;LIVE-Dauer (Min);Ø Zuschauer;Spitze Zuschauer;Verweildauer;Schenker;Neue Follower", `${tag(-3)};1.250;95;48;130;210;14;23`, `${tag(-2)};2.100;120;62;180;245;19;31`, `${tag(-1)};890;45;30;70;180;8;12`, ].join("\n"); /* ERST DIE VORSCHAU -- sie schreibt nichts. */ const v = await (await rufe(dogi.keks, `/workspace/api/leistung/${idNora}/import/vorschau`, { text: csv }, "POST")).json(); ok(v.neu === 3 && v.ersetzt === 0 && v.fehlerhaft === 0, `Vorschau: ${v.neu} neu, ${v.ersetzt} ersetzt, ${v.fehlerhaft} fehlerhaft`); /* Die Spalten wurden erkannt. */ ok(v.spalten?.diamanten === 1 && v.spalten?.verweildauer_s === 5, `Spalten erkannt: Diamanten=${v.spalten?.diamanten}, Verweildauer=${v.spalten?.verweildauer_s}`); const p2 = new DatabaseSync(process.env.WORKSPACE_DB); const vorher = p2.prepare("SELECT COUNT(*) AS n FROM leistung WHERE creator_id = ?").get(idNora).n; p2.close(); ok(vorher === 0, `die Vorschau hat NICHTS geschrieben (${vorher} Zeilen)`); /* Jetzt übernehmen. */ const u = await (await rufe(dogi.keks, `/workspace/api/leistung/${idNora}/import`, { text: csv }, "POST")).json(); ok(u.geschrieben === 3, `übernommen: ${u.geschrieben} Tage`); /* DAS DEUTSCHE ZAHLENFORMAT: "1.250" sind 1250, nicht 1,25. */ const p3 = new DatabaseSync(process.env.WORKSPACE_DB); const z = p3.prepare("SELECT * FROM leistung WHERE creator_id = ? AND tag = ?") .get(idNora, tag(-3)); ok(z?.diamanten === 1250, `"1.250" wurde als ${z?.diamanten} gelesen (nicht als 1,25)`); ok(z?.quelle === "import", `Quelle: ${z?.quelle}`); ok(z?.gueltiger_tag === 1, `gültiger Tag aus der Dauer geschlossen (95 Min > 0)`); p3.close(); /* ZWEIMAL EINLESEN — der häufigste Fehler bei Importen. */ const u2 = await (await rufe(dogi.keks, `/workspace/api/leistung/${idNora}/import`, { text: csv }, "POST")).json(); const p4 = new DatabaseSync(process.env.WORKSPACE_DB); const n = p4.prepare("SELECT COUNT(*) AS n FROM leistung WHERE creator_id = ?").get(idNora).n; p4.close(); ok(n === 3, `dieselbe Datei zweimal: ${n} Zeilen (nicht 6) — überschrieben statt verdoppelt`); /* EINE VON HAND GESCHRIEBENE NOTIZ ÜBERLEBT den Import. Sie steht in keinem Export -- würde er sie leeren, wäre die Erklärung für einen Einbruch weg. */ await rufe(dogi.keks, `/workspace/api/leistung/${idNora}/${tag(-1)}`, { diamanten: 890, notiz: "war krank" }, "PUT"); await rufe(dogi.keks, `/workspace/api/leistung/${idNora}/import`, { text: csv }, "POST"); const p5 = new DatabaseSync(process.env.WORKSPACE_DB); const mitNotiz = p5.prepare("SELECT notiz FROM leistung WHERE creator_id = ? AND tag = ?") .get(idNora, tag(-1)); p5.close(); ok(mitNotiz?.notiz === "war krank", `die Notiz "${mitNotiz?.notiz}" hat den Import überlebt`); /* Englisches Format muss auch gehen. */ const csvEn = [ "Date,Diamonds,Duration,Avg Viewers", `${tag(-5)},"1,234",60,45`, ].join("\n"); const v2 = await (await rufe(dogi.keks, `/workspace/api/leistung/${idNora}/import/vorschau`, { text: csvEn }, "POST")).json(); ok(v2.neu === 1 && v2.beispiele?.[0]?.diamanten === 1234, `englisches Format: "1,234" wurde als ${v2.beispiele?.[0]?.diamanten} gelesen`); /* Kaputte Zeilen werden gemeldet, nicht verschluckt. */ const csvKaputt = [ "Datum;Diamanten", `${tag(-1)};100`, "keindatum;200", `${tag(20)};300`, ].join("\n"); const v3 = await (await rufe(dogi.keks, `/workspace/api/leistung/${idNora}/import/vorschau`, { text: csvKaputt }, "POST")).json(); ok(v3.fehlerhaft === 2 && v3.probleme?.length === 2, `2 kaputte Zeilen gemeldet: ${v3.probleme?.map((p) => p.grund).join(" · ")}`); } /* ======================================================================= 7. Die Frühwarnung ======================================================================= */ console.log("\n=== Frühwarnung ==="); { const p = new DatabaseSync(process.env.WORKSPACE_DB); p.prepare("DELETE FROM leistung").run(); /* Luna: alles in Ordnung -- gleichbleibende Zahlen, frisch angemeldet, gerade ein Gespräch gehabt. */ const setz = p.prepare(`INSERT INTO leistung (creator_id, tag, diamanten, dauer_min, gueltiger_tag, erfasst, quelle) VALUES (?,?,?,?,?,?, 'hand')`); for (let w = 0; w < 3; w++) { for (let t = 0; t < 4; t++) setz.run(idLuna, tag(-(w * 7 + t + 1)), 300, 60, 1, jetzt); } p.prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt, idLuna); p.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von) VALUES ('Call','call',?,30,?,?,?)`).run(tag(-2) + "T10:00", idLuna, jetzt, idDogi); /* Nora: fallende Zahlen über drei Wochen, LIVE-Tage brechen weg, lange nicht angemeldet, nie ein Gespräch. */ for (let t = 0; t < 5; t++) setz.run(idNora, tag(-(14 + t + 1)), 400, 60, 1, jetzt); for (let t = 0; t < 3; t++) setz.run(idNora, tag(-(7 + t + 1)), 200, 60, 1, jetzt); setz.run(idNora, tag(-1), 50, 30, 1, jetzt); p.prepare("UPDATE personen SET letzter_login = ? WHERE id = ?") .run(new Date(Date.now() - 40 * 86400000).toISOString(), idNora); p.close(); const f = await holen(dogi.keks, "/workspace/api/fruehwarnung"); const nora = f.creator?.find((c) => c.name === "Nora"); const lunaW = f.creator?.find((c) => c.name === "Luna"); ok(!!nora, `Nora hat Befunde (${nora?.signale?.length || 0} Signale)`); ok(!lunaW, `Luna hat KEINE — ein System, das jede Woche Alarm gibt, wird nicht gelesen`); const arten = (nora?.signale || []).map((s) => s.art); ok(arten.includes("zahlen_fallen"), `Signal "Zahlen fallen": ${arten.includes("zahlen_fallen")}`); ok(arten.includes("tage_weg"), `Signal "LIVE-Tage brechen weg"`); ok(arten.includes("nicht_angemeldet"), `Signal "lange nicht angemeldet"`); ok(arten.includes("nie_gesprochen"), `Signal "nie ein Gespräch"`); /* JEDES SIGNAL IST EIN SATZ, keine Zahl. Das ist die Kernentscheidung gegen einen Score. */ const ersterSatz = nora?.signale?.[0]?.text || ""; ok(/\D/.test(ersterSatz) && ersterSatz.length > 20, `die Signale sind Sätze, keine Punktzahl: "${ersterSatz}"`); /* KEIN SCORE nach außen. */ ok(nora && nora._rang === undefined && nora.punkte === undefined && nora.score === undefined, `keine Gesamtnote in der Antwort — bewusst`); /* Ein Satz für die Startseite. */ ok(/Nora/.test(f.satz || ""), `Satz für die Startseite: "${f.satz}"`); /* EIN CREATOR SIEHT DIE FRÜHWARNUNG NICHT. Das ist eine Arbeitsgrundlage für die Betreuung, keine Mitteilung an die betroffene Person. */ const beiLuna = await holen(luna.keks, "/workspace/api/fruehwarnung"); ok(beiLuna.gilt_nicht === true && (beiLuna.creator || []).length === 0, `ein Creator bekommt keine Frühwarnung zu sehen`); /* Und ein Scout sieht nur seine eigenen. */ const beiPat = await holen(pat.keks, "/workspace/api/fruehwarnung"); ok(!(beiPat.creator || []).some((c) => c.name === "Nora"), `der Scout sieht nur seine Betreuten (${(beiPat.creator || []).map((c) => c.name).join(", ") || "keine"})`); } /* ======================================================================= GEGENPROBE ======================================================================= */ console.log("\n=== Gegenprobe ==="); { /* Schlägt die Frühwarnung an, wenn wirklich nichts ist? Sie darf nicht -- sonst wäre "Nora hat Befunde" oben wertlos. */ const p = new DatabaseSync(process.env.WORKSPACE_DB); p.prepare("DELETE FROM leistung").run(); p.prepare("UPDATE personen SET letzter_login = ? WHERE rolle = 'creator'").run(jetzt); p.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von) VALUES ('Call','call',?,30,?,?,?)`).run(tag(-1) + "T10:00", idNora, jetzt, idDogi); p.close(); const f = await holen(dogi.keks, "/workspace/api/fruehwarnung"); ok((f.creator || []).length === 0 && f.satz === null, `ohne Auffälligkeiten meldet sie nichts (${(f.creator || []).length} Befunde)`); /* Und ohne Anmeldung gar nichts. */ for (const weg of ["/workspace/api/leistung", "/workspace/api/fruehwarnung"]) { const a = await fetch(BASIS + weg); ok(a.status === 401, `${weg} ohne Anmeldung: ${a.status}`); } } d.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`); /* Der Server läuft weiter -- siehe pruef-chat-optik. */ process.exit(fehler ? 1 : 0);