/* ===================================================================== DIE ZAHL AUF DEM APP-SYMBOL (30.09.2026) Filipe: „ich will dass du die benarichtigungen perfektionierst … und das sehr hochwertig." Das Abzeichen ist die kleine Zahl, die auf dem Startbildschirm am Symbol der installierten App klebt. Es fehlte ganz — `setAppBadge` kam im ganzen Haus nicht ein einziges Mal vor. --------------------------------------------------------------------- WARUM ES UNGELESENE NACHRICHTEN ZÄHLT UND NICHTS SONST Ein Abzeichen muss WEGGEHEN können. Ungelesene Nachrichten verschwinden, sobald man sie liest. Eine überfällige Aufgabe verschwindet nicht dadurch, dass man die App öffnet — eine Zahl, die dauerhaft dasteht, ist nach drei Tagen keine Auskunft mehr, sondern ein Fleck. Dann übersieht man auch die, die etwas bedeutet. Dieselbe Überlegung wie bei der Warnung, die immer kommt. Deshalb ist Abschnitt 3 der wichtigste in dieser Datei: Er misst nicht, dass die Zahl STEIGT, sondern dass sie wieder FÄLLT. --------------------------------------------------------------------- WAS HIER GEMESSEN WIRD — UND ZWAR AM ECHTEN WEG Ein nachgebauter Browser meldet ein Gerät an, behält seinen privaten Schlüssel, und ein nachgebauter Push-Dienst nimmt die Nachricht entgegen. Aufgemacht wird sie mit dem Schlüssel des Geräts — also genau so, wie es auf Filipes Handy passiert. Ohne das Aufmachen wäre die Prüfung wertlos: Der Inhalt ist verschlüsselt, und `benachrichtige` gibt die Zahl nicht zurück. Man sähe nur, DASS etwas verschickt wurde, nicht WAS darin steht. --------------------------------------------------------------------- GEGENPROBEN (Abschnitt 4) Ohne sie hieße „die Zahl ist da" nur, dass irgendeine Zahl da ist: * Eine Benachrichtigung OHNE Zahl darf auch keine mitschicken. Sonst löschte eine Aufgabenerinnerung das Abzeichen des Chats. * Etwas, das keine Zahl ist, darf nicht durchrutschen. Aufrufen mit: node server/pruef-abzeichen.mjs ===================================================================== */ import { mkdtempSync, rmSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { createServer } from "node:http"; import { eigenerPort } from "./helfer-port.mjs"; import { pushAufmachen } from "./helfer-push-aufmachen.mjs"; import { jsOhneKommentar } from "./helfer-ohne-kommentar.mjs"; const HIER = dirname(fileURLToPath(import.meta.url)); const WURZEL = join(HIER, ".."); const PORT = await eigenerPort(import.meta, "pruef-abzeichen"); const DIENST = await eigenerPort(import.meta, "pruef-abzeichen (Push-Dienst)", 1); const ordner = mkdtempSync(join(tmpdir(), "ws-abz-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; /* ---- KEINE RUHEZEIT, UND ZWAR HERGESTELLT STATT GEHOFFT ---------- `chat_nachricht` trägt keine eigene Ruheregel, gilt also als „immer": zwischen 22 und 7 Uhr geht nichts hinaus. Ohne diese zwei Zeilen wäre die Prüfung eine Zeitbombe — grün am Nachmittag, rot um halb zwölf nachts, und beides ohne eine Aussage über die Software. PUSH_RUHE_AB = 24 gesperrt ab Stunde 24, die es nicht gibt PUSH_RUHE_BIS = 0 und frei ab Stunde 0 Damit ist `s >= 24 || s < 0` zu jeder Tageszeit falsch. Die Uhr wird nicht gefälscht, nur die Regel klar gesetzt — dieselbe Technik wie in pruef-push-ziel, dort andersherum. */ process.env.PUSH_RUHE_AB = "24"; process.env.PUSH_RUHE_BIS = "0"; 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(120_000, "pruef-abzeichen"); await new Promise((r) => setTimeout(r, 900)); /* Eigene Zuhörer NEBEN denen aus index.js — `process.on` ergänzt, es ersetzt nicht. Sonst notiert das Auffangnetz des Betriebs meinen Absturz nur, und der Lauf sieht von außen aus wie „läuft noch". */ process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f?.stack || f); process.exit(1); }); process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f?.stack || f); process.exit(1); }); const BASIS = `http://127.0.0.1:${PORT}`; let fehler = 0, geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; /* DER DRITTE AUSGANG. Eine Prüfung, die ihre Voraussetzung nicht hat, muss das SAGEN — „grün" und „rot" sind beide gelogen. */ const konntenicht = (t) => { geprueft++; console.log(" ?? konnte nicht nachsehen: " + t); fehler++; }; /* ---- Der nachgebaute Push-Dienst -------------------------------------- Er hebt die Rümpfe auf, damit wir hineinsehen können. Ein echter Dienst kann das NICHT (das beweist pruef-push-weg) — hier geht es nur deshalb, weil wir gleich auch den privaten Schlüssel des „Geräts" besitzen. */ const pakete = []; const dienst = createServer((q, a) => { const teile = []; q.on("data", (t) => teile.push(t)); q.on("end", () => { pakete.push(Buffer.concat(teile)); a.writeHead(201); a.end(); }); }); await new Promise((r) => dienst.listen(DIENST, "127.0.0.1", r)); const schliessen = () => new Promise((f) => { if (!dienst.listening) return f(); dienst.close(() => f()); }); /* ---- Bestand ----------------------------------------------------------- */ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createECDH } = await import("node:crypto"); /* Einmal anklopfen, damit index.js die Datenbank angelegt hat. */ 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 salz = randomBytes(16).toString("hex"); const hash = scryptSync(code, salz, 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 (?,?,?,?,32768,1,?)`).run(name, rolle, hash, salz, jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idAyla = anlegen("Ayla", "modi", "CODE-TEAM-0001"); /* DAS GERÄT — und wir behalten seinen privaten Schlüssel. Ein erfundener Schlüssel würde beim Verschlüsseln scheitern; dann wäre „nichts angekommen" die Antwort auf eine ganz andere Frage. */ const geraet = createECDH("prime256v1"); geraet.generateKeys(); const geheim = randomBytes(16); d.prepare(`INSERT INTO push_anmeldungen (person_id, endpunkt, p256dh, auth, erstellt) VALUES (?,?,?,?,?)`) .run(idAyla, `http://127.0.0.1:${DIENST}/push/${idAyla}`, geraet.getPublicKey().toString("base64url"), geheim.toString("base64url"), jetzt); /* ---- Anmelden an der Crew-Wand ------------------------------------------ `fetch` setzt den Host-Kopf selbst; für die Wand braucht es node:http. Danach genügt der Keks. */ const { request: httpAnfrage } = await import("node:http"); function anmeldenAn(rolle, code, wand = "crew.dogfather-universe.com") { return new Promise((fertig) => { const rumpf = JSON.stringify({ rolle, code }); const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(rumpf), Host: wand, }, }, (antwort) => { antwort.resume(); antwort.on("end", () => fertig({ ok: antwort.statusCode === 200, keks: (antwort.headers["set-cookie"] || []).map((k) => k.split(";")[0]).join("; "), wand, })); }); a.on("error", () => fertig({ ok: false, keks: "", wand })); a.end(rumpf); }); } /** Ein Aufruf MIT Wand — und warum das hier nicht `fetch` sein darf. * * Das Haus einer Sitzung kommt aus dem Host-Feld (sitzungLesen liest * `req.get("host")`), und `fetch` setzt dieses Feld selbst aus der * Adresse; ein mitgegebenes ignoriert es. Über 127.0.0.1 ist das * Haus damit weder crew noch agentur — `hausWo` gibt „1=1" zurück, * und Abschnitt 7 hätte die Hausregel an einer Verbindung gemessen, * die gar kein Haus hat. * * Genau das ist beim ersten Anlauf passiert: Die Prüfung blieb rot, * nachdem die Regel schon eingebaut war. Ein Messfehler, der wie ein * Befund aussieht — und andersherum hätte er einen echten Befund * verdeckt. */ function rufe(sitzung, weg, koerper, methode = "GET") { const rumpf = koerper === undefined ? undefined : JSON.stringify(koerper); return new Promise((fertig) => { const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: weg, method: methode, headers: { "Content-Type": "application/json", Cookie: sitzung.keks, Host: sitzung.wand, ...(rumpf === undefined ? {} : { "Content-Length": Buffer.byteLength(rumpf) }), }, }, (antwort) => { const teile = []; antwort.on("data", (t) => teile.push(t)); antwort.on("end", () => { const text = Buffer.concat(teile).toString("utf8"); fertig({ ok: antwort.statusCode >= 200 && antwort.statusCode < 300, status: antwort.statusCode, json: () => { try { return JSON.parse(text); } catch { return {}; } }, }); }); }); a.on("error", () => fertig({ ok: false, status: 0, json: () => ({}) })); a.end(rumpf); }); } const dogi = await anmeldenAn("admin", "CODE-DOGI-0001"); const ayla = await anmeldenAn("modi", "CODE-TEAM-0001"); if (!dogi.ok || !ayla.ok) { konntenicht("die Anmeldung hat nicht geklappt — ohne sie ist nichts zu messen"); await schliessen(); d.close(); rmSync(ordner, { recursive: true, force: true }); process.exit(1); } /* Ein Zweiergespräch. Ayla schaut NICHT zu (kein Ereignisstrom offen) — nur dann entsteht überhaupt eine Benachrichtigung. */ const raumId = ((await rufe(dogi, "/workspace/api/chat/direkt", { mit: idAyla }, "POST")).json()).raum_id; /** Schickt eine Nachricht und wartet, bis das Paket beim Push-Dienst * liegt — auf das ERGEBNIS, nicht auf die Uhr. `chatEreignis` ruft * die Benachrichtigung mit `void` auf; die Antwort der Route ist * also schon da, bevor irgendetwas verschickt wurde. */ async function schreibenUndWarten(text, frist = 8000) { const vorher = pakete.length; const a = await rufe(dogi, `/workspace/api/chat/raeume/${raumId}/nachrichten`, { text }, "POST"); if (!a.ok) return { fehlgeschlagen: `HTTP ${a.status}` }; const bis = Date.now() + frist; while (pakete.length === vorher && Date.now() < bis) { await new Promise((r) => setTimeout(r, 60)); } if (pakete.length === vorher) return { fehlgeschlagen: `nach ${frist / 1000} s nichts angekommen` }; return { paket: pakete[pakete.length - 1] }; } /** Das letzte Paket aufmachen und als Objekt zurückgeben. */ const inhalt = (paket) => JSON.parse(pushAufmachen(paket, geraet, geheim)); /* ======================================================================= 1. DIE ZAHL KOMMT ÜBERHAUPT MIT ======================================================================= */ console.log(""); console.log("=== 1. Die Zahl reist in der Benachrichtigung mit ==="); let eins = null; { const r = await schreibenUndWarten("Erste Nachricht"); if (r.fehlgeschlagen) { konntenicht(`die erste Nachricht kam nicht am Push-Dienst an (${r.fehlgeschlagen})`); } else { eins = inhalt(r.paket); ok(typeof eins.titel === "string" && eins.titel.length > 0, `die Benachrichtigung ist lesbar angekommen ("${eins.titel}")`); ok(eins.zahl === 1, `und trägt die Zahl 1 für das App-Symbol (zahl=${JSON.stringify(eins.zahl)})`); } } /* ======================================================================= 2. SIE ZÄHLT MIT ======================================================================= */ console.log(""); console.log("=== 2. Bei der zweiten Nachricht steht 2 darauf ==="); { const r = await schreibenUndWarten("Zweite Nachricht"); if (r.fehlgeschlagen) { konntenicht(`die zweite Nachricht kam nicht an (${r.fehlgeschlagen})`); } else { /* OHNE DIESE ZEILE beweist Abschnitt 1 nur, dass irgendeine Eins mitkommt — eine fest eingebaute täte dasselbe. */ ok(inhalt(r.paket).zahl === 2, `die Zahl wächst mit (zahl=${JSON.stringify(inhalt(r.paket).zahl)})`); } } /* ======================================================================= 3. UND SIE GEHT WIEDER WEG — das Wichtigste an der ganzen Sache ======================================================================= */ console.log(""); console.log("=== 3. Wer liest, wird das Abzeichen los ==="); { const letzte = d.prepare("SELECT MAX(id) AS n FROM chat_nachrichten WHERE raum_id = ?") .get(raumId)?.n; const a = await rufe(ayla, `/workspace/api/chat/raeume/${raumId}/gelesen`, { bis: letzte }, "PUT"); ok(a.ok, `Ayla liest bis Nachricht ${letzte} (HTTP ${a.status})`); const z = (await rufe(ayla, "/workspace/api/chat/ungelesen")).json(); ok(z.ungelesen === 0, `danach steht die Zahl auf null — das Abzeichen verschwindet (${z.ungelesen})`); /* UND SIE FÄNGT WIEDER BEI EINS AN. Das trennt „die Zahl wird zurückgesetzt" von „die Zahl wird nur nicht mehr geschickt". */ const r = await schreibenUndWarten("Dritte Nachricht"); if (r.fehlgeschlagen) { konntenicht(`die dritte Nachricht kam nicht an (${r.fehlgeschlagen})`); } else { ok(inhalt(r.paket).zahl === 1, `die nächste Nachricht zählt wieder ab eins (zahl=${JSON.stringify(inhalt(r.paket).zahl)})`); } } /* ======================================================================= 4. GEGENPROBEN — was KEINE Zahl tragen darf ======================================================================= */ console.log(""); console.log("=== 4. Gegenproben ==="); { const push = await import("./workspace-push.js"); /* EINE MELDUNG OHNE ZAHL DARF AUCH KEINE MITSCHICKEN. Das ist die Zeile, an der die ganze Überlegung hängt: Käme bei einer Aufgabenerinnerung `zahl: 0` mit, würde sie das Abzeichen löschen — und die ungelesenen Nachrichten wären vom Startbildschirm verschwunden, ohne dass jemand sie gelesen hat. */ const vorher = pakete.length; await push.benachrichtige(idAyla, "test", { titel: "Erinnerung", text: "Etwas ganz anderes" }); if (pakete.length === vorher) { konntenicht("die Probe-Benachrichtigung kam nicht an"); } else { const o = inhalt(pakete[pakete.length - 1]); ok(!("zahl" in o), `eine Meldung ohne Zahl trägt auch keine (${JSON.stringify(Object.keys(o))})`); } /* UND ETWAS, DAS KEINE ZAHL IST, RUTSCHT NICHT DURCH. `Number .isFinite` und nicht `typeof === "number"`: NaN wäre sonst eine Zahl, und `setAppBadge(NaN)` ist keine Auskunft. */ const vorher2 = pakete.length; await push.benachrichtige(idAyla, "test", { titel: "Krumm", text: "Probe", zahl: Number.NaN }); if (pakete.length === vorher2) { konntenicht("die zweite Probe kam nicht an"); } else { ok(!("zahl" in inhalt(pakete[pakete.length - 1])), "eine Zahl, die keine ist (NaN), kommt gar nicht erst mit"); } /* DIE GEGENPROBE ZUR GEGENPROBE. Ohne sie hieße „keine zahl dabei" womöglich nur, dass NIE eine mitgeht — dann wären die drei Abschnitte darüber grün gewesen, weil sie alle dasselbe Nichts gemessen hätten. */ const vorher3 = pakete.length; await push.benachrichtige(idAyla, "test", { titel: "Gerade", text: "Probe", zahl: 7 }); if (pakete.length === vorher3) { konntenicht("die dritte Probe kam nicht an"); } else { ok(inhalt(pakete[pakete.length - 1]).zahl === 7, "eine echte Zahl geht dagegen mit — die Messung unterscheidet also"); } } /* ======================================================================= 5. WER DIE ZAHL IM BROWSER SETZT ======================================================================= */ console.log(""); console.log("=== 5. Die eine Stelle in der Kopfleiste ==="); { /* ==== GEMESSEN WIRD AM KOMMENTARFREIEN TEXT (30.09.2026) ======= Beim ersten Lauf war Abschnitt 6 rot — zu Unrecht. Die Suche nahm die ERSTE Fundstelle von `setAppBadge`, und die steht im Kommentar darüber („`setAppBadge` gibt es nicht überall"). Sie hat damit den Kommentar vermessen, nicht den Code. Dieselbe Falle wie in pruef-bewegung, dort unter „ein Beispiel im Kommentar zählt nicht". Sie fällt nur auf, wenn sie zufällig rot wird — andersherum hätte ein Kommentar eine fehlende Zeile jahrelang gedeckt. */ const kopf = jsOhneKommentar( readFileSync(join(WURZEL, "workspace", "assets", "js", "kopf.js"), "utf8")); /* SIE MUSS IN `chatZahlZeigen` STEHEN und nirgends sonst. Diese Funktion wird beim Laden, aus dem Ereignisstrom und aus dem Chat gerufen — eine zweite Zählung wäre die, die irgendwann abweicht. */ const block = kopf.slice(kopf.indexOf("window.chatZahlZeigen")); const ende = block.indexOf("\n };"); const rumpf = ende > 0 ? block.slice(0, ende) : ""; ok(rumpf.includes("setAppBadge"), "das Abzeichen wird in chatZahlZeigen() gesetzt"); ok(rumpf.includes("clearAppBadge"), "und bei null wieder gelöscht — sonst bliebe es für immer stehen"); /* KEINE ZWEITE STELLE. `setAppBadge` darf im ganzen Haus nur dort und im Service Worker vorkommen. */ const zweite = []; const { readdirSync } = await import("node:fs"); const suche = (verz) => { for (const e of readdirSync(verz, { withFileTypes: true })) { const p = join(verz, e.name); if (e.isDirectory()) { suche(p); continue; } if (!e.name.endsWith(".js")) continue; const t = jsOhneKommentar(readFileSync(p, "utf8")); if (!/setAppBadge|clearAppBadge/.test(t)) continue; const kurz = p.slice(WURZEL.length + 1).replace(/\\/g, "/"); if (kurz !== "workspace/assets/js/kopf.js" && kurz !== "workspace/sw.js") { zweite.push(kurz); } } }; suche(join(WURZEL, "workspace")); ok(zweite.length === 0, zweite.length === 0 ? "und es gibt keine zweite Stelle, die das Abzeichen anfasst" : `ZWEITE STELLE: ${zweite.join(", ")}`); /* GEGENPROBE: Die Suche findet überhaupt etwas — sonst bewiese die Null oben nur, dass sie nichts liest. */ ok(/setAppBadge/.test(kopf), "die Suche findet setAppBadge in kopf.js (sie liest also wirklich)"); } /* ======================================================================= 6. DER SERVICE WORKER — für die geschlossene App ======================================================================= */ console.log(""); console.log("=== 6. Wenn die App zu ist ==="); { const sw = jsOhneKommentar(readFileSync(join(WURZEL, "workspace", "sw.js"), "utf8")); ok(/Number\.isFinite\(d\.zahl\)/.test(sw), "der Service Worker rührt das Abzeichen nur an, wenn eine Zahl dabei ist"); /* KEIN BEDINGUNGSLOSES LÖSCHEN. Genau das wäre der stille Schaden: Jede beliebige Meldung nähme dem Chat sein Abzeichen weg. */ const zeilen = sw.split("\n").filter((z) => /clearAppBadge/.test(z)); ok(zeilen.length > 0 && zeilen.every((z) => /else/.test(z)), zeilen.length ? `gelöscht wird nur im else-Zweig von „Zahl > 0" (${zeilen.length} Stelle(n))` : "KEIN clearAppBadge gefunden — dann geht das Abzeichen nie weg"); /* Und es darf nichts zum Absturz bringen: Firefox kennt die Funktion nicht, Safari nur für installierte Apps. Eine Zugabe, die die Benachrichtigung mitreißt, wäre ein schlechtes Geschäft. */ /* AB DER WACHE GEMESSEN, nicht ab irgendeinem Vorkommen. `indexOf` nimmt das erste — und das war beim ersten Lauf das im Kommentar. */ const stelle = sw.indexOf("Number.isFinite(d.zahl)"); const umher = sw.slice(stelle, stelle + 400); ok(stelle >= 0 && /try\s*\{/.test(umher) && /catch/.test(umher), "und der Versuch ist abgesichert — ein Gerät ohne Abzeichen bekommt trotzdem seine Meldung"); } /* ======================================================================= 7. DIE ZAHL GEHOERT ZU EINEM HAUS Seit dem 24.09.2026 sieht jedes Haus nur seinen eigenen Bestand -- auch DogFather, der in beiden arbeitet. Die Raumliste haelt sich daran (`hausWo(req.person, "r")`), die Zaehlung der ungelesenen Nachrichten wurde nie danach gefragt. Das faellt jetzt ins Gewicht: Bis heute war es eine Zahl an einem Knopf. Ab heute steht dieselbe Zahl auf dem Startbildschirm -- und eine Zahl, die etwas Falsches zeigt, ist schlimmer als keine, weil man ihr glaubt. ======================================================================= */ console.log(""); console.log("=== 7. Jede Adresse zaehlt nur ihr eigenes Haus ==="); { const idMana = anlegen("Cigdem", "manager", "CODE-MANA-0001"); const mana = await anmeldenAn("manager", "CODE-MANA-0001", "workspace.dogfather-universe.com"); if (!mana.ok) { konntenicht("die Managerin kam nicht an der Agenturwand herein"); } else { /* Ein Gespraech im ANDEREN Haus. `direktRaum` traegt das Haus der Person ein, die es anlegt -- hier also "agentur". */ const raumA = ((await rufe(mana, "/workspace/api/chat/direkt", { mit: idDogi }, "POST")).json()).raum_id; ok(Number.isInteger(raumA), `die Agentur legt ein Gespraech an (Raum ${raumA})`); const vorher = (await rufe(dogi, "/workspace/api/chat/ungelesen")).json(); await rufe(mana, `/workspace/api/chat/raeume/${raumA}/nachrichten`, { text: "Agentursache" }, "POST"); const nachher = (await rufe(dogi, "/workspace/api/chat/ungelesen")).json(); ok(nachher.ungelesen === vorher.ungelesen, `DogFather zaehlt auf crew. keine Agenturnachricht mit ` + `(vorher ${vorher.ungelesen}, nachher ${nachher.ungelesen})`); /* GEGENPROBE: Auf der AGENTURADRESSE muss dieselbe Nachricht gezaehlt werden. Ohne sie hiesse "nicht mitgezaehlt" womoeglich nur, dass gar nichts gezaehlt wird. */ const dogiAgentur = await anmeldenAn("admin", "CODE-DOGI-0001", "workspace.dogfather-universe.com"); if (!dogiAgentur.ok) { konntenicht("DogFather kam nicht an der Agenturwand herein"); } else { const dort = (await rufe(dogiAgentur, "/workspace/api/chat/ungelesen")).json(); ok(dort.ungelesen >= 1, `auf der Agenturadresse steht sie dagegen sehr wohl (${dort.ungelesen})`); } } } /* ------------------------------------------------------------------- */ await schliessen(); d.close(); /* AUFRAEUMEN IST KEIN BEFUND. Der Server hält die Datenbank offen, solange der Prozess läuft; unter Windows gibt `rmSync` darauf EPERM. Das ist ein Rest im Temp-Ordner, kein Fehler in der Software — und er als Absturz gemeldet hätte die ganze Prüfung rot gefärbt, obwohl alle 15 Messungen grün waren. Genau so entsteht ein Rot, das man sich abgewöhnt zu lesen. */ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest im Temp */ } console.log(""); console.log(fehler === 0 ? `ALLES IN ORDNUNG (${geprueft} Pruefungen)` : `${fehler} von ${geprueft} Pruefungen sind nicht in Ordnung.`); process.exit(fehler === 0 ? 0 : 1);