Drei Sachen aus einem Bildschirmfoto-Satz.
=== 1. EIN ANDERER STIL, NICHT DIESELBE SPRACHE MIT NEUEN DETAILS ===
Filipe, zum dritten Mal an derselben Stelle: "du verstehst es wirklich
nicht ... ich will eine komplette aenderung vom aussehen. vom
hintergrund und von der grossen kachel. ich will einen ganz anderen
stil ... die leute sollen morgen nichts mehr wieder erkennen vom
aussehen her."
WARUM MEINE ZWEI ANLAEUFE DAVOR NICHT GEREICHT HABEN -- und das ist
kein Geschmacksstreit, sondern ein Fund:
Der Umriss des Chats kommt gar nicht aus chat.css. Er steht in
module.css, in einer Liste von 50 Klassen, und `.chat` ist eine davon:
Fase oben links, Kantenlicht, Punktraster, drei goldene Eckwinkel.
module.css wird NACH chat.css geladen -- und die Staerke des `:is(...)`
dort ist (0,2,0), weil `.gruppe[data-gruppe]` mit in der Liste steht.
Jede meiner Regeln war gleich stark und kam frueher. Deshalb stimmte
beides: "ich habe den Rand geaendert" und "der Rand ist derselbe". Ich
habe zweimal Details INNERHALB eines Rahmens geaendert, den ich nicht
angefasst hatte -- und den erkennt man zuerst.
Der neue Block steht als eine klar benannte Schicht am Ende von
chat.css, mit `.inhalt.chat-seite` -- eine Klasse mehr als module.css,
kein `!important` (das waere eine Tuer, die man nie wieder zubekommt).
ALT NEU
Fase oben links rundum 26 px weich
drei goldene Eckwinkel keine -- der Koerper traegt sich selbst
Punktraster drei weiche Lichter im Hintergrund
1-px-Rahmen ueberall kein Rahmen, Lichtkante innen
Gold als Leitfarbe Lavendel/Violett, Blasen wie gehabt
Kaesten nebeneinander Koerper mit Tiefe und farbigem Schatten
DIE LEITFARBE WIRD AN EINER STELLE GETAUSCHT, nicht an zwanzig. Im
ersten Anlauf habe ich zehn Regeln einzeln umgefaerbt und danach im
Bild gesehen, dass Suchfeld, "Neu", Zaehler und Fokusrahmen weiter
golden waren -- sie nehmen alle `--akzent` und `--rand`. Jetzt stehen
beide am `<main>` der Chatseite. Uebersicht, Kalender und Aufgaben
behalten ihr Gold; nur der Chat soll nicht wiederzuerkennen sein.
#b9a7ff UND NICHT #7a5cff, und das ist gerechnet, nicht gewaehlt: Die
Akzentfarbe ist hier auch FLAECHE unter dunkler Schrift (die
Ungelesen-Marke). Das satte Violett kommt dort auf 3,7:1 -- zu wenig.
Das helle auf 8,9:1, und als Schrift auf dunklem Grund genauso.
WAS UNANGETASTET BLEIBT: `--blasengrund`, `--blase-text`,
`--blase-leise`, `--namen-anteil`. An ihnen haengen die Messungen von
pruef-chatkachel (12 Kacheln) und pruef-chat-neu (360 Ringtoene). Ein
Stilwechsel darf eine Zusage nicht nebenbei aufheben.
pruef-chat-optik hat sofort einen echten Schaden gemeldet: Der neue
Stil nahm allen Blasen den Rahmen -- und damit auch den, mit dem eine
NICHT ABGESCHICKTE Nachricht markiert ist ("der Unterschied ist auch zu
SEHEN, nicht nur im Merkmal (Rand 0px)"). Genau dafuer steht die Zeile
dort. Der Warnton sitzt jetzt zusaetzlich im inneren Saum.
=== 2. GESPRAECHE ANHEFTEN ===
Filipe: "ich will dass man auch individuel jeder fuer sich auch in der
liste chats fixieren kann. auch mehrere nicht nur eins."
Drei Aussagen, und jede wird einzeln geprueft:
"fixieren" -> `fixiert_am` an der TEILNEHMER-Zeile; Angeheftetes
steht oben, darunter geht die gewohnte Reihenfolge
weiter.
"individuell" -> die Spalte haengt an der Person, nicht am Raum. Eine
Spalte an `chat_raeume` haette alles andere genauso
erfuellt und jedem im Raum das Gespraech oben
hingeklebt -- gemerkt haette man es erst, wenn sich
jemand beschwert. Die Gegenprobe prueft deshalb
ausdruecklich, dass es bei Luna weder markiert ist
noch nach oben rutscht.
"auch mehrere" -> keine Obergrenze. Ein Zeitstempel statt Ja/Nein
kostet dasselbe und beantwortet die Frage mit,
in welcher Reihenfolge mehrere stehen: zuletzt
angeheftet oben.
Die Nadel steht IMMER an der Zeile, nicht erst beim Ueberfahren -- am
Handy gibt es kein Ueberfahren (dieselbe Entscheidung wie am 23.09. bei
den Handgriffen), und eine Spalte, die mal da ist und mal nicht, laesst
die Namen daneben wandern. Sie liegt schraeg, solange nichts
angeheftet ist, und steht aufrecht, wenn doch -- das sieht man auch
ohne Farbe.
Der Zustand wird GESCHICKT, nicht errechnet (`an: true/false`): Ein
Schalter, der den Gegenwert selbst ausrechnet, kippt bei zwei schnellen
Klicks oder zwei offenen Fenstern in den falschen Zustand.
Die Karte ist seit heute die ZEILE und nicht mehr der Knopf darin --
im ersten Anlauf sass die Nadel sichtbar ausserhalb der Flaeche, wie
ein Knopf, der danebengefallen ist.
=== 3. UNTEN WIEDER LUFT ===
"schieb das bisschen hoeher bitte, weil das ist unten zu nah am rand."
14 px Polsterung. Sie geht nach INNEN (`border-box`), macht die Seite
also nicht laenger -- sonst waere das Schreibfeld wieder unter den
Bildrand gerutscht, und genau darum ging es am 09.09. schon einmal.
Geprueft: pruef-chat (neuer Abschnitt Anheften, 14 Punkte, alle gruen),
pruef-chat-optik, pruef-chatkachel (40), pruef-chat-neu (32),
pruef-chat-ausbau (64), pruef-chat-kanaele (81), pruef-chat-aufloesen
(126), pruef-chat-anhaenge (109), pruef-erwaehnung (129),
pruef-css-klassen, pruef-tippziele (11), pruef-lesbarkeit.
Co-Authored-By: Claude Opus 5 <[email protected]>
523 lines
25 KiB
JavaScript
523 lines
25 KiB
JavaScript
/* CHAT — wer darf mit wem, und wer bekommt nichts zu sehen.
|
||
|
||
Wunsch Filipe, 06.09.2026. Auf Nachfrage entschieden: Zweier-
|
||
Gespräche UND Gruppen; Manager und Scouts dürfen sich auch ohne
|
||
gemeinsamen Creator schreiben.
|
||
|
||
WARUM DIESE PRÜFUNG ZUERST KAM, vor der Oberfläche: Ein Chat ist der
|
||
Ort, an dem Leute Dinge schreiben, die nicht jeder lesen soll. Ein
|
||
Fehler in der Sichtbarkeit ist hier teurer als überall sonst im
|
||
Haus -- eine falsche Zahl auf einer Kachel ärgert, ein mitgelesenes
|
||
Gespräch ist ein Vertrauensbruch.
|
||
|
||
GEMESSEN WIRD:
|
||
1. Die Regel selbst: Wer erscheint in wessen Partnerliste?
|
||
2. Kann man sie umgehen, indem man die Anfrage von Hand baut?
|
||
3. Kommt man an einen fremden Verlauf, wenn man die Raumnummer rät?
|
||
4. Ist eine Gruppe ein Schlupfloch zu jemandem, den man einzeln
|
||
nicht anschreiben dürfte?
|
||
5. Und die Alltagsfragen: Gibt es je Paar genau EIN Gespräch?
|
||
Zählt "ungelesen" richtig? Bleibt ein Zurücknehmen sichtbar?
|
||
|
||
GEGENPROBEN durchgehend: Jede Sperre wird auch von der erlaubten
|
||
Seite versucht. Eine Prüfung, die immer "verboten" sagt, beweist
|
||
nichts. */
|
||
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-chat-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
|
||
const { eigenerPort } = await import("./helfer-port.mjs");
|
||
const PORT = await eigenerPort(import.meta, "die Chatpruefung");
|
||
|
||
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++; };
|
||
|
||
/* ---- Ein Bestand mit ECHTER Struktur ------------------------------------
|
||
|
||
Zwei getrennte Ketten, damit "fremd" wirklich fremd ist:
|
||
Cigdem (Manager) → Patrick (Scout) → Luna (Creator)
|
||
Meral (Manager) → Sven (Scout) → Nora (Creator)
|
||
Dazu DogFather über allem.
|
||
|
||
Ohne die zweite Kette liesse sich nicht prüfen, ob ein Creator an
|
||
einen fremden Creator kommt -- es gäbe schlicht keinen. */
|
||
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 idCigdem = anlegen("Cigdem", "manager", "CODE-MANA-0001");
|
||
const idMeral = anlegen("Meral", "manager", "CODE-MANA-0002");
|
||
const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||
const idSven = anlegen("Sven", "scout", "CODE-SCOU-0002");
|
||
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);
|
||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idNora, idSven, jetzt);
|
||
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idPat, idCigdem, jetzt);
|
||
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idSven, idMeral, jetzt);
|
||
|
||
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 cigdem = await anmelden("manager", "CODE-MANA-0001");
|
||
const meral = await anmelden("manager", "CODE-MANA-0002");
|
||
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, cigdem, meral, pat, luna, nora].every((x) => x.ok && x.keks),
|
||
"alle sechs Rollen sind angemeldet");
|
||
|
||
/* =======================================================================
|
||
1. Die Regel: wer erscheint in wessen Liste?
|
||
======================================================================= */
|
||
console.log("\n=== Mit wem kann man schreiben? ===");
|
||
{
|
||
const namen = async (keks) => {
|
||
const { personen } = await holen(keks, "/workspace/api/chat/partner");
|
||
return (personen || []).map((p) => p.name).sort();
|
||
};
|
||
|
||
const beiLuna = await namen(luna.keks);
|
||
/* Ein Creator: sein Scout, dessen Manager, DogFather. Kein fremder
|
||
Creator, kein fremder Scout. */
|
||
ok(JSON.stringify(beiLuna) === JSON.stringify(["Cigdem", "Filipe", "Patrick"]),
|
||
`Creator Luna: ${beiLuna.join(", ")}`);
|
||
ok(!beiLuna.includes("Nora"),
|
||
"Luna sieht die andere Creatorin NICHT — Creator bleiben getrennt");
|
||
ok(!beiLuna.includes("Sven") && !beiLuna.includes("Meral"),
|
||
"und auch nicht den fremden Scout oder dessen Manager");
|
||
|
||
const beiPat = await namen(pat.keks);
|
||
/* Ein Scout: sein Creator, DogFather -- UND das ganze Team
|
||
(Entscheidung vom 06.09.2026: "für Absprachen im Team"). */
|
||
ok(beiPat.includes("Luna") && beiPat.includes("Filipe"),
|
||
`Scout Patrick erreicht seinen Creator und DogFather`);
|
||
ok(beiPat.includes("Meral") && beiPat.includes("Sven"),
|
||
`und das Team: ${beiPat.join(", ")}`);
|
||
ok(!beiPat.includes("Nora"),
|
||
"aber NICHT die fremde Creatorin — die Öffnung gilt nur fürs Team");
|
||
|
||
const beiDogi = await namen(dogi.keks);
|
||
ok(beiDogi.length === 6, `DogFather erreicht alle ${beiDogi.length} anderen`);
|
||
ok(!beiDogi.includes("Filipe"), "aber nicht sich selbst");
|
||
}
|
||
|
||
/* =======================================================================
|
||
2. Lässt sich die Regel von Hand umgehen?
|
||
======================================================================= */
|
||
console.log("\n=== Die Regel sitzt im Server, nicht in der Oberfläche ===");
|
||
{
|
||
/* Luna baut die Anfrage selbst und will Nora anschreiben. */
|
||
const a = await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idNora }, "POST");
|
||
ok(a.status === 403, `Luna → Nora von Hand: abgelehnt (${a.status})`);
|
||
|
||
const b = await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idSven }, "POST");
|
||
ok(b.status === 403, `Luna → fremder Scout: abgelehnt (${b.status})`);
|
||
|
||
/* GEGENPROBE: Zu ihrem eigenen Scout geht es. Ohne diesen Nachweis
|
||
bewiese "403" nur, dass die Route überhaupt ablehnt. */
|
||
const c = await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idPat }, "POST");
|
||
ok(c.ok, `Luna → ihr eigener Scout: geht (${c.status})`);
|
||
|
||
/* Und sich selbst anschreiben ergibt keinen Sinn. */
|
||
const e = await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idLuna }, "POST");
|
||
ok(e.status === 403, `sich selbst anschreiben: abgelehnt (${e.status})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
3. Ein fremder Verlauf, mit geratener Nummer
|
||
======================================================================= */
|
||
console.log("\n=== Kommt man an fremde Gespräche? ===");
|
||
let raumLunaPat = 0;
|
||
{
|
||
const { raum_id } = await (await rufe(luna.keks, "/workspace/api/chat/direkt",
|
||
{ mit: idPat }, "POST")).json();
|
||
raumLunaPat = raum_id;
|
||
ok(Number.isInteger(raumLunaPat) && raumLunaPat > 0, `Gespräch Luna↔Patrick ist #${raumLunaPat}`);
|
||
|
||
await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
|
||
{ text: "Vertraulich: mein Umsatz war 3200." }, "POST");
|
||
|
||
/* Nora rät die Nummer. */
|
||
const a = await rufe(nora.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
|
||
ok(a.status === 404, `Fremde liest Verlauf: ${a.status} (404, nicht 403 — die Nummer soll nichts verraten)`);
|
||
|
||
/* Und sie versucht hineinzuschreiben. */
|
||
const b = await rufe(nora.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
|
||
{ text: "Hallo?" }, "POST");
|
||
ok(b.status === 404, `Fremde schreibt hinein: ${b.status}`);
|
||
|
||
/* Auch als gelesen markieren geht nicht -- das wäre sonst ein Weg,
|
||
die Existenz eines Raums zu erfahren. */
|
||
const c = await rufe(nora.keks, `/workspace/api/chat/raeume/${raumLunaPat}/gelesen`,
|
||
{ bis: 999 }, "PUT");
|
||
ok(c.status === 404, `Fremde markiert als gelesen: ${c.status}`);
|
||
|
||
/* GEGENPROBE: Patrick, der drin ist, kommt heran. */
|
||
const e = await holen(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
|
||
ok(e.nachrichten?.length === 1 && /3200/.test(e.nachrichten[0].text),
|
||
`Patrick liest den Verlauf (${e.nachrichten?.length} Nachricht)`);
|
||
|
||
/* Selbst DogFather kommt nicht ungefragt in ein fremdes Gespräch.
|
||
Das ist Absicht: Ein Chat, in dem der Chef stillschweigend mitliest,
|
||
ist kein Chat. Er darf jederzeit selbst schreiben -- aber sichtbar. */
|
||
const f = await rufe(dogi.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
|
||
ok(f.status === 404,
|
||
`auch DogFather liest fremde Gespräche NICHT mit (${f.status}) — er ist kein Teilnehmer`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
4. Genau EIN Gespräch je Paar
|
||
======================================================================= */
|
||
console.log("\n=== Ein Paar, ein Gespräch ===");
|
||
{
|
||
/* Von der anderen Seite geöffnet muss derselbe Raum kommen -- sonst
|
||
schreibt einer ins Leere, während der andere wartet. */
|
||
const { raum_id: vonPat } = await (await rufe(pat.keks, "/workspace/api/chat/direkt",
|
||
{ mit: idLuna }, "POST")).json();
|
||
ok(vonPat === raumLunaPat,
|
||
`Patrick öffnet dasselbe Gespräch (#${vonPat} = #${raumLunaPat}), keine zweite Kopie`);
|
||
|
||
/* Und mehrfaches Öffnen legt nichts Neues an. */
|
||
for (let i = 0; i < 3; i++) {
|
||
await rufe(luna.keks, "/workspace/api/chat/direkt", { mit: idPat }, "POST");
|
||
}
|
||
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const n = p.prepare("SELECT COUNT(*) AS n FROM chat_raeume WHERE art = 'direkt'").get().n;
|
||
p.close();
|
||
ok(n === 1, `nach fünfmaligem Öffnen: ${n} Gespräch in der Datenbank`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
5. Gruppen — und ob sie ein Schlupfloch sind
|
||
======================================================================= */
|
||
console.log("\n=== Gruppen ===");
|
||
{
|
||
/* DER PUNKT: Luna darf Nora nicht einzeln anschreiben. Kann sie es
|
||
über eine Gruppe? */
|
||
const a = await rufe(luna.keks, "/workspace/api/chat/gruppe",
|
||
{ name: "Umweg", mit: [idPat, idNora] }, "POST");
|
||
ok(a.status === 403,
|
||
`eine Gruppe ist KEIN Schlupfloch: ${a.status} (Nora war dabei)`);
|
||
|
||
/* GEGENPROBE: Ohne Nora geht dieselbe Gruppe. */
|
||
const b = await rufe(luna.keks, "/workspace/api/chat/gruppe",
|
||
{ name: "Luna, Patrick & Cigdem", mit: [idPat, idCigdem] }, "POST");
|
||
ok(b.status === 201, `mit erlaubten Leuten: angelegt (${b.status})`);
|
||
const { raum_id: gruppe } = await b.json();
|
||
|
||
const sicht = await holen(luna.keks, `/workspace/api/chat/raeume/${gruppe}/nachrichten`);
|
||
ok(sicht.raum?.teilnehmer?.length === 3,
|
||
`die Gruppe hat ${sicht.raum?.teilnehmer?.length} Mitglieder`);
|
||
ok(sicht.raum?.name === "Luna, Patrick & Cigdem",
|
||
`und ihren Namen: "${sicht.raum?.name}"`);
|
||
|
||
/* Wer sie angelegt hat, führt sie. */
|
||
const chefin = sicht.raum.teilnehmer.find((t) => t.leitung);
|
||
ok(chefin?.id === idLuna, `die Leitung liegt bei ${chefin?.name}`);
|
||
|
||
/* Ohne Namen keine Gruppe. */
|
||
const c = await rufe(luna.keks, "/workspace/api/chat/gruppe",
|
||
{ name: "", mit: [idPat] }, "POST");
|
||
ok(c.status === 400, `ohne Namen: abgelehnt (${c.status})`);
|
||
|
||
/* Und ohne Leute auch nicht. */
|
||
const e = await rufe(luna.keks, "/workspace/api/chat/gruppe",
|
||
{ name: "Allein", mit: [] }, "POST");
|
||
ok(e.status === 400, `ohne Mitglieder: abgelehnt (${e.status})`);
|
||
|
||
/* Nora sieht die Gruppe nicht in ihrer Liste. */
|
||
const { raeume } = await holen(nora.keks, "/workspace/api/chat/raeume");
|
||
ok(!(raeume || []).some((r) => r.id === gruppe),
|
||
`Nora sieht die fremde Gruppe nicht (${raeume?.length || 0} eigene Gespräche)`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
6. Ungelesen — die Zahl, auf die man sich verlässt
|
||
======================================================================= */
|
||
console.log("\n=== Ungelesene Nachrichten ===");
|
||
{
|
||
const vorher = (await holen(pat.keks, "/workspace/api/chat/ungelesen")).ungelesen;
|
||
|
||
await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
|
||
{ text: "Erste" }, "POST");
|
||
await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
|
||
{ text: "Zweite" }, "POST");
|
||
|
||
const nachher = (await holen(pat.keks, "/workspace/api/chat/ungelesen")).ungelesen;
|
||
ok(nachher === vorher + 2, `Patrick: ${vorher} → ${nachher} ungelesen`);
|
||
|
||
/* Die EIGENEN zählen nicht mit -- sonst stünde nach jeder eigenen
|
||
Nachricht eine rote Zahl an der eigenen Kopfleiste. */
|
||
const beiLuna = (await holen(luna.keks, "/workspace/api/chat/ungelesen")).ungelesen;
|
||
ok(beiLuna === 0, `Luna hat ${beiLuna} ungelesene (ihre eigenen zählen nicht)`);
|
||
|
||
/* Lesen setzt sie zurück. */
|
||
const verlauf = await holen(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
|
||
const letzteId = verlauf.nachrichten.at(-1).id;
|
||
await rufe(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/gelesen`,
|
||
{ bis: letzteId }, "PUT");
|
||
const danach = (await holen(pat.keks, "/workspace/api/chat/ungelesen")).ungelesen;
|
||
ok(danach === 0, `nach dem Lesen: ${danach}`);
|
||
|
||
/* NUR VORWÄRTS: Ein verspäteter Aufruf mit einer alten Nummer darf
|
||
den Stand nicht zurückdrehen -- sonst stünde Gelesenes wieder als
|
||
ungelesen da. */
|
||
await rufe(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/gelesen`, { bis: 1 }, "PUT");
|
||
const zurueck = (await holen(pat.keks, "/workspace/api/chat/ungelesen")).ungelesen;
|
||
ok(zurueck === 0, `ein alter "gelesen bis 1" dreht nichts zurück (${zurueck})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
7. Zurücknehmen
|
||
======================================================================= */
|
||
console.log("\n=== Eine Nachricht zurücknehmen ===");
|
||
{
|
||
const a = await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
|
||
{ text: "Das war ein Versehen" }, "POST");
|
||
const { nachricht } = await a.json();
|
||
|
||
/* Patrick darf sie NICHT zurücknehmen -- fremde Worte aus einem
|
||
Gespräch zu nehmen, in dem Absprachen stehen, wäre ein Eingriff in
|
||
die Geschichte. */
|
||
const b = await rufe(pat.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`, undefined, "DELETE");
|
||
ok(b.status === 403, `fremde Nachricht zurücknehmen: abgelehnt (${b.status})`);
|
||
|
||
/* Auch DogFather nicht. */
|
||
const c = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`, undefined, "DELETE");
|
||
ok(c.status === 404 || c.status === 403,
|
||
`auch DogFather nicht (${c.status})`);
|
||
|
||
/* Die eigene schon. */
|
||
const e = await rufe(luna.keks, `/workspace/api/chat/nachrichten/${nachricht.id}`, undefined, "DELETE");
|
||
ok(e.ok, `die eigene: geht (${e.status})`);
|
||
|
||
/* UND SIE IST WIRKLICH WEG (22.09.2026 abends).
|
||
------------------------------------------------------------------
|
||
Hier stand das Gegenteil: „sie bleibt sichtbar -- als
|
||
zurueckgenommen, nicht als Loch." Das war die bewusste
|
||
Entscheidung bis heute Nachmittag.
|
||
|
||
Filipe: „Geloeschte Nachrichten verschwinden vollstaendig -- keine
|
||
Spur, kein ‚wurde geloescht'-Hinweis, bei niemandem, auch nicht
|
||
bei DogFather."
|
||
|
||
Die Zeile misst jetzt beides: dass sie fehlt UND dass der Rest des
|
||
Verlaufs noch da ist. Ohne den zweiten Teil waere ein Verlauf, der
|
||
ueberhaupt nichts mehr liefert, ebenfalls gruen. */
|
||
const verlauf = await holen(pat.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`);
|
||
const weg = verlauf.nachrichten.find((n) => n.id === nachricht.id);
|
||
ok(!weg && verlauf.nachrichten.length > 0,
|
||
`sie steht in keinem Verlauf mehr, und die uebrigen`
|
||
+ ` ${verlauf.nachrichten.length} stehen noch da`);
|
||
ok(!verlauf.nachrichten.some((n) => n.zurueckgenommen),
|
||
"und es steht nirgends ein Hinweis, dass hier etwas stand");
|
||
ok(!JSON.stringify(verlauf).includes("Das war ein Versehen"),
|
||
"der Text ist wirklich weg, nicht nur ausgeblendet");
|
||
}
|
||
|
||
/* =======================================================================
|
||
8. Grenzen der Eingabe
|
||
======================================================================= */
|
||
console.log("\n=== Was nicht hineingeht ===");
|
||
{
|
||
for (const [was, koerper, status] of [
|
||
["leerer Text", { text: "" }, 400],
|
||
["nur Leerzeichen", { text: " " }, 400],
|
||
["ohne Feld", {}, 400],
|
||
["zu lang (4001)", { text: "z".repeat(4001) }, 400],
|
||
]) {
|
||
const a = await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
|
||
koerper, "POST");
|
||
ok(a.status === status, `${was}: ${a.status}`);
|
||
}
|
||
/* GEGENPROBE: 4000 Zeichen gehen. */
|
||
const a = await rufe(luna.keks, `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`,
|
||
{ text: "z".repeat(4000) }, "POST");
|
||
ok(a.status === 201, `genau 4000 Zeichen: ${a.status} — die Grenze ist nicht zu streng`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
9. Ohne Anmeldung gar nichts
|
||
======================================================================= */
|
||
console.log("\n=== Ohne Anmeldung ===");
|
||
{
|
||
for (const [was, weg, methode] of [
|
||
["Partnerliste", "/workspace/api/chat/partner", "GET"],
|
||
["Gesprächsliste", "/workspace/api/chat/raeume", "GET"],
|
||
["Verlauf", `/workspace/api/chat/raeume/${raumLunaPat}/nachrichten`, "GET"],
|
||
["Ungelesen", "/workspace/api/chat/ungelesen", "GET"],
|
||
["Strom", "/workspace/api/chat/strom", "GET"],
|
||
]) {
|
||
const a = await fetch(BASIS + weg, { method: methode });
|
||
ok(a.status === 401, `${was}: ${a.status}`);
|
||
}
|
||
}
|
||
|
||
d.close();
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
|
||
/* =====================================================================
|
||
GESPRAECHE OBEN ANHEFTEN (25.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: „ich will dass man auch individuel jeder fuer sich auch in
|
||
der liste chats fixieren kann. auch mehrere nicht nur eins."
|
||
|
||
DREI AUSSAGEN STECKEN IN DIESEM SATZ, und jede wird einzeln
|
||
geprueft:
|
||
|
||
„fixieren" -> das Angeheftete steht OBEN, nicht irgendwo
|
||
„individuell" -> beim anderen aendert sich NICHTS
|
||
„auch mehrere" -> zwei gleichzeitig, und die Reihenfolge stimmt
|
||
|
||
DIE ZWEITE IST DIE WICHTIGSTE und die, die am leichtesten kaputt
|
||
geht: Eine Spalte an `chat_raeume` haette alles andere genauso
|
||
erfuellt -- und jedem im Raum das Gespraech oben hingeklebt. Man
|
||
haette es erst gemerkt, wenn sich jemand beschwert.
|
||
|
||
GEGENPROBEN: fremder Raum (404) und das Loesen (steht wieder
|
||
unten). Ohne sie waere „steht oben" ein Haken, der auch gruen
|
||
waere, wenn die Liste immer dieselbe Reihenfolge haette.
|
||
===================================================================== */
|
||
console.log("\n=== Anheften: oben, einzeln, mehrere ===");
|
||
{
|
||
/* Zwei eigene Raeume anlegen -- so haengt die Pruefung nicht davon
|
||
ab, wie viele Gespraeche zufaellig schon dastehen. Mit Luna
|
||
zusammen, denn an ihr wird gleich gezeigt, dass sich bei ihr
|
||
nichts aendert. */
|
||
const machGruppe = async (name) => {
|
||
const a = await rufe(dogi.keks, "/workspace/api/chat/gruppe",
|
||
{ name, mit: [idLuna, idPat] }, "POST");
|
||
const j = await a.json().catch(() => ({}));
|
||
return j.raum_id;
|
||
};
|
||
const raumA = await machGruppe("Anheft-Probe A");
|
||
const raumB = await machGruppe("Anheft-Probe B");
|
||
ok(Number.isInteger(raumA) && Number.isInteger(raumB) && raumA !== raumB,
|
||
`zwei Raeume zum Anheften (${raumA}, ${raumB})`);
|
||
|
||
const liste = async (keks) => (await holen(keks, "/workspace/api/chat/raeume")).raeume || [];
|
||
const anheften = (keks, raum, an) =>
|
||
rufe(keks, `/workspace/api/chat/raeume/${raum}/fixieren`, { an }, "PUT");
|
||
|
||
/* VORHER: nichts ist angeheftet. Ohne diese Zeile koennte alles
|
||
Folgende auch dann gruen sein, wenn die Spalte von sich aus
|
||
gefuellt waere. */
|
||
const vorher = await liste(dogi.keks);
|
||
ok(vorher.length >= 2 && vorher.every((r) => r.fixiert === false),
|
||
`vorher ist nichts angeheftet (${vorher.length} Gespraeche)`);
|
||
|
||
/* ---- 1. „fixieren": oben ---------------------------------------- */
|
||
const wo = vorher.findIndex((r) => r.id === raumA);
|
||
const a1 = await anheften(dogi.keks, raumA, true);
|
||
const j1 = await a1.json().catch(() => ({}));
|
||
ok(a1.status === 200 && j1.fixiert === true,
|
||
`Raum A laesst sich anheften (HTTP ${a1.status}, fixiert ${j1.fixiert})`);
|
||
const nachA = await liste(dogi.keks);
|
||
ok(nachA[0]?.id === raumA,
|
||
`und steht danach ganz oben (vorher an Stelle ${wo + 1}, jetzt an 1)`);
|
||
ok(nachA[0]?.fixiert === true, " und traegt die Marke dafuer");
|
||
|
||
/* ---- 2. „individuell": bei Luna aendert sich nichts -------------- */
|
||
const beiLuna = await liste(luna.keks);
|
||
const lunaA = beiLuna.find((r) => r.id === raumA);
|
||
ok(!!lunaA && lunaA.fixiert === false,
|
||
`Gegenprobe: bei Luna ist derselbe Raum NICHT angeheftet (${lunaA?.fixiert})`);
|
||
/* UND ER STEHT BEI IHR AUCH NICHT OBEN. Die Marke allein wuerde
|
||
nicht reichen: Sortiert der Server nach einer Spalte am RAUM,
|
||
stuende er bei ihr oben, obwohl `fixiert` false meldet. */
|
||
ok(beiLuna.length < 2 || beiLuna[0].id !== raumA
|
||
|| beiLuna.filter((r) => r.fixiert).length > 0,
|
||
` und rutscht bei ihr auch nicht nach oben (oben steht ${beiLuna[0]?.id})`);
|
||
|
||
/* ---- 3. „auch mehrere" ------------------------------------------ */
|
||
await anheften(dogi.keks, raumB, true);
|
||
const nachB = await liste(dogi.keks);
|
||
ok(nachB.filter((r) => r.fixiert).length === 2,
|
||
`zwei gleichzeitig angeheftet (${nachB.filter((r) => r.fixiert).length})`);
|
||
ok(nachB[0]?.id === raumB && nachB[1]?.id === raumA,
|
||
`und das zuletzt angeheftete steht oben (${nachB[0]?.id}, dann ${nachB[1]?.id})`);
|
||
ok(nachB.slice(2).every((r) => r.fixiert === false),
|
||
" darunter geht die gewohnte Reihenfolge weiter");
|
||
|
||
/* ---- 4. Loesen --------------------------------------------------- */
|
||
const a2 = await anheften(dogi.keks, raumB, false);
|
||
ok(a2.status === 200, `es laesst sich wieder loesen (HTTP ${a2.status})`);
|
||
const nachLos = await liste(dogi.keks);
|
||
ok(nachLos[0]?.id === raumA && nachLos.filter((r) => r.fixiert).length === 1,
|
||
`danach haengt nur noch A oben (oben ${nachLos[0]?.id}, `
|
||
+ `${nachLos.filter((r) => r.fixiert).length} angeheftet)`);
|
||
|
||
/* ---- 5. Ein fremder Raum ---------------------------------------- */
|
||
/* Nora sitzt in keinem der beiden. Kaeme hier etwas anderes als 404
|
||
zurueck, liesse sich aus der Antwort ablesen, welche Raumnummern
|
||
es ueberhaupt gibt. */
|
||
const fremd = await anheften(nora.keks, raumA, true);
|
||
ok(fremd.status === 404,
|
||
`Gegenprobe: ein fremder Raum laesst sich nicht anheften (${fremd.status})`);
|
||
const beiNora = await liste(nora.keks);
|
||
ok(beiNora.every((r) => r.fixiert === false),
|
||
" und bei ihr haengt danach auch nichts");
|
||
}
|
||
|
||
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
|
||
/* HIER IST process.exit() RICHTIG -- anders als in pruef-push-weg.
|
||
|
||
Diese Pruefung startet einen Express-Server, und der laeuft von
|
||
selbst weiter: offene Verbindungen, ein Zeitgeber fuer die
|
||
Sicherung, bei dieser Pruefung zusaetzlich die offenen
|
||
Ereignisstroeme des Chats. Mit `exitCode` allein wartet Node ewig
|
||
darauf, dass all das zu Ende geht -- der Lauf lief in die
|
||
Zeitueberschreitung, obwohl alle Pruefungen schon durch waren.
|
||
|
||
Der Absturz, der in pruef-push-weg gegen exit() sprach, kam von
|
||
fetch-Verbindungen mitten im Schliessen. Die gibt es hier nicht
|
||
mehr: Die Browser sind zu, bevor diese Zeile laeuft. */
|
||
process.exit(fehler ? 1 : 0);
|