Files
dogfather-universe/server/pruef-abzeichen.mjs
T
DogFatherGitandClaude Opus 5 d5c84c7608 Abzeichen am App-Symbol - und die Zahl gehoert zu einem Haus
Die kleine Zahl auf dem Symbol des Startbildschirms. Sie fehlte ganz;
setAppBadge kam im Haus kein einziges Mal vor.

ZWEI ENTSCHEIDUNGEN

Sie zaehlt ungelesene Nachrichten und nichts sonst. Ein Abzeichen
muss weggehen koennen: Nachrichten verschwinden, sobald man sie
liest; eine ueberfaellige Aufgabe verschwindet nicht dadurch, dass
man die App oeffnet. Eine Zahl, die dauerhaft dasteht, ist nach drei
Tagen kein Hinweis mehr, sondern ein Fleck.

Und sie hat EINE Quelle. Im Browser haengt sie an chatZahlZeigen() -
der einen Stelle, an der die Zahl ohnehin gesetzt wird (beim Laden,
aus dem Ereignisstrom, beim Lesen). Fuer die geschlossene App - wo
das Abzeichen ueberhaupt erst etwas wert ist - reist dieselbe Zahl in
der Benachrichtigung mit. Der Service Worker setzt sie nur, wenn eine
dabei ist: Eine Aufgabenerinnerung mit "0" haette dem Chat sein
Abzeichen weggenommen.

DER FUND NEBENBEI

Beim Herausloesen der Abfrage fiel auf, dass sie nie nach dem Haus
gefragt hat - die Raumliste zwanzig Zeilen darueber tut es laengst.
Gemessen: Eine Managerin schreibt DogFather an der Agenturwand an,
sein Zaehler auf crew. springt von 0 auf 1. Seit dem 24.09. soll das
nicht mehr sein. Aufgefallen ist es erst jetzt, weil dieselbe Zahl ab
heute auf dem Startbildschirm steht - und eine Zahl, die etwas
Falsches zeigt, ist schlimmer als keine.

Behoben ueber hausWo(); die Meldung nimmt das Haus des Raums, aus dem
sie stammt.

GEPRUEFT

server/pruef-abzeichen.mjs, 19 Messungen am echten Weg: ein
nachgebauter Browser macht die Benachrichtigung mit seinem privaten
Schluessel auf und liest die Zahl heraus. Mit Gegenproben - ohne Zahl
kommt keine mit, NaN rutscht nicht durch, eine echte Sieben schon.
Und Abschnitt 7 wird rot, sobald man die Hausregel wieder herausnimmt
(nachgestellt).

Zwei eigene Messfehler unterwegs, beide im Text festgehalten: eine
Suche, die im Kommentar landete statt im Code (jetzt ueber
jsOhneKommentar), und Aufrufe ohne Host-Feld - ueber 127.0.0.1 gibt
es kein Haus, die Pruefung mass also eine Regel an einer Verbindung,
die sie gar nicht kennt.

helfer-push-aufmachen.mjs: das Entschluesseln stand als lokale
Funktion in pruef-push-weg; zwei Abschriften waeren die, die
auseinanderlaufen.

Nachbarlaeufe gruen: push, push-ziel, push-weg (17 unveraendert),
arten, portnummern, struktur, chat, chat-kanaele, chatkachel, anruf,
haus-trennung, zwischenspeicher.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-30 13:39:55 +02:00

551 lines
24 KiB
JavaScript

/* =====================================================================
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);