Files
dogfather-universe/server/pruef-rueckmeldung.mjs
T
DogFatherGitandClaude Opus 5 8a214ecd0e Keine Mischung mehr -- Team Dogi nur noch bei Team Dogi
Filipe: "ich will dass du im workspace alles von team dogi weg nimmst.
nur was ich im kalender eintrage soll ich im workspace sehen und im
team dogi. aber die kacheln von team dogi soll ich nur bei team dogi
sehen und die von workspace nur bei workspace bitte. ich will keine
mischung mehr von kacheln. wie gesagt nur die der kalender soll
verbunden sein von dogfather sonst nichts."

GEMESSEN, WIE SCHLIMM ES WAR: Auf der Agenturadresse bekam DogFather
0 eigene Kacheln ueber die Schnittstelle und 17 fremde -- Dein Team,
Ideen-Board, Rueckmeldung, Wer sieht was, Entwicklung, Werdegang,
Talente und den ganzen Treff samt Moderation. Zwei Betriebe auf einer
Seite.

Das Bittere daran: Dieselbe Datei hatte den Fehler fuer die ANDERE
Richtung am 10.09. schon erkannt und behoben ("Fuenfundzwanzig
Kacheln, von denen zwei Drittel Creator, Scouts und Agentur betreffen,
waeren dort Fenster in ein Haus, in dem er gerade nicht ist"). Nur
andersherum stand es weiter so.

DIE AUSNAHME BRAUCHTE NICHTS: Nachgemessen filtern die Terminabfragen
in workspace-kalender.js NICHT nach Haus. Was er eintraegt, steht
ohnehin auf beiden Adressen. Die Verbindung, die er will, existierte
schon -- sie musste nur nicht zerschnitten werden.

STATT SIEBZEHN BRETTERN STEHT DORT EINE TUER. Ohne sie waere von der
Agenturadresse aus kein Weg mehr zu Team Dogi sichtbar; er muesste die
Adresse tippen. Das ist die Sorte Sackgasse, die dieses Haus nicht
baut. Eine Tuer ist keine Mischung: Sie zeigt kein Brett, sie geht
hinueber. Und sie kommt vom Server, nur fuer `admin` -- in keiner
ausgelieferten Datei steht sie.

MEIN ERSTER ENTWURF GAB SIE AUCH DER RECHTEN HAND UND DER MODERATION.
pruef-modi-checkliste hat es sofort gemeldet: "Modi bekommt 1
Zusatzkachel(n), erwartet keine". Wer NUR zu Team Dogi gehoert, hat
auf der Agenturadresse nichts zu suchen -- die Tuer waere dort seine
einzige Kachel gewesen, also eine Seite, die aus nichts als einem
Ausgang besteht.

VIER PRUEFUNGEN VERLANGTEN DANACH DIE ALTE WELT. Keine davon war ein
Mangel; alle vier haben am falschen Ort gesucht:

  pruef-haus-trennung  verlangte woertlich, dass die Team-Kachel AUCH
     auf der Agenturadresse steht. Sie prueft jetzt die Trennung in
     beide Richtungen -- kein Brett von Team Dogi drueben, aber die
     eine Tuer -- und leitet die Namen aus der Antwort von crew. ab,
     statt sie abzuschreiben.
  pruef-start-ansicht  zaehlte acht Gruppennamen in fester
     Reihenfolge. Gemeint war eine Ordnung ("die Community steht ganz
     unten, denn dort stehen Namen und Saetze von Zuschauern"), und
     die wird jetzt als Regel geprueft -- bedingt, mit drittem Ausgang
     auf Adressen, wo es diese Gruppen gar nicht gibt.
  pruef-treff und pruef-rueckmeldung fragten ueber die Agenturadresse
     nach Brettern, die dort nicht mehr stehen. Sie fragen jetzt dort,
     wo diese Bretter zuhause sind.

EINE HALBE STUNDE WAERE DABEI FAST VERLOREN GEGANGEN: `fetch` kann den
Host-Kopf nicht setzen -- er ist ein verbotener Kopfzeilenname und
wird STILLSCHWEIGEND verworfen. Die Anmeldung ueber die Crew-Wand
genuegt nicht; jede einzelne Abfrage braucht die Adresse. Deshalb geht
die Messung jetzt ueber http.request, so wie es `anmelden()` in
derselben Datei laengst tut.

pruef-haus-trennung 74/0 · pruef-modi-checkliste 75/0 · pruef-treff
72/0 · pruef-rueckmeldung 30/0 · pruef-start-ansicht 153/0 ·
pruef-haus-seiten 34/0 · pruef-entwicklung 46/0 · pruef-befinden
113/0 · pruef-rechte-umstellen 46/0

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 14:00:14 +02:00

353 lines
16 KiB
JavaScript

/* RÜCKMELDUNG IN BEIDE RICHTUNGEN (10.09.2026)
Kapitel 5 und 6 des Pflichtenhefts:
"Nicht nur ich soll meine Modis bewerten oder ihnen Feedback geben
können. Auch die Modis sollen mir Feedback geben können. Sie sollen
mir beispielsweise sagen können: Was könnte ich verbessern?"
"Es soll nicht nur dazu dienen, Leistungen zu kontrollieren. Es soll
vor allem dabei helfen, als Team besser zu werden."
---------------------------------------------------------------------
DIE PRÜFUNG, DERENTWEGEN ES DIESE DATEI GIBT
Wer beim Schreiben „nur an DogFather" wählt, verlässt sich darauf.
Diese Zusage hat genau eine Stelle, an der sie gilt --
`sichtbarEintrag()` --, und genau zwei Arten, still zu brechen:
Sie könnte NICHT WEIT GENUG gehen: Die Liste ist gefiltert, aber
über die Nummer der Zeile (Ändern, Löschen) kommt man trotzdem
heran. Deshalb wird hier auch von hinten geklopft.
Sie könnte ZU WEIT gehen: `nur_leitung` ist eine neue Spalte, und
jede Zeile, die es vorher gab, hat dort NULL. In SQL ist
`NULL = 0` nicht falsch, sondern UNBEKANNT -- ohne COALESCE wäre
der gesamte alte Bestand von einer Minute auf die andere
unsichtbar gewesen. Ein leeres Brett sieht nicht nach Fehler aus;
das hätte niemand gemeldet.
Beide Fälle stehen unten, jeder mit seiner Gegenprobe.
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-rueck-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "die Rueckmeldungspruefung");
process.env.PORT = `${PORT}`;
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
await import("./index.js");
await new Promise((r) => setTimeout(r, 1000));
const BASIS = `http://127.0.0.1:${PORT}`;
/* Netz und Notbremse -- index.js faengt sonst jeden Absturz ab, und der
Lauf sieht von aussen aus wie "laeuft noch". */
for (const art of ["uncaughtException", "unhandledRejection"]) {
process.on(art, (f) => { console.error(""); console.error("ABBRUCH:", f?.stack || f); process.exit(1); });
}
setTimeout(() => { console.error(""); console.error("ABBRUCH: 120 s vorbei."); process.exit(1); },
120000).unref?.();
let fehler = 0, geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Vanessa", "hand", "CODE-HAND-0001");
const idAyla = anlegen("Ayla", "modi", "CODE-TEAM-0001");
anlegen("Ben", "modi", "CODE-TEAM-0002");
anlegen("Cigdem", "manager", "CODE-MANA-0001");
/* Team-Rollen kommen nur ueber die Crew-Wand herein, Agenturrollen nur
ueber die Agenturwand -- die Regel vom 10.09.2026. `fetch` kann den
Host-Kopf nicht setzen (undici setzt ihn aus der Adresse), deshalb
node:http. */
const { request: httpAnfrage } = await import("node:http");
function anmelden(rolle, code, wand = "crew.dogfather-universe.com") {
return new Promise((fertig) => {
const rumpf = JSON.stringify({ rolle, code });
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST",
headers: { "Content-Type": "application/json",
"Content-Length": Buffer.byteLength(rumpf), Host: wand },
}, (antwort) => {
antwort.resume();
antwort.on("end", () => fertig({
ok: antwort.statusCode === 200,
keks: (antwort.headers["set-cookie"] || []).map((k) => k.split(";")[0]).join("; "),
}));
});
a.on("error", () => fertig({ ok: false, keks: "" }));
a.end(rumpf);
});
}
const rufe = (keks, weg, koerper, methode = "GET") =>
fetch(BASIS + weg, {
method: methode,
headers: { "Content-Type": "application/json", Cookie: keks },
body: koerper === undefined ? undefined : JSON.stringify(koerper),
});
const holen = async (keks, weg) => (await rufe(keks, weg)).json();
/* MIT EINER BESTIMMTEN ADRESSE FRAGEN -- UND WARUM NICHT MIT `fetch`
(21.09.2026).
Welches Haus jemand sieht, entscheidet der Host der EINZELNEN
Anfrage. Eine Anmeldung ueber die Crew-Wand genuegt nicht, wenn die
naechste Abfrage an 127.0.0.1 geht.
`fetch` kann das nicht: `Host` ist ein verbotener Kopfzeilenname und
wird STILLSCHWEIGEND verworfen -- kein Fehler, keine Warnung, die
Anfrage geht einfach an die falsche Adresse. Genau deshalb benutzt
auch `anmelden()` weiter unten schon `http.request` statt `fetch`.
Eine halbe Stunde Suche waere das gewesen, haette die Pruefung nicht
Kachel fuer Kachel gemeldet, was sie findet. */
const holenVon = (keks, weg, wand) => new Promise((fertig, schief) => {
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: weg, method: "GET",
headers: { Cookie: keks, Host: wand },
}, (antwort) => {
let text = "";
antwort.on("data", (s2) => { text += s2; });
antwort.on("end", () => {
try { fertig(JSON.parse(text || "{}")); } catch { fertig({}); }
});
});
a.on("error", schief);
a.end();
});
const dogi = await anmelden("admin", "CODE-DOGI-0001");
const hand = await anmelden("hand", "CODE-HAND-0001");
const ayla = await anmelden("modi", "CODE-TEAM-0001");
const ben = await anmelden("modi", "CODE-TEAM-0002");
const cigdem = await anmelden("manager", "CODE-MANA-0001", "workspace.dogfather-universe.com");
ok([dogi, hand, ayla, ben, cigdem].every((x) => x.ok && x.keks), "alle fuenf sind angemeldet");
const WEG = "/workspace/api/bereich/rueckmeldung";
const titel = async (keks) =>
((await holen(keks, WEG)).eintraege || []).map((e) => e.titel);
/* =======================================================================
1. Es gibt den Bereich, und er hat die Fragen aus dem Pflichtenheft
======================================================================= */
console.log("");
console.log("=== Der Bereich ===");
{
const a = await holen(dogi.keks, WEG);
ok(a.einstellung?.name === "Rückmeldung", `DogFather kann ihn oeffnen (${a.einstellung?.name})`);
const arten = Object.keys(a.einstellung?.arten || {});
ok(arten.length === 9, `neun Fragen (${arten.join(", ")})`);
ok(a.einstellung?.vertraulich === true, "und er kennt die vertrauliche Zeile");
const b = await holen(ayla.keks, WEG);
ok(b.einstellung?.name === "Rückmeldung", "das Team kann ihn auch oeffnen");
/* GEGENPROBE: Die Agentur hat hier nichts zu suchen -- 404 und nicht
403, sonst waere allein die Antwort schon eine Auskunft. */
const c = await rufe(cigdem.keks, WEG);
ok(c.status === 404, `ein Manager kommt nicht hinein (${c.status})`);
}
/* =======================================================================
2. Beide Richtungen
======================================================================= */
console.log("");
console.log("=== Wer schreibt wem ===");
let offenId = 0, geheimId = 0;
{
const a = await rufe(ayla.keks, WEG, {
art: "gut", titel: "Die Absprachen vor dem Live helfen sehr",
datum: "2026-09-10", dringlichkeit: "niedrig",
}, "POST");
offenId = (await a.json())?.id;
ok(a.status === 201 && offenId > 0, `ein Teammitglied schreibt fuers Team (${a.status})`);
const b = await rufe(ayla.keks, WEG, {
art: "dogfather", titel: "Du unterbrichst uns oft mitten im Satz",
datum: "2026-09-10", dringlichkeit: "mittel", nur_leitung: 1,
}, "POST");
geheimId = (await b.json())?.id;
ok(b.status === 201 && geheimId > 0, `und eine nur an DogFather (${b.status})`);
const c = await rufe(dogi.keks, WEG, {
art: "behalten", titel: "Die Schichtübergabe so lassen",
datum: "2026-09-10", dringlichkeit: "niedrig",
}, "POST");
ok(c.status === 201, `DogFather schreibt in dieselbe Sammlung (${c.status})`);
/* Die Spalte muss wirklich gesetzt sein -- nicht nur die Antwort
stimmen. Ohne diese Zeile waere die Pruefung gruen, wenn das Feld
stillschweigend verworfen wuerde. */
const zeile = d.prepare("SELECT nur_leitung FROM eintraege WHERE id = ?").get(geheimId);
ok(zeile?.nur_leitung === 1, `die Zeile steht als vertraulich in der Datenbank (${zeile?.nur_leitung})`);
const offen = d.prepare("SELECT nur_leitung FROM eintraege WHERE id = ?").get(offenId);
ok(offen?.nur_leitung === 0, `und die andere als offen (${offen?.nur_leitung})`);
}
/* =======================================================================
3. DIE ZUSAGE: „nur DogFather" heisst nur DogFather
======================================================================= */
console.log("");
console.log("=== Wer sieht was ===");
{
const beiDogi = await titel(dogi.keks);
ok(beiDogi.some((t) => t.startsWith("Du unterbrichst")),
`DogFather sieht die vertrauliche Zeile (${beiDogi.length} Eintraege)`);
const beiAyla = await titel(ayla.keks);
ok(beiAyla.some((t) => t.startsWith("Du unterbrichst")),
"die Schreiberin sieht ihre eigene");
/* DIE EIGENTLICHE PRUEFUNG. Die rechte Hand sieht sonst ueberall
dasselbe wie DogFather -- hier nicht. */
const beiHand = await titel(hand.keks);
ok(!beiHand.some((t) => t.startsWith("Du unterbrichst")),
`die rechte Hand sieht sie NICHT (${beiHand.join(" | ")})`);
const beiBen = await titel(ben.keks);
ok(!beiBen.some((t) => t.startsWith("Du unterbrichst")),
"ein anderes Teammitglied auch nicht");
/* GEGENPROBE: Sonst koennte "sieht sie nicht" auch heissen "sieht
ueberhaupt nichts". Die OFFENE Zeile muss bei beiden ankommen. */
ok(beiHand.some((t) => t.startsWith("Die Absprachen")),
"die offene Zeile sehen beide sehr wohl");
ok(beiBen.some((t) => t.startsWith("Die Absprachen")),
"auch das andere Teammitglied");
}
/* =======================================================================
4. Und von hinten? Die Nummer ist kein Schluessel
======================================================================= */
console.log("");
console.log("=== Ueber die Nummer ===");
{
const a = await rufe(hand.keks, `${WEG}/${geheimId}`, { titel: "umgeschrieben" }, "PATCH");
ok(a.status === 404, `die rechte Hand kann sie nicht aendern (${a.status})`);
const b = await rufe(hand.keks, `${WEG}/${geheimId}`, undefined, "DELETE");
ok(b.status === 404, `und nicht loeschen (${b.status})`);
const heil = d.prepare("SELECT titel FROM eintraege WHERE id = ?").get(geheimId);
ok(heil?.titel?.startsWith("Du unterbrichst"), "die Zeile steht unveraendert da");
/* GEGENPROBE: An der OFFENEN Zeile darf sie arbeiten -- sonst hiesse
die 404 oben nur "diese Route geht fuer sie nie". */
const c = await rufe(hand.keks, `${WEG}/${offenId}`, { status: "erledigt" }, "PATCH");
ok(c.status === 200, `an der offenen Zeile darf sie arbeiten (${c.status})`);
}
/* =======================================================================
5. Der alte Bestand (NULL) bleibt sichtbar
======================================================================= */
console.log("");
console.log("=== Was vor der Spalte da war ===");
{
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, erstellt, erstellt_von, nur_leitung)
VALUES ('rueckmeldung','idee','Aus der Zeit vor der Spalte',?,?,?,NULL)`)
.run("2026-09-01", jetzt, idAyla);
const beiHand = await titel(hand.keks);
ok(beiHand.some((t) => t === "Aus der Zeit vor der Spalte"),
"eine Zeile mit NULL gilt als offen und bleibt sichtbar");
/* Die Gegenprobe zur Gegenprobe: Genau diese Zeile MIT 1 waere weg. */
d.prepare("UPDATE eintraege SET nur_leitung = 1 WHERE titel = 'Aus der Zeit vor der Spalte'").run();
const nachher = await titel(hand.keks);
ok(!nachher.some((t) => t === "Aus der Zeit vor der Spalte"),
"dieselbe Zeile auf 1 gesetzt ist fuer sie weg -- die Regel greift wirklich");
d.prepare("UPDATE eintraege SET nur_leitung = NULL WHERE titel = 'Aus der Zeit vor der Spalte'").run();
}
/* =======================================================================
6. In anderen Bereichen gibt es das Feld nicht
======================================================================= */
console.log("");
console.log("=== Kein Versteck in anderen Bereichen ===");
{
const a = await rufe(ayla.keks, "/workspace/api/bereich/ideen", {
art: "content", titel: "Eine ganz normale Idee", datum: "2026-09-10",
nur_leitung: 1,
}, "POST");
const id = (await a.json())?.id;
ok(a.status === 201, `eine Idee laesst sich anlegen (${a.status})`);
const zeile = d.prepare("SELECT nur_leitung FROM eintraege WHERE id = ?").get(id);
ok(!zeile?.nur_leitung,
`das Feld wird dort stillschweigend verworfen (${zeile?.nur_leitung})`);
const beiHand = await holen(hand.keks, "/workspace/api/bereich/ideen");
ok((beiHand.eintraege || []).some((e) => e.id === id),
"und die Idee steht fuer alle da -- niemand kann sich dort verstecken");
}
/* =======================================================================
7. Die Kachel steht bei allen dreien -- mit demselben Namen
======================================================================= */
console.log("");
console.log("=== Die Kachel ===");
{
/* GEFRAGT WIRD IM EIGENEN HAUS (21.09.2026).
Bis heute fragte dieser Abschnitt ueber 127.0.0.1 -- also ueber
eine Pruefadresse, auf der die Agentursicht gilt. Solange
DogFather dort auch die Kacheln von Team Dogi bekam, fiel das
nicht auf.
Seit dem 21.09. bekommt er sie dort nicht mehr: Filipe wollte
"keine mischung mehr von kacheln", Team Dogi steht nur noch auf
seiner eigenen Adresse. Die Pruefung war damit rot, ohne dass die
Kachel fehlt -- sie hat nur am falschen Ort gesucht.
Gefragt wird deshalb mit dem Host der Crew-Wand, also genau dort,
wo diese Kachel zuhause ist. */
const CREW_WAND = "crew.dogfather-universe.com";
const kacheln = async (keks) => {
const i = await holenVon(keks, "/workspace/api/ich", CREW_WAND);
return [...(i.bereiche || []), ...(i.bereiche_zusatz || [])];
};
for (const [wer, keks] of [["DogFather", dogi.keks], ["die rechte Hand", hand.keks],
["das Team", ayla.keks]]) {
const k = (await kacheln(keks)).filter((x) => x.ziel === "bereich.html?b=rueckmeldung");
ok(k.length === 1 && k[0].name === "Rückmeldung",
`${wer}: genau eine Kachel, und sie heisst gleich (${k.map((x) => x.name).join("/") || "keine"})`);
}
/* Der Farbton muss neu sein -- zwei Kacheln in derselben Farbe sind
ein Fehler, den pruef-start-ansicht sonst erst am Bildschirm
findet. */
const alle = await kacheln(dogi.keks);
const toene = alle.map((x) => x.ton).filter((t) => t !== undefined);
ok(new Set(toene).size === toene.length,
`kein Farbton doppelt (${toene.length} Kacheln vom Server)`);
}
d.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log("");
console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"} -- ${geprueft} geprueft, ${fehler} Fehler`);
process.exit(fehler ? 1 : 0);