Files
DogFatherGitandClaude Opus 5 24ef154a2c pruef-haus-seiten: zwei gepflegte Seitenlisten durch eine Ableitung ersetzt
GEFUNDEN BEIM NACHGEHEN DER ALTLASTEN

    FEHL  calls.html ist auf crew. noch offen (200)
    FEHL  alle 8 Agenturseiten sind auf der Team-Adresse zu

Richtig gemessen und trotzdem kein Befund: Die Seite SOLL dort offen
sein. Am 22.09. hat Team Dogi die Kachel "Calls & Protokolle"
bekommen; seither gehoert calls.html zum Team. Rot war die Liste,
nicht das Haus.

Es waren ZWEI Listen von Hand -- neun Agenturseiten in Abschnitt 2,
fuenfzehn Teamseiten in Abschnitt 3 -- und in beiden fehlte dieselbe
Aenderung. Zwei gepflegte Listen fuer dieselbe Frage sind zwei
Gelegenheiten, eine zu vergessen.

ABGELEITET STATT GEPFLEGT

Welche Seiten es auf einer Adresse gibt, entscheidet
`gehoertAufDieseAdresse`: Eine Seite gehoert hierher, wenn sie unter
den KACHELN dieser Person steht. Die Pruefung fragt jetzt dieselben
Kacheln (`bereicheFuer` + `zusatzBereicheFuer`) und misst danach ueber
HTTP.

Das beweist NICHT, dass die Regel richtig gedacht ist -- das steht in
der Regel selbst. Es beweist, dass die Schranke tut, was die Kacheln
versprechen, und dass keine Seite durchrutscht, die auf keiner Kachel
steht. Und es gibt keine Zahl mehr, die morgen falsch ist.

ZWEI SORTEN GEHOEREN WEDER DEM EINEN NOCH DEM ANDEREN HAUS

Beim ersten Anlauf zaehlte die Ableitung `crew-index.html`,
`index.html` und `start.html` als Agenturseiten -- und verlangte damit
etwa, dass crew-index.html auf der Agenturadresse steht, wo sie zu
Recht ein 404 ist.

  1. DIE EINGANGSWAENDE (OHNE_ANMELDUNG): jede gehoert zu IHRER
     Adresse, auf der anderen gibt es sie nicht.
  2. DIE SEITEN OHNE KACHEL (OHNE_KACHEL_UEBERALL): der eigene
     Steckbrief, die Regeln des Treffs -- sie gehoeren dem Menschen,
     nicht dem Haus.

Mein erster Versuch las beide Listen mit einem regulaeren Ausdruck aus
dem Quelltext. Das ging schief (leere Mengen, vier neue Fehlalarme)
und war auch der falsche Gedanke: Eine Liste, die man PARST, ist eine
Abschrift mit Zwischenschritt. `OHNE_KACHEL_UEBERALL` ist jetzt
ausgeleitet, beide werden importiert.

Gemessen: 10 Seiten gehoeren nicht zu Team Dogi (automation, content,
leistung, profil, report, scouting, startcheck, team, teilen,
uebersicht), 21 gehoeren dorthin. 38 Pruefungen, 0 Fehler.

Dazu eine Gegenprobe zur Ableitung selbst: `calls.html` darf nicht
mehr unter den Agenturseiten stehen. Stuende es dort, waere die
Ableitung kaputt -- und die Pruefung wuerde denselben Fehlalarm wie
zuvor melden, nur mit mehr Umweg.

NEBENBEI GEKLAERT: team.html schickt einen admin auf der Team-Adresse
auf start.html. Am 22.09. als "auffaellig, nicht untersucht" notiert
-- nachgesehen ist es RICHTIG: team.html ist die Teamuebersicht der
Agentur, Team Dogi hat teamlage.html. Die Umleitung ist die
Haustrennung bei der Arbeit, kein Fehler. Sie steht jetzt in der
abgeleiteten Liste der zehn.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-23 00:32:48 +02:00

452 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
Auf der Adresse von Team Dogi gibt es die Agenturseiten nicht
(15.09.2026)
Filipe, mit dem Bildschirmfoto des Start-Checks auf crew.:
„das gibt es in der app nicht, dass ist nur auf der workspace app
aber nicht hier."
---------------------------------------------------------------------
ES WAR GRÖSSER ALS DIESE EINE SEITE
Die Haustrennung entscheidet, WAS jemand sieht — Daten und Kacheln.
Sie entschied nie, welche SEITEN es auf einer Adresse gibt. Die
Rechtetafel kennt nur Rollen, keine Adressen. Zwischen beidem lag
das Loch: zehn Agenturseiten waren auf crew. erreichbar, drei davon
sogar für einen Modi.
---------------------------------------------------------------------
UND DIE GEGENPROBE IST HIER DAS WICHTIGSTE
Eine Regel, die Seiten wegnimmt, muss beweisen, dass sie die
RICHTIGEN wegnimmt — und vor allem, dass auf der Agenturadresse
nichts fehlt. Sonst hätte ich Filipe eine Seite repariert und ihm
zehn andere genommen, ohne es zu merken.
Aufrufen mit: node server/pruef-haus-seiten.mjs
===================================================================== */
import { mkdtempSync, rmSync, readdirSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta, "pruef-haus-seiten");
const ordner = mkdtempSync(join(tmpdir(), "ws-haus-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(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 }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(240_000, "pruef-haus-seiten");
await new Promise((r) => setTimeout(r, 700));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const CREW = "crew.dogfather-universe.com";
const HAUS = "workspace.dogfather-universe.com";
/* DIE NUMMER VON DOGFATHER. Die abgeleiteten Seitenlisten (Abschnitt
2 und 3) brauchen eine PERSON, nicht nur einen Keks: `bereicheFuer`
fragt Rolle und Haus, und zwar an derselben Stelle wie der Server.
Ein zweiter Nachbau hier waere die zweite Wahrheit. */
let idDogi = 0;
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const ws = await import("./workspace.js");
const rechte = await import("./rechte.js");
const jetzt = new Date().toISOString();
{
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const s = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", s, jetzt);
const 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", s).update(code).digest("hex"), jetzt);
};
idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Rieke", "hand", "CODE-HAND-0001");
anlegen("Frida", "modi", "CODE-MODI-0001");
anlegen("Mara", "manager", "CODE-MANA-0001");
anlegen("Sam", "creator", "CODE-CREA-0001");
const idSam = d.prepare("SELECT id FROM personen WHERE name = 'Sam'").get().id;
/* EIN ECHTER ANLASS FÜR DEN HINWEIS. Ohne ihn gäbe es ihn nirgends,
und „er steht nicht auf crew.“ wäre ein grüner Haken über der Null.
DIE TABELLE NICHT SELBST ANLEGEN: Der erste Entwurf schrieb hier
ein CREATE TABLE IF NOT EXISTS mit erfundenen Spalten hin. Die
echte Tabelle war längst da, das IF NOT EXISTS schwieg, und der
INSERT scheiterte an „no column named schluessel“ -- die Prüfung
hätte gemeldet, der Hinweis stehe nirgends, und das wäre richtig
und wertlos gewesen. Gefunden hat es die Gegenprobe. */
const spalten = d.prepare("PRAGMA table_info(startcheck)").all().map((x) => x.name);
if (!spalten.includes("bewertung")) throw new Error("startcheck sieht anders aus als erwartet");
d.prepare("INSERT INTO startcheck (creator_id, feld, punkt, bewertung, geaendert)"
+ " VALUES (?,?,?,?,?)")
.run(idSam, "technik", "licht", "handlung", jetzt);
d.close();
}
function ruf(pfad, host, methode, kopf = {}, koerper = null) {
return new Promise((fertig, schief) => {
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf } },
(antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({
code: antwort.statusCode, text, ort: antwort.headers.location || "", kopf: antwort.headers }));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
async function anmelden(rolle, code, host) {
const r = await ruf("/workspace/api/anmelden", host, "POST", {}, { rolle, code });
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
}
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
const kDogiCrew = await anmelden("admin", "CODE-DOGI-0001", CREW);
const kHand = await anmelden("hand", "CODE-HAND-0001", CREW);
const kModi = await anmelden("modi", "CODE-MODI-0001", CREW);
const kMana = await anmelden("manager", "CODE-MANA-0001", HAUS);
ok(!!kDogi && !!kDogiCrew && !!kHand && !!kModi && !!kMana,
"vier Menschen sind angemeldet, DogFather auf beiden Adressen");
/** Kommt man auf diese Seite? 200 ja, 302 nein. */
const seite = async (datei, host, keks) =>
ruf("/workspace/" + datei, host, "GET", { cookie: keks });
const SEITEN = readdirSync(new URL("../workspace/", import.meta.url))
.filter((n) => n.endsWith(".html"));
/* =======================================================================
1. DIE GEMELDETE SEITE
======================================================================= */
melde("");
melde("=== 1. Der Start-Check ===");
{
const a = await seite("startcheck.html", CREW, kDogiCrew);
ok(a.code === 302, `auf der Team-Adresse gibt es ihn nicht (${a.code})`);
ok(/start\.html$/.test(a.ort), `man landet auf der Startseite (${a.ort})`);
/* DIE GEGENPROBE, OHNE DIE DIE ZEILE DARÜBER NICHTS BEWEIST:
Auf der Agenturadresse ist der Start-Check unverändert da. */
const b = await seite("startcheck.html", HAUS, kDogi);
ok(b.code === 200, `auf der Agenturadresse ist er da (${b.code})`);
ok(/Start-Check/.test(b.text), "und es ist wirklich die Seite");
/* UND EIN MODI KAM DA AUCH HIN -- das war der unangenehmste Teil. */
const m = await seite("startcheck.html", CREW, kModi);
ok(m.code === 302, `auch für einen Modi ist er weg (${m.code})`);
}
/* =======================================================================
2. ALLE AGENTURSEITEN, NICHT NUR DIESE EINE
======================================================================= */
melde("");
melde("=== 2. Die übrigen Agenturseiten ===");
{
/* =====================================================================
DIE LISTE WIRD ABGELEITET, NICHT GEPFLEGT (23.09.2026)
=====================================================================
Hier stand eine Liste von neun Dateinamen, und daneben in
Abschnitt 3 eine zweite mit fünfzehn. Am 22.09. hat Filipe die
Kachel „Calls & Protokolle" für Team Dogi bekommen — `calls.html`
gehört seither zum Team. In KEINER der beiden Listen wurde das
nachgezogen, und die Prüfung meldete:
FEHL calls.html ist auf crew. noch offen (200)
Das war richtig gemessen und trotzdem kein Befund: Die Seite SOLL
dort offen sein. Rot war die Liste, nicht das Haus.
Dieselbe Krankheit wie die abgeschriebene Spaltenliste beim
Datenumbau und die Portnummern: Eine Liste, die jemand pflegen
muss, wird beim nächsten Umbau vergessen.
---------------------------------------------------------------------
WORAUS JETZT ABGELEITET WIRD — und was das beweist
Welche Seiten es auf einer Adresse gibt, entscheidet
`gehoertAufDieseAdresse`: Eine Seite gehört hierher, wenn sie
unter den KACHELN dieser Person steht (oder in der kurzen Liste
der Seiten ohne Kachel). Die Kacheln kommen aus `bereicheFuer`,
die Erreichbarkeit aus der Schranke in workspace.js — zwei
getrennte Wege.
Diese Prüfung fragt die Kacheln und misst danach über HTTP. Sie
beweist damit NICHT, dass die Regel richtig gedacht ist — das
steht in der Regel selbst. Sie beweist, dass die Schranke tut,
was die Kacheln versprechen, und dass keine Seite durchrutscht,
die auf keiner Kachel steht.
Und sie hat keine Zahl mehr, die morgen falsch ist.
===================================================================== */
const { bereicheFuer, zusatzBereicheFuer } = await import("./workspace.js");
const alleSeiten = readdirSync(new URL("../workspace/", import.meta.url))
.filter((f) => f.endsWith(".html"));
const dogiAufCrew = { id: idDogi, rolle: "admin", haus: "crew" };
const seinsAufCrew = new Set(
[...(bereicheFuer(dogiAufCrew) || []), ...(zusatzBereicheFuer(dogiAufCrew) || [])]
.map((k) => String(k?.ziel || "").split("?")[0])
.filter(Boolean));
/* ZWEI SORTEN, DIE WEDER DEM EINEN NOCH DEM ANDEREN HAUS GEHOEREN
-- und die beim ersten Anlauf faelschlich als Agenturseiten
gezaehlt wurden:
1. DIE EINGANGSWAENDE (OHNE_ANMELDUNG in rechte.js):
index.html und crew-index.html. Jede gehoert zu IHRER
Adresse und ist dort ohne Anmeldung offen; auf der anderen
gibt es sie nicht. Sie als "Agenturseite" zu fuehren hiesse
zu verlangen, dass crew-index.html auf der Agenturadresse
steht -- dort ist sie zu Recht ein 404.
2. DIE SEITEN OHNE KACHEL (OHNE_KACHEL_UEBERALL in
workspace.js): der eigene Steckbrief, die Regeln des Treffs
und Verwandtes. Sie gehoeren dem Menschen, nicht dem Haus.
BEIDE LISTEN WERDEN GELESEN, nicht abgeschrieben -- sonst waeren
es zwei weitere Listen, die jemand pflegen muss, und genau das
sollte hier ja aufhoeren. */
const { OHNE_KACHEL_UEBERALL } = await import("./workspace.js");
const { OHNE_ANMELDUNG } = await import("./rechte.js");
const nurName = (p2) => String(p2).replace("/workspace/", "");
const ohneKachel = new Set([...OHNE_KACHEL_UEBERALL].map(nurName));
const eingaenge = new Set(OHNE_ANMELDUNG.map(nurName));
ok(eingaenge.size >= 2 && ohneKachel.size >= 3,
`${eingaenge.size} Eingangswaende, ${ohneKachel.size} Seiten ohne Kachel`
+ " -- beide Listen kommen aus dem Quelltext, nicht von hier");
const AGENTUR = alleSeiten.filter((f) =>
!seinsAufCrew.has(f) && !ohneKachel.has(f) && !eingaenge.has(f));
ok(AGENTUR.length >= 5,
`${AGENTUR.length} Seiten gehoeren NICHT zu Team Dogi (${AGENTUR.join(", ")})`);
ok(seinsAufCrew.size >= 5,
`und ${seinsAufCrew.size} gehoeren dorthin`);
/* DIE GEGENPROBE ZUR ABLEITUNG SELBST: calls.html steht seit dem
22.09. auf der Team-Seite. Stuende es hier wieder unter AGENTUR,
waere die Ableitung kaputt -- und die Pruefung wuerde denselben
Fehlalarm wie zuvor melden, nur mit mehr Umweg. */
ok(!AGENTUR.includes("calls.html"),
"calls.html zaehlt als Teamseite -- die Ableitung folgt der Kachel von gestern");
let zu = 0;
for (const s of AGENTUR) {
const a = await seite(s, CREW, kDogiCrew);
if (a.code === 302) zu++; else ok(false, `${s} ist auf crew. noch offen (${a.code})`);
}
ok(zu === AGENTUR.length, `alle ${zu} Agenturseiten sind auf der Team-Adresse zu`);
/* GEGENPROBE: auf der Agenturadresse ist jede einzelne noch da. */
let offen = 0;
for (const s of AGENTUR) {
const a = await seite(s, HAUS, kDogi);
if (a.code === 200) offen++; else ok(false, `${s} fehlt jetzt auf workspace. (${a.code})`);
}
ok(offen === AGENTUR.length,
`und auf der Agenturadresse sind alle ${offen} unverändert da`);
/* UND FÜR EINE MANAGERIN, die es dort auch nur dort gibt. */
const mana = await seite("scouting.html", HAUS, kMana);
ok(mana.code === 200, `eine Managerin kommt weiterhin ins Scouting (${mana.code})`);
}
/* =======================================================================
3. WAS ZUM TEAM GEHÖRT, BLEIBT
======================================================================= */
melde("");
melde("=== 3. Und was bleibt ===");
{
/* AUCH HIER ABGELEITET (23.09.2026) -- aus denselben Kacheln wie in
Abschnitt 2. Diese Liste war die zweite, in der `calls.html`
gefehlt hat. Zwei gepflegte Listen fuer dieselbe Frage sind zwei
Gelegenheiten, eine zu vergessen. */
const { bereicheFuer: bf, zusatzBereicheFuer: zbf } = await import("./workspace.js");
const aufCrew = { id: idDogi, rolle: "admin", haus: "crew" };
const TEAM = [...new Set(
[...(bf(aufCrew) || []), ...(zbf(aufCrew) || [])]
.map((k) => String(k?.ziel || "").split("?")[0])
.filter((z) => z && z.endsWith(".html") && !/^https?:/i.test(z)))];
let da = 0;
for (const s of TEAM) {
const a = await seite(s, CREW, kDogiCrew);
if (a.code === 200) da++; else ok(false, `${s} ist auf crew. verschwunden (${a.code})`);
}
ok(da === TEAM.length, `alle ${da} Seiten des Teams sind da`);
/* EIN MODI BEHÄLT SEINE. Besonders die beiden, die keine Kachel
haben und trotzdem ihm gehören. */
for (const s of ["entwicklung.html", "befinden.html", "treff-regeln.html", "aufgaben.html"]) {
const a = await seite(s, CREW, kModi);
ok(a.code === 200, `ein Modi kommt weiterhin auf ${s} (${a.code})`);
}
/* Aber nicht auf die der Leitung -- das war schon vorher so und
muss es bleiben. */
const verwehrt = await seite("personen.html", CREW, kModi);
ok(verwehrt.code === 302, `und nicht auf personen.html (${verwehrt.code})`);
}
/* =======================================================================
4. DIE REGEL SELBST
======================================================================= */
melde("");
melde("=== 4. Die Regel ===");
{
/* SIE IST ABGELEITET, NICHT GEPFLEGT: Was eine Kachel hat, ist
erreichbar. Gemessen statt behauptet. */
const person = { id: 1, rolle: "modi", haus: "crew" };
const kacheln = [...(ws.bereicheFuer(person) || []), ...(ws.zusatzBereicheFuer(person) || [])]
.map((k) => "/workspace/" + String(k.ziel || "").split("?")[0]);
const alle = kacheln.every((p) => ws.gehoertAufDieseAdresse(person, p));
ok(alle, `alle ${kacheln.length} Kacheln eines Modis führen auf erreichbare Seiten`);
/* EINE NEUE SEITE IST AUF crew. AUTOMATISCH ZU -- das ist die
Richtung, in die diese Regel altern soll. */
ok(!ws.gehoertAufDieseAdresse(person, "/workspace/gibt-es-noch-nicht.html"),
"eine Seite, die es noch nicht gibt, ist dort automatisch zu");
/* UND AUF DER AGENTURADRESSE ÄNDERT SIE GAR NICHTS. */
const agentur = { id: 2, rolle: "creator", haus: null };
ok(SEITEN.every((s) => ws.gehoertAufDieseAdresse(agentur, "/workspace/" + s)),
`im anderen Haus lässt sie alle ${SEITEN.length} Seiten durch`);
/* SIE ÄNDERT KEINE RECHTE. Was jemand darf, steht weiter allein in
rechte.js -- die Tafel ist unberührt. */
ok((rechte.tafelJetzt()["/workspace/startcheck.html"] || []).includes("modi"),
"in der Rechtetafel steht der Start-Check unverändert");
ok((await seite("startcheck.html", CREW, kModi)).code === 302,
"erreichbar ist er auf crew. trotzdem nicht — zwei verschiedene Fragen");
}
/* =======================================================================
5. KEINE SEITE FÄLLT ZWISCHEN DIE STÜHLE
======================================================================= */
melde("");
melde("=== 5. Der Durchgang über alle Seiten ===");
{
/* JEDE .html EINZELN, für jede der drei Rollen des Teams. Es geht
nicht darum, ob sie offen oder zu ist -- sondern darum, dass
KEINE davon eine Antwort gibt, die weder 200 noch 302 ist. Ein
500 an dieser Stelle wäre die Regel, die an einer Seite stolpert. */
let gezaehlt = 0, seltsam = 0;
const zu = { admin: [], hand: [], modi: [] };
for (const [rolle, keks] of [["admin", kDogiCrew], ["hand", kHand], ["modi", kModi]]) {
for (const s of SEITEN) {
const a = await seite(s, CREW, keks);
gezaehlt++;
if (a.code !== 200 && a.code !== 302 && a.code !== 404) {
seltsam++;
ok(false, `${s} für ${rolle}: ${a.code}`);
}
if (a.code !== 200) zu[rolle].push(s);
}
}
ok(gezaehlt === SEITEN.length * 3,
`${gezaehlt} Aufrufe: ${SEITEN.length} Seiten × 3 Rollen`);
ok(seltsam === 0, "keine einzige antwortet mit einem Fehler");
/* UND DIE ZAHLEN GEHÖREN IN DIE BEDINGUNG, nicht nur in den Text:
Ein Durchgang, bei dem alles offen ist, hätte nichts geprüft. */
ok(zu.admin.length > 8, `für DogFather sind ${zu.admin.length} Seiten zu`);
ok(zu.modi.length > zu.admin.length,
`für einen Modi mehr als für DogFather (${zu.modi.length})`);
}
/* =======================================================================
6. EIN HINWEIS AUF EINE SEITE, DIE ES HIER NICHT GIBT
Filipe, nach dem Ausliefern: „wieso steht es noch da?" -- „1 Punkt im
Start-Check braucht Handlung", auf crew., mit einem Pfeil auf eine
Seite, die dort gesperrt ist.
Das war die dritte Stelle: nicht Seite, nicht Schnittstelle, sondern
eine Liste von VERWEISEN. Sie bekommt jetzt dieselbe Regel.
======================================================================= */
melde("");
melde("=== 6. Hinweise und Suche ===");
{
const hinweise = async (host, keks) => {
const a = await ruf("/workspace/api/hinweise", host, "GET", { cookie: keks });
try { return JSON.parse(a.text).hinweise || []; } catch { return []; }
};
/* ZUERST DIE GEGENPROBE: Auf der Agenturadresse MUSS er dastehen.
Ohne sie beweist „er ist auf crew. weg" nur, dass es ihn nirgends
gibt. */
const drueben = await hinweise(HAUS, kDogi);
const dort = drueben.find((h) => h.art === "startcheck_handlung");
ok(!!dort, `auf der Agenturadresse steht der Hinweis (${dort?.text || "—"})`);
ok(dort?.ziel === "startcheck.html", `und zeigt auf den Start-Check (${dort?.ziel})`);
/* UND HIER IST DER GEMELDETE FEHLER. */
const hier = await hinweise(CREW, kDogiCrew);
ok(!hier.some((h) => h.art === "startcheck_handlung"),
"auf der Team-Adresse steht er nicht mehr");
ok(!hier.some((h) => /startcheck|scouting|content|profil|calls/.test(String(h.ziel || ""))),
`und kein einziger Hinweis zeigt dorthin (${hier.length} Hinweise insgesamt)`);
/* ABER DIE HINWEISE DES TEAMS BLEIBEN. Eine Regel, die alle
wegnimmt, wäre keine Lösung, sondern eine leere Startseite. */
ok(hier.every((h) => !h.ziel
|| ws.gehoertAufDieseAdresse({ rolle: "admin", haus: "crew" },
"/workspace/" + String(h.ziel).split("?")[0])),
"jeder verbliebene Hinweis führt auf eine Seite, die es hier gibt");
/* DIESELBE SORTE VERWEIS: die Suche. */
const suche = async (host, keks, q) => {
const a = await ruf(`/workspace/api/suche?q=${encodeURIComponent(q)}`, host, "GET",
{ cookie: keks });
try { return JSON.parse(a.text); } catch { return { gruppen: [] }; }
};
/* ZUERST WIEDER DIE GEGENPROBE. „Kein Treffer zeigt dorthin“ ist
wertlos, solange es gar keinen Treffer gibt -- genau der grüne
Haken über der Null, vor dem die Hausregel warnt. Also erst
nachweisen, dass die Suche diesen Menschen auf der Agenturadresse
WIRKLICH findet. */
const s0 = await suche(HAUS, kDogi, "Sam");
const zieleH = (s0.gruppen || []).flatMap((g) => g.treffer || []).map((t) => t.ziel || "");
ok(zieleH.length > 0, `auf der Agenturadresse findet die Suche Sam (${zieleH.length} Treffer)`);
ok(zieleH.some((z) => /profil.html/.test(z)),
`und ein Treffer führt auf sein Creator-Profil (${zieleH.join(", ")})`);
const s1 = await suche(CREW, kDogiCrew, "Sam");
const ziele = (s1.gruppen || []).flatMap((g) => g.treffer || []).map((t) => t.ziel || "");
ok(!ziele.some((z) => /profil|scouting|calls|content/.test(z)),
`auf der Team-Adresse verweist keiner dorthin (${ziele.length} Treffer)`);
ok(s1.gesamt === ziele.length,
`und die Zahl nennt nur, was wirklich dasteht (${s1.gesamt})`);
ok((s1.gruppen || []).every((g) => (g.treffer || []).length > 0),
"keine Überschrift ohne Treffer darunter");
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);