Filipe: "DU HAST WIEDER EINE KLEINE AENDERUNG UEBERALL GEMACHT ANSTATT
EINE RIESEN AENDERUNG." Er hatte recht, und der Grund war jedes Mal
derselbe: Ich habe das MATERIAL getauscht (Mattglas, Leuchtschiene,
Verlauf) und die FORM gelassen. Ein abgerundetes Rechteck bleibt ein
abgerundetes Rechteck -- und die Silhouette ist das Einzige, was man aus
fuenf Metern erkennt.
Jetzt ist die Form eine andere: abgeschnittene Ecke oben links
(clip-path, echte Silhouette), ein Kantenlicht in der Kategoriefarbe
darauf, Eckwinkel unten rechts, ein feines Raster statt Koernung.
35 Bauteile auf allen 18 Seiten, in einer eigenen Datei (module.css).
DREI DINGE, DIE DABEI SCHIEFGINGEN UND JETZT ABGESICHERT SIND
1. Die Regeln standen in :where() -- Spezifitaet null, also gewann jede
aeltere .kachel::before-Regel. Jetzt :is().
2. module.css stand an DRITTER Stelle im Ladeweg. Auf Calls,
Automationen, Bereich und Dateien hat sie damit gar nichts bewirkt:
Die Seitendateien setzen dort selbst border-radius und box-shadow und
kommen spaeter. Genau das ergab wieder "ueberall ein bisschen". Sie
wird jetzt als LETZTE geladen, geprueft auf allen 18 Seiten.
3. Die Klassenliste steht siebenmal in der Datei (CSS kennt keine
Variable fuer Selektoren). Eine vergessene Kopie faellt niemandem
auf -- pruef-css-klassen vergleicht sie deshalb alle, mit Gegenprobe.
DER KALENDER: NUR NOCH, WAS EINEN ANGEHT
Filipe: "calls oder termine soll jeder nur sehen die er selber macht
oder jeden betrifft." Das gilt auch fuer DogFather, und das ist die
eigentliche Aenderung -- er bekam bisher 1=1. "Jeden betrifft" heisst:
ohne Gegenueber und ohne Teilnehmerliste. Wer niemanden eintraegt, meint
alle. Die Rollenfrage entfaellt im Kalender damit vollstaendig.
SECHS PRUEFUNGEN, DIE EINE WELT GEMESSEN HABEN, DIE ES NICHT MEHR GIBT
Alle sechs wurden auf die neue Regel umgeschrieben, keine geloescht --
loeschen haette die Zahl gesenkt und den neuen Weg ungeprueft gelassen:
ics, serien, sicht, spicy, teilnehmer, tagesblick, team.
UND VIER ECHTE FUNDE, DIE DABEI HERAUSFIELEN
* pruef-workspace-seiten war seit dem Bildumbau von heute Frueh auf
ALLEN 32 Seiten rot: Sie verlangte noch das eine Motiv. Die neue
Bedingung ist schaerfer als beide alten -- das geladene Bild muss zu
dem Merkmal passen, das die Seite selbst traegt. Und die Meldung sagt
jetzt, WELCHE Bedingung gefallen ist.
* pruef-leistung-optik hat sich selbst ausgesperrt (meldete sich als
Manager an, der die Zahlen seit Filipes Anweisung nicht mehr sehen
darf) und stuerzte danach ab: 2 Pruefungen statt 56. Der Absturz hat
verdeckt, dass sie sieben Fingerziele als "zu klein" meldete -- es
waren die unsichtbaren Knoepfe der zugeklappten Woche, 0x0.
* personen.html beim Manager: Der Erklaersatz ("Codes, Sperren und das
Protokoll bleiben bei DogFather") kam nie an -- das Element gab es
nicht, und das && davor hat den Fehlgriff verschluckt. Die Liste stand
ausserdem dauerhaft auf aria-busy="true": fuer einen Screenreader lud
die Seite fuer immer.
* pruef-schranke hielt personen.html noch fuer DogFather-only. Die Seite
wechselt jetzt die Erwartung, statt aus der Pruefung zu verschwinden:
Seite auf fuer die Leitung, Verwaltungsdaten dahinter weiterhin zu.
Die Anmeldeseite wurde nicht angefasst (nur ihr Versionsstempel).
Co-Authored-By: Claude Opus 5 <[email protected]>
441 lines
22 KiB
JavaScript
441 lines
22 KiB
JavaScript
/* =====================================================================
|
|
DIE ROLLE "SPICY MEDIA"
|
|
|
|
Wunsch Filipe (07.09.2026): "eine neue rolle ... soll die gleichen
|
|
rechte haben wie dogfather ausser, bei automationen sollen die nicht
|
|
sehen und die kategorie personen und zugaenge sollen die nur leute
|
|
hinzufuegen koennen ... aber die sollen meine privaten chats und so
|
|
nicht sehen."
|
|
|
|
---------------------------------------------------------------------
|
|
ZWEI DINGE WERDEN HIER GEPRUEFT, UND DAS ZWEITE IST DAS WICHTIGERE
|
|
|
|
1. DER UMBAU DER PERSONENTABELLE. Eine Rolle ist ein erlaubter Wert in
|
|
einer Spalte, und der steckt in einem CHECK. Ein CHECK laesst sich
|
|
nicht aendern -- die Tabelle muss neu gebaut werden. Auf `personen`
|
|
zeigen ZWEIUNDFUENFZIG Fremdschluessel; jede Sitzung, jede Aufgabe,
|
|
jeder Termin haengt daran. Deshalb wird der Weg hier nicht "im
|
|
Prinzip" geprueft, sondern WIRKLICH GEGANGEN: Die Pruefung baut
|
|
eine Datenbank im alten Stand nach, fuellt sie mit Personen UND
|
|
abhaengigen Zeilen, laesst die echte Anwendung darueberlaufen und
|
|
zaehlt danach jede Zeile und jedes Feld nach.
|
|
|
|
2. DIE GRENZEN DER ROLLE. Zu jedem "darf" steht das "darf nicht"
|
|
daneben. Eine Rolle, von der nur geprueft ist, was sie kann, ist
|
|
keine Rolle, sondern eine Vermutung.
|
|
|
|
Aufruf: node server/pruef-spicy.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, existsSync, readdirSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname, basename } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawn } from "node:child_process";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { scryptSync, randomBytes } from "node:crypto";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
notbremse(300_000, "die Spicy-Pruefung");
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
|
const PORT = 4351;
|
|
await portMussFreiSein(PORT, "die Spicy-Pruefung");
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-spicy-"));
|
|
const DB = join(ordner, "workspace.db");
|
|
|
|
let fehler = 0, gemacht = 0;
|
|
const ok = (b, t) => { gemacht++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
let kind = null;
|
|
async function starten() {
|
|
kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
|
env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT),
|
|
SITE_ACCESS_SECRET: "lokaler-test",
|
|
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
|
|
stdio: ["ignore", "pipe", "pipe"] });
|
|
let aus = "";
|
|
kind.stdout.on("data", (d) => { aus += d; });
|
|
kind.stderr.on("data", (d) => { aus += d; });
|
|
for (let i = 0; i < 100; i++) {
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
if (kind.exitCode !== null) break;
|
|
try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
|
if (a.status < 500) return () => aus; } catch { /* noch nicht oben */ }
|
|
}
|
|
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
|
}
|
|
async function stoppen() {
|
|
if (!kind) return;
|
|
kind.kill(); kind = null;
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. Eine Datenbank im ALTEN Stand -- mit Personen und allem, was daran
|
|
haengt. Ohne die abhaengigen Zeilen pruefte der Umbau nur eine
|
|
Tabelle statt eines Netzes.
|
|
======================================================================= */
|
|
console.log("=== Der alte Stand ===");
|
|
await starten();
|
|
await stoppen();
|
|
|
|
const jetzt = new Date().toISOString();
|
|
const anlegen = (d, name, rolle, code) => {
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salz, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salz, 32768, jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
};
|
|
|
|
let idDogi, idMax, idLuna, spaltenVorher, personenVorher;
|
|
{
|
|
const d = new DatabaseSync(DB);
|
|
/* Die Tabelle auf den alten CHECK zuruecksetzen -- WOERTLICH so, wie
|
|
sie vor heute aussah, aber mit allen Spalten, die inzwischen
|
|
dazugekommen sind. Genau das ist der Fall, der gefaehrlich ist. */
|
|
d.exec("PRAGMA foreign_keys = OFF");
|
|
const alt = d.prepare(
|
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='personen'").get().sql;
|
|
const zurueck = alt
|
|
/* Auch hier muss `IF NOT EXISTS` ins Muster -- sonst waere der
|
|
"alte Stand" gar keiner: Die Ersetzung liefe ins Leere, `CREATE
|
|
TABLE IF NOT EXISTS personen` waere wegen des vorhandenen Tisches
|
|
ein Leerlauf, und die Pruefung testete den heutigen Stand gegen
|
|
sich selbst. */
|
|
.replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i,
|
|
"CREATE TABLE personen_alt")
|
|
.replace(/rolle\s+IN\s*\([^)]*\)/i, "rolle IN ('admin','manager','scout','creator')");
|
|
/* Wenn der Rueckbau nicht greift, ist alles danach wertlos -- dann
|
|
prueft diese Datei den heutigen Stand gegen sich selbst. Deshalb
|
|
hier abbrechen statt weitermachen. */
|
|
const regelDanach = zurueck.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
|
|
if (!zurueck.includes("personen_alt") || regelDanach.includes("'spicy'")) {
|
|
console.log(" FEHL der alte Stand liess sich nicht nachbauen -- Muster passt nicht.");
|
|
process.exit(1);
|
|
}
|
|
d.exec("BEGIN");
|
|
d.exec(zurueck);
|
|
const sp = d.prepare("PRAGMA table_info(personen)").all().map((z) => `"${z.name}"`).join(", ");
|
|
d.exec(`INSERT INTO personen_alt (${sp}) SELECT ${sp} FROM personen;`);
|
|
d.exec("DROP TABLE personen;");
|
|
d.exec("ALTER TABLE personen_alt RENAME TO personen;");
|
|
d.exec("COMMIT");
|
|
d.exec("PRAGMA foreign_keys = ON");
|
|
|
|
idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001");
|
|
idMax = anlegen(d, "Max", "manager", "CODE-MAX-00001");
|
|
idLuna = anlegen(d, "Luna", "creator", "CODE-LUNA-0001");
|
|
/* Ein Profilbild und ein Feld aus einer SPAETER ergaenzten Spalte --
|
|
genau die verliert man beim Abschreiben einer Spaltenliste. */
|
|
d.prepare("UPDATE personen SET bild = ?, ueber_mich = ?, tiktok = ? WHERE id = ?")
|
|
.run("abcdef0123456789abcdef0123456789.jpg", "Ich streame seit 2024.", "luna_live", idLuna);
|
|
/* Abhaengige Zeilen aus verschiedenen Tabellen. */
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
|
|
.run(idLuna, idMax, jetzt);
|
|
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, creator_id, erstellt, erstellt_von)
|
|
VALUES ('technik','problem','Ton bricht ab',?,?,?,?)`)
|
|
.run(jetzt.slice(0, 10), idLuna, jetzt, idMax);
|
|
|
|
spaltenVorher = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
|
|
personenVorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n;
|
|
|
|
/* GEGENPROBE: Der ALTE Stand muss 'spicy' wirklich ablehnen -- sonst
|
|
gaebe es nichts umzustellen, und alles danach bewiese nichts. */
|
|
let abgelehnt = false;
|
|
try {
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,'spicy','x','y',1,1,?)")
|
|
.run("Darf noch nicht", jetzt);
|
|
} catch { abgelehnt = true; }
|
|
d.close();
|
|
ok(abgelehnt, "der alte Stand lehnt die Rolle 'spicy' wirklich ab");
|
|
ok(personenVorher === 3, `${personenVorher} Personen und abhaengige Zeilen angelegt`);
|
|
ok(spaltenVorher.includes("bild") && spaltenVorher.includes("tiktok"),
|
|
`die Tabelle hat ${spaltenVorher.length} Spalten, auch die spaeter ergaenzten`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. Die Anwendung laeuft darueber
|
|
======================================================================= */
|
|
console.log("\n=== Die Umstellung ===");
|
|
const ausgabe = await starten();
|
|
/* Die Umstellung laeuft beim ersten Datenbankzugriff, nicht beim
|
|
Zuhoeren am Anschluss -- ihre Meldung kann also erst kurz nach dem
|
|
Start in der Ausgabe stehen. Eine Anfrage stellen und kurz warten. */
|
|
await fetch(BASIS + '/workspace/', { redirect: 'manual' }).catch(() => {});
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
console.log("--- Serverausgabe ---");
|
|
for (const z of ausgabe().split(String.fromCharCode(10))) if (z.includes("workspace]")) console.log(" " + z);
|
|
{
|
|
ok(/Rolle 'spicy' freigeschaltet/.test(ausgabe()),
|
|
"die Umstellung meldet sich im Protokoll");
|
|
ok(/Sicherung vor der Umstellung/.test(ausgabe()),
|
|
"und hat vorher eine Sicherung geschrieben");
|
|
const sicherungen = readdirSync(ordner).filter((n) => n.includes("vor-spicy"));
|
|
ok(sicherungen.length === 1, `die Sicherungsdatei liegt da (${sicherungen[0] || "keine"})`);
|
|
}
|
|
{
|
|
const d = new DatabaseSync(DB, { readOnly: true });
|
|
const spalten = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
|
|
const fehlend = spaltenVorher.filter((n) => !spalten.includes(n));
|
|
ok(fehlend.length === 0,
|
|
fehlend.length ? `nach der Umstellung fehlen Spalten: ${fehlend.join(", ")}`
|
|
: `alle ${spalten.length} Spalten sind noch da`);
|
|
|
|
const n = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n;
|
|
ok(n === personenVorher, `alle ${n} Personen sind noch da (vorher ${personenVorher})`);
|
|
|
|
const luna = d.prepare("SELECT * FROM personen WHERE id = ?").get(idLuna);
|
|
ok(luna?.bild === "abcdef0123456789abcdef0123456789.jpg",
|
|
`das Profilbild hat den Umbau ueberlebt (${luna?.bild ? "ja" : "WEG"})`);
|
|
ok(luna?.ueber_mich === "Ich streame seit 2024." && luna?.tiktok === "luna_live",
|
|
"auch die spaeter ergaenzten Textspalten");
|
|
|
|
const b = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?").get(idLuna);
|
|
ok(b?.betreuer_id === idMax, `die Betreuung zeigt noch auf Max (${b?.betreuer_id})`);
|
|
const e = d.prepare("SELECT creator_id FROM eintraege WHERE titel = 'Ton bricht ab'").get();
|
|
ok(e?.creator_id === idLuna, `der Technik-Eintrag noch auf Luna (${e?.creator_id})`);
|
|
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
|
ok(kaputt.length === 0, `keine verwaisten Verweise (${kaputt.length})`);
|
|
|
|
/* GEGENPROBE, DIE WICHTIGSTE: Die Schranke muss noch BEISSEN. Eine
|
|
Umstellung, die nebenbei den CHECK entfernt, sieht aus wie ein
|
|
Erfolg und ist der schlimmere Ausgang. */
|
|
d.close();
|
|
const s = new DatabaseSync(DB);
|
|
let erfunden = false;
|
|
try {
|
|
s.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,'quatsch','x','y',1,1,?)")
|
|
.run("Erfundene Rolle", jetzt);
|
|
} catch { erfunden = true; }
|
|
ok(erfunden, "die Schranke beisst noch -- eine erfundene Rolle wird abgelehnt");
|
|
const idSpicy = anlegen(s, "Agentur", "spicy", "CODE-SPICY-001");
|
|
ok(Number.isInteger(idSpicy) && idSpicy > 0, "'spicy' laesst sich jetzt anlegen");
|
|
s.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. Was die Rolle darf -- und was nicht
|
|
======================================================================= */
|
|
console.log("\n=== Rechte ===");
|
|
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 }) });
|
|
const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
|
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
|
return keks;
|
|
};
|
|
const keksSpicy = await anmelden("spicy", "CODE-SPICY-001");
|
|
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const keksMax = await anmelden("manager", "CODE-MAX-00001");
|
|
|
|
const ruf = async (art, weg, keks, koerper) => {
|
|
const a = await fetch(BASIS + weg, { method: art,
|
|
headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS },
|
|
body: koerper ? JSON.stringify(koerper) : undefined });
|
|
let daten = null; try { daten = await a.json(); } catch { /* leer erlaubt */ }
|
|
return { status: a.status, daten };
|
|
};
|
|
|
|
{
|
|
const { status, daten } = await ruf("GET", "/workspace/api/ich", keksSpicy);
|
|
ok(status === 200 && daten?.rolle === "spicy", `die Anmeldung klappt (${daten?.rolle})`);
|
|
ok(daten?.rolle_name === "Spicy Media", `und heisst in der Oberflaeche "${daten?.rolle_name}"`);
|
|
}
|
|
{
|
|
/* SIEHT ALLES: alle Personen, alle Bereiche. */
|
|
const { daten } = await ruf("GET", "/workspace/api/personen", keksSpicy);
|
|
const namen = (daten?.personen || []).map((p) => p.name).sort();
|
|
ok(namen.includes("Luna") && namen.includes("Max") && namen.includes("Filipe"),
|
|
`sie sieht alle: ${namen.join(", ")}`);
|
|
}
|
|
{
|
|
const { status } = await ruf("GET", "/workspace/api/uebersicht/creator", keksSpicy);
|
|
ok(status === 200, `das Dashboard steht ihr offen (HTTP ${status})`);
|
|
}
|
|
{
|
|
/* NICHT: Automationen. */
|
|
const a = await fetch(BASIS + "/workspace/automation.html", {
|
|
headers: { Cookie: keksSpicy }, redirect: "manual" });
|
|
ok(a.status === 302 || a.status === 403 || a.status === 404,
|
|
`die Automationen-Seite bleibt zu (HTTP ${a.status})`);
|
|
const { status } = await ruf("GET", "/workspace/api/sicherung/zustand", keksSpicy);
|
|
ok(status === 404, `und die Sicherungs-Schnittstelle auch (HTTP ${status})`);
|
|
}
|
|
{
|
|
/* NICHT: die Personenverwaltung. Anlegen ja, alles andere nein. */
|
|
const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksSpicy);
|
|
ok(status === 404, `die Personenverwaltung bleibt zu (HTTP ${status})`);
|
|
const loeschen = await ruf("DELETE", `/workspace/api/verwaltung/personen/${idLuna}`, keksSpicy);
|
|
ok(loeschen.status === 404, `loeschen geht nicht (HTTP ${loeschen.status})`);
|
|
const code = await ruf("POST", `/workspace/api/verwaltung/personen/${idLuna}/code`, keksSpicy);
|
|
ok(code.status === 404, `einen Code neu setzen auch nicht (HTTP ${code.status})`);
|
|
}
|
|
{
|
|
/* ABER: Leute anlegen -- Creator und Manager. */
|
|
const c = await ruf("POST", "/workspace/api/creator-anlegen", keksSpicy, { name: "NeuerCreator" });
|
|
ok(c.status === 201 && c.daten?.rolle === "creator",
|
|
`sie legt einen Creator an (HTTP ${c.status}, ${c.daten?.rolle})`);
|
|
const m = await ruf("POST", "/workspace/api/manager-anlegen", keksSpicy, { name: "NeuerManager" });
|
|
ok(m.status === 201 && m.daten?.rolle === "manager",
|
|
`und einen Manager (HTTP ${m.status}, ${m.daten?.rolle})`);
|
|
}
|
|
{
|
|
/* GEGENPROBE: Ein Manager kann KEINEN Manager anlegen -- sonst waere
|
|
die zweite Tuer nur Zierde. */
|
|
const { status } = await ruf("POST", "/workspace/api/manager-anlegen", keksMax, { name: "VonMax" });
|
|
ok(status === 404, `ein Manager kommt durch diese Tuer NICHT (HTTP ${status})`);
|
|
}
|
|
{
|
|
/* NICHT: fremde Chats.
|
|
|
|
Der Chat haengt ausschliesslich an der Teilnehmerliste -- Spicy
|
|
Media ist nirgends Teilnehmerin, also sieht sie nichts. Geprueft
|
|
wird mit einem ECHTEN Gespraech zwischen DogFather und Max.
|
|
|
|
DER AUFBAU MUSS GELINGEN, SONST BEWEIST DER REST NICHTS. Beim
|
|
ersten Anlauf stand hier ein falscher Weg (/chat/raum statt
|
|
/chat/direkt). Das Gespraech entstand gar nicht -- und "Spicy Media
|
|
sieht 0 Gespraeche" war trotzdem gruen, weil es ueberhaupt keine
|
|
gab. Genau die Sorte gruener Haken, die nichts geprueft hat.
|
|
Deshalb ist das Anlegen jetzt selbst eine Pruefung, und die
|
|
Raumnummer wird nachgehalten. */
|
|
const raum = await ruf("POST", "/workspace/api/chat/direkt", keksDogi, { mit: idMax });
|
|
const raumId = raum.daten?.raum_id;
|
|
ok(raum.status === 200 && Number.isInteger(raumId),
|
|
`DogFather und Max haben ein Gespraech (HTTP ${raum.status}, Raum ${raumId})`);
|
|
const nachricht = await ruf("POST", `/workspace/api/chat/raeume/${raumId}/nachrichten`,
|
|
keksDogi, { text: "Streng geheim." });
|
|
ok(nachricht.status === 200 || nachricht.status === 201,
|
|
`und eine Nachricht darin (HTTP ${nachricht.status})`);
|
|
|
|
/* GEGENPROBE: Max SIEHT das Gespraech -- sonst pruefte die Zeile
|
|
darunter nur, dass die Liste immer leer ist. */
|
|
const beiMax = await ruf("GET", "/workspace/api/chat/raeume", keksMax);
|
|
ok((beiMax.daten?.raeume || []).length === 1,
|
|
`Max sieht es (${(beiMax.daten?.raeume || []).length})`);
|
|
|
|
const meine = await ruf("GET", "/workspace/api/chat/raeume", keksSpicy);
|
|
const raeume = meine.daten?.raeume || [];
|
|
ok(raeume.length === 0, `Spicy Media sieht 0 Gespraeche (${raeume.length})`);
|
|
const rein = await ruf("GET", `/workspace/api/chat/raeume/${raumId}/nachrichten`, keksSpicy);
|
|
ok(rein.status === 403 || rein.status === 404,
|
|
`und kommt auch mit der Raumnummer nicht hinein (HTTP ${rein.status})`);
|
|
}
|
|
{
|
|
/* ===================================================================
|
|
WAS DOGFATHER GEHOERT, SIEHT SPICY MEDIA NICHT (07.09.2026)
|
|
|
|
Filipe: "wieso sieht die spicy rolle meine daten die ich habe und
|
|
speichere, meins sollen die nicht sehen von der rolle dogfather."
|
|
|
|
Beim ersten Anlauf bekam Spicy Media dieselbe Regel wie DogFather
|
|
(`1=1`) -- damit stimmte der Ueberblick und nebenbei standen seine
|
|
eigenen Termine, Aufgaben, Dateien und Eintraege mit drin.
|
|
|
|
Geprueft wird mit ZWEI Datensaetzen je Art: einem von DogFather und
|
|
einem von Max. Mit nur einem waere jedes Ergebnis mehrdeutig -- eine
|
|
leere Liste koennte auch heissen, dass die Regel ALLES ausblendet.
|
|
Genau das ist der Unterschied zwischen "sieht das Richtige nicht"
|
|
und "sieht gar nichts". */
|
|
/* ORTSZEIT, nicht UTC. `toISOString().slice(0,10)` liegt zwischen
|
|
Mitternacht und zwei Uhr einen Tag daneben -- die Pruefung waere
|
|
dann nachts rot, ohne dass sich etwas geaendert haette. Gefunden
|
|
von pruef-struktur, das genau danach sucht. */
|
|
const jetztHier = new Date();
|
|
const zz = (n) => String(n).padStart(2, "0");
|
|
const heute = `${jetztHier.getFullYear()}-${zz(jetztHier.getMonth() + 1)}-${zz(jetztHier.getDate())}`;
|
|
await ruf("POST", "/workspace/api/bereich/technik", keksDogi,
|
|
{ art: "problem", titel: "DogFathers Notiz", datum: heute });
|
|
await ruf("POST", "/workspace/api/bereich/technik", keksMax,
|
|
{ art: "problem", titel: "Max seine Notiz", datum: heute, creator_id: idLuna });
|
|
|
|
const { daten } = await ruf("GET", "/workspace/api/bereich/technik", keksSpicy);
|
|
const titel = (daten?.eintraege || []).map((e) => e.titel);
|
|
ok(!titel.includes("DogFathers Notiz"),
|
|
`DogFathers Eintrag ist fuer Spicy Media unsichtbar (sie sieht: ${titel.join(", ") || "nichts"})`);
|
|
ok(titel.includes("Max seine Notiz"),
|
|
"Max seiner steht aber da -- die Regel blendet nicht einfach alles aus");
|
|
|
|
/* Dasselbe fuer Aufgaben und Termine -- drei Module, drei Wege, und
|
|
jeder haette einzeln vergessen werden koennen. */
|
|
await ruf("POST", "/workspace/api/aufgaben", keksDogi, { titel: "DogFathers Aufgabe" });
|
|
await ruf("POST", "/workspace/api/aufgaben", keksMax,
|
|
{ titel: "Max seine Aufgabe", creator_id: idLuna });
|
|
const auf = await ruf("GET", "/workspace/api/aufgaben", keksSpicy);
|
|
const aTitel = (auf.daten?.aufgaben || []).map((a) => a.titel);
|
|
ok(!aTitel.includes("DogFathers Aufgabe"),
|
|
`auch seine Aufgabe nicht (sie sieht: ${aTitel.join(", ") || "nichts"})`);
|
|
ok(aTitel.includes("Max seine Aufgabe"), "Max seine schon");
|
|
|
|
/* Der Kalender folgt seit dem 07.09.2026 einer ANDEREN Regel als
|
|
Bereiche und Aufgaben, und das ist Absicht (Begruendung steht bei
|
|
termineSichtbar in workspace.js): Dort entscheidet die Rolle, hier
|
|
entscheidet allein die BETEILIGUNG. Wer niemanden eintraegt, meint
|
|
alle -- also sieht Spicy Media DogFathers teilnehmerlosen Termin
|
|
sehr wohl, Max' Verabredung mit Luna dagegen nicht. Genau
|
|
umgekehrt zu den beiden Bloecken darueber. Die zwei Zeilen stehen
|
|
hier zusammen, weil erst beide zeigen, dass wirklich die
|
|
Beteiligung zaehlt und nicht die Rolle -- eine allein waere auch
|
|
erfuellt, wenn schlicht alles oder gar nichts ankaeme. */
|
|
await ruf("POST", "/workspace/api/termine", keksDogi,
|
|
{ titel: "DogFathers Team-Termin", art: "termin", beginn: heute + "T20:00", dauer_min: 30 });
|
|
await ruf("POST", "/workspace/api/termine", keksMax,
|
|
{ titel: "Max sein Termin", art: "termin", beginn: heute + "T21:00", dauer_min: 30,
|
|
creator_id: idLuna });
|
|
const term = await ruf("GET", `/workspace/api/termine?von=${heute}&bis=${heute}`, keksSpicy);
|
|
const tTitel = (term.daten?.termine || []).map((t) => t.titel);
|
|
ok(tTitel.includes("DogFathers Team-Termin"),
|
|
`der Termin ohne Teilnehmer geht alle an -- auch sie (${tTitel.join(", ") || "nichts"})`);
|
|
ok(!tTitel.includes("Max sein Termin"),
|
|
"Max' Verabredung mit Luna dagegen nicht -- die geht nur die beiden an");
|
|
|
|
/* GEGENPROBE: DogFather selbst sieht weiterhin BEIDE. Ohne sie
|
|
bewiesen die Zeilen oben nur, dass irgendetwas gefiltert wird. */
|
|
const beiDogi = await ruf("GET", "/workspace/api/bereich/technik", keksDogi);
|
|
const dTitel = (beiDogi.daten?.eintraege || []).map((e) => e.titel);
|
|
ok(dTitel.includes("DogFathers Notiz") && dTitel.includes("Max seine Notiz"),
|
|
`DogFather sieht weiterhin beide (${dTitel.length})`);
|
|
}
|
|
{
|
|
/* Und DogFather bleibt der Einzige mit dem letzten Wort. */
|
|
const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi);
|
|
ok(status === 200, `DogFather kommt weiterhin in die Verwaltung (HTTP ${status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. Die Anmeldeseite kennt die Rolle
|
|
======================================================================= */
|
|
console.log("\n=== Die Anmeldeseite ===");
|
|
{
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 950 } });
|
|
const seite = await ctx.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
const rollen = await seite.$$eval(".rolle", (k) => k.map((x) => x.dataset.rolle));
|
|
ok(rollen[0] === "spicy", `sie steht ganz oben (${rollen.join(", ")})`);
|
|
await seite.click('.rolle[data-rolle="spicy"]');
|
|
await seite.fill("#code", "CODE-SPICY-001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
|
await seite.waitForTimeout(1500);
|
|
const kacheln = await seite.$$eval(".kachel__name", (k) => k.map((x) => x.textContent.trim()));
|
|
ok(kacheln.includes("Personen & Zugänge"),
|
|
`sie sieht die Personen-Kachel (${kacheln.length} Kacheln)`);
|
|
ok(!kacheln.includes("Automationen"), "und die Automationen-Kachel NICHT");
|
|
await ctx.close();
|
|
await browser.close();
|
|
}
|
|
|
|
await stoppen();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
|
|
|
|
console.log(`\n${gemacht} Pruefungen`);
|
|
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
|
process.exit(fehler ? 1 : 0);
|