Der Rundumcheck geht weiter. Diese zwoelf standen im Nachtlauf als rot
und waren es nicht: Jede hat den Umbau vom 24.09. (die Haustrennung),
den vom 25.09. (die zugetragenen Punkte) oder eine Umbenennung
mitbekommen -- nur ihre Erwartung nicht.
DAS IST KEIN TROST. Ein Fehlalarm zeigt auf eine echte Stelle und nennt
eine plausible Zahl; man sucht danach an richtigem Code. Und zwoelf rote
Zeilen, die immer rot sind, nehmen dem ganzen Satz die Stimme.
=== DIESELBE WURZEL, SECHSMAL: DAS FALSCHE HAUS ===
Seit dem 24.09. antwortet der Server auf der Agenturadresse nicht mehr
ueber Team-Dogi-Leute -- und genau das taten diese Pruefungen:
pruef-nachwuchs legte einen Modi ueber die Agenturadresse an
(400). Einundzwanzig Meldungen hingen an diesem
einen Aufruf. 82 Zeilen auf crew. umgestellt,
aus „Mara, Managerin" wurde die linke Hand --
im Teamhaus ist sie das, was gemeint war.
230 -> 262 Pruefungen, 0 Fehler.
pruef-schritt dasselbe beim Aufgabenbrett (404/400).
pruef-treff-werkzeuge fragte die Einladungsliste eines Modis auf der
Agenturadresse ab -- dort ist er nicht.
pruef-rechte-umstellen benutzte `wissen.html` als zweite Probeseite.
Die ist laut Rechtetafel fuer alle offen und auf
crew. trotzdem zu: Dort kommt man nur auf Seiten,
fuer die es eine Kachel gibt. Jetzt material.html.
pruef-personen-formular verglich eine Erwartung FUER die Agentur mit
einer Messung OHNE Adresse (127.0.0.1) -- vier
gegen acht Rollen. Beides war richtig, nur nicht
dasselbe.
pruef-treff suchte „Support" zwischen den Brettern. Er steht
in „Fuer dich", wo er hingehoert -- eine kaputte
Seite zu melden ist kein Aushang. Die Liste stand
ZWEIMAL in der Datei; nachgezogen wurde eine.
Jetzt eine, von beiden benutzt.
=== ZWEIMAL: EINE FESTE ZAHL, DIE DIE SEITE UEBERHOLT HAT ===
pruef-community-sicht erlaubte „3 bis 8 Seiten". Es sind zwoelf, und
jede gehoert dorthin. Statt einer Spanne steht
jetzt die LISTE da: Kommt eine dazu, wird die
Zeile rot und nennt sie beim Namen. Eine Spanne
haette elf statt zwoelf stillschweigend
durchgelassen -- in beide Richtungen.
pruef-nachwuchs schaltete EINE Person ab und erwartete, dass die
Ampel danach schweigt. Der Kommentar darueber
sagt selbst: „Die Schwelle wird nicht
abgeschrieben, sondern aus dem Verhalten
abgeleitet" -- eine feste Anzahl abzuschalten ist
aber eine Abschrift in anderer Waehrung. Jetzt
wird abgeschaltet, BIS es kippt.
=== UND VIERMAL EIN WERKZEUG, DAS STUMPF WAR ===
pruef-scout-zuteilung suchte `.person__zeile` -- eine Klasse, die es
nicht mehr gibt. `querySelectorAll` liefert dafuer
ein leeres Feld, `.some()` darauf ist immer
false: Die Pruefung war nicht rot, weil etwas
fehlte, sondern weil sie nichts ansehen konnte.
Jetzt `.person`, und die ANZAHL der gefundenen
Karten steht in einer eigenen Bedingung.
pruef-loeschen suchte `::before` in einem Fenster von 600
Zeichen ab dem Selektor. Das misst die Laenge des
KOMMENTARS: Am 25.09. kam eine Begruendung von
zwanzig Zeilen dazu, und die Regel rutschte
hinaus. Jetzt wird nach dem Selektor gesucht.
pruef-portnummern hielt jedes `listen(` fuer einen Serverstart --
auch das blosse Nachsehen, ob eine Nummer frei
ist. Damit mahnte sie ausgerechnet die Datei an,
die das Vergeben der Nummern prueft. Jetzt zaehlt
der Import von `index.js`; dazu vier Gegenproben,
damit die engere Fassung nicht zum blinden Fleck
wird.
pruef-spicy stellte eine Person auf „manager" und dann
weiter. An Managern aendert seit dem 22.09. nur
DogFather etwas -- die Pruefung hatte sich selbst
die Tuer zugezogen. Jetzt kommt „manager" zuletzt,
und dass danach nichts mehr geht, ist eine eigene
Zeile. Aus dem Stolperstein wird eine Aussage.
=== EINER WAR FAST EIN BEFUND ===
pruef-alle-sehen-es meldete „highlight: DogFather 0, Community 0".
Das Brett hat als einziges keinen Katalog (dort
stehen echte Hoehepunkte, keine Vorlagen), und an
einem leeren Brett ist „sehen beide dasselbe?"
nicht zu messen: null gleich null waere auch dann
wahr, wenn die Community gar nichts duerfte.
Die Pruefung legt dort jetzt selbst einen Eintrag
an -- und musste dabei zweimal lernen: Der Eintrag
gehoert VOR den Katalog (danach ist die
Schreibbremse ausgeloest, 429), und er muss
FREIGEGEBEN werden, sonst sieht die Community ihn
zu Recht nicht. Beides steht jetzt als Aussage in
der Pruefung.
GEMESSEN: alle zwoelf gruen -- 43, 10, 31, 262, 43, 15, 56, 69, 37, 85,
73 und 80 Pruefungen, zusammen 804, kein Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
434 lines
21 KiB
JavaScript
434 lines
21 KiB
JavaScript
/* =====================================================================
|
||
LÖSCHEN HEISST LÖSCHEN — und wer es darf
|
||
=====================================================================
|
||
|
||
Filipe am 22.09.2026:
|
||
|
||
„Gelöschte Nachrichten verschwinden vollständig – keine Spur, kein
|
||
‚wurde gelöscht'-Hinweis, bei niemandem, auch nicht bei
|
||
DogFather."
|
||
„Löschen darf weiterhin jeder nur seine eigenen – außer DogFather
|
||
und rechte Hand: die dürfen jede Nachricht löschen, für den
|
||
Notfall."
|
||
|
||
---------------------------------------------------------------------
|
||
WARUM DAS NICHT „EIN STATUS WENIGER" IST
|
||
|
||
Vorher blieb die Zeile stehen: Text geleert, `weg_am` gesetzt, und
|
||
der Chat zeigte „Nachricht zurückgenommen". Es war so begründet --
|
||
„ein Loch im Verlauf wirft mehr Fragen auf als der Hinweis". Das
|
||
Argument beantwortet nur eine andere Frage: Ein Hinweis „hier stand
|
||
etwas" MARKIERT die Stelle. Wer etwas aus Versehen schreibt, will es
|
||
weg haben und nicht unterstrichen.
|
||
|
||
Ein echtes DELETE hat aber vier Ränder, und an jedem kann eine Spur
|
||
übrig bleiben. Genau die werden hier gemessen:
|
||
|
||
1. die Zeile selbst
|
||
2. Reaktionen darauf (chat_reaktionen, ON DELETE CASCADE)
|
||
3. Erwähnungen darin (chat_erwaehnungen, ON DELETE CASCADE)
|
||
4. `letzte_am` am Raum -- sonst stünde er in der Liste ganz
|
||
oben, mit einem Zeitpunkt, zu dem
|
||
es nichts mehr gibt
|
||
|
||
Die Punkte 2 und 3 hängen an `PRAGMA foreign_keys = ON`. Dass die
|
||
Regel dort steht, ist eine Behauptung im Quelltext; dass sie WIRKT,
|
||
sagt nur eine Messung.
|
||
|
||
---------------------------------------------------------------------
|
||
DIE RECHTEGRENZE HAT VIER FÄLLE, NICHT ZWEI
|
||
|
||
Eine Prüfung, die nur „der Eigene darf" und „ein Fremder darf nicht"
|
||
misst, übersieht die Hälfte: Die linke Hand führt Team Dogi mit
|
||
(`fuehrtTeamDogi`) und darf es trotzdem NICHT -- Filipe hat zwei
|
||
Rollen genannt, nicht drei. Und Spicy Media, das sonst fast alles
|
||
darf, darf private Chats nicht einmal sehen.
|
||
===================================================================== */
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
import { eigenerPort } from "./helfer-port.mjs";
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-loesch-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
const PORT = await eigenerPort(import.meta, "pruef-loeschen");
|
||
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");
|
||
notbremse(120_000, "pruef-loeschen");
|
||
await new Promise((r) => setTimeout(r, 800));
|
||
|
||
let fehler = 0, geprueft = 0, offen = 0;
|
||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
||
|
||
/* ---------- Leute ------------------------------------------------------ */
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const jetzt = new Date().toISOString();
|
||
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);
|
||
|
||
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, 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;
|
||
}
|
||
|
||
const idChef = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
|
||
const idLinke = anlegen("Lino", "linke", "CODE-LINK-0001");
|
||
const idModi = anlegen("Frida", "modi", "CODE-MODI-0001");
|
||
const idModi2 = anlegen("Nele", "modi", "CODE-MODI-0002");
|
||
|
||
/* ---------- Anfragen (node:http wegen des Host-Kopfes) ----------------- */
|
||
const { request: httpAnfrage } = await import("node:http");
|
||
const CREW = "crew.dogfather-universe.com";
|
||
function anfrage(pfad, { methode = "GET", keks = "", rumpf = null } = {}) {
|
||
return new Promise((fertig, schief) => {
|
||
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
|
||
const a = httpAnfrage({
|
||
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
|
||
headers: {
|
||
Host: CREW, Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/`,
|
||
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
|
||
...(keks ? { Cookie: keks } : {}),
|
||
},
|
||
}, (antwort) => {
|
||
const st = [];
|
||
antwort.on("data", (x) => st.push(x));
|
||
antwort.on("end", () => {
|
||
const text = Buffer.concat(st).toString("utf8");
|
||
let json = null; try { json = JSON.parse(text); } catch { /* kein JSON */ }
|
||
fertig({ code: antwort.statusCode, json,
|
||
keks: [].concat(antwort.headers["set-cookie"] || [])
|
||
.map((c) => c.split(";")[0]).join("; ") });
|
||
});
|
||
});
|
||
a.on("error", schief);
|
||
if (daten) a.write(daten);
|
||
a.end();
|
||
});
|
||
}
|
||
const anmelden = async (rolle, code) =>
|
||
(await anfrage("/workspace/api/anmelden", { methode: "POST", rumpf: { rolle, code } })).keks;
|
||
const ruf = (keks, pfad, o = {}) => anfrage(pfad, { ...o, keks });
|
||
|
||
const keksChef = await anmelden("admin", "CODE-DOGI-0001");
|
||
const keksHand = await anmelden("hand", "CODE-HAND-0001");
|
||
const keksLinke = await anmelden("linke", "CODE-LINK-0001");
|
||
const keksModi = await anmelden("modi", "CODE-MODI-0001");
|
||
/* Nele schreibt die fremden Nachrichten -- sie ist in keinem der
|
||
vier Faelle die Loeschende, damit "fremd" fuer alle vier
|
||
wirklich fremd ist. */
|
||
const keksModi2 = await anmelden("modi", "CODE-MODI-0002");
|
||
|
||
console.log("=== Löschen heißt Löschen ===");
|
||
console.log("");
|
||
|
||
/* Ein Kanal, in dem alle drin sind -- die Kanalregel vom selben Tag
|
||
holt sie beim ersten Blick in die Liste hinein. */
|
||
const kanal = await ruf(keksChef, "/workspace/api/chat/kanal",
|
||
{ methode: "POST", rumpf: { kategorie: "chat", mit: [] } });
|
||
const raumId = kanal.json?.raum_id;
|
||
await ruf(keksChef, "/workspace/api/chat/raeume");
|
||
const drin = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM chat_teilnehmer WHERE raum_id = ? AND raus_am IS NULL").get(raumId).n;
|
||
ok(kanal.code === 201 && drin === 5, `ein Kanal mit allen fünf steht (${drin})`);
|
||
|
||
const schreiben = async (keks, text) => {
|
||
const a = await ruf(keks, `/workspace/api/chat/raeume/${raumId}/nachrichten`,
|
||
{ methode: "POST", rumpf: { text } });
|
||
return a.json?.id || a.json?.nachricht?.id || null;
|
||
};
|
||
|
||
/* =====================================================================
|
||
1. EINE EIGENE NACHRICHT -- MIT ALLEM DRAN
|
||
===================================================================== */
|
||
console.log("--- Eine eigene löschen, mit Reaktion und Erwähnung ---");
|
||
|
||
const idNachricht = await schreiben(keksModi, "Hallo @Rieke, das hier ist ein Versehen");
|
||
ok(Number.isInteger(idNachricht), `sie ist geschrieben (#${idNachricht})`);
|
||
|
||
await ruf(keksChef, `/workspace/api/chat/nachrichten/${idNachricht}/reaktion`,
|
||
{ methode: "PUT", rumpf: { zeichen: "\u{1F44D}" } });
|
||
|
||
const vorher = {
|
||
zeile: d.prepare("SELECT COUNT(*) AS n FROM chat_nachrichten WHERE id = ?").get(idNachricht).n,
|
||
reaktion: d.prepare("SELECT COUNT(*) AS n FROM chat_reaktionen WHERE nachricht_id = ?")
|
||
.get(idNachricht).n,
|
||
erwaehnung: d.prepare("SELECT COUNT(*) AS n FROM chat_erwaehnungen WHERE nachricht_id = ?")
|
||
.get(idNachricht).n,
|
||
};
|
||
ok(vorher.zeile === 1 && vorher.reaktion === 1 && vorher.erwaehnung === 1,
|
||
`vorher: Zeile ${vorher.zeile}, Reaktion ${vorher.reaktion}, Erwähnung ${vorher.erwaehnung}`
|
||
+ " — es gibt also wirklich etwas zu löschen");
|
||
|
||
const weg = await ruf(keksModi, `/workspace/api/chat/nachrichten/${idNachricht}`,
|
||
{ methode: "DELETE" });
|
||
ok(weg.code === 200, `die eigene löschen geht (${weg.code})`);
|
||
|
||
const nachher = {
|
||
zeile: d.prepare("SELECT COUNT(*) AS n FROM chat_nachrichten WHERE id = ?").get(idNachricht).n,
|
||
reaktion: d.prepare("SELECT COUNT(*) AS n FROM chat_reaktionen WHERE nachricht_id = ?")
|
||
.get(idNachricht).n,
|
||
erwaehnung: d.prepare("SELECT COUNT(*) AS n FROM chat_erwaehnungen WHERE nachricht_id = ?")
|
||
.get(idNachricht).n,
|
||
};
|
||
ok(nachher.zeile === 0, "die Zeile ist weg — nicht geleert, weg");
|
||
ok(nachher.reaktion === 0 && nachher.erwaehnung === 0,
|
||
`und Reaktion (${nachher.reaktion}) und Erwähnung (${nachher.erwaehnung}) mit ihr`
|
||
+ " — ON DELETE CASCADE wirkt wirklich");
|
||
|
||
/* KEINE SPUR IM VERLAUF -- auch nicht bei DogFather. */
|
||
const verlaufChef = await ruf(keksChef, `/workspace/api/chat/raeume/${raumId}/nachrichten`);
|
||
const alle = verlaufChef.json?.nachrichten || [];
|
||
ok(!alle.some((n) => n.id === idNachricht),
|
||
"sie steht in keinem Verlauf mehr — auch nicht in dem von DogFather");
|
||
ok(!alle.some((n) => n.zurueckgenommen),
|
||
"und es steht nirgends eine zurückgenommene Nachricht");
|
||
|
||
/* =====================================================================
|
||
2. DIE VIER FÄLLE DER RECHTEGRENZE
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("--- Wer darf eine FREMDE löschen ---");
|
||
|
||
const faelle = [
|
||
["DogFather", keksChef, 200, "er ist einer der zwei, die es dürfen"],
|
||
["die rechte Hand", keksHand, 200, "sie ist die zweite"],
|
||
["die linke Hand", keksLinke, 403,
|
||
"sie führt Team Dogi mit und darf es trotzdem nicht — genannt waren zwei Rollen"],
|
||
["ein Modi", keksModi, 403, "er nicht"],
|
||
];
|
||
let geprueftFaelle = 0;
|
||
for (const [wer, keks, erwartet, warum] of faelle) {
|
||
const id = await schreiben(keksModi2, `Fremde Nachricht für ${wer}`);
|
||
if (!Number.isInteger(id)) { unklar(`für ${wer} ließ sich keine Nachricht anlegen`); continue; }
|
||
const a = await ruf(keks, `/workspace/api/chat/nachrichten/${id}`, { methode: "DELETE" });
|
||
const nochDa = d.prepare("SELECT COUNT(*) AS n FROM chat_nachrichten WHERE id = ?").get(id).n;
|
||
geprueftFaelle++;
|
||
ok(a.code === erwartet && (erwartet === 200 ? nochDa === 0 : nochDa === 1),
|
||
`${wer}: ${a.code} (erwartet ${erwartet}), Zeile danach ${nochDa} — ${warum}`);
|
||
}
|
||
ok(geprueftFaelle === faelle.length,
|
||
`alle ${faelle.length} Fälle wirklich durchgespielt (${geprueftFaelle})`);
|
||
|
||
|
||
/* =====================================================================
|
||
3. DER RAUM VERGISST DEN ZEITPUNKT
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("--- Was der Raum danach über sich sagt ---");
|
||
|
||
const letzteId = await schreiben(keksModi, "Die allerletzte");
|
||
const vorLoeschen = d.prepare("SELECT letzte_am FROM chat_raeume WHERE id = ?").get(raumId).letzte_am;
|
||
await ruf(keksModi, `/workspace/api/chat/nachrichten/${letzteId}`, { methode: "DELETE" });
|
||
const nachLoeschen = d.prepare("SELECT letzte_am FROM chat_raeume WHERE id = ?").get(raumId).letzte_am;
|
||
ok(!!vorLoeschen && vorLoeschen !== nachLoeschen,
|
||
`der Raum rechnet seinen letzten Zeitpunkt neu (${vorLoeschen} → ${nachLoeschen})`);
|
||
|
||
/* =====================================================================
|
||
4. DAS BABYBLAUE HERZ
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("--- Babyblau statt Blau ---");
|
||
|
||
const { ERLAUBT } = await import("./workspace-reaktionen.js");
|
||
ok(ERLAUBT.has("\u{1FA75}"), "das babyblaue Herz steht im Katalog");
|
||
ok(!ERLAUBT.has("\u{1F499}"), "das normale blaue nicht mehr");
|
||
|
||
/* GEGENPROBE AM LAUFENDEN SERVER: Was der Katalog sagt, muss auch der
|
||
Weg zum Setzen sagen. Zwei Listen, die auseinanderlaufen, wären hier
|
||
besonders unangenehm -- man setzt ein Zeichen und es verschwindet. */
|
||
const idFuerHerz = await schreiben(keksModi, "Ein Satz zum Liebhaben");
|
||
const babyOk = await ruf(keksChef, `/workspace/api/chat/nachrichten/${idFuerHerz}/reaktion`,
|
||
{ methode: "PUT", rumpf: { zeichen: "\u{1FA75}" } });
|
||
const blauAb = await ruf(keksHand, `/workspace/api/chat/nachrichten/${idFuerHerz}/reaktion`,
|
||
{ methode: "PUT", rumpf: { zeichen: "\u{1F499}" } });
|
||
ok(babyOk.code === 200, `ein babyblaues Herz geht durch (${babyOk.code})`);
|
||
ok(blauAb.code === 400, `ein blaues wird abgewiesen (${blauAb.code})`);
|
||
|
||
/* =====================================================================
|
||
5. IM CHAT-SKRIPT STEHT DER SATZ NICHT MEHR
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("--- Und im Browser ---");
|
||
|
||
const { readFileSync } = await import("node:fs");
|
||
const { dirname } = await import("node:path");
|
||
const { fileURLToPath } = await import("node:url");
|
||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||
const chatJs = readFileSync(join(HIER, "..", "workspace", "assets", "js", "chat.js"), "utf8");
|
||
/* NUR AUSSERHALB VON KOMMENTAREN. Die Begründung, warum der Satz weg
|
||
ist, nennt ihn -- und muss das auch dürfen. Gesucht wird deshalb
|
||
nach ihm als Zeichenkette in Anführungszeichen, also so, wie er auf
|
||
den Bildschirm käme. */
|
||
const alsText = /(['"`])Nachricht zurückgenommen\1/.test(chatJs);
|
||
ok(!alsText, "der Satz steht in chat.js an keiner Stelle mehr,"
|
||
+ " an der ihn jemand lesen koennte");
|
||
ok(/Nachricht zurückgenommen/.test(chatJs),
|
||
"Gegenprobe: der Satz kommt in den Kommentaren durchaus vor —"
|
||
+ " die Suche oben unterscheidet also wirklich");
|
||
|
||
/* =====================================================================
|
||
5b. DER LOESCHKNOPF IST ERREICHBAR -- AUCH OHNE MAUS
|
||
=====================================================================
|
||
|
||
ZWEI FEHLER AN DERSELBEN STELLE, beide erst im Bildschirmfoto
|
||
aufgefallen (23.09.2026):
|
||
|
||
(1) `opacity: 0`, sichtbar erst beim Ueberfahren mit der Maus. Das
|
||
war vertretbar, solange der Knopf nur die EIGENE Nachricht
|
||
zuruecknahm. Seit er auch das Notfall-Loeschen traegt, ist er
|
||
auf einem Handy dauerhaft unsichtbar -- und er belegte trotzdem
|
||
87 px, also eine Luecke in der Fusszeile, die nichts erklaert.
|
||
|
||
(2) Der Notfall-Knopf bekam den Warnton, gedaempft gemischt mit der
|
||
leisen Schrift. Nachgerechnet: 1,91:1 auf Babyblau und 4,06 bis
|
||
4,13 auf den dunklen Toenen -- unlesbar auf der einen, zu wenig
|
||
auf allen anderen. Die leise Schriftfarbe ist genau der Wert,
|
||
der ueberall 4,5:1 schafft; wer daran mischt, gibt diese Zusage
|
||
auf.
|
||
|
||
Beides steht in der Stilvorlage und laesst sich dort pruefen. Eine
|
||
Messung im Browser waere genauer, braucht aber eine fremde
|
||
Nachricht, eine Rolle mit dem Recht und einen Zeiger -- fuer eine
|
||
Frage, deren Antwort in zwei Zeilen CSS steht.
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("--- Der Loeschknopf in der Stilvorlage ---");
|
||
{
|
||
const chatCss = readFileSync(
|
||
join(HIER, "..", "workspace", "assets", "css", "chat.css"), "utf8");
|
||
const regel = (wahl) => {
|
||
const i = chatCss.indexOf(wahl + " {");
|
||
return i < 0 ? null : chatCss.slice(i, chatCss.indexOf("}", i));
|
||
};
|
||
|
||
const grund = regel(".chat-nachricht__weg-knopf");
|
||
ok(!!grund, "die Regel zum Loeschknopf steht in chat.css");
|
||
ok(!!grund && !/opacity:\s*0/.test(grund),
|
||
"er ist nicht auf durchsichtig gesetzt -- auf einem Handy gibt es kein"
|
||
+ " Ueberfahren, und ein Knopf, den man dort nicht sieht, gibt es dort nicht");
|
||
|
||
const notfall = regel('.chat-nachricht__weg-knopf[data-notfall="ja"]');
|
||
ok(!!notfall, "und die Regel fuer das Notfall-Loeschen");
|
||
/* EIN EINFACHER TEXTVERGLEICH, KEIN REGULAERER AUSDRUCK.
|
||
-------------------------------------------------------------------
|
||
Hier stand `/\bcolor:/` -- gedacht als Wortgrenze, damit
|
||
`color-mix` nicht mitzaehlt. Geschrieben wurde es aber durch eine
|
||
Python-Zeichenkette, und dort ist `\b` das BACKSPACE-ZEICHEN. In
|
||
der Datei landete `/<0x08>color:/`, also ein Ausdruck, der nie
|
||
etwas findet.
|
||
|
||
Die Zeile darunter (die Gegenprobe) wurde dadurch rot -- und nur
|
||
deshalb ist es aufgefallen. DIESE Zeile hier war gruen, ohne je
|
||
etwas gesucht zu haben: genau der Fehler, vor dem die Hausregel
|
||
warnt, und ohne die Gegenprobe haette ihn niemand gesehen.
|
||
|
||
Jetzt zwei schlichte Textsuchen: „color:" muss vorkommen, und
|
||
Treffer, die zu `color-mix:` gehoeren, gibt es nicht -- die
|
||
Eigenschaft heisst `color-mix` nur als WERT, nie als Name vor
|
||
einem Doppelpunkt. */
|
||
ok(!!notfall && !notfall.includes("color:"),
|
||
"sie setzt KEINE eigene Schriftfarbe -- die Markierung sitzt im Punkt"
|
||
+ " davor, so wie beim Namen in der Blase");
|
||
/* ==== NACH DEM SELEKTOR SUCHEN, NICHT IN 600 ZEICHEN (25.09.2026)
|
||
|
||
Hier wurde ab der Fundstelle ein Fenster von 600 Zeichen
|
||
abgesucht. Das misst in Wahrheit, wie lang der KOMMENTAR
|
||
dazwischen ist: Am 25.09. kam eine Begruendung von zwanzig Zeilen
|
||
dazu (aus dem Punkt wurde ein Papierkorb), und das `::before`
|
||
rutschte aus dem Fenster. Die Pruefung meldete „der Punkt ist
|
||
nicht da" fuer etwas, das zweiundzwanzig Zeilen tiefer steht.
|
||
|
||
Gesucht wird jetzt nach dem SELEKTOR selbst. Der ist da oder
|
||
nicht; wie viel Text davor steht, geht die Frage nichts an. */
|
||
ok(chatCss.includes('.chat-nachricht__weg-knopf[data-notfall="ja"]::before'),
|
||
"der Punkt ist wirklich da (::before)");
|
||
|
||
/* GEGENPROBE: Die Suche muss eine Schriftfarbe auch FINDEN koennen.
|
||
Sonst waere „setzt keine" nur die Abwesenheit einer Messung. */
|
||
ok((regel(".chat-nachricht__von") || "").includes("color:"),
|
||
"Gegenprobe: in der Regel zum Namen findet dieselbe Suche sehr wohl eine Farbe");
|
||
}
|
||
|
||
/* =====================================================================
|
||
6. DIE UMSTELLUNG RAEUMT WIRKLICH AUF
|
||
|
||
ENTSTANDEN AUS EINEM FEHLER, DER SCHON LIVE WAR (22.09.2026, 23:33).
|
||
|
||
In der ersten Fassung stand `d.prepare("DELETE ...").changes` statt
|
||
`.run().changes` -- die Eigenschaft der vorbereiteten ANWEISUNG,
|
||
nicht die des Laufs. Die Anweisung wurde vorbereitet und nie
|
||
ausgefuehrt.
|
||
|
||
NICHTS STUERZTE AB, NICHTS WAR ROT. Der einzige Hinweis war ein Wort
|
||
im Protokoll: "undefined zurueckgenommene Nachrichten endgueltig
|
||
entfernt". Haette dort niemand hingesehen, waeren die fuenf Zeilen
|
||
liegen geblieben -- und weil die Sicherung VOR dem Loeschen
|
||
geschrieben wird, waere bei jedem Neustart eine weitere
|
||
Sicherungsdatei entstanden.
|
||
|
||
DESHALB WIRD HIER DIE ZAHL GEPRUEFT, nicht nur das Ergebnis. `0`
|
||
und `undefined` sehen in einer Meldung fast gleich aus; in einer
|
||
Bedingung nicht.
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("--- Die Umstellung fuer alte Zeilen ---");
|
||
|
||
{
|
||
const { loeschspurenAufraeumen } = await import("./workspace.js");
|
||
const idAlt = await schreiben(keksModi, "Aus einer alten Fassung");
|
||
/* Den alten Zustand von Hand herstellen: Text leer, weg_am gesetzt.
|
||
Genau so sahen die fuenf Zeilen in der echten Datenbank aus. */
|
||
d.prepare("UPDATE chat_nachrichten SET text = '', weg_am = ? WHERE id = ?")
|
||
.run(new Date().toISOString(), idAlt);
|
||
const davor = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE weg_am IS NOT NULL AND text = ''").get().n;
|
||
ok(davor === 1, `eine alte zurueckgenommene Zeile steht da (${davor})`);
|
||
|
||
/* OHNE PFAD: Dann schreibt die Funktion keine Sicherung -- hier wird
|
||
das Aufraeumen gemessen, nicht das Sichern. */
|
||
const erg = loeschspurenAufraeumen(d, null);
|
||
ok(typeof erg.entfernt === "number",
|
||
`sie meldet eine ZAHL (${typeof erg.entfernt}: ${erg.entfernt})`
|
||
+ " — genau hier stand vorher undefined");
|
||
ok(erg.entfernt === 1, `und zwar die richtige (${erg.entfernt})`);
|
||
const danach = d.prepare(
|
||
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE weg_am IS NOT NULL AND text = ''").get().n;
|
||
ok(danach === 0, `die Zeile ist wirklich weg (${danach} uebrig)`);
|
||
|
||
/* WIEDERHOLBAR: Beim zweiten Mal gibt es nichts zu tun, und es wird
|
||
auch nichts gesichert. Ohne diese Zeile waere nicht bewiesen, dass
|
||
bei jedem Neustart nicht doch wieder etwas passiert. */
|
||
const nochmal = loeschspurenAufraeumen(d, null);
|
||
ok(nochmal.entfernt === 0 && nochmal.gesichert === null,
|
||
`beim zweiten Mal passiert nichts (${nochmal.entfernt}, Sicherung ${nochmal.gesichert})`);
|
||
}
|
||
|
||
/* ---------- Ende ------------------------------------------------------ */
|
||
d.close();
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt manchmal */ }
|
||
|
||
console.log("");
|
||
console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}`
|
||
+ ` -- ${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
|
||
process.exit(fehler === 0 ? 0 : 1);
|