Files
dogfather-universe/server/pruef-haus-luecke.mjs
T
DogFatherGitandClaude Opus 5 5d9c8cd377 Haustrennung: kein Eintrag mehr aus dem anderen Haus
Filipe, 24.09.2026: "wenn ich bei der einen was mache soll nichts bei
der anderen passieren." -- und heute: "ja los".

GEMESSEN, BEVOR ETWAS ANGEFASST WURDE (jedes Brett x beide Adressen x
vier Rollen):

  AGENTUR-Adresse   DogFather  18 Bretter
                    Manager     6 Bretter
                    Creator     6 Bretter
                    Modi        kommt nicht rein (401)
  CREW-Adresse      DogFather  18 Bretter
                    Modi       13 Bretter
                    Manager     kommt nicht rein (401)
                    Creator     kommt nicht rein (401)

Die Anmeldung war also dicht. Durch griff genau EINE Rolle: DogFather.
Er wohnt in beiden Haeusern, und die Riegel in sichtbarEintrag()
fragten nach der ROLLE, nicht nach der Adresse. Auf der Crew-Adresse
stand damit das Brett der Agentur samt Inhalt.

Nachgemessen ist der Durchgriff AELTER als der gestrige Eventkarten-
Umbau -- zweimal gemessen, mit und ohne ihn, gleiches Ergebnis.

RIEGEL 0 in sichtbarEintrag(): Wer auf einer Adresse angemeldet ist,
sieht nur Eintraege dieses Hauses. Er haengt den uebrigen Riegeln
UM, statt in jeden Ausgang geschrieben zu werden -- die Funktion hat
drei Rueckgabepunkte, und der naechste waere sonst wieder offen.

Nachher, dieselbe Messung: DogFather sieht auf der Agenturadresse nur
Agentur-Eintraege, auf der Crew-Adresse nur die des Rudels. Manager,
Creator und Modi unveraendert.

WAS DABEI SCHIEFGING UND WIE ES AUFFIEL

1. Die erste Fassung liess bei `haus IS NULL` den BEREICH entscheiden.
   pruef-haus-trennung.mjs wurde sofort rot: "Lunas Live vom Montag"
   verschwand von der Agenturadresse. `live`, `technik` und
   `community` tragen BEIDES -- die Kacheln von Team Dogi und die
   Creator-Akten. Eine Regel, die jedem Brett genau ein Haus zuweist,
   kann das nicht. Jetzt bleibt ein Eintrag ohne Haus sichtbar: ein
   Eintrag zu viel faellt auf, ein fehlender nicht.

2. Damit NULL kein Dauerloch ist: FUENF von ACHT Stellen, die
   Eintraege anlegen, setzten `haus` gar nicht (workspace-video.js,
   -content.js, -bewerbung.js, -treff.js, -vorlagen.js). Nachgetragen.

3. Und `person.haus` war dafuer der falsche Massstab: Legt DogFather
   ueber die Agenturadresse ein Highlight an, gehoert es trotzdem dem
   Rudel -- sonst sieht die Community es nie. pruef-treff.mjs hat das
   gefunden (2 Fehler). Neu: hausFuerNeuenEintrag() -- bei den sieben
   Brettern des Rudels entscheidet das BRETT, sonst die Adresse.

NEU: pruef-haus-luecke.mjs (12 Pruefungen). Sie sucht die
Einfuege-Stellen im Quelltext und wird rot, sobald eine neunte
dazukommt, die `haus` vergisst -- mit Gegenprobe, dass das Suchmuster
eine solche Stelle auch wirklich erkennt. Ein Kommentar daneben haette
es nicht verhindert; das steht so schon im Projektgedaechtnis.

NEBENBEI: In bereich.js stand seit gestern `|| "Agentur-Events"` als
Rueckfall fuer das Etikett der Vorschau. pruef-treff.mjs verbietet
das zu Recht -- wie ein Brett heisst, haengt am Haus, und der Server
sagt es. Der Rueckfall ist weg; fehlt die Angabe, steht lieber kein
Etikett da als ein falsches.

pruef-treff.mjs nachgezogen (85 -> 86 Pruefungen, nicht weniger): Die
Zusage "DogFather sieht den Beitrag" wird jetzt auf der Crew-Adresse
geprueft, mit Gegenprobe fuer die Agenturadresse.

GEPRUEFT, alle gruen:
  pruef-haus-luecke     12    pruef-haus-trennung  100
  pruef-crew-adresse   169    pruef-treff           86
  pruef-eventkarte      77    pruef-agentur         62
  pruef-haus-seiten     38    pruef-eintrag-bild    24
  pruef-vorlagen        24    pruef-video, -content, -bewerbung,
                              -bereiche-lesend: in Ordnung

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-03 23:33:03 +02:00

232 lines
11 KiB
JavaScript

/* =====================================================================
DIE HAUSTRENNUNG BEI DEN EINTRAEGEN (03.10.2026)
Filipe, 24.09.2026: "wenn ich bei der einen was mache soll nichts bei
der anderen passieren. ... Kein Bestand wird geteilt."
WAS AM 03.10.2026 GEMESSEN WURDE (server/_mess-haeuser.mjs, jedes
Brett x beide Adressen x vier Rollen): Die Anmeldung war dicht --
eine Agenturrolle kam nicht auf die Crew-Adresse und umgekehrt.
Durch griff genau EINE Rolle: DogFather. Er wohnt in beiden
Haeusern, und die Riegel in sichtbarEintrag() fragten nach der
ROLLE, nicht nach der Adresse. Auf der Crew-Adresse stand damit das
Brett der Agentur samt Inhalt.
Behoben durch Riegel 0 in sichtbarEintrag(): Wer auf einer Adresse
angemeldet ist, sieht nur Eintraege dieses Hauses.
---------------------------------------------------------------------
WARUM ES DIESE DATEI BRAUCHT UND NICHT NUR DEN RIEGEL
Der Riegel haengt an der Spalte `eintraege.haus`. Die wird beim
ANLEGEN gesetzt -- und zwar von Hand, an jeder Stelle einzeln, die
einen Eintrag anlegt. Gemessen am 03.10.2026: FUENF von ACHT
Stellen taten es nicht (workspace-video.js, workspace-content.js,
workspace-bewerbung.js, workspace-treff.js, workspace-vorlagen.js).
Das ist dieselbe Falle wie die abgeschriebene Spaltenliste vom
11.09.2026: Eine Angabe, die an acht Stellen von Hand gepflegt
werden muss, ist an der neunten vergessen -- und zwar still.
Ein Kommentar daneben verhindert das nicht (auch das steht so im
Projektgedaechtnis: der Hinweis stand drei Tage lang direkt neben
der Stelle und wurde beim naechsten Mal trotzdem nicht gelesen).
Eine Pruefung schon. Diese hier wird rot, sobald eine neunte Stelle
dazukommt, die `haus` vergisst.
===================================================================== */
import { mkdtempSync, rmSync, appendFileSync, readFileSync, readdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-hl-"));
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 Haus-Luecken-Pruefung");
process.env.PORT = String(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 PROTOKOLL = join(process.cwd(), "pruef-haus-luecke-lauf.txt");
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
const melde = (z) => {
console.log(z);
try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ }
};
let fehler = 0, geprueft = 0;
const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* =======================================================================
1. OHNE SERVER: Schreibt jede Stelle das Haus mit?
======================================================================= */
melde("=== 1. Jede Stelle, die einen Eintrag anlegt, setzt `haus` ===");
{
const dateien = readdirSync(".")
.filter((f) => f.startsWith("workspace") && f.endsWith(".js"));
/* DIE DATEIEN WERDEN GESUCHT, NICHT AUFGEZAEHLT. Eine Liste von
Dateinamen hier waere genau die Sorte Angabe, die beim naechsten
neuen Modul veraltet -- und dann prueft diese Datei die neue
Stelle nicht, ohne dass etwas rot wird. */
ok(dateien.length > 10, `${dateien.length} Servermodule durchsucht`);
const stellen = [];
for (const datei of dateien) {
const text = readFileSync(datei, "utf8");
/* Jede Einfuegung in `eintraege` samt ihrer Spaltenliste. Die
Spaltenliste endet an der ersten schliessenden Klammer vor
VALUES. */
const muster = /INSERT\s+INTO\s+eintraege\s*\(([^)]*)\)/gi;
let t;
while ((t = muster.exec(text)) !== null) {
const zeile = text.slice(0, t.index).split("\n").length;
const spalten = t[1].split(",").map((x) => x.trim());
stellen.push({ datei, zeile, hatHaus: spalten.includes("haus") });
}
}
ok(stellen.length >= 8, `${stellen.length} Einfuege-Stellen gefunden (erwartet mindestens 8)`);
const ohne = stellen.filter((s) => !s.hatHaus);
for (const s of stellen) {
melde(` -- ${s.datei}:${s.zeile} ${s.hatHaus ? "setzt haus" : "OHNE haus"}`);
}
ok(ohne.length === 0, ohne.length
? `diese Stellen vergessen das Haus: ${ohne.map((s) => s.datei + ":" + s.zeile).join(", ")}`
: "alle setzen es");
/* GEGENPROBE: Findet dieses Muster ueberhaupt etwas, das OHNE haus
ist? Sonst koennte es an einem Tippfehler im regulaeren Ausdruck
liegen, und die Pruefung waere dauerhaft gruen, ohne je etwas
gesehen zu haben. */
const probe = "INSERT INTO eintraege (bereich, art, titel)\nVALUES (?,?,?)";
const m = /INSERT\s+INTO\s+eintraege\s*\(([^)]*)\)/i.exec(probe);
ok(!!m && !m[1].split(",").map((x) => x.trim()).includes("haus"),
"das Suchmuster erkennt eine Stelle OHNE haus (Gegenprobe)");
const probe2 = "INSERT INTO eintraege (bereich, art, haus)\nVALUES (?,?,?)";
const m2 = /INSERT\s+INTO\s+eintraege\s*\(([^)]*)\)/i.exec(probe2);
ok(!!m2 && m2[1].split(",").map((x) => x.trim()).includes("haus"),
"und eine MIT haus (Gegenprobe andersherum)");
}
/* =======================================================================
2. MIT SERVER: Greift der Riegel wirklich?
======================================================================= */
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-haus-luecke");
await new Promise((r) => setTimeout(r, 1000));
const BASIS = "http://127.0.0.1:" + PORT;
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;
}
anlegen("Dogfather", "admin", "CODE-DOGI-0001");
const heute = new Date().toISOString().slice(0, 10);
/* Drei Marken auf demselben Brett: eine je Haus und eine ohne. */
for (const [titel, haus] of [["Marke-CREW", "crew"], ["Marke-AGENTUR", "agentur"], ["Marke-OHNE", null]]) {
d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum, dringlichkeit, erstellt, erstellt_von, haus)"
+ " VALUES (?,?,'anschlag','ansage','offen',?, 'mittel', ?, 1, ?)")
.run(titel, "Test.", heute, jetzt, haus);
}
d.close();
const { request } = await import("node:http");
function roh(host, pfad, o = {}) {
return new Promise((f, s) => {
const daten = o.rumpf === undefined ? null : Buffer.from(JSON.stringify(o.rumpf));
const a = request({
host: "127.0.0.1", port: PORT, path: pfad,
method: o.methode || (daten ? "POST" : "GET"),
headers: {
Host: host, Origin: "https://" + host,
...(o.keks ? { Cookie: o.keks } : {}),
...(daten ? { "Content-Length": daten.length, "Content-Type": "application/json" } : {}),
},
}, (r) => {
const t = [];
r.on("data", (x) => t.push(x));
r.on("end", () => f({ code: r.statusCode, kopf: r.headers, text: Buffer.concat(t).toString("utf8") }));
});
a.on("error", s);
if (daten) a.write(daten);
a.end();
});
}
const CREW = "crew.dogfather-universe.com";
const AGEN = "workspace.dogfather-universe.com";
async function titelAuf(host) {
const anm = await roh(host, "/workspace/api/anmelden", { rumpf: { rolle: "admin", code: "CODE-DOGI-0001" } });
if (anm.code !== 200) return null;
const keks = [].concat(anm.kopf["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
const a = await roh(host, "/workspace/api/bereich/anschlag", { keks });
if (a.code !== 200) return null;
try { return (JSON.parse(a.text).eintraege || []).map((e) => e.titel).sort(); } catch { return null; }
}
melde("\n=== 2. DogFather sieht nur das Haus seiner Adresse ===");
{
const aufCrew = await titelAuf(CREW);
const aufAgentur = await titelAuf(AGEN);
/* DER DRITTE AUSGANG: Antwortet der Server gar nicht, ist das kein
"alles sauber" -- dann konnte hier nichts gemessen werden. */
if (!aufCrew || !aufAgentur) {
melde("ABBRUCH: das Brett antwortet nicht -- hier wurde nichts geprueft.");
process.exit(3);
}
melde(" -- auf crew.: " + JSON.stringify(aufCrew));
melde(" -- auf workspace.: " + JSON.stringify(aufAgentur));
ok(aufCrew.includes("Marke-CREW"), "auf crew. steht die Crew-Marke");
ok(!aufCrew.includes("Marke-AGENTUR"), "auf crew. steht die Agentur-Marke NICHT");
ok(aufAgentur.includes("Marke-AGENTUR"), "auf workspace. steht die Agentur-Marke");
ok(!aufAgentur.includes("Marke-CREW"), "auf workspace. steht die Crew-Marke NICHT");
/* DER RUECKFALL IST ABSICHT UND WIRD ALS SOLCHER GEPRUEFT: Ein
Eintrag ohne Haus bleibt in beiden sichtbar. Das ist die sichere
Richtung -- ein Eintrag zu viel faellt auf, ein fehlender nicht.
Teil 1 sorgt dafuer, dass dieser Fall gar nicht erst entsteht. */
ok(aufCrew.includes("Marke-OHNE") && aufAgentur.includes("Marke-OHNE"),
"ein Eintrag OHNE Haus bleibt in beiden sichtbar (bewusster Rueckfall)");
/* GEGENPROBE ZUR MESSUNG SELBST: Wenn hier nur eine Marke stuende,
waeren drei der Zusagen oben gruen, ohne etwas zu beweisen. */
ok(aufCrew.length === 2 && aufAgentur.length === 2,
`je zwei von drei Marken sind sichtbar (${aufCrew.length} / ${aufAgentur.length})`);
}
melde("\n=== 3. Ohne Adresse wird nicht gefiltert ===");
{
/* Auf 127.0.0.1 gibt es kein Haus -- dort muss alles sichtbar
bleiben, sonst waeren alle Pruefungen des Hauses still gruen und
blind. Genau davor warnt crew-adresse.js dreimal. */
const alles = await titelAuf("127.0.0.1:" + PORT);
ok(alles !== null && alles.length === 3,
`auf einer Pruefadresse stehen alle drei Marken (${alles ? alles.length : "keine Antwort"})`);
}
melde("");
melde(`${geprueft} geprueft, ${fehler} Fehler`);
if (!geprueft) { melde("ABBRUCH: nichts geprueft -- das ist ein Fehler, kein Erfolg."); process.exit(3); }
process.exit(fehler ? 1 : 0);