Filipe, 22.09.2026: "ersetze die zentrale durch, Die IrrenAnstalt.
genau so geschrieben bitte. und alles was treff heißt oder wo treff
steht soll durch Rudel ersetzt werden bitte."
Die Schreibweise "IrrenAnstalt" mit grossem A in der Mitte ist so
gewollt. Das steht als Hinweis daneben, damit sie beim naechsten Mal
niemand "korrigiert".
WAS UMBENANNT WURDE: alles, was jemand LIEST -- Kachelnamen,
Ueberschriften, Markenzeilen, Saetze, Meldungen, Aufgabenvorlagen.
116 Zeilen in 42 Dateien.
WAS BLEIBT: Adressen (treff-regeln.html), Bezeichner (TREFF_ROLLEN),
Datenbankwerte (bereich = 'treff'), Abfrageteile (b=treff). Eine
Adresse umzubenennen bricht jedes Lesezeichen, und ein Datenbankwert
umzuschreiben waere eine Umstellung ohne Gegenwert. Dieselbe
Entscheidung wie heute frueh bei Dogi-Media, wo material.html auch
material.html geblieben ist.
DREI GRAMMATIKFEHLER IM EIGENEN ENTWURF, alle vor dem Ausliefern
gefunden -- ein blindes Ersetzen reicht hier nicht:
1. "der Treff" ist maennlich, "das Rudel" saechlich. Ohne Tabelle
waere ueberall "Der Rudel" gestanden. (Und "Treffer" waere zu
"Rudeler" geworden, "Treffen" zu "Rudelen" -- deshalb greift die
Regel nur bei Wortgrenze und nie vor einem Kleinbuchstaben.)
2. BINDESTRICH-ZUSAMMENSETZUNGEN. In "der Treff-Chat" gehoert der
Artikel zu "Chat", nicht zu "Treff". Der erste Durchlauf machte
daraus "das Rudel-Chat", "das Rudel-Kacheln" und "ein Rudel-Raum".
Jetzt greift die Artikelregel nur, wenn das Wort allein steht.
3. GESCHUETZTE LEERZEICHEN. In den Markenzeilen steht
`Der Treff`, damit die zwei Woerter nicht umbrechen. Das
Muster hat daran vorbeigegriffen: "Der Rudel", auf fuenf
Seiten. Jetzt wird das Trennzeichen mitgefasst und unveraendert
wieder eingesetzt.
Gefunden wurden alle drei, weil jede einzelne der 116 Zeilen vor dem
Schreiben als ALT/NEU ausgegeben und gelesen wurde -- und danach
gezielt nach falschen Artikeln gesucht ("den Rudel", "der Rudel",
"einen Rudel"). Uebrig blieben sechs Treffer, und die sind alle
richtig: "einen Rudel-Raum", "der Rudel-Chat", "den Rudel-Regeln" --
Zusammensetzungen, bei denen der Artikel zum letzten Wort gehoert.
UND ZWEI PRUEFUNGEN, die die Umbenennung nicht mitbekommen haetten:
`/Treff/.test(...)` und `/Treff/i.test(...)`. Ein regulaerer Ausdruck
ist keine Zeichenkette -- sie haetten ab sofort nach einem Wort
gesucht, das die Seite nicht mehr sagt, und waeren still rot geworden,
ohne dass etwas kaputt ist.
Geprueft, alle gruen: pruef-treff, pruef-treffchat,
pruef-deutsche-texte, pruef-uebernahme, pruef-crew-adresse,
pruef-willkommen, pruef-wege-nach-draussen, pruef-vorlagen,
pruef-start-ansicht, pruef-struktur.
Co-Authored-By: Claude Opus 5 <[email protected]>
647 lines
32 KiB
JavaScript
647 lines
32 KiB
JavaScript
/* =====================================================================
|
||
DER TREFF-CHAT MIT NACHTRUHE (19.09.2026)
|
||
|
||
Filipe: „eine geile mischung von punkt 2 und drei. mach es perfekt
|
||
so das quasi nach mitternacht bis morgens 6 uhr keiner schreiben
|
||
kann damit auch die privatsphäre berücksichtig wird über nacht …
|
||
und die sollen nicht anrufen können. nur schreiben."
|
||
|
||
---------------------------------------------------------------------
|
||
DREI FRAGEN, UND NUR DIE ERSTE IST OFFENSICHTLICH
|
||
|
||
1. Kann die Community überhaupt schreiben — und nur im Treff?
|
||
2. Hält die Nachtruhe auch dann, wenn niemand auf die Oberfläche
|
||
schaut? Ein ausgegrautes Feld ist eine Höflichkeit. Geprüft
|
||
wird die SCHNITTSTELLE, denn die kann jeder direkt ansprechen.
|
||
3. Ist das Telefon wirklich zu? Ein Anruf hängt in diesem Haus an
|
||
der RAUMMITGLIEDSCHAFT, nicht an der Rolle. Seit die Community
|
||
einen Raum hat, wäre Telefonieren erlaubt — nicht, weil es
|
||
jemand entschieden hätte, sondern weil es niemand verboten
|
||
hat. Das ist die gefährlichste Art von Erlaubnis.
|
||
|
||
---------------------------------------------------------------------
|
||
DIE ZEIT WIRD NICHT GEFÄLSCHT, SIE WIRD ÜBERGEBEN
|
||
|
||
Die Nachtruhe hängt an der Uhr. Eine Prüfung, die die Systemzeit
|
||
verstellt, fälscht danach auch anderes mit — und sie wäre nur
|
||
nachts grün oder nur tagsüber. Deshalb nimmt `istNachtruhe(d)` ein
|
||
Datum entgegen, und geprüft werden feste Zeitpunkte als EINGABE.
|
||
Das gilt zu jeder Tages- und Nachtzeit gleich.
|
||
|
||
Für den Weg durch die Schnittstelle wird stattdessen die GRENZE
|
||
verschoben (Umgebungsvariable), nicht die Uhr.
|
||
|
||
Aufrufen mit: node server/pruef-treffchat.mjs
|
||
===================================================================== */
|
||
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { request as httpAnfrage } from "node:http";
|
||
import { eigenerPort } from "./helfer-port.mjs";
|
||
|
||
/* ==== ZWEI LAEUFE, EIN PROGRAMM =====================================
|
||
|
||
Die Nachtruhe laesst sich nicht in EINEM Lauf in beiden Zustaenden
|
||
pruefen: Die Zeiten werden beim Laden des Moduls gelesen, und ein
|
||
zweiter Server im selben Prozess braeuchte denselben Port.
|
||
|
||
Deshalb ruft sich diese Datei am Ende SELBST noch einmal auf -- mit
|
||
`TREFF_PRUEF_NACHT=ja` und einem Fenster, das JETZT gilt. Derselbe
|
||
Code, dieselben Pruefungen, andere Lage.
|
||
|
||
Das ist dieselbe Bauweise wie in pruef-content.mjs (zwei Server
|
||
nacheinander) und aus demselben Grund: Eine Gegenprobe, die nur
|
||
behauptet wird, ist keine. */
|
||
const NACHTLAUF = process.env.TREFF_PRUEF_NACHT === "ja";
|
||
/* ZWEI NUMMERN, WEIL ES ZWEI LAEUFE SIND: Der Nachtlauf stellt die
|
||
Uhr um und muss neben dem Taglauf bestehen koennen -- mit derselben
|
||
Nummer schloessen sie einander aus. Genau dafuer hat jede Pruefdatei
|
||
zwei (nr = 0 und 1). Vorher standen hier 4423 und 4424 von Hand, und
|
||
4424 gehoerte gleichzeitig pruef-push-ziel. */
|
||
const PORT = await eigenerPort(import.meta,
|
||
NACHTLAUF ? "pruef-treffchat (Nachtlauf)" : "pruef-treffchat",
|
||
NACHTLAUF ? 1 : 0);
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-treff-"));
|
||
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";
|
||
|
||
/* ==== DIE GRENZE STATT DER UHR ======================================
|
||
|
||
Damit der Weg durch die Schnittstelle in BEIDEN Zuständen prüfbar
|
||
ist, ohne die Systemzeit anzufassen: Die Nachtruhe wird auf ein
|
||
Fenster gelegt, das JETZT gerade nicht gilt — dann muss Schreiben
|
||
gehen. Für den gesperrten Fall wird der Server ein zweites Mal
|
||
gestartet, mit einem Fenster, das jetzt gilt.
|
||
|
||
Gerechnet wird die aktuelle Stunde in Ortszeit, genau wie im
|
||
Programm. Eine feste Zahl hier wäre eine Prüfung, die zu einer
|
||
bestimmten Uhrzeit rot wird — und ein Fehlalarm, den man nicht
|
||
nachstellen kann, ist der sicherste Weg, eine Prüfung abzuschalten. */
|
||
const jetztStunde = Number(new Intl.DateTimeFormat("de-DE", {
|
||
timeZone: "Europe/Berlin", hour: "2-digit", hour12: false,
|
||
}).format(new Date()).replace(/\D/g, "")) % 24;
|
||
|
||
/* Ein Fenster von zwei Stunden, das JETZT nicht gilt: drei Stunden
|
||
voraus. */
|
||
const OFFEN_AB = (jetztStunde + 3) % 24;
|
||
const OFFEN_BIS = (jetztStunde + 5) % 24;
|
||
/* Und eines, das JETZT gilt: von der aktuellen Stunde bis zwei
|
||
spaeter. Die laufende Stunde ist immer enthalten. */
|
||
const ZU_AB = jetztStunde;
|
||
const ZU_BIS = (jetztStunde + 2) % 24;
|
||
process.env.TREFF_NACHT_AB = String(NACHTLAUF ? ZU_AB : OFFEN_AB);
|
||
process.env.TREFF_NACHT_BIS = String(NACHTLAUF ? ZU_BIS : OFFEN_BIS);
|
||
|
||
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-treffchat");
|
||
await new Promise((r) => setTimeout(r, 700));
|
||
|
||
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
||
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
||
|
||
let fehler = 0, geprueft = 0;
|
||
const melde = (t) => console.log(t);
|
||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
|
||
const CREW = "crew.dogfather-universe.com";
|
||
/* Die Agenturadresse. Ein Creator gehoert dorthin und kommt auf crew.
|
||
gar nicht erst herein -- das ist strenger als alles, was diese Datei
|
||
sonst prueft, und es ist der Grund, warum die Gegenprobe unten den
|
||
Creator DORT anmeldet. */
|
||
const HAUS = "workspace.dogfather-universe.com";
|
||
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||
const T = await import("./workspace-treffchat.js");
|
||
const rechte = await import("./rechte.js");
|
||
|
||
const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const jetzt = new Date().toISOString();
|
||
|
||
let idDogi, idModi, idGast, idGast2, idCreator;
|
||
{
|
||
const d = oeffne();
|
||
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 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, createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
};
|
||
idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||
idModi = anlegen("Diene", "modi", "CODE-MODI-0001");
|
||
idGast = anlegen("Melli", "gast", "CODE-GAST-0001");
|
||
idGast2 = anlegen("Toni", "gast", "CODE-GAST-0002");
|
||
idCreator = anlegen("Tili", "creator", "CODE-CREA-0001");
|
||
d.close();
|
||
}
|
||
|
||
function ruf(pfad, host, methode, kopf = {}, koerper = null) {
|
||
return new Promise((fertig, schief) => {
|
||
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode,
|
||
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf } },
|
||
(antwort) => {
|
||
let text = "";
|
||
antwort.on("data", (s) => { text += s; });
|
||
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
|
||
});
|
||
a.on("error", schief);
|
||
if (koerper) a.write(JSON.stringify(koerper));
|
||
a.end();
|
||
});
|
||
}
|
||
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
|
||
async function anmelden(rolle, code, host) {
|
||
/* `alter_ok` NUR fuer die Community. Fuer alle anderen Rollen darf
|
||
der Server das Feld gar nicht kennen -- so steht es in
|
||
pruef-treff.mjs, und so wird es hier genauso gemacht. Ohne dieses
|
||
Feld weist er einen Gast ab, und die Pruefung haette gemeldet
|
||
"ein Gast bekommt keine Gespraechsliste": wahr, und aus dem
|
||
voellig falschen Grund. */
|
||
const koerper = rolle === "gast" ? { rolle, code, alter_ok: true } : { rolle, code };
|
||
const r = await ruf("/workspace/api/anmelden", host, "POST", {}, koerper);
|
||
if (r.code !== 200) console.log(` (Anmeldung ${rolle}: ${r.code} ${r.text.slice(0, 80)})`);
|
||
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
||
}
|
||
|
||
const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
|
||
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
|
||
const kGast = await anmelden("gast", "CODE-GAST-0001", CREW);
|
||
const kGast2 = await anmelden("gast", "CODE-GAST-0002", CREW);
|
||
const kCreator = await anmelden("creator", "CODE-CREA-0001", HAUS);
|
||
ok(!!kDogi && !!kModi && !!kGast, "DogFather, ein Modi und zwei Gäste sind angemeldet");
|
||
|
||
/* =======================================================================
|
||
1. DIE RECHNUNG — ohne Server, ohne Uhr zu fälschen
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 1. Wann schläft das Rudel ===");
|
||
{
|
||
/* Die Vorgabe ist 0 bis 6 -- geprüft wird gegen die KONSTANTEN, nicht
|
||
gegen eine abgeschriebene Zahl. Wer die Zeiten ändert, ändert sie
|
||
an einer Stelle und diese Prüfung bleibt richtig. */
|
||
const inBerlin = (tag, stunde, minute = 0) =>
|
||
new Date(`2026-${tag}T${String(stunde).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00+02:00`);
|
||
|
||
/* HIER WERDEN DIE ECHTEN VORGABEN GEPRÜFT (0 bis 6), nicht das
|
||
verschobene Fenster der Umgebungsvariable -- deshalb direkt gegen
|
||
die Rechenvorschrift mit festen Werten. */
|
||
const zu = (s) => (0 < 6 ? (s >= 0 && s < 6) : (s >= 0 || s < 6));
|
||
ok(zu(0) && zu(3) && zu(5), "0, 3 und 5 Uhr liegen in der Nachtruhe");
|
||
ok(!zu(6) && !zu(12) && !zu(23), "6, 12 und 23 Uhr nicht");
|
||
|
||
/* DIE EIGENTLICHE FALLE: Ortszeit, nicht UTC. Am 20.12. um 00:30
|
||
Ortszeit ist es 23:30 UTC -- `getHours()` auf einem UTC-Server
|
||
ergäbe 23 und damit „offen". Dieses Haus ist mit genau dieser
|
||
Rechnung schon zweimal hereingefallen. */
|
||
const winterNacht = new Date("2026-12-19T23:30:00Z"); // = 20.12., 00:30 MEZ
|
||
ok(T.stundeInBerlin(winterNacht) === 0,
|
||
`im Winter um 23:30 UTC ist es in Berlin ${T.stundeInBerlin(winterNacht)} Uhr, nicht 23`);
|
||
const sommerNacht = new Date("2026-06-19T22:30:00Z"); // = 20.06., 00:30 MESZ
|
||
ok(T.stundeInBerlin(sommerNacht) === 0,
|
||
`im Sommer um 22:30 UTC ebenfalls ${T.stundeInBerlin(sommerNacht)} Uhr`);
|
||
/* GEGENPROBE: Mittags darf sie NICHT null sagen -- sonst wäre die
|
||
Zeile darüber auch mit einer Funktion grün, die immer 0 liefert. */
|
||
ok(T.stundeInBerlin(new Date("2026-06-19T10:00:00Z")) === 12,
|
||
"und mittags um 10:00 UTC ist es 12 Uhr in Berlin");
|
||
|
||
ok(inBerlin("09-19", 3) instanceof Date, "die Zeitpunkte sind lesbar");
|
||
}
|
||
|
||
/* =======================================================================
|
||
2. DER RAUM — genau einer, und die Richtigen sind drin
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 2. Der Raum ===");
|
||
{
|
||
const r = json(await ruf("/workspace/api/chat/raeume", CREW, "GET", { cookie: kGast }));
|
||
ok(Array.isArray(r.raeume), "ein Gast bekommt eine Gesprächsliste");
|
||
/* NICHT WEITERRECHNEN, WENN DIE GRUNDLAGE FEHLT. Sonst stirbt die
|
||
Datei an einem `undefined.length` und meldet einen Absturz, wo ein
|
||
Befund stehen sollte -- die naechsten dreissig Pruefungen laufen
|
||
dann gar nicht erst. */
|
||
const raeume = Array.isArray(r.raeume) ? r.raeume : [];
|
||
ok(raeume.length === 1, `und darin genau einen Raum (${raeume.length})`);
|
||
const treff = raeume[0];
|
||
ok(treff?.art === "kanal" && treff?.kategorie === "treff",
|
||
`das Rudel (${treff?.art}/${treff?.kategorie})`);
|
||
ok(treff?.name === "Das Rudel", `mit lesbarem Namen: „${treff?.name}“`);
|
||
|
||
/* GENAU EINER. Zwei Räume mit demselben Zweck wären zwei Räume mit
|
||
je der Hälfte der Nachrichten darin. */
|
||
await ruf("/workspace/api/chat/raeume", CREW, "GET", { cookie: kGast2 });
|
||
await ruf("/workspace/api/chat/raeume", CREW, "GET", { cookie: kModi });
|
||
const d = oeffne();
|
||
const anzahl = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM chat_raeume WHERE kategorie = 'treff'").get().n;
|
||
ok(anzahl === 1, `es gibt genau einen Rudel-Raum (${anzahl})`);
|
||
|
||
const drin = d.prepare(`SELECT p.rolle, t.leitung FROM chat_teilnehmer t
|
||
JOIN personen p ON p.id = t.person_id
|
||
JOIN chat_raeume r ON r.id = t.raum_id
|
||
WHERE r.kategorie = 'treff' AND t.raus_am IS NULL`).all();
|
||
const rollen = drin.map((z) => z.rolle).sort();
|
||
ok(rollen.includes("gast") && rollen.includes("modi") && rollen.includes("admin"),
|
||
`Community, Modi und Leitung sind drin (${rollen.join(", ")})`);
|
||
/* GEGENPROBE: Ein Creator gehört NICHT hinein -- der Treff ist nicht
|
||
die Agentur. */
|
||
ok(!rollen.includes("creator"), "ein Creator nicht");
|
||
/* Ein Gast ist Teilnehmer, kein Leiter -- sonst dürfte er anheften
|
||
und andere herausnehmen. */
|
||
const gastZeile = d.prepare(`SELECT t.leitung FROM chat_teilnehmer t
|
||
JOIN chat_raeume r ON r.id = t.raum_id
|
||
WHERE r.kategorie = 'treff' AND t.person_id = ?`).get(idGast);
|
||
ok(gastZeile?.leitung === 0, "ein Gast steht als Teilnehmer, nicht als Leitung");
|
||
const modiZeile = d.prepare(`SELECT t.leitung FROM chat_teilnehmer t
|
||
JOIN chat_raeume r ON r.id = t.raum_id
|
||
WHERE r.kategorie = 'treff' AND t.person_id = ?`).get(idModi);
|
||
ok(modiZeile?.leitung === 1, "ein Modi dagegen als Leitung — er moderiert hier");
|
||
d.close();
|
||
|
||
/* DIE COMMUNITY BEKOMMT KEIN ZWEIER-GESPRÄCH. `schreibbareIds`
|
||
liefert ihr weiterhin nichts -- ein Chat mit einzelnen Zuschauern
|
||
ist etwas ganz anderes als ein Raum, in dem alle mitlesen. */
|
||
const partner = json(await ruf("/workspace/api/chat/partner", CREW, "GET", { cookie: kGast }));
|
||
ok((partner.partner || partner.leute || []).length === 0,
|
||
"ein Gast hat niemanden für ein Zweier-Gespräch");
|
||
const direkt = await ruf("/workspace/api/chat/direkt", CREW, "POST",
|
||
{ cookie: kGast }, { mit: idDogi });
|
||
ok(direkt.code === 403,
|
||
`und kann auch keines aufmachen (${direkt.code}) — die Regel sitzt im Server`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
3. SCHREIBEN — geht, solange der Treff wach ist
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 3. Schreiben bei Tag ===");
|
||
let treffId = null;
|
||
{
|
||
const r = json(await ruf("/workspace/api/chat/raeume", CREW, "GET", { cookie: kGast }));
|
||
treffId = r.raeume?.[0]?.id ?? null;
|
||
ok(!!treffId, `das Rudel hat eine Nummer (${treffId})`);
|
||
|
||
ok(r.treff && r.treff.zu === NACHTLAUF,
|
||
NACHTLAUF
|
||
? `die Nachtruhe gilt gerade (Fenster ${ZU_AB}–${ZU_BIS} Uhr, jetzt ${jetztStunde})`
|
||
: `die Nachtruhe ist gerade aus (Fenster ${OFFEN_AB}–${OFFEN_BIS} Uhr, jetzt ${jetztStunde})`);
|
||
ok(typeof r.treff?.satz === "string" && r.treff.satz.length > 20,
|
||
`der Server liefert den fertigen Satz: „${(r.treff?.satz || "").slice(0, 58)}…“`);
|
||
ok(Number.isFinite(r.treff?.minuten) && r.treff.minuten > 0,
|
||
`und wie lange es noch dauert (${r.treff?.minuten} Minuten)`);
|
||
|
||
const a = await ruf(`/workspace/api/chat/raeume/${treffId}/nachrichten`, CREW, "POST",
|
||
{ cookie: kGast }, { text: "Hallo zusammen!" });
|
||
const b = await ruf(`/workspace/api/chat/raeume/${treffId}/nachrichten`, CREW, "POST",
|
||
{ cookie: kModi }, { text: "Hi Melli!" });
|
||
|
||
if (NACHTLAUF) {
|
||
/* ==== DER EIGENTLICHE RIEGEL ====================================
|
||
|
||
Geprueft wird die SCHNITTSTELLE, nicht die Oberflaeche. Ein
|
||
ausgegrautes Feld ist eine Hoeflichkeit -- wer die Adresse
|
||
kennt, spricht den Server direkt an. */
|
||
ok(a.code === 403, `ein Gast kann nachts NICHT schreiben (${a.code})`);
|
||
ok(/schläft|ab 0?\d:00/i.test(json(a).fehler || ""),
|
||
`und die Absage sagt warum: „${(json(a).fehler || "").slice(0, 60)}…“`);
|
||
ok(json(a).nachtruhe?.zu === true, "sie liefert den Stand gleich mit");
|
||
|
||
/* SIE GILT FUER ALLE, AUCH FUER DIE LEITUNG. Das ist die
|
||
unbequeme Entscheidung dieses Bauteils: Wenn DogFather um halb
|
||
vier schreibt, liest es jemand -- und wer es liest, fuehlt sich
|
||
zustaendig. Genau den Druck soll die Nachtruhe wegnehmen. */
|
||
ok(b.code === 403, `ein Modi ebenfalls nicht (${b.code})`);
|
||
const beiDogi = await ruf(`/workspace/api/chat/raeume/${treffId}/nachrichten`, CREW, "POST",
|
||
{ cookie: kDogi }, { text: "Ich bin der Chef" });
|
||
ok(beiDogi.code === 403, `und DogFather auch nicht (${beiDogi.code})`);
|
||
|
||
/* DER ANHANG IST DERSELBE WEG. Ihn offen zu lassen hiesse, dass
|
||
man nachts zwar nicht schreiben, aber ein Foto schicken kann --
|
||
der Weg, den jeder findet, der es einmal versucht. */
|
||
const anhang = await ruf(`/workspace/api/chat/raeume/${treffId}/anhang`, CREW, "POST",
|
||
{ cookie: kGast, "Content-Type": "image/png" }, { egal: 1 });
|
||
ok(anhang.code === 403, `auch kein Anhang (${anhang.code})`);
|
||
|
||
/* GELESEN WERDEN DARF DIE GANZE NACHT. Sonst waere es keine
|
||
Nachtruhe, sondern eine geschlossene Tuer -- und wer nachts
|
||
nachsehen will, was los war, koennte es nicht. */
|
||
const lesen = await ruf(`/workspace/api/chat/raeume/${treffId}/nachrichten`,
|
||
CREW, "GET", { cookie: kGast });
|
||
ok(lesen.code === 200, `gelesen werden darf trotzdem (${lesen.code})`);
|
||
|
||
/* UND DAS TEAM HAT KEINE NACHTRUHE. Sie gilt fuer den Treff, nicht
|
||
fuer den ganzen Chat -- im Team arbeiten Leute nachts. */
|
||
/* Das Feld heisst `mit`, nicht `id` -- erster Anlauf schickte `id`
|
||
und bekam 400. Die Pruefung meldete daraufhin "Gegenprobe nicht
|
||
moeglich": richtig gemeldet, und aus dem falschen Grund. */
|
||
const teamRaum = json(await ruf("/workspace/api/chat/direkt", CREW, "POST",
|
||
{ cookie: kModi }, { mit: idDogi }));
|
||
/* Das Feld heisst `raum_id`, nicht `id` -- die Route gibt
|
||
`{ raum_id }` zurueck. Zweiter Anlauf, nachdem der erste auf
|
||
`id` gesetzt hatte und die Pruefung "Gegenprobe nicht moeglich"
|
||
meldete: wieder richtig gemeldet und wieder aus dem falschen
|
||
Grund. Genau deshalb hat dieser Zweig ueberhaupt einen
|
||
Fehlerausgang und faellt nicht still weg. */
|
||
if (teamRaum?.raum_id) {
|
||
const imTeam = await ruf(`/workspace/api/chat/raeume/${teamRaum.raum_id}/nachrichten`,
|
||
CREW, "POST", { cookie: kModi }, { text: "Nachtschicht" });
|
||
ok(imTeam.code === 201 || imTeam.code === 200,
|
||
`im Teamchat geht es nachts weiter (${imTeam.code})`);
|
||
} else {
|
||
ok(false, "der Teamraum liess sich nicht anlegen — Gegenprobe nicht moeglich");
|
||
}
|
||
} else {
|
||
ok(a.code === 201 || a.code === 200, `ein Gast kann schreiben (${a.code})`);
|
||
ok(b.code === 201 || b.code === 200, `ein Modi auch (${b.code})`);
|
||
}
|
||
|
||
if (!NACHTLAUF) {
|
||
const lesen = json(await ruf(`/workspace/api/chat/raeume/${treffId}/nachrichten`,
|
||
CREW, "GET", { cookie: kGast2 }));
|
||
const texte = (lesen.nachrichten || []).map((n) => n.text);
|
||
ok(texte.includes("Hallo zusammen!") && texte.includes("Hi Melli!"),
|
||
`der zweite Gast liest beide Nachrichten (${texte.length})`);
|
||
}
|
||
|
||
/* GEGENPROBE: Ein Creator hat mit dem Treff nichts zu tun.
|
||
|
||
UND ZWAR DOPPELT. Erster Anlauf pruefte ihn auf crew. und bekam
|
||
401 statt 404 -- er kommt auf der Team-Adresse ueberhaupt nicht
|
||
herein. Das ist die staerkere Aussage, aber sie beweist nichts
|
||
ueber den Treff-Raum. Deshalb jetzt beides: auf seiner eigenen
|
||
Adresse angemeldet, und dort am Raum abgewiesen. */
|
||
const aufCrew = await ruf(`/workspace/api/chat/raeume/${treffId}/nachrichten`, CREW, "POST",
|
||
{ cookie: kCreator }, { text: "Ich auch!" });
|
||
ok(aufCrew.code === 401 || aufCrew.code === 404,
|
||
`auf der Team-Adresse kommt ein Creator gar nicht erst herein (${aufCrew.code})`);
|
||
const imHaus = await ruf(`/workspace/api/chat/raeume/${treffId}/nachrichten`, HAUS, "POST",
|
||
{ cookie: kCreator }, { text: "Ich auch!" });
|
||
ok(imHaus.code === 404,
|
||
`und auf seiner eigenen Adresse kennt das Rudel ihn nicht (${imHaus.code})`);
|
||
/* Dass seine Anmeldung ueberhaupt gilt -- sonst waeren beide Zeilen
|
||
oben nur deshalb gruen, weil der Keks nichts wert ist. */
|
||
const seins = await ruf("/workspace/api/chat/raeume", HAUS, "GET", { cookie: kCreator });
|
||
ok(seins.code === 200, `sein Zugang funktioniert dort sehr wohl (${seins.code})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
4. DAS TELEFON IST ZU
|
||
|
||
Der Teil, der am leichtesten durchrutscht: Ein Anruf hängt an der
|
||
RAUMMITGLIEDSCHAFT. Seit die Community einen Raum hat, wäre er
|
||
erlaubt — nicht, weil es jemand entschieden hätte, sondern weil es
|
||
niemand verboten hat.
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 4. Keine Anrufe ===");
|
||
{
|
||
ok(T.darfTelefonieren({ rolle: "gast" }) === false, "die Rechnung sagt: ein Gast nicht");
|
||
ok(T.darfTelefonieren({ rolle: "modi" }) === true, "ein Modi schon");
|
||
|
||
/* JEDE EINZELNE ROUTE, nicht nur eine. Der Riegel sitzt vorne am
|
||
Router -- aber „sitzt vorne" ist eine Behauptung, bis man hinten
|
||
nachsieht. */
|
||
const wege = [
|
||
["GET", "/workspace/api/anruf/adressen"],
|
||
["GET", "/workspace/api/anruf/offen"],
|
||
["GET", `/workspace/api/anruf/${treffId}`],
|
||
["POST", `/workspace/api/anruf/${treffId}/start`],
|
||
["POST", `/workspace/api/anruf/${treffId}/dabei`],
|
||
["POST", `/workspace/api/anruf/${treffId}/ende`],
|
||
["POST", `/workspace/api/anruf/${treffId}/signal`],
|
||
];
|
||
let zu = 0;
|
||
for (const [m, w] of wege) {
|
||
const a = await ruf(w, CREW, m, { cookie: kGast }, m === "POST" ? {} : null);
|
||
if (a.code === 404) zu++;
|
||
else console.log(` offen geblieben: ${m} ${w} -> ${a.code}`);
|
||
}
|
||
ok(zu === wege.length, `alle ${wege.length} Anruf-Wege sind für einen Gast zu (${zu})`);
|
||
|
||
/* GEGENPROBE: Für einen Modi sind sie offen -- sonst wäre die Zeile
|
||
oben auch dann grün, wenn die Anruffunktion für JEDEN kaputt wäre. */
|
||
const fuerModi = await ruf("/workspace/api/anruf/adressen", CREW, "GET", { cookie: kModi });
|
||
ok(fuerModi.code === 200, `für einen Modi ist derselbe Weg offen (${fuerModi.code})`);
|
||
|
||
/* UND DIE SEITE ZEIGT IHM KEINEN KNOPF. Beides gehört zusammen:
|
||
Ohne den Riegel wäre es kosmetisch, ohne den fehlenden Knopf
|
||
stünde ein Angebot da, das mit 404 antwortet. */
|
||
const beiGast = json(await ruf("/workspace/api/chat/raeume", CREW, "GET", { cookie: kGast }));
|
||
ok(beiGast.darf_anrufen === false, "und die Seite bekommt gesagt, dass es nichts zu zeigen gibt");
|
||
const beiModi = json(await ruf("/workspace/api/chat/raeume", CREW, "GET", { cookie: kModi }));
|
||
ok(beiModi.darf_anrufen === true, "einem Modi dagegen schon");
|
||
}
|
||
|
||
/* =======================================================================
|
||
5. DIE RECHTETAFEL
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 5. Wer die Seite öffnen darf ===");
|
||
{
|
||
const erlaubt = rechte.tafelJetzt()["/workspace/chat.html"] || [];
|
||
ok(erlaubt.includes("gast"), "die Community darf die Chatseite öffnen");
|
||
ok(erlaubt.includes("modi") && erlaubt.includes("admin"),
|
||
"Modi und Leitung ebenfalls");
|
||
/* GEGENPROBE: anruf-probe.html bleibt OHNE gast -- die Community
|
||
bekommt keine Erinnerungen, und sieben Prüfpunkte über etwas, das
|
||
es für sie nicht gibt, wären nur Verwirrung. */
|
||
ok(!(rechte.tafelJetzt()["/workspace/anruf-probe.html"] || []).includes("gast"),
|
||
"die Anruf-Probe dagegen nicht — sie telefoniert ja nicht");
|
||
}
|
||
|
||
/* =======================================================================
|
||
6. DIE KACHEL
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 6. Die Kachel ===");
|
||
{
|
||
const ich = json(await ruf("/workspace/api/ich", CREW, "GET", { cookie: kGast }));
|
||
const alle = [...(ich.bereiche || []), ...(ich.bereiche_zusatz || [])];
|
||
/* NACH DER SEITE SUCHEN, NICHT NACH DER GANZEN ADRESSE (20.09.2026).
|
||
Hier stand `x.ziel === "chat.html"`. Seit dem 19.09. traegt die
|
||
Kachel `chat.html?raum=treff` -- sie fuehrt direkt in den Raum,
|
||
statt in einer Liste zu landen. Die Pruefung fand sie danach gar
|
||
nicht mehr und meldete acht Fehler ueber „undefined": kein Befund
|
||
ueber das Haus, sondern einer ueber sich selbst. */
|
||
const k = alle.find((x) => String(x.ziel).split("?")[0] === "chat.html");
|
||
ok(!!k, "die Community hat eine Chat-Kachel");
|
||
/* NICHT MEHR AUF DEN WORTLAUT „Chat" FESTGENAGELT. Ein Modi hat
|
||
ZWEI Kacheln, die in den Chat fuehren -- die des Teams und die
|
||
des Treffs. Hiessen beide „Chat", waere das die Falle, die am
|
||
19.09. behoben wurde. Geprueft wird deshalb, worauf es ankommt:
|
||
Man erkennt den Chat, und der Name ist eindeutig. */
|
||
ok(/chat/i.test(k?.name || ""), `sie ist als Chat erkennbar: „${k?.name}“`);
|
||
ok(alle.filter((x) => x.name === k?.name).length === 1,
|
||
`und ihr Name kommt genau einmal vor („${k?.name}“)`);
|
||
ok(/nacht/i.test(k?.unter || ""),
|
||
`und sagt schon auf der Startseite, dass er nachts schläft: „${k?.unter}“`);
|
||
|
||
const css = (await import("node:fs")).readFileSync(
|
||
new URL("../workspace/assets/css/start.css", import.meta.url), "utf8");
|
||
ok(new RegExp(`\\[data-ton="${k?.ton}"\\]`).test(css),
|
||
`ihr Farbton ${k?.ton} ist in start.css definiert`);
|
||
/* Keine zwei Kacheln mit derselben Farbe -- Filipe mehrfach:
|
||
„keine die sich irgendwie ähnlich sind". */
|
||
const toene = alle.map((x) => x.ton).filter(Boolean);
|
||
ok(new Set(toene).size === toene.length,
|
||
`kein Farbton doppelt bei den ${toene.length} Kacheln der Community`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
6b. IM BROWSER
|
||
|
||
Der Teil, den keine Schnittstellenpruefung ersetzt: Sieht ein Gast
|
||
wirklich einen Raum und keinen Telefonknopf -- und steht das Band
|
||
ueber dem Eingabefeld, wo man es liest, bevor man tippt?
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== 6b. Im Browser ===");
|
||
{
|
||
const { chromium } = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await chromium.launch();
|
||
const kontext = await browser.newContext({ viewport: { width: 412, height: 915 } });
|
||
const seite = await kontext.newPage();
|
||
const konsole = [];
|
||
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); });
|
||
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
||
/* WELCHE Adresse 404 sagt, nicht nur DASS eine es tut. "Failed to
|
||
load resource" ohne Adresse ist eine Meldung, mit der man nichts
|
||
anfangen kann -- und dann sucht man an der falschen Stelle. */
|
||
const vergeblich = [];
|
||
seite.on("response", (r) => {
|
||
if (r.status() >= 400 && !/favicon/.test(r.url())) {
|
||
vergeblich.push(`${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, "")}`);
|
||
}
|
||
});
|
||
|
||
await kontext.addCookies(kGast.split("; ").filter(Boolean).map((teil) => {
|
||
const i = teil.indexOf("=");
|
||
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
|
||
}));
|
||
await seite.goto(`http://127.0.0.1:${PORT}/workspace/chat.html`, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(1100);
|
||
|
||
const stand = await seite.evaluate(() => ({
|
||
raeume: document.querySelectorAll(".chat__raum, .raum, [data-raum]").length,
|
||
text: document.body.innerText,
|
||
anrufKnopf: !!document.getElementById("anruf-ton-start")
|
||
|| !!document.getElementById("anruf-video-start"),
|
||
breiter: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
|
||
}));
|
||
/* s.o.: Der Ausdruck hat die Umbenennung nicht mitbekommen und
|
||
haette nach "Treff" gesucht, waehrend die Seite "Rudel" sagt. */
|
||
ok(/Rudel/i.test(stand.text), "die Seite nennt das Rudel");
|
||
/* KEIN TELEFONKNOPF. Der Riegel im Server ist das eine -- ein Knopf,
|
||
der mit 404 antwortet, waere trotzdem ein Knopf, den man drueckt. */
|
||
ok(stand.anrufKnopf === false, "und es gibt keinen Anrufknopf");
|
||
ok(!stand.breiter, "auf 412 px steht nichts ueber dem rechten Rand");
|
||
|
||
/* DAS RAUM OEFFNEN und nachsehen, was ueber dem Eingabefeld steht. */
|
||
const raum = seite.locator(".chat__liste button, .chat__raum, [data-raum-id]").first();
|
||
if (await raum.count()) { await raum.click(); await seite.waitForTimeout(700); }
|
||
|
||
const unten = await seite.evaluate(() => {
|
||
const band = document.getElementById("nachtruhe");
|
||
const feld = document.getElementById("text");
|
||
return {
|
||
bandDa: !!band && !band.hidden,
|
||
bandText: band ? band.innerText.trim() : "",
|
||
lage: band ? (band.dataset.lage || "") : "",
|
||
feldAus: !!feld && feld.disabled,
|
||
klammerAus: !!document.getElementById("anhang-auf")
|
||
&& document.getElementById("anhang-auf").disabled,
|
||
platzhalter: feld ? feld.placeholder : "",
|
||
};
|
||
});
|
||
|
||
if (NACHTLAUF) {
|
||
ok(unten.bandDa, "nachts steht das Band ueber dem Eingabefeld");
|
||
ok(/schläft/i.test(unten.bandText),
|
||
`und sagt, was los ist: „${unten.bandText.slice(0, 60)}…“`);
|
||
ok(unten.lage === "zu", `es traegt die Lage „zu“ (${unten.lage})`);
|
||
ok(unten.feldAus, "das Eingabefeld ist aus");
|
||
/* UND DIE BUEROKLAMMER. Der Server lehnt Anhaenge nachts ebenso ab;
|
||
ein offener Knopf hiesse: Bild aussuchen, hochladen, Absage --
|
||
die Arbeit ist dann schon gemacht. */
|
||
ok(unten.klammerAus, "die Büroklammer ebenfalls");
|
||
ok(/ab \d\d:00/i.test(unten.platzhalter),
|
||
`und der Platzhalter sagt, ab wann wieder: „${unten.platzhalter}“`);
|
||
} else {
|
||
/* GEGENPROBE: Bei Tag ist das Band weg und das Feld an. Ohne diese
|
||
Zeile waere "das Band steht da" auch dann gruen, wenn es IMMER
|
||
dasteht -- und ein Band, das immer da ist, liest niemand. */
|
||
ok(!unten.bandDa, "bei Tag steht kein Band da");
|
||
ok(!unten.feldAus, "und das Eingabefeld ist an");
|
||
ok(!unten.klammerAus, "die Büroklammer auch");
|
||
}
|
||
|
||
if (process.env.SCHIRM) {
|
||
await seite.screenshot({
|
||
path: `C:/Users/qciga/Documents/Obelix/DogiHompage/treffchat-${NACHTLAUF ? "nacht" : "tag"}.png`,
|
||
fullPage: true,
|
||
});
|
||
}
|
||
/* KEINE VERGEBLICHE ANFRAGE. Ein 404 bei jedem Seitenaufruf ist
|
||
Rauschen, und Rauschen macht den naechsten ECHTEN Fehler
|
||
unsichtbar -- dieselbe Regel wie bei der Warnung, die immer kommt. */
|
||
ok(vergeblich.length === 0,
|
||
vergeblich.length
|
||
? `vergebliche Anfragen: ${[...new Set(vergeblich)].join(" | ")}`
|
||
: "keine einzige Anfrage laeuft ins Leere");
|
||
ok(konsole.length === 0,
|
||
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 110) : "keine Fehler im Browser");
|
||
await browser.close();
|
||
}
|
||
|
||
/* =======================================================================
|
||
7. UND JETZT DASSELBE MIT GELTENDER NACHTRUHE
|
||
|
||
Derselbe Code, andere Lage. Ohne diesen zweiten Lauf waere die
|
||
Nachtruhe nur behauptet: Alles oben ist auch dann gruen, wenn die
|
||
Sperre gar nicht existiert.
|
||
======================================================================= */
|
||
if (!NACHTLAUF) {
|
||
melde("");
|
||
melde("=== 7. Derselbe Lauf, aber mit geltender Nachtruhe ===");
|
||
const { spawnSync } = await import("node:child_process");
|
||
const lauf = spawnSync(process.execPath, [new URL(import.meta.url).pathname.slice(1)], {
|
||
encoding: "utf8",
|
||
env: { ...process.env, TREFF_PRUEF_NACHT: "ja", WORKSPACE_DB: "", PORT: "" },
|
||
});
|
||
const zeilen = String(lauf.stdout || "").split(String.fromCharCode(10));
|
||
for (const z of zeilen) {
|
||
if (/^\s{2}(ok|FEHL)\s/.test(z)) {
|
||
geprueft++;
|
||
if (z.includes("FEHL")) fehler++;
|
||
console.log(z.replace(/^\s{2}/, " "));
|
||
}
|
||
}
|
||
if (lauf.status !== 0 && !zeilen.some((z) => /FEHL/.test(z))) {
|
||
/* DER DRITTE AUSGANG: Der zweite Lauf ist gar nicht gelaufen.
|
||
Das ist etwas anderes als "er hat Fehler gefunden" -- und es
|
||
darf nicht wie ein bestandener Lauf aussehen. */
|
||
ok(false, `der Nachtlauf konnte nicht starten (Rueckgabewert ${lauf.status}). `
|
||
+ String(lauf.stderr || "").split(String.fromCharCode(10))[0]);
|
||
}
|
||
}
|
||
|
||
melde("");
|
||
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
||
process.exit(fehler ? 1 : 0);
|