Files
dogfather-universe/server/pruef-bereiche-lesend.mjs
T
DogFatherGitandClaude Opus 5 d7cf598b00 "Heute" war zwei Stunden lang gestern -- und vier Fehler auf Handy und PC
Auftrag: "einen grossen Check machen, ob alles klappt, auf dem Handy und
PC." Dafuer ein neuer Rundgang (pruef-grosscheck.mjs), der stumpf ueber
alles geht: 19 Workspace-Seiten mal 4 Rollen mal 2 Bildschirmgroessen
plus 33 oeffentliche Seiten, zweimal. 208 Seiten, 50 774 Elemente.

Solche Rundgaenge finden andere Fehler als gezielte Pruefungen: nicht
den falsch gerechneten Wert, sondern die Seite, die bei genau einer
Rolle ueberlaeuft.

=== DER WICHTIGSTE FUND: "heute" war in UTC gerechnet ===

Der Check lief um 01:10 Uhr. Ortszeit war der 2. September, in UTC noch
der 1. -- und in diesem Fenster rechnete die Anwendung an ZWOELF Stellen
"heute" als toISOString(), also in UTC. Server und Benutzer stehen beide
auf Europe/Berlin.

Was das im Alltag bedeutete, jede Nacht zwischen 0 und 2 Uhr:
  * eine heute faellige Aufgabe galt noch nicht als faellig
  * eine um Mitternacht ueberfaellig gewordene erschien erst um 2 Uhr
  * der Filter "Heute faellig" zeigte den Vortag
  * Datumsfelder schlugen gestern vor
  * der Kalender begann seine Vorgabe einen Tag zu frueh

Also genau dann, wenn nach einem Stream gearbeitet wird.

kalender.js machte es die ganze Zeit RICHTIG -- samt Begruendung, warum
die ARITHMETIK trotzdem in UTC laufen muss (UTC-Mittag ueberlebt die
Zeitumstellung; wer lokal rechnet, verliert am 27. Oktober einen Tag).
Diese Trennung gilt jetzt ueberall, aus je einer Quelle:
  RECHNEN mit Datumsangaben  -> UTC-Mittag, unveraendert
  WELCHER TAG IST HEUTE      -> Ortszeit (heuteLokal/tagLokal im Server,
                                window.heuteLokal in kopf.js)

WIE ES AUFFIEL, und das ist die eigentliche Lehre: Zuerst schlugen zwei
Pruefungen fehl -- und die Ursache lag in IHNEN, sie rechneten selbst in
UTC (36 Stellen in 17 Dateien). Nach deren Reparatur schlugen sie WIEDER
fehl, und erst da zeigten sie auf die Anwendung. Wer beim ersten Mal
aufgehoert haette ("ist ja nur die Pruefung"), haette den echten Fehler
nie gesehen.

Nachtrag desselben Musters: pruef-uebersicht legte den Termin weiterhin
in UTC an, waehrend die Erwartung schon auf Ortszeit stand. Wer eine
Datumsrechnung umstellt, muss BEIDE Seiten umstellen -- die, die
schreibt, und die, die prueft.

=== VIER FEHLER AUF HANDY UND PC ===

1. Ein langer Creator-Name ("SpongBobSchwammKopf") schob die Startseite
   auf dem Handy um 48 Pixel aus dem Bild -- ein Wort ohne Trennstelle,
   und die Seite liess sich seitlich wegschieben. Trifft echte Namen:
   Creator heissen selten "Tim".

2. Die klebende Speicherleiste verdeckte auf dem Handy ein Textfeld.
   Beim Tippen sieht man die eigene Zeile nicht. Behoben mit
   scroll-margin-bottom (WCAG 2.2, 2.4.11 "Focus Not Obscured").

3./4. Zwei Beschriftungen waren mit 9,6 px (Uebersicht: "ueberfaellig",
   "dringend", "offen") und 9,9 px (Kalender: "heute") zu klein. Fuers
   Handy gab es laengst eine Ausnahme -- nur der Rechner war vergessen
   worden. Ausgerechnet die Woerter, die den Zahlen ihre Bedeutung geben.

=== WAS KEINE FEHLER WAREN ===

Der erste Durchgang meldete 19 Maengel, die keine waren. Alle einzeln im
Quelltext nachgeprueft und dem Rundgang beigebracht:
  * Kacheln und Kopfzeilen "abgeschnitten" -- das Wasserzeichen ragt
    ABSICHTLICH ueber den Rand (steht so im Quelltext)
  * "verdeckt: wahl2__echt" -- das echte <select> liegt absichtlich
    unsichtbar unter seinem Knopf
  * "zurueck-knopf__text abgeschnitten" -- das uebliche Muster fuer
    "nur fuer Vorleseprogramme"
  * drei "zu kleine" Verweise -- WCAG 2.5.8 nimmt Verweise im Fliesstext
    AUSDRUECKLICH aus. Eine Pruefung, die ihre eigene Messlatte nicht
    kennt, misst nichts.

Beim vierten Punkt haette ich fast an der falschen Stelle repariert.

Und statt die Sticky-Meldung abzuschalten (dann faende sie auch echte
Ueberdeckungen nie mehr), wurde sie GENAUER: Ueberdeckt etwas Klebendes
ein Eingabefeld, ist das nur in Ordnung, wenn das Feld genug
scroll-margin-bottom hat, um darunter hervorzukommen. Aus einer vagen
Meldung wird eine pruefbare Zusage.

Vier Gegenproben belegen, dass der Rundgang ueberhaupt etwas finden
kann: ein zu breites Element, ein winziger Knopf, ein winziger Verweis
AUSSERHALB eines Satzes und ein wirklich abgeschnittenes Wort werden
alle gemeldet. Ohne diesen Nachweis waere "alles in Ordnung" wertlos.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-02 01:51:28 +02:00

180 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===================================================================
Pruefung: Ein Creator kann in den Bereichen NICHTS mehr schreiben.
Wunsch Filipe 31.08.2026: "die creator sollen da nur die sehen die wir
ihnen eintragen, also dogfather manager und scout."
Der Schwerpunkt liegt auf den VERNEINENDEN Faellen -- dass DogFather
schreiben darf, faellt sofort auf; dass ein Creator es nicht mehr kann,
merkt man nur, wenn man es ausdruecklich versucht. Und zwar auf JEDEM
Weg und in JEDEM der fuenf Bereiche, nicht nur in dem einen, den man
gerade im Kopf hat.
=================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-bereiche-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4183";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1";
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, 700));
const BASIS = "http://127.0.0.1:4183";
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 180_000).unref?.();
/* ORTSZEIT, nicht UTC. Die Anwendung rechnet "heute" nach der Uhr des
Benutzers; toISOString() liefert dagegen UTC und damit zwischen
Mitternacht und 2 Uhr den VORTAG. In diesem Fenster schlug diese
Pruefung fehl, ohne dass etwas kaputt war. */
const tagText = (d = new Date()) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`
+ `-${String(d.getDate()).padStart(2, "0")}`;
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
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("Dogfather", "admin", "CODE-DOGI-0001");
const idScout = anlegen("Ein Scout", "scout", "CODE-SCOU-0001");
const idAnna = anlegen("Anna", "creator", "CODE-ANNA-0001");
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idAnna, idScout, jetzt);
/* Ein Eintrag, den ANNA sich frueher selbst haette anlegen koennen --
damit auch das Loeschen "eigener" Eintraege geprueft wird. */
const eigenerId = (() => {
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, creator_id, erstellt, erstellt_von) VALUES ('live','auswertung','Von Anna selbst','alt',?,'offen',?,?,?)")
.run(tagText(), idAnna, jetzt, idAnna);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
})();
d.close();
const anmelden = async (rolle, code) => {
const a = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle, code }), redirect: "manual" });
return (a.headers.getSetCookie?.() || []).map((c) => c.split(";")[0]).join("; ");
};
const ruf = (pfad, keks, opt = {}) => fetch(BASIS + pfad, {
...opt, headers: { cookie: keks, "Content-Type": "application/json", ...(opt.headers || {}) },
});
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
const kScout = await anmelden("scout", "CODE-SCOU-0001");
const kCreator = await anmelden("creator", "CODE-ANNA-0001");
const BEREICHE = ["live", "content", "technik", "community", "schutz"];
console.log("\n1) DogFather trägt in jedem Bereich ein");
const angelegt = {};
for (const b of BEREICHE) {
const arten = { live: "auswertung", content: "idee", technik: "problem", community: "moderation", schutz: "vorfall" };
const a = await ruf(`/workspace/api/bereich/${b}`, kDogi, {
method: "POST",
body: JSON.stringify({ art: arten[b], titel: "Von der Betreuung", text: "…",
datum: tagText(), creator_id: idAnna }),
});
const j = await a.json().catch(() => ({}));
angelegt[b] = j.id;
ok(a.ok, `${b}: angelegt (${a.status})`);
}
console.log("\n2) Der Scout darf ebenfalls – er betreut Anna");
{
const a = await ruf("/workspace/api/bereich/live", kScout, {
method: "POST",
body: JSON.stringify({ art: "vorbereitung", titel: "Vom Scout", datum: tagText(), creator_id: idAnna }),
});
ok(a.ok, `Scout darf eintragen (${a.status})`);
}
console.log("\n3) Der Creator SIEHT alles, was zu ihm gehört");
for (const b of BEREICHE) {
const a = await ruf(`/workspace/api/bereich/${b}`, kCreator);
const j = await a.json().catch(() => ({}));
const anzahl = (j.eintraege || []).length;
ok(a.ok && anzahl >= 1, `${b}: sichtbar, ${anzahl} Eintrag/Einträge (${a.status})`);
if (b === "live") {
ok((j.eintraege || []).some((e) => e.titel === "Von der Betreuung"),
"der Eintrag der Betreuung ist dabei");
ok((j.eintraege || []).some((e) => e.titel === "Vom Scout"),
"der Eintrag des Scouts ist dabei");
}
}
console.log("\n4) Der Creator kann NICHTS schreiben – in keinem Bereich, auf keinem Weg");
for (const b of BEREICHE) {
const neu = await ruf(`/workspace/api/bereich/${b}`, kCreator, {
method: "POST",
body: JSON.stringify({ art: "idee", titel: "Selbst geschrieben", datum: tagText() }),
});
ok(neu.status === 403, `${b}: anlegen abgelehnt (${neu.status})`);
const id = angelegt[b];
const aend = await ruf(`/workspace/api/bereich/${b}/${id}`, kCreator, {
method: "PATCH", body: JSON.stringify({ titel: "Umgeschrieben" }),
});
ok(aend.status === 403, `${b}: ändern abgelehnt (${aend.status})`);
const weg = await ruf(`/workspace/api/bereich/${b}/${id}`, kCreator, { method: "DELETE" });
ok(weg.status === 403, `${b}: löschen abgelehnt (${weg.status})`);
}
console.log("\n5) Auch den EIGENEN alten Eintrag nicht mehr");
{
/* Das war vorher ausdruecklich erlaubt ("wer ihn selbst geschrieben
hat"). Genau der Weg muss jetzt auch zu sein -- sonst bliebe ein
Schlupfloch fuer alles, was vor der Umstellung entstanden ist. */
const weg = await ruf(`/workspace/api/bereich/live/${eigenerId}`, kCreator, { method: "DELETE" });
ok(weg.status === 403, `eigener Alteintrag lässt sich nicht löschen (${weg.status})`);
const aend = await ruf(`/workspace/api/bereich/live/${eigenerId}`, kCreator, {
method: "PATCH", body: JSON.stringify({ status: "erledigt" }) });
ok(aend.status === 403, `und auch nicht auf erledigt setzen (${aend.status})`);
}
console.log("\n6) Nichts hat sich tatsächlich verändert");
{
const a = await ruf("/workspace/api/bereich/live", kDogi);
const { eintraege } = await a.json();
ok(eintraege.some((e) => e.titel === "Von der Betreuung"), "der Eintrag steht unverändert da");
ok(eintraege.some((e) => e.titel === "Von Anna selbst"), "der Alteintrag ist noch da");
ok(!eintraege.some((e) => e.titel === "Selbst geschrieben"), "nichts vom Creator ist durchgerutscht");
ok(!eintraege.some((e) => e.titel === "Umgeschrieben"), "nichts wurde umgeschrieben");
ok(eintraege.every((e) => e.status === "offen"), "kein Status wurde verändert");
}
console.log("\n7) Die Antwort erklärt, was los ist");
{
const a = await ruf("/workspace/api/bereich/live", kCreator, {
method: "POST", body: JSON.stringify({ art: "idee", titel: "x", datum: tagText() }) });
const j = await a.json();
console.log(" ->", j.fehler);
ok(/Betreuer/.test(j.fehler || ""), "die Meldung sagt, wer einträgt");
ok(/siehst/.test(j.fehler || ""), "und dass er trotzdem alles sieht");
}
console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler === 0 ? 0 : 1);