Nachtrag zu cebdd88e. Ein Bildschirmfoto der fertigen Seite hat drei
Dinge gezeigt, die keine Pruefung sehen konnte.
1. DIE KACHEL WAR IMMER NOCH ORANGE.
Filipe: "die hauptfarbe der kachel soll auch babyblau sein mit bissl
lila." Die Seite trug zwar `--akzent: #a8d8ff`, aber der Kopf faerbt
sich ueber `--ton` -- und der stand am body auf Ton 44, dem Orange
vom 24.09. Die Variable war gesetzt und wirkungslos.
Ton 44 ist jetzt #a8d8ff. WEIL DIE FARBE DIESMAL AUS EINEM WUNSCH
kam und nicht aus einer Rechnung, wurde sie nachgemessen:
Kontrast gegen den dunklen Grund 12,46:1 (Hausgrenze 4,5)
Abstand zur naechsten Kachelfarbe 0,1125 (Ton 15)
Engstes Paar im Haus ohnehin 0,0154
-> 7,3-fach weiter weg als das schwaechste Glied
Babyblau haelt die Buntheitsgrenze (0,12) NICHT und kann sie nicht
halten -- das liegt am Wort. Nachgemessen mit einer Suche ueber den
ganzen Blaubereich: Es gibt KEINE Farbe, die gleichzeitig babyblau
aussieht, Buntheit >= 0,12 und Abstand >= 0,09 schafft; der beste
Kandidat kommt auf 0,075 Abstand. Der Blaubereich ist von sechs
Toenen besetzt.
DIE GRENZE WURDE DESHALB NICHT GESENKT, sondern BENANNT ausgenommen
(`blassErlaubt` in pruef-kachelfarben). Eine gesenkte Grenze gaelte
fuer alle 45 Toene; eine benannte gilt fuer eine und steht mit
ihrem Grund da. Und sie kostet etwas: Wer blass sein darf, muss
beim Abstand >= 0,10 halten -- gemessen, nicht versprochen, mit
Gegenprobe.
NEBENBEI ZWEI ALTE ROTE BEHOBEN: Die Prüfung war seit dem 24.09.
rot (engstes Paar 0,0862 zwischen Ton 41 und 45, und zwei Farben in
der Regenbogenkachel stimmten nicht mehr). Ton 45 neu gerechnet
(0,0914) und die Kachel nachgezogen. 22 -> 26 Pruefungen, 0 Fehler.
2. DIE LEITUNG SAH DIE KNOEPFE DES MELDERS.
Der Server lehnte sie richtig mit 404 ab -- die Karte bot sie ihr
trotzdem an. `darf_bestaetigen` war eine Aussage ueber die MELDUNG
statt ueber den BETRACHTER. Zwei Knoepfe, die nur eine Fehlermeldung
koennen, sind schlimmer als gar keine.
Jetzt fragen Route UND Anzeige dieselbe Funktion `darfBestaetigen`.
Es ist ausdruecklich "ist der Melder" und nicht "ist nicht Leitung":
Die Leitung darf ihre EIGENE Meldung bestaetigen, nur keine fremde.
Und die Leitung sieht bei "wartet" jetzt, wer dran ist -- "Liegt bei
Miss" mit ruhig atmendem Punkt, dazu "Noch etwas nachschicken ..."
statt "Behoben - nachfragen ...". Sie hat ja schon nachgefragt.
3. NACHLEGEN HAETTE DEN VERLAUF VERDOPPELT.
Antwortet die Leitung ein zweites Mal, waehrend die Meldung beim
Melder liegt, entstand eine ZWEITE Zeile "Runde 1" -- und sein
spaeteres Urteil haette beide gleichzeitig beschriftet. Jetzt
ersetzt ON CONFLICT die Antwort in derselben Runde.
DER EIGENTLICHE FUND STECKTE IM INDEX: `CREATE UNIQUE INDEX IF NOT
EXISTS` unter dem alten Namen tut auf dem Server NICHTS -- dort gibt
es den Namen schon, als gewoehnlichen Index, und IF NOT EXISTS
sieht nur den Namen, nicht die Bauart. Der Index waere nie eindeutig
geworden, ON CONFLICT haette kein Ziel gefunden, und das Nachlegen
waere abgebrochen -- genau dort, wo lokal alles gruen ist, weil jede
Pruefung ihre Datenbank frisch anlegt. Gefunden beim Durchspielen
auf einer KOPIE der echten Datenbank. Der alte Index wird jetzt
ausdruecklich weggenommen, der neue heisst anders.
GEMESSEN:
- pruef-support 59 -> 63 Pruefungen, 0 Fehler (darunter: die Leitung
bekommt die Knoepfe NICHT, und Nachlegen laesst EINE Runde stehen).
- Die Index-Umstellung auf einer Kopie der echten Datenbank
durchgespielt: alter Index weg, neuer eindeutig, ON CONFLICT trifft.
- pruef-kachelfarben 26/0, pruef-css-klassen, pruef-deutsche-texte: gruen.
- server/mess-support-runde.mjs (neu) macht vier Bilder: Leitung,
Melder, Melder auf dem Handy, und den Verlauf nach zwei Runden.
Es misst den "wer ist dran"-Hinweis ausdruecklich mit -- der war
einmal stumm ausgefallen (before() auf einem Element ohne
Elternknoten tut nichts, ohne Fehlermeldung).
Co-Authored-By: Claude Opus 5 <[email protected]>
583 lines
26 KiB
JavaScript
583 lines
26 KiB
JavaScript
/* =====================================================================
|
||
DER SUPPORT (24.09.2026)
|
||
|
||
Filipe: „da fehlt auch eine support kachel. die jeder sieht. jeder
|
||
benutzen kann. jeder kann da probleme von der seite melden und
|
||
hinweisen. fotos mit schicken mit einem text. … also bei dogfather
|
||
und der rechten hand soll die kachel anders gebaut sein weil die
|
||
sind die die sich um die probleme kümmern."
|
||
|
||
GEPRUEFT WIRD, WAS ER BESTELLT HAT — und drei Dinge, die er nicht
|
||
bestellt hat, weil sie sonst niemandem auffallen:
|
||
|
||
* JEDE Rolle sieht die Kachel und kommt auf die Seite. Nicht vier
|
||
von acht -- genau daran ist heute der Benachrichtigungs-Knopf
|
||
gescheitert.
|
||
* Eine fremde Meldung bleibt fremd. Wer meldet, sieht seine
|
||
eigene; alles andere gehoert der Leitung.
|
||
* Eine als Bild getarnte HTML-Datei kommt nicht herein. Das ist
|
||
der Standardfehler bei selbstgebauten Uploads und von aussen
|
||
nicht zu sehen.
|
||
===================================================================== */
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-sup-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
const { eigenerPort } = await import("./helfer-port.mjs");
|
||
const PORT = await eigenerPort(import.meta, "die Supportpruefung");
|
||
|
||
process.env.PORT = `${PORT}`;
|
||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-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");
|
||
notbremse(240_000, "pruef-support");
|
||
await new Promise((r) => setTimeout(r, 900));
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
|
||
let fehler = 0, geprueft = 0;
|
||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
const melde = (t) => console.log(t);
|
||
|
||
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 kennSchluessel = randomBytes(32).toString("hex");
|
||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||
.run("code_kennung_schluessel", kennSchluessel, 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", kennSchluessel).update(code).digest("hex"), jetzt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
|
||
/* ALLE ACHT ROLLEN. „Die jeder sieht" heisst jede -- und die Liste
|
||
steht hier vollstaendig, damit eine fehlende auffaellt. */
|
||
const ROLLEN = [
|
||
["admin", "Filipe"], ["hand", "VanVan"], ["linke", "Funny"],
|
||
["modi", "Miss"], ["spicy", "Spicy Media"], ["manager", "Patrick"],
|
||
["scout", "Kevin"], ["creator", "Luna"], ["gast", "Community-Mitglied"],
|
||
];
|
||
const CODES = {};
|
||
for (const [i, [rolle, name]] of ROLLEN.entries()) {
|
||
CODES[rolle] = `CODE-S${String(i).padStart(2, "0")}-000001`;
|
||
anlegen(name, rolle, CODES[rolle]);
|
||
}
|
||
d.close();
|
||
|
||
/* ---------- Anmelden ohne Browser ------------------------------------
|
||
Die Sitzung kommt aus einem echten Anmelde-Aufruf; danach reist sie
|
||
als Keks mit. Ein Browser waere hier Aufwand ohne Gewinn -- geprueft
|
||
werden Wege, nicht Aussehen. */
|
||
/* DIE COMMUNITY KOMMT NUR AUF EINER ADRESSE HEREIN (gemessen, nicht
|
||
geraten): `crew.dogfather-universe.com`. Auf 127.0.0.1 antwortet
|
||
die Anmeldung mit 401 -- beim ersten Lauf stand deshalb
|
||
"gast: kommt nicht herein", und das sah aus wie ein schwerer
|
||
Befund am Support. Es war einer an meiner Messung.
|
||
|
||
Versucht wird zuerst ohne Host, dann mit -- so faellt auch auf,
|
||
wenn eine Rolle PLOETZLICH einen braucht. */
|
||
const CREW_HOST = "crew.dogfather-universe.com";
|
||
|
||
/* ZWEI DINGE, DIE ICH ERST NACH DREI FEHLLAEUFEN GEMESSEN HABE --
|
||
beide stehen in pruef-treff.mjs, das sie seit dem 15.09. richtig
|
||
macht:
|
||
|
||
1. `fetch` VERWIRFT DEN Host-KOPF. Er steht auf der Liste der
|
||
verbotenen Kopfzeilen; Node setzt stattdessen die Adresse des
|
||
Ziels. Wer eine Hausadresse braucht, muss node:http nehmen.
|
||
2. DIE COMMUNITY BESTAETIGT IHR ALTER (`alter_ok`). Ohne das Feld
|
||
antwortet die Anmeldung mit 401 -- und das sah aus wie ein
|
||
Zugangsproblem am Support.
|
||
|
||
Beim ersten Lauf stand deshalb dreimal rot, und kein einziges Mal
|
||
lag es am Support. */
|
||
const { request } = await import("node:http");
|
||
function roh(pfad, host, koerper) {
|
||
return new Promise((fertig, schief) => {
|
||
const daten = koerper ? JSON.stringify(koerper) : null;
|
||
const a = request({
|
||
host: "127.0.0.1", port: PORT, path: pfad,
|
||
method: daten ? "POST" : "GET",
|
||
headers: {
|
||
...(host ? { Host: host } : {}),
|
||
Origin: host ? `https://${host}` : BASIS,
|
||
...(daten ? { "Content-Type": "application/json",
|
||
"Content-Length": Buffer.byteLength(daten) } : {}),
|
||
},
|
||
}, (antwort) => {
|
||
let text = "";
|
||
antwort.on("data", (t) => { text += t; });
|
||
antwort.on("end", () => fertig({
|
||
code: antwort.statusCode, kopf: antwort.headers, text,
|
||
}));
|
||
});
|
||
a.on("error", schief);
|
||
if (daten) a.write(daten);
|
||
a.end();
|
||
});
|
||
}
|
||
|
||
async function anmeldeVersuch(rolle, host) {
|
||
const r = await roh("/workspace/api/anmelden", host,
|
||
rolle === "gast"
|
||
? { rolle, code: CODES[rolle], alter_ok: true }
|
||
: { rolle, code: CODES[rolle] });
|
||
if (r.code !== 200) return null;
|
||
const keks = [].concat(r.kopf["set-cookie"] || [])
|
||
.map((z) => z.split(";")[0]).join("; ");
|
||
return keks || null;
|
||
}
|
||
const HOST_FUER = {};
|
||
async function anmelden(rolle) {
|
||
const ohne = await anmeldeVersuch(rolle, null);
|
||
if (ohne) { HOST_FUER[rolle] = null; return ohne; }
|
||
const mit = await anmeldeVersuch(rolle, CREW_HOST);
|
||
if (mit) { HOST_FUER[rolle] = CREW_HOST; return mit; }
|
||
return null;
|
||
}
|
||
|
||
/** Ein winziges, gueltiges PNG (1x1, schwarz) -- echte Kopfbytes,
|
||
* damit die Erkennung wirklich etwas zu erkennen hat. */
|
||
const PNG = Buffer.from(
|
||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk"
|
||
+ "YPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64");
|
||
|
||
const kekse = {};
|
||
melde("\n=== Alle neun Rollen kommen herein ===");
|
||
for (const [rolle] of ROLLEN) {
|
||
kekse[rolle] = await anmelden(rolle);
|
||
ok(!!kekse[rolle], `${rolle}: angemeldet`);
|
||
}
|
||
|
||
/* Fuer Rollen OHNE Hausadresse genuegt fetch -- es ist kuerzer und
|
||
kann Buffer als Rumpf. Wer eine Adresse braucht, geht ueber
|
||
node:http (siehe oben, `fetch` verwirft den Host-Kopf). */
|
||
async function hol(rolle, pfad, einst = {}) {
|
||
if (HOST_FUER[rolle]) {
|
||
const r = await rohMitKeks(rolle, pfad, einst);
|
||
return r;
|
||
}
|
||
return fetch(BASIS + pfad, {
|
||
...einst,
|
||
headers: { Cookie: kekse[rolle] || "", Origin: BASIS, ...(einst.headers || {}) },
|
||
});
|
||
}
|
||
|
||
/** Derselbe Aufruf ueber node:http -- mit Hausadresse und Sitzung.
|
||
* Gibt etwas zurueck, das sich wie eine fetch-Antwort benutzen
|
||
* laesst: `status`, `ok`, `json()`, `headers.get()`. */
|
||
function rohMitKeks(rolle, pfad, einst = {}) {
|
||
return new Promise((fertig, schief) => {
|
||
const rumpf = einst.body === undefined ? null
|
||
: (Buffer.isBuffer(einst.body) ? einst.body : Buffer.from(String(einst.body)));
|
||
const a = request({
|
||
host: "127.0.0.1", port: PORT, path: pfad,
|
||
method: einst.method || "GET",
|
||
headers: {
|
||
Host: HOST_FUER[rolle],
|
||
Origin: `https://${HOST_FUER[rolle]}`,
|
||
Cookie: kekse[rolle] || "",
|
||
...(rumpf ? { "Content-Length": rumpf.length } : {}),
|
||
...(einst.headers || {}),
|
||
},
|
||
}, (antwort) => {
|
||
const teile = [];
|
||
antwort.on("data", (t) => teile.push(t));
|
||
antwort.on("end", () => {
|
||
const text = Buffer.concat(teile).toString("utf8");
|
||
fertig({
|
||
status: antwort.statusCode,
|
||
ok: antwort.statusCode >= 200 && antwort.statusCode < 300,
|
||
headers: { get: (n) => antwort.headers[String(n).toLowerCase()] || null },
|
||
json: async () => JSON.parse(text),
|
||
text: async () => text,
|
||
});
|
||
});
|
||
});
|
||
a.on("error", schief);
|
||
if (rumpf) a.write(rumpf);
|
||
a.end();
|
||
});
|
||
}
|
||
|
||
/* =====================================================================
|
||
1. DIE KACHEL SIEHT JEDER
|
||
===================================================================== */
|
||
melde("\n=== Die Kachel steht bei jeder Rolle ===");
|
||
{
|
||
const ohne = [];
|
||
for (const [rolle] of ROLLEN) {
|
||
/* DIE ROUTE HEISST /api/ich, nicht /api/start. Beim ersten Lauf
|
||
stand hier ein erfundener Name, und alle neun Rollen meldeten
|
||
"Kachel fehlt (HTTP 404)" -- ein Messfehler, der wie ein
|
||
Totalausfall aussah. */
|
||
const a = await hol(rolle, "/workspace/api/ich");
|
||
if (!a.ok) { ohne.push(`${rolle} (HTTP ${a.status})`); continue; }
|
||
const r = await a.json();
|
||
/* `bereiche` ist null, wenn die Oberflaeche ihre eigene Liste
|
||
nimmt -- dann steht die Kachel in assets/js/bereiche.js und
|
||
wird eine Pruefung weiter unten geprueft. */
|
||
if (r.bereiche === null) { ohne.push(`${rolle} (eigene Liste im Browser)`); continue; }
|
||
const da = (r.bereiche || []).some((k) => k?.ziel === "support.html");
|
||
if (!da) ohne.push(rolle);
|
||
}
|
||
/* DIE AGENTUR-ROLLEN bekommen `bereiche: null` und ihre Liste aus
|
||
dem Browser. Das ist kein Mangel, aber es muss genannt werden --
|
||
sonst sieht "alle haben sie" gruen aus, obwohl vier gar nicht
|
||
geprueft wurden. */
|
||
const ausBrowser = ohne.filter((x) => x.includes("eigene Liste"));
|
||
const echtOhne = ohne.filter((x) => !x.includes("eigene Liste"));
|
||
ok(echtOhne.length === 0,
|
||
"jede Rolle mit Server-Liste hat die Support-Kachel"
|
||
+ (echtOhne.length ? ` -- FEHLT bei: ${echtOhne.join(", ")}` : ""));
|
||
melde(` (${ausBrowser.length} Rollen nehmen ihre Liste aus dem Browser)`);
|
||
|
||
/* UND DIE BROWSER-LISTE AUCH. Ohne diese Zeile waere die Haelfte
|
||
der Rollen ungeprueft -- und genau dort hat heute schon einmal
|
||
etwas gefehlt. */
|
||
const { readFileSync } = await import("node:fs");
|
||
const { fileURLToPath } = await import("node:url");
|
||
const bereicheJs = readFileSync(fileURLToPath(
|
||
new URL("../workspace/assets/js/bereiche.js", import.meta.url)), "utf8");
|
||
ok(/ziel:\s*'support\.html'/.test(bereicheJs),
|
||
"auch die Kachelliste im Browser kennt den Support");
|
||
}
|
||
|
||
/* =====================================================================
|
||
2. AUF DIE SEITE DARF JEDER
|
||
===================================================================== */
|
||
melde("\n=== Die Seite ist fuer jede Rolle offen ===");
|
||
{
|
||
const zu = [];
|
||
for (const [rolle] of ROLLEN) {
|
||
const a = await hol(rolle, "/workspace/support.html", { redirect: "manual" });
|
||
if (a.status !== 200) zu.push(`${rolle}=${a.status}`);
|
||
}
|
||
ok(zu.length === 0,
|
||
"support.html antwortet jeder Rolle mit 200"
|
||
+ (zu.length ? ` -- nicht bei: ${zu.join(", ")}` : ""));
|
||
}
|
||
|
||
/* =====================================================================
|
||
3. MELDEN — mit Text, mit Bild, und was nicht geht
|
||
===================================================================== */
|
||
melde("\n=== Melden ===");
|
||
let meldungMitBild = 0;
|
||
{
|
||
/* Ohne Text geht nichts -- sonst stehen leere Zeilen im Eingang. */
|
||
const leer = await hol("creator", "/workspace/api/support", {
|
||
method: "POST", headers: { "x-text": encodeURIComponent("hm") },
|
||
});
|
||
ok(leer.status === 400, `ein Wort reicht nicht (${leer.status})`);
|
||
|
||
const nurText = await hol("creator", "/workspace/api/support", {
|
||
method: "POST",
|
||
headers: { "x-text": encodeURIComponent("Der Haken auf der Aufgabenseite geht nicht.") },
|
||
});
|
||
ok(nurText.status === 201, `Text allein genuegt (${nurText.status})`);
|
||
|
||
const mitBild = await hol("modi", "/workspace/api/support", {
|
||
method: "POST",
|
||
headers: {
|
||
"x-text": encodeURIComponent("Hier stimmt die Farbe nicht, siehe Bild."),
|
||
"x-seite": "/workspace/chat.html",
|
||
"Content-Type": "application/octet-stream",
|
||
},
|
||
body: PNG,
|
||
});
|
||
ok(mitBild.status === 201, `mit Bild geht auch (${mitBild.status})`);
|
||
if (mitBild.status === 201) {
|
||
const j = await mitBild.json();
|
||
meldungMitBild = j.meldung?.id || 0;
|
||
ok(!!j.meldung?.bild, "und die Antwort nennt die Adresse des Bildes");
|
||
} else { ok(false, "kein Bild in der Antwort"); }
|
||
|
||
/* DER STANDARDFEHLER BEI UPLOADS: eine HTML-Datei mit Bildnamen.
|
||
Sie liefe im Browser als Seite DIESER Domain, mit Zugriff auf die
|
||
Sitzung des Empfaengers. */
|
||
const getarnt = await hol("scout", "/workspace/api/support", {
|
||
method: "POST",
|
||
headers: {
|
||
"x-text": encodeURIComponent("Test mit einer getarnten Datei."),
|
||
"Content-Type": "image/png",
|
||
},
|
||
body: Buffer.from("<html><script>alert(1)</script></html>"),
|
||
});
|
||
ok(getarnt.status === 415,
|
||
`als Bild getarntes HTML wird abgelehnt (${getarnt.status})`);
|
||
|
||
/* Und ein PDF ebenfalls -- dateiErkennen kennt es, der Support
|
||
will es nicht. */
|
||
const pdf = await hol("scout", "/workspace/api/support", {
|
||
method: "POST",
|
||
headers: {
|
||
"x-text": encodeURIComponent("Test mit einem PDF im Anhang."),
|
||
"Content-Type": "application/pdf",
|
||
},
|
||
body: Buffer.concat([Buffer.from("%PDF-1.4\n"), Buffer.alloc(64, 0x20)]),
|
||
});
|
||
ok(pdf.status === 415, `ein PDF ist kein Foto (${pdf.status})`);
|
||
}
|
||
|
||
/* =====================================================================
|
||
4. WER SIEHT WAS
|
||
===================================================================== */
|
||
melde("\n=== Wer sieht was ===");
|
||
{
|
||
const alsCreator = await (await hol("creator", "/workspace/api/support")).json();
|
||
ok(alsCreator.leitung === false, "ein Creator ist nicht die Leitung");
|
||
ok(alsCreator.meldungen.length === 1,
|
||
`und sieht genau seine eigene (${alsCreator.meldungen.length})`);
|
||
|
||
const alsModi = await (await hol("modi", "/workspace/api/support")).json();
|
||
ok(alsModi.meldungen.length === 1 && alsModi.meldungen[0].id === meldungMitBild,
|
||
"ein Modi sieht seine eigene und nicht die des Creators");
|
||
|
||
for (const rolle of ["admin", "hand"]) {
|
||
const r = await (await hol(rolle, "/workspace/api/support")).json();
|
||
ok(r.leitung === true && r.meldungen.length >= 2,
|
||
`${rolle} sieht alle (${r.meldungen.length})`);
|
||
}
|
||
for (const rolle of ["linke", "modi", "spicy", "manager", "scout", "creator", "gast"]) {
|
||
const r = await (await hol(rolle, "/workspace/api/support")).json();
|
||
ok(r.leitung !== true, `${rolle} ist NICHT die Leitung`);
|
||
}
|
||
|
||
/* DAS BILD EINER FREMDEN MELDUNG bleibt fremd. */
|
||
const fremd = await hol("creator", `/workspace/api/support/${meldungMitBild}/bild`);
|
||
ok(fremd.status === 404,
|
||
`das Bild einer fremden Meldung gibt es fuer andere nicht (${fremd.status})`);
|
||
|
||
const eigen = await hol("modi", `/workspace/api/support/${meldungMitBild}/bild`);
|
||
ok(eigen.status === 200, `der Melder sieht sein eigenes Bild (${eigen.status})`);
|
||
ok(eigen.headers.get("content-type") === "image/png",
|
||
`ausgeliefert wird der ERKANNTE Typ (${eigen.headers.get("content-type")})`);
|
||
ok((eigen.headers.get("x-content-type-options") || "") === "nosniff",
|
||
"mit nosniff");
|
||
ok(/sandbox/.test(eigen.headers.get("content-security-policy") || ""),
|
||
"und einer Richtlinie, die jede Ausfuehrung nimmt");
|
||
|
||
const leitungSieht = await hol("hand", `/workspace/api/support/${meldungMitBild}/bild`);
|
||
ok(leitungSieht.status === 200, "die Leitung sieht es auch");
|
||
}
|
||
|
||
/* =====================================================================
|
||
5. SICH KUEMMERN
|
||
===================================================================== */
|
||
melde("\n=== Sich kuemmern ===");
|
||
{
|
||
/* Wer nicht Leitung ist, kann nichts aendern -- 404 und nicht 403:
|
||
Wer keine Zustaendigkeit hat, soll nicht erfahren, dass es sie
|
||
gibt. */
|
||
const versuch = await hol("creator", `/workspace/api/support/${meldungMitBild}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "erledigt", antwort: "einfach mal zu" }),
|
||
});
|
||
ok(versuch.status === 404, `ein Creator kann nichts erledigen (${versuch.status})`);
|
||
|
||
const dran = await hol("hand", `/workspace/api/support/${meldungMitBild}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "in_arbeit" }),
|
||
});
|
||
ok(dran.status === 200, `die rechte Hand uebernimmt (${dran.status})`);
|
||
|
||
/* ZUMACHEN OHNE SATZ GEHT NICHT. Der Melder sieht nur diesen Satz. */
|
||
const ohneSatz = await hol("hand", `/workspace/api/support/${meldungMitBild}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "erledigt" }),
|
||
});
|
||
ok(ohneSatz.status === 400, `erledigen ohne Satz wird abgelehnt (${ohneSatz.status})`);
|
||
|
||
const zu = await hol("hand", `/workspace/api/support/${meldungMitBild}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "erledigt", antwort: "Behoben, die Farbe stimmt wieder." }),
|
||
});
|
||
ok(zu.status === 200, `mit Satz geht es (${zu.status})`);
|
||
|
||
/* =====================================================================
|
||
DER MELDER HAT DAS LETZTE WORT (25.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: „ich will dass die leute die mir was geschickt haben im
|
||
support, auch meine notiz bekommen wenn ich fertig bin. damit die
|
||
bescheid wissen und dan anklicken koennen, es funktioniert, oder
|
||
noch nicht. und erst wenn es funktioniert gedrueckt wird, will ich
|
||
dass alles richtig fertig ist. perfektionier den ganzen weg und
|
||
mit dem gedanken das manchmal sachen mehrmal nicht sofort perfekt
|
||
sein werden."
|
||
|
||
HIER STAND, dass „erledigt" die Meldung zumacht und der Melder
|
||
danach Stand und Antwort sieht. Das war richtig -- und ist es seit
|
||
heute nicht mehr: „erledigt" schickt sie ZUERST an den Melder.
|
||
Erst sein Druck schliesst.
|
||
|
||
GEMESSEN WIRD DER GANZE WEG, einmal durch, mit einer Runde, die
|
||
schiefgeht -- genau der Fall, den Filipe ausdruecklich genannt
|
||
hat („manchmal ... nicht sofort perfekt"). Eine Pruefung, die nur
|
||
den glatten Weg kennt, misst die Haelfte.
|
||
===================================================================== */
|
||
const standVon = async (wer, id) => {
|
||
const d = await (await hol(wer, "/workspace/api/support")).json();
|
||
return (d.meldungen || []).find((m) => m.id === id) || null;
|
||
};
|
||
|
||
{
|
||
/* ---- Runde 1: die Leitung sagt „behoben" ---------------------- */
|
||
const nach1 = await standVon("modi", meldungMitBild);
|
||
ok(nach1?.stand === "wartet",
|
||
`„erledigt" schickt die Meldung zuerst an den Melder (${nach1?.stand})`);
|
||
ok(/Behoben/.test(nach1?.antwort || "") && nach1?.darf_bestaetigen === true,
|
||
`er sieht die Notiz und darf bestaetigen (${nach1?.darf_bestaetigen})`);
|
||
|
||
/* UND DIE LEITUNG SIEHT DIESE KNOEPFE NICHT. Im ersten Anlauf tat
|
||
sie es: Der Server lehnte sie richtig mit 404 ab, die Karte bot
|
||
ihr die Knöpfe trotzdem an -- `darf_bestaetigen` war eine
|
||
Aussage ueber die Meldung statt ueber den Betrachter. Gefunden
|
||
hat das kein Test, sondern ein Bildschirmfoto. Ab jetzt findet
|
||
es der Test. */
|
||
const beiDerLeitung = await standVon("hand", meldungMitBild);
|
||
ok(beiDerLeitung?.darf_bestaetigen === false,
|
||
`die Leitung bekommt diese Knoepfe NICHT (${beiDerLeitung?.darf_bestaetigen})`);
|
||
ok((nach1?.verlauf || []).length === 1 && nach1.verlauf[0].nr === 1,
|
||
`und der Verlauf hat Runde 1 (${(nach1?.verlauf || []).length})`);
|
||
|
||
/* GEGENPROBE: Die Leitung darf NICHT fuer ihn bestaetigen. Ohne
|
||
diese Zeile koennte sie ihre eigene Arbeit abnicken, und der
|
||
ganze Umweg waere Zierde. */
|
||
const fremd = await hol("hand", `/workspace/api/support/${meldungMitBild}/rueckmeldung`, {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ geht: true }),
|
||
});
|
||
ok(fremd.status === 404,
|
||
`Gegenprobe: die Leitung bestaetigt nicht fuer den Melder (${fremd.status})`);
|
||
|
||
/* ---- NACHLEGEN ERSETZT, ES VERDOPPELT NICHT ------------------
|
||
|
||
Der Leitung faellt hinterher noch etwas ein, waehrend die
|
||
Meldung schon beim Melder liegt. Ohne ON CONFLICT entstuende
|
||
eine ZWEITE Zeile „Runde 1", und sein spaeteres Urteil traefe
|
||
beide gleichzeitig -- der Verlauf zeigte dasselbe Urteil unter
|
||
zwei verschiedenen Antworten. */
|
||
const nachgelegt = await hol("hand", `/workspace/api/support/${meldungMitBild}`, {
|
||
method: "PATCH", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "erledigt", antwort: "Behoben – und der Rand stimmt auch wieder." }),
|
||
});
|
||
ok(nachgelegt.status === 200, `die Leitung legt noch etwas nach (${nachgelegt.status})`);
|
||
|
||
const nachNachtrag = await standVon("modi", meldungMitBild);
|
||
ok((nachNachtrag?.verlauf || []).length === 1,
|
||
`der Verlauf hat immer noch EINE Runde (${(nachNachtrag?.verlauf || []).length})`);
|
||
ok(/Rand stimmt/.test((nachNachtrag?.verlauf || [])[0]?.antwort || ""),
|
||
"und darin steht der neue Satz, nicht der alte");
|
||
ok(nachNachtrag?.stand === "wartet",
|
||
`die Meldung liegt weiter beim Melder (${nachNachtrag?.stand})`);
|
||
|
||
/* ---- „Noch nicht" -- und ohne Wort geht das nicht ------------- */
|
||
const ohneWort = await hol("modi", `/workspace/api/support/${meldungMitBild}/rueckmeldung`, {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ geht: false }),
|
||
});
|
||
ok(ohneWort.status === 400,
|
||
`„noch nicht" ohne Wort wird abgelehnt (${ohneWort.status})`);
|
||
|
||
const zurueck = await hol("modi", `/workspace/api/support/${meldungMitBild}/rueckmeldung`, {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ geht: false, text: "Die Farbe ja, das Datum nicht." }),
|
||
});
|
||
ok(zurueck.status === 200, `mit Wort geht es zurueck (${zurueck.status})`);
|
||
|
||
const nach2 = await standVon("hand", meldungMitBild);
|
||
ok(nach2?.stand === "in_arbeit" && nach2?.runde === 2,
|
||
`die Meldung ist wieder in Arbeit, Runde 2 (${nach2?.stand}, ${nach2?.runde})`);
|
||
ok((nach2?.verlauf || [])[0]?.urteil === "nicht"
|
||
&& /Datum/.test((nach2?.verlauf || [])[0]?.urteil_text || ""),
|
||
`und Runde 1 traegt sein Urteil (${(nach2?.verlauf || [])[0]?.urteil})`);
|
||
|
||
/* ---- Runde 2: diesmal klappt es ------------------------------- */
|
||
const zu2 = await hol("hand", `/workspace/api/support/${meldungMitBild}`, {
|
||
method: "PATCH", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "erledigt", antwort: "Das Datum stimmt jetzt auch." }),
|
||
});
|
||
ok(zu2.status === 200, `die Leitung antwortet ein zweites Mal (${zu2.status})`);
|
||
|
||
const bestaetigt = await hol("modi", `/workspace/api/support/${meldungMitBild}/rueckmeldung`, {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ geht: true }),
|
||
});
|
||
ok(bestaetigt.status === 200, `der Melder bestaetigt (${bestaetigt.status})`);
|
||
|
||
const ende = await standVon("modi", meldungMitBild);
|
||
ok(ende?.stand === "erledigt",
|
||
`ERST JETZT ist die Meldung erledigt (${ende?.stand})`);
|
||
ok((ende?.verlauf || []).length === 2,
|
||
`und der ganze Weg steht da -- beide Runden (${(ende?.verlauf || []).length})`);
|
||
ok((ende?.verlauf || [])[1]?.urteil === "geht",
|
||
` mit „geht" an der letzten (${(ende?.verlauf || [])[1]?.urteil})`);
|
||
|
||
/* ERLEDIGT IST ERLEDIGT -- auch fuer die Leitung. */
|
||
const nochmal = await hol("admin", `/workspace/api/support/${meldungMitBild}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "in_arbeit", antwort: "doch nicht" }),
|
||
});
|
||
ok(nochmal.status === 409,
|
||
`eine erledigte Meldung wird nicht wieder geoeffnet (${nochmal.status})`);
|
||
|
||
/* UND AUCH DER MELDER KANN NICHT NOCHMAL. Ohne diese Zeile liesse
|
||
sich eine geschlossene Sache durch ein zweites „noch nicht"
|
||
wieder aufmachen -- „zugemacht" waere dann eine Meinung. */
|
||
const nochmal2 = await hol("modi", `/workspace/api/support/${meldungMitBild}/rueckmeldung`, {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ geht: false, text: "doch nicht" }),
|
||
});
|
||
ok(nochmal2.status === 409,
|
||
` und der Melder kann sie auch nicht wieder aufmachen (${nochmal2.status})`);
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
6. GEGENPROBEN — kann diese Pruefung ueberhaupt rot werden?
|
||
===================================================================== */
|
||
melde("\n=== Gegenproben (jede MUSS anschlagen) ===");
|
||
{
|
||
/* Eine Meldung, die es nicht gibt. */
|
||
const nichts = await hol("admin", "/workspace/api/support/999999");
|
||
ok(nichts.status === 404, "eine erfundene Nummer gibt es nicht");
|
||
|
||
/* Ohne Anmeldung kommt niemand an die Liste. */
|
||
const ohne = await fetch(BASIS + "/workspace/api/support");
|
||
ok(ohne.status === 401 || ohne.status === 302 || ohne.status === 404,
|
||
`ohne Anmeldung keine Liste (${ohne.status})`);
|
||
|
||
/* Und ohne Anmeldung auch nicht an ein Bild. */
|
||
const ohneBild = await fetch(BASIS + `/workspace/api/support/${meldungMitBild}/bild`);
|
||
ok(ohneBild.status !== 200, `ohne Anmeldung kein Bild (${ohneBild.status})`);
|
||
}
|
||
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
|
||
console.log("");
|
||
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
||
process.exit(fehler ? 1 : 0);
|