Gemessen: 165 Pruefdateien, 129 verschiedene Nummern -- NEUNZEHN
doppelt, vier davon dreifach. Niemand hatte das gewollt; jede neue
Pruefung wurde von einer vorhandenen abgeschrieben, und die Nummer kam
mit. Meine eigene Notiz sagte "sieben" -- auch eine Bestandsliste
altert.
Der Waechter faengt den Schaden ab, aber er kann nur melden, was schon
passiert ist: Zwei Pruefungen mit derselben Nummer koennen nie
gleichzeitig laufen, und ein liegengebliebener Prozess der einen laesst
die andere abbrechen mit einer Meldung, die wie ein Befund aussieht.
Genau das ist mir am 20.09. zweimal passiert.
Jetzt leitet jede Datei ihre Nummer aus ihrer STELLE IM ALPHABET ab
(eigenerPort in helfer-port.mjs), zwei je Datei. Nicht ueber eine
Pruefsumme: Bei 165 Namen in 4900 Nummern waeren nach dem
Geburtstagsproblem rund DREI Zusammenstoesse zu erwarten -- ein Hash
tauscht eine sichtbare Doppelung gegen eine unsichtbare. Die Stelle im
Alphabet ist eindeutig von der Bauart her.
--- ZWEI FEHLER AUF DEM WEG, BEIDE LEHRREICH -------------------------
1. DER ERSTE VERSUCH WAR GRUEN UND KAPUTT. Ersetzt wurde mit einem
Muster: "([^"]*4231[^"]*)". Das hielt
{ host: "127.0.0.1", port: 4231, path: "/404.html" }
fuer eine Zeichenkette -- ein Muster kann eine oeffnende nicht von
einer schliessenden Anfuehrung unterscheiden. Heraus kam
{ host: "127.0.0.1`, port: ${PORT}, path: `/404.html" }
also GUELTIGER Code ohne port-Feld. `node --check` sagte gruen fuer
alle 164 Dateien. Aufgefallen ist es erst, weil ich vier Vertreter
gegen eine vorher gemessene Grundlinie laufen liess: pruef-schranke
39/0 vorher, 38/1 nachher.
Alles zurueckgenommen und mit einem Zerleger neu gemacht, der weiss,
ob eine Stelle Code, Zeichenkette, Vorlage, Kommentar oder
regulaerer Ausdruck ist. In pruef-ics stand die Nummer in einem
regulaeren Ausdruck -- der wird jetzt gebaut statt hingeschrieben.
2. EIN MODUL, DAS BEIM IMPORTIEREN ARBEITET, IST EINE FALLE. Der
zweite Durchgang importierte den ersten, um seine Mechanik zu
benutzen -- und fuehrte dessen Hauptlauf gleich mit aus. Die
zweiten Nummern wurden dadurch als erste behandelt, zwei Aufrufe
bekamen dieselbe Nummer, und in pruef-content stand `const PORT`
zweimal.
--- WAS DAS DAUERHAFT HAELT -----------------------------------------
pruef-portnummern.mjs (neu, 8 Pruefungen) fragt nicht "welche Nummern
sind doppelt", sondern "wer traegt ueberhaupt noch eine von Hand ein"
und "wer startet einen Server, ohne seine Nummer abzuleiten". Die
zweite Frage hat sofort etwas gefunden, das in KEINER Doppelungsliste
stand: pruef-push-weg belegte 4341 und 4342, rief den Waechter aber
gar nicht auf -- dieselbe Nummer wie pruef-agentur. Eine Liste zeigt
nur, was auf ihr steht.
Mit Gegenprobe: Eine unbekannte Datei bekommt keine geratene Nummer,
sondern einen Abbruch, und eine dritte Nummer je Datei gibt es nicht.
--- NACHGEMESSEN ----------------------------------------------------
Sechzehn Pruefungen gegen ihre vorher gemessene Grundlinie, je eine
Vertreterin jeder umgestellten Bauweise (eine Nummer, zwei Nummern,
Nummer in einer Zeichenkette, in einer Vorlage, in einem regulaeren
Ausdruck, dynamische Einfuhr, Nachtlauf mit zwei Laeufen):
crew-adresse 132/0 · schranke 39/0 · content 45/0 · entwicklung 46/0
anruf 127/0 · ics 37/0 · push-weg 20/0 · arten 28/0 · video 67/0
kanalzeile 14/0 · agentur 62/0 · spicy 83/0 · creator-anlegen 50/0
push-ziel 10/0 · portnummern 8/0
Alle exakt wie vorher. Zwei waren schon vorher rot und sind es
unveraendert geblieben (crew-wand-bild 41/4, teilen 14/3) -- per
`git stash` belegt, nicht angenommen.
Co-Authored-By: Claude Opus 5 <[email protected]>
948 lines
48 KiB
JavaScript
948 lines
48 KiB
JavaScript
/* =====================================================================
|
|
Wie geht's dir? (15.09.2026)
|
|
|
|
Filipe, mit dem Bildschirmfoto der Kachel: „ich will dass du diese
|
|
seite perfektionnierst, denn gerade wenn ich drauf drücke geht die
|
|
entwicklungsseite auf."
|
|
|
|
---------------------------------------------------------------------
|
|
DER FEHLER WAR SCHLIMMER ALS EIN FALSCHER VERWEIS
|
|
|
|
Auf der Kachel steht „die Antworten sieht nur du" — und sie öffnete
|
|
eine Seite voller Namen und Beobachtungen über ANDERE Menschen. Die
|
|
erste Prüfung hier ist deshalb: Führt die Kachel dorthin, wo sie
|
|
hinführt zu führen verspricht?
|
|
|
|
---------------------------------------------------------------------
|
|
UND DIE EIGENTLICHE FRAGE: BLEIBT ES PRIVAT?
|
|
|
|
Ein Verlauf über das Befinden eines Menschen ist das Empfindlichste,
|
|
was dieses Haus speichert. Er existiert nur, weil er IHM hilft. Also:
|
|
|
|
Sieht DogFather ihn? (darf nicht)
|
|
Steht etwas davon im Protokoll? (nur DASS, nie WAS)
|
|
Ändert er die Ampel? (darf nicht)
|
|
|
|
Aufrufen mit: node server/pruef-befinden.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, 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-befinden");
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-bef-"));
|
|
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(180_000, "pruef-befinden");
|
|
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);
|
|
/* WIE VIELE FRAGEN ES GIBT -- aus dem Katalog, nicht abgeschrieben.
|
|
|
|
Vier Zeilen dieser Datei nannten frueher feste Zahlen ("2 Runden x 6",
|
|
"zwoelf Antworten", "alle sechs Fragen"). Am 17.09.2026 wurden aus
|
|
sechs Fragen zwoelf, und sie waren ab da rot, ohne dass irgendetwas
|
|
kaputt war. Eine feste Zahl ist eine Rechnung von gestern. */
|
|
const anzahlFragen = (await import("./workspace-entwicklungs-punkte.js"))
|
|
.ENTWICKLUNG_PUNKTE.befinden.length;
|
|
/* Und wie viele davon der Kern sind -- aus demselben Grund abgeleitet. */
|
|
const kernAnzahl = (await import("./workspace-entwicklungs-punkte.js"))
|
|
.ENTWICKLUNG_PUNKTE.befinden.filter((p) => p.kern).length;
|
|
|
|
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";
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const modul = await import("./workspace-befinden.js");
|
|
const kat = await import("./workspace-entwicklungs-punkte.js");
|
|
const rechte = await import("./rechte.js");
|
|
|
|
const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
|
|
let idDogi, idFrida;
|
|
{
|
|
const d = oeffne();
|
|
const schluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", schluessel, 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", schluessel).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
};
|
|
idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
idFrida = anlegen("Frida", "modi", "CODE-MODI-0001");
|
|
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, kopf: antwort.headers }));
|
|
});
|
|
a.on("error", schief);
|
|
if (koerper) a.write(JSON.stringify(koerper));
|
|
a.end();
|
|
});
|
|
}
|
|
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
|
|
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("; ");
|
|
}
|
|
/* DOGFATHER AUF DER TEAM-ADRESSE.
|
|
|
|
Nicht aus Bequemlichkeit: Auf workspace. liefert der Server ihm
|
|
`bereiche: null` -- der Browser nimmt dort seine eigene Liste, und
|
|
die kennt diese Kachel gar nicht. Die Kachel, die Filipe im
|
|
Bildschirmfoto angetippt hat, steht auf crew. Wer sie auf der
|
|
falschen Adresse sucht, prueft an ihr vorbei. */
|
|
const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
|
|
const kFrida = await anmelden("modi", "CODE-MODI-0001", CREW);
|
|
ok(!!kDogi && !!kFrida, "DogFather und ein Modi sind angemeldet");
|
|
|
|
/* =======================================================================
|
|
1. DIE KACHEL HÄLT, WAS SIE VERSPRICHT
|
|
|
|
Der gemeldete Fehler. Er war schlimmer als ein falscher Verweis: Auf
|
|
der Kachel steht „die Antworten sieht nur du" -- und sie oeffnete
|
|
eine Seite voller Namen anderer Menschen.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Die Kachel ===");
|
|
{
|
|
const ich = json(await ruf("/workspace/api/ich", CREW, "GET", { cookie: kDogi }));
|
|
const alle = [...(ich.bereiche || []), ...(ich.bereiche_zusatz || [])];
|
|
const kachel = alle.find((k) => /Wie geht/i.test(k.name || ""));
|
|
ok(!!kachel, `die Kachel „${kachel?.name}“ steht bei DogFather`);
|
|
ok(kachel?.ziel === "befinden.html",
|
|
`und zeigt auf die eigene Seite (${kachel?.ziel})`);
|
|
/* GEGENPROBE -- ohne sie beweist die Zeile nichts: Die Kachel
|
|
„Entwicklung" zeigt weiterhin auf die Entwicklungsseite. */
|
|
/* Der Kachelname wurde am 19.09.2026 von "Entwicklung" zu "Eure
|
|
Aufgaben" geaendert (sie fuehrt auf den Aufgabenkatalog). Gesucht
|
|
wird deshalb ueber das ZIEL -- das aendert sich nicht, wenn jemand
|
|
den Namen nachschaerft. */
|
|
const andere = alle.find((k) => k.ziel === "entwicklung.html");
|
|
ok(andere?.ziel === "entwicklung.html",
|
|
`die Kachel „Entwicklung“ zeigt weiterhin dorthin (${andere?.ziel})`);
|
|
|
|
ok((rechte.tafelJetzt()["/workspace/befinden.html"] || []).includes("modi"),
|
|
"die Seite steht in der Rechtetafel und ein Modi darf sie öffnen");
|
|
ok(!(rechte.tafelJetzt()["/workspace/befinden.html"] || []).includes("creator"),
|
|
"die Agentur nicht — sie hat auch keine Entwicklungskarte");
|
|
|
|
const beiFrida = json(await ruf("/workspace/api/ich", CREW, "GET", { cookie: kFrida }));
|
|
const ihre = [...(beiFrida.bereiche || []), ...(beiFrida.bereiche_zusatz || [])]
|
|
.find((k) => /Wie geht/i.test(k.name || ""));
|
|
ok(ihre?.ziel === "befinden.html", "und bei einem Modi führt sie ebenfalls dorthin");
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. SECHS FRAGEN, EIN RHYTHMUS
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Sechs Fragen, ein Rhythmus ===");
|
|
{
|
|
const d = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
|
|
|
|
/* ==== KERN UND ROTATION (19.09.2026) ==============================
|
|
|
|
Filipe wollte "viel mehr Fragen" UND "kurz und knapp". Geloest
|
|
ueber fuenf feste Fragen plus drei wechselnde: Jede Runde bleibt
|
|
kurz, ueber die Monate wird alles abgedeckt -- und der Katalog
|
|
kann wachsen, ohne dass eine Runde laenger wird.
|
|
============================================================== */
|
|
const kern = kat.ENTWICKLUNG_PUNKTE.befinden.filter((p) => p.kern);
|
|
const rest = kat.ENTWICKLUNG_PUNKTE.befinden.filter((p) => !p.kern);
|
|
ok(kern.length > 0, `${kern.length} Fragen sind als Kern markiert`);
|
|
ok(rest.length > modul.ZUSATZ_PRO_RUNDE,
|
|
`${rest.length} wechseln sich ab — mehr als die ${modul.ZUSATZ_PRO_RUNDE} Plätze je Runde`);
|
|
ok(d.punkte?.length === kern.length + modul.ZUSATZ_PRO_RUNDE,
|
|
`${d.punkte?.length} Fragen in dieser Runde (${kern.length} feste + ${modul.ZUSATZ_PRO_RUNDE})`);
|
|
ok(d.punkte.length < kat.ENTWICKLUNG_SELBST.length,
|
|
`weniger als die ${kat.ENTWICKLUNG_SELBST.length} des Katalogs — genau darum geht es`);
|
|
ok(d.fragen_gesamt === kat.ENTWICKLUNG_SELBST.length,
|
|
"und die Seite sagt, wie viele es insgesamt gibt");
|
|
/* JEDE KERNFRAGE IST DABEI. Der Riegel: Waere eine davon in der
|
|
Rotation, erfuehre man die wichtigste Auskunft im Schnitt sechs
|
|
Wochen zu spaet. */
|
|
ok(kern.every((k) => d.punkte.some((p) => p.schluessel === k.schluessel)),
|
|
"jede Kernfrage ist in dieser Runde dabei");
|
|
/* GEGENPROBE: Es sind NICHT alle -- sonst waere die Rotation nur
|
|
behauptet. */
|
|
ok(rest.some((x) => !d.punkte.some((p) => p.schluessel === x.schluessel)),
|
|
"und mindestens eine wechselnde Frage ist diesmal NICHT dran");
|
|
/* IN KATALOGREIHENFOLGE. Wer sie jede Runde anders sortiert sieht,
|
|
liest sie jedes Mal neu statt sie wiederzuerkennen. */
|
|
const ordnung = kat.ENTWICKLUNG_PUNKTE.befinden.map((p) => p.schluessel);
|
|
const gezeigt = d.punkte.map((p) => p.schluessel);
|
|
ok(gezeigt.join() === [...gezeigt].sort((a, b) => ordnung.indexOf(a) - ordnung.indexOf(b)).join(),
|
|
"die Fragen stehen in der Reihenfolge des Katalogs");
|
|
|
|
/* ÜBER VIER RUNDEN KOMMT JEDE FRAGE DRAN. Eine Frage, die nie
|
|
drankommt, koennte man ebenso gut loeschen -- und genau das
|
|
passiert, wenn der Versatz und die Zahl der Fragen einen
|
|
gemeinsamen Teiler haben. */
|
|
{
|
|
/* DIE GRENZE WIRD GERECHNET, NICHT EINGETRAGEN (20.09.2026).
|
|
|
|
Hier stand fest „6 Runden". Das war richtig, solange es sieben
|
|
wechselnde Fragen bei drei je Runde gab. Mit dreizehn Fragen ist
|
|
es nur noch Zufall, ob die Zahl passt -- und eine Pruefung, die
|
|
bei jedem Katalogzuwachs von Hand nachgestellt werden muss, wird
|
|
irgendwann nachgestellt, statt den Fehler zu zeigen.
|
|
|
|
Der Anspruch ist stattdessen: Es darf nicht laenger dauern, als
|
|
es MUSS, plus eine Runde Luft. Weniger als ceil(wechselnde /
|
|
Plaetze) geht rechnerisch nicht; mehr als eine Runde darueber
|
|
heisst, dass die Auswahl sich im Kreis dreht.
|
|
|
|
GENAU SO IST DER FEHLER AUFGEFALLEN, den diese Zeile gefunden
|
|
hat: Eine erste Fassung der feldbewussten Rotation brauchte
|
|
ZWOELF Runden statt vier -- fast ein halbes Jahr, bis jede Frage
|
|
einmal gestellt war. */
|
|
const wechselnde = kat.ENTWICKLUNG_SELBST.filter((p) => !p.kern).length;
|
|
const mindestens = Math.ceil(wechselnde / modul.ZUSATZ_PRO_RUNDE);
|
|
const grenze = mindestens + 1;
|
|
|
|
const gesehen = new Set();
|
|
let gebraucht = null;
|
|
for (let r = 0; r < grenze * 4; r++) {
|
|
for (const p of modul.fragenFuer(idFrida, r).alle) gesehen.add(p.schluessel);
|
|
if (gebraucht === null && gesehen.size === kat.ENTWICKLUNG_SELBST.length) {
|
|
gebraucht = r + 1;
|
|
}
|
|
}
|
|
ok(gesehen.size === kat.ENTWICKLUNG_SELBST.length,
|
|
`jede der ${kat.ENTWICKLUNG_SELBST.length} Fragen kommt überhaupt dran (${gesehen.size})`);
|
|
ok(gebraucht !== null && gebraucht <= grenze,
|
|
`und zwar in ${gebraucht} Runden — rechnerisch nötig sind ${mindestens}, erlaubt ${grenze}`);
|
|
|
|
/* UND JEDE RUNDE STREUT ÜBER DIE FELDER. Vier Zusatzfragen aus
|
|
demselben Feld waeren kein Befinden mehr, sondern ein Interview
|
|
zu einem Thema -- genau das passiert, wenn der Ring dem nach
|
|
Themen sortierten Katalog folgt. */
|
|
let engste = 99;
|
|
for (let r = 0; r < 12; r++) {
|
|
const felder = new Set(modul.fragenFuer(idFrida, r).zusatz.map((p) => p.feld));
|
|
engste = Math.min(engste, felder.size);
|
|
}
|
|
ok(engste >= Math.min(3, modul.ZUSATZ_PRO_RUNDE),
|
|
`auch die engste Runde streut über mindestens 3 Felder (${engste})`);
|
|
/* Und die Auswahl ist STABIL: derselbe Stand, dieselbe Liste.
|
|
Zufall hiesse, dass bei jedem Neuladen etwas anderes dasteht. */
|
|
ok(modul.fragenFuer(idFrida, 2).alle.map((p) => p.schluessel).join()
|
|
=== modul.fragenFuer(idFrida, 2).alle.map((p) => p.schluessel).join(),
|
|
"zweimal dieselbe Runde ergibt zweimal dieselben Fragen");
|
|
ok(modul.fragenFuer(idFrida, 2).alle.map((p) => p.schluessel).join()
|
|
!== modul.fragenFuer(idFrida, 3).alle.map((p) => p.schluessel).join(),
|
|
"die nächste Runde ergibt andere — sonst wäre es keine Rotation");
|
|
}
|
|
|
|
/* Die Recherche: fuenf bis fuenfzehn, unter zwei Minuten. */
|
|
ok(d.punkte.length >= 5 && d.punkte.length <= 15,
|
|
"zwischen fünf und fünfzehn — kurz genug, dass man sie wirklich beantwortet");
|
|
ok(d.rhythmus_tage === modul.RHYTHMUS_TAGE,
|
|
`alle ${d.rhythmus_tage} Tage wird wieder gefragt`);
|
|
ok(d.rhythmus?.faellig === true, "am Anfang ist sofort etwas fällig");
|
|
ok(d.rhythmus?.letzte === null, "und es gab noch keine Runde");
|
|
ok(Array.isArray(d.verlauf) && d.verlauf.length === 0, "der Verlauf ist leer");
|
|
|
|
/* EINE HALBE RUNDE IST KEINE RUNDE. */
|
|
const halb = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
|
|
ok(halb.code === 400, `ohne Antworten lässt sich nichts festhalten (${halb.code})`);
|
|
ok(json(halb).fehler === "unvollstaendig"
|
|
&& json(halb).offen === d.punkte.length,
|
|
`und die Absage sagt, wie viele fehlen (${json(halb).offen} von ${json(halb).gesamt})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. EINE RUNDE, EIN VERLAUF
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Der Verlauf entsteht ===");
|
|
{
|
|
const d = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
|
|
for (const p of d.punkte) {
|
|
await ruf(`/workspace/api/entwicklung/mich/${p.schluessel}`, CREW, "PUT",
|
|
{ cookie: kFrida }, { stufe: "laeuft" });
|
|
}
|
|
const erste = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
|
|
ok(erste.code === 201, `mit allen Antworten geht es (${erste.code})`);
|
|
ok(json(erste).anzahl === d.punkte.length,
|
|
`${json(erste).anzahl} Antworten sind in den Verlauf gewandert`);
|
|
|
|
const nach = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
|
|
ok(nach.verlauf.length === 1, `der Verlauf hat ${nach.verlauf.length} Runde`);
|
|
ok(nach.rhythmus.faellig === false,
|
|
"und es ist erst einmal nichts mehr fällig — die Seite fragt nicht jeden Tag");
|
|
ok(nach.rhythmus.vollstaendig === true, "die Runde gilt als vollständig");
|
|
|
|
/* ==== DER FEHLER, DEN DIESE PRUEFUNG SELBST FESTGESCHRIEBEN HAT ===
|
|
|
|
Hier stand bis zum 19.09.2026:
|
|
|
|
eine Antwort aendern
|
|
POST /runde
|
|
ok(zweite.code === 201, "eine zweite Runde geht auch sofort")
|
|
|
|
Gruen -- und ein Beleg fuer genau das Gegenteil dessen, was sie
|
|
pruefen sollte. Eine Runde galt naemlich als vollstaendig, sobald
|
|
zu jeder Frage IRGENDEIN Stand vorlag. Nach der ersten Runde ist
|
|
das immer der Fall: Die Antworten bleiben stehen. Ab da liess sich
|
|
jede weitere Runde abschliessen, OHNE eine einzige Frage
|
|
anzufassen -- die alten Antworten wanderten mit neuem Datum ein
|
|
zweites Mal in den Verlauf.
|
|
|
|
Der Verlauf zeigte dann eine gerade Linie, und die bedeutete nicht
|
|
"es ist gleich geblieben", sondern "niemand hat gefragt". Genau
|
|
die Sorte Zahl, die dieses Haus verbietet: eine, die etwas
|
|
Falsches behauptet, und der man glaubt.
|
|
|
|
Ab jetzt zaehlt nur, was NACH der letzten Runde gesetzt wurde.
|
|
============================================================== */
|
|
const leer = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
|
|
ok(leer.code === 400,
|
|
`eine zweite Runde ohne neue Antwort geht NICHT (${leer.code})`);
|
|
ok(json(leer).offen === d.punkte.length,
|
|
`und zwar sind alle ${json(leer).offen} Fragen wieder offen — `
|
|
+ "die alten Antworten zaehlen nicht als neue");
|
|
|
|
const zwischen = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
|
|
/* KEINE EINZIGE gilt als frisch. Bei den Fragen, die schon beim
|
|
letzten Mal dran waren, steht die alte Antwort sichtbar da; bei den
|
|
neu hereinrotierten gibt es noch gar keine. Beides ist "offen". */
|
|
ok(zwischen.punkte.every((p) => !p.frisch),
|
|
"keine Antwort gilt als frisch — auch die vom letzten Mal nicht");
|
|
ok(zwischen.punkte.some((p) => p.meins && !p.frisch),
|
|
"die alten stehen trotzdem sichtbar da („vom letzten Mal“)");
|
|
|
|
/* GEGENPROBE: Mit frischen Antworten geht es sehr wohl. Ohne sie
|
|
waere die Zeile oben auch dann gruen, wenn ueberhaupt keine Runde
|
|
mehr moeglich waere. */
|
|
const zweiteFragen = zwischen.punkte;
|
|
for (const p of zweiteFragen) {
|
|
await ruf(`/workspace/api/entwicklung/mich/${p.schluessel}`, CREW, "PUT",
|
|
{ cookie: kFrida }, { stufe: p.schluessel === d.punkte[0].schluessel ? "hakt" : "laeuft" });
|
|
}
|
|
const zweite = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
|
|
ok(zweite.code === 201, `mit frischen Antworten geht die zweite Runde (${zweite.code})`);
|
|
|
|
const nach2 = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
|
|
ok(nach2.verlauf.length === 2, `jetzt ${nach2.verlauf.length} Runden`);
|
|
const neu = nach2.verlauf[0].antworten[d.punkte[0].schluessel];
|
|
const alt = nach2.verlauf[1].antworten[d.punkte[0].schluessel];
|
|
ok(neu === "hakt" && alt === "laeuft",
|
|
`und man sieht die Veränderung (${alt} → ${neu})`);
|
|
/* DIE ANTWORTEN DER ALTEN RUNDE BLEIBEN, WIE SIE WAREN. Ein Verlauf,
|
|
der sich rueckwirkend aendert, ist kein Verlauf. */
|
|
ok(Object.keys(nach2.verlauf[1].antworten).length === d.punkte.length,
|
|
"die alte Runde steht unverändert da");
|
|
/* UND DIE ANALYSE SIEHT ALLE FRAGEN, nicht nur die dieser Runde --
|
|
der Verlauf hoert nicht auf, wenn eine Frage gerade nicht dran ist. */
|
|
ok(nach2.analyse?.zeilen?.length === kat.ENTWICKLUNG_SELBST.length,
|
|
`die Auswertung deckt alle ${kat.ENTWICKLUNG_SELBST.length} Fragen ab `
|
|
+ `(${nach2.analyse?.zeilen?.length}), nicht nur die ${nach2.punkte.length} dieser Runde`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. ES BLEIBT PRIVAT
|
|
|
|
Die eigentliche Frage. Ein Verlauf ueber das Befinden eines Menschen
|
|
ist das Empfindlichste, was dieses Haus speichert.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Privat ===");
|
|
{
|
|
/* DogFather bekommt SEINEN Verlauf, nicht ihren. */
|
|
const seiner = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kDogi }));
|
|
ok(seiner.verlauf.length === 0,
|
|
`DogFather hat einen eigenen, leeren Verlauf (${seiner.verlauf.length})`);
|
|
|
|
/* Es gibt gar keinen Weg zu einem fremden. */
|
|
const fremd = await ruf(`/workspace/api/befinden?person=${idFrida}`, CREW, "GET", { cookie: kDogi });
|
|
ok(json(fremd).verlauf?.length === 0,
|
|
"auch mit einer fremden Nummer in der Adresse bekommt er nur seinen eigenen");
|
|
|
|
/* Im Protokoll steht nur, DASS eine Runde war -- nie was darin stand. */
|
|
const d = oeffne();
|
|
const spur = d.prepare(
|
|
"SELECT detail FROM protokoll WHERE aktion = 'befinden_runde' ORDER BY id DESC").all();
|
|
const verlaufZeilen = d.prepare("SELECT COUNT(*) AS n FROM befinden_verlauf").get().n;
|
|
const runProRunde = d.prepare(
|
|
"SELECT COUNT(*) AS n FROM befinden_verlauf GROUP BY runde ORDER BY runde")
|
|
.all().map((z) => z.n);
|
|
d.close();
|
|
ok(spur.length === 2, `${spur.length} Runden stehen im Protokoll`);
|
|
ok(spur.every((z) => !/laeuft|hakt|waechst|unklar/.test(z.detail || "")),
|
|
`aber keine einzige Antwort darin (${spur[0]?.detail})`);
|
|
/* DIE ZAHL WIRD GERECHNET, NICHT ABGESCHRIEBEN.
|
|
|
|
Hier stand `=== 12` mit dem Kommentar "2 Runden x 6". Am
|
|
17.09.2026 wurden aus sechs Fragen zwoelf, und drei Zeilen in
|
|
dieser Datei waren ab da rot, ohne dass irgendetwas kaputt war.
|
|
Eine feste Zahl ist eine Rechnung von gestern. */
|
|
/* ABGELEITET, NICHT GERECHNET (19.09.2026, mit der Rotation).
|
|
|
|
Hier stand `2 * anzahlFragen` -- zwei Runden mal ALLE Fragen. Seit
|
|
eine Runde nur Kern plus drei Zusatzfragen enthaelt, stimmt das
|
|
nicht mehr. Und die naechste feste Zahl waere derselbe Fehler noch
|
|
einmal: Wer ZUSATZ_PRO_RUNDE aendert, haette sie wieder rot.
|
|
|
|
Gezaehlt wird deshalb, was WIRKLICH in den Runden steht. */
|
|
ok(verlaufZeilen === runProRunde.reduce((x, y) => x + y, 0),
|
|
`im Verlauf liegen ${verlaufZeilen} Antworten (${runProRunde.join(" + ")})`);
|
|
ok(runProRunde.length === 2 && runProRunde.every((n) => n === runProRunde[0]),
|
|
`zwei gleich grosse Runden zu je ${runProRunde[0]} Fragen`);
|
|
|
|
/* UND DIE AMPEL ZAEHLT WEITER NUR DEN AKTUELLEN STAND, ohne Namen. */
|
|
const a = await ruf("/workspace/api/entwicklung/ampel", CREW, "GET", { cookie: kDogi });
|
|
ok(a.code === 200, `die Ampel antwortet (${a.code})`);
|
|
ok(!/Frida/.test(a.text), "und nennt keinen Namen");
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. DIE ENTWICKLUNGSSEITE TUT NUR NOCH EINE SACHE
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Eine Seite, ein Zweck ===");
|
|
{
|
|
const e = readFileSync(new URL("../workspace/entwicklung.html", import.meta.url), "utf8");
|
|
const b = readFileSync(new URL("../workspace/befinden.html", import.meta.url), "utf8");
|
|
|
|
ok(/href="befinden\.html"/.test(e),
|
|
"die Entwicklungsseite verweist auf die sechs Fragen");
|
|
ok(!/id="meins"/.test(e),
|
|
"und zeichnet sie nicht mehr selbst — zwei Stände wären zwei Wahrheiten");
|
|
ok(/id="fragen"/.test(b) && /id="verlauf"/.test(b),
|
|
"die neue Seite hat die Fragen und den Verlauf");
|
|
/* AUF DER EIGENEN SEITE STEHT KEIN FREMDER NAME -- nicht einmal ein
|
|
Platz dafuer. */
|
|
ok(!/personen-block|meine-karte|abweichung/.test(b),
|
|
"und keinen einzigen Abschnitt über andere Menschen");
|
|
|
|
/* DIE SKRIPT-QUELLEN, NICHT DER ROHE TEXT (korrigiert 20.09.2026).
|
|
|
|
Hier stand `!/entwicklung\.js/.test(b)` -- eine Textsuche ueber die
|
|
GANZE Datei. Die schlug an einem KOMMENTAR an, der voellig zu
|
|
Recht die Serverdatei `workspace-entwicklung.js` benennt. Die
|
|
Pruefung meldete damit einen Fehler an einer Stelle, an der nichts
|
|
kaputt war, und stand der eigentlichen Frage im Weg.
|
|
|
|
Die eigentliche Frage lautet: Welche Dateien LAEDT die Seite? Die
|
|
steht in den `src`-Angaben und nirgends sonst. Dieselbe Lehre wie
|
|
bei pruef-schranke am 19.09.: Wer Text sucht, misst Text -- nicht
|
|
die Sache. */
|
|
const quellen = [...b.matchAll(/<script[^>]+src="([^"]+)"/g)]
|
|
/* Nur der Dateiname -- ohne Pfad und ohne Versionsstempel. Damit
|
|
braucht es hier kein Muster, das man falsch schreiben kann: Ein
|
|
Vergleich auf Gleichheit hat keine Randfaelle. (Der erste Anlauf
|
|
benutzte eine Wortgrenze in der Zeichenkette und bekam dadurch
|
|
ein echtes Steuerzeichen in die Datei -- die Pruefung verglich
|
|
danach gegen etwas, das es nirgends gibt, und die Gegenprobe war
|
|
still gruen. Genau die Sorte Pruefung, die nichts prueft.) */
|
|
.map((m) => m[1].split("?")[0].split("/").pop());
|
|
ok(quellen.length > 0, `die Seite laedt ueberhaupt Skripte (${quellen.length})`);
|
|
ok(quellen.includes("befinden.js"),
|
|
`sie laedt ihr eigenes Skript (${quellen.join(", ")})`);
|
|
ok(!quellen.includes("entwicklung.js"),
|
|
"und nicht das der Entwicklungsseite — zwei Stände wären zwei Wahrheiten");
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. IM BROWSER
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Im Browser ===");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
|
|
/* ALS MODI, NICHT ALS DOGFATHER: Die Seite gehoert dem Menschen, der
|
|
sie oeffnet -- und der haeufigste Fall ist ein Modi. Der Keks kommt
|
|
von der Anmeldung oben, weil Playwright die Host-Kopfzeile beim
|
|
Seitenaufruf nicht setzen kann. */
|
|
await kontext.addCookies(kFrida.split("; ").filter(Boolean).map((teil) => {
|
|
const i = teil.indexOf("=");
|
|
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(700);
|
|
|
|
/* DER GEMELDETE FEHLER, IM BROWSER NACHGESTELLT: auf die Kachel
|
|
druecken und nachsehen, wo man landet. */
|
|
const ziel = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('a')]
|
|
.find((a) => /Wie geht/i.test(a.textContent || ''));
|
|
return k ? k.getAttribute('href') : null;
|
|
});
|
|
ok(/befinden\.html/.test(ziel || ""),
|
|
`die Kachel auf der Startseite führt zu ${ziel}`);
|
|
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/befinden.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(900);
|
|
|
|
const stand = await seite.evaluate(() => ({
|
|
fragen: document.querySelectorAll('.e-punkt--meins').length,
|
|
knoepfe: document.querySelectorAll('.e-wahl__knopf').length,
|
|
takt: document.getElementById('takt')?.hidden === false,
|
|
taktText: document.getElementById('takt-satz')?.textContent.trim() || '',
|
|
verlauf: document.getElementById('verlauf-block')?.hidden === false,
|
|
spalten: document.querySelectorAll('.b-verlauf__zeile--kopf .b-verlauf__datum').length,
|
|
punkte: document.querySelectorAll('.b-verlauf__punkt').length,
|
|
/* Die Zeilen OHNE die Kopfzeile -- sie traegt die Datumsangaben,
|
|
keine Antworten. */
|
|
zeilen: document.querySelectorAll('.b-verlauf__zeile:not(.b-verlauf__zeile--kopf)').length,
|
|
text: document.querySelector('main')?.innerText || '',
|
|
}));
|
|
if (process.env.SCHIRM) {
|
|
await seite.screenshot({
|
|
path: "C:/Users/qciga/Documents/Obelix/DogiHompage/blick-befinden.png",
|
|
fullPage: true });
|
|
}
|
|
ok(stand.fragen >= 5, `${stand.fragen} Fragen stehen auf der Seite`);
|
|
ok(stand.knoepfe === stand.fragen * 4, `mit je vier Knöpfen (${stand.knoepfe})`);
|
|
ok(stand.takt === true, `der Rhythmus steht oben: „${stand.taktText.slice(0, 70)}“`);
|
|
/* FRIDA HAT ZWEI RUNDEN -- bei ihr MUSS er dastehen. */
|
|
ok(stand.verlauf === true, "ihr Verlauf steht auf der Seite");
|
|
ok(stand.spalten === 2, `mit zwei Ständen nebeneinander (${stand.spalten})`);
|
|
/* Zeilen x Spalten des Gitters. Die Zeilen sind die Fragen MIT
|
|
GESCHICHTE -- nach zwei Runden mit Rotation sind das mehr als die
|
|
acht dieser Runde und weniger als alle zwoelf. Gerechnet, nicht
|
|
angenommen: Eine feste Zahl waere hier beim naechsten Umbau still
|
|
falsch. */
|
|
ok(stand.punkte === stand.zeilen * stand.spalten && stand.punkte > 0,
|
|
`${stand.punkte} Felder im Gitter (${stand.zeilen} Fragen x ${stand.spalten} Stände)`);
|
|
ok(stand.zeilen > kernAnzahl && stand.zeilen <= anzahlFragen,
|
|
`${stand.zeilen} Fragen haben schon eine Geschichte — mehr als der Kern (${kernAnzahl}), `
|
|
+ `weniger als der ganze Katalog (${anzahlFragen})`);
|
|
/* KEIN FREMDER NAME AUF DEM BILDSCHIRM. Ihr eigener darf oben in der
|
|
Kopfleiste stehen -- gemessen wird deshalb nur der Inhalt, und
|
|
gesucht wird nach DogFather, also nach dem, der hier nichts zu
|
|
suchen hat. */
|
|
ok(!/Filipe/.test(stand.text), "und kein anderer Mensch steht darauf");
|
|
|
|
/* ==== ZWEI DINGE, DIE KEIN ZAHLENVERGLEICH FINDET ================
|
|
|
|
Beide sind beim Ansehen des Bildschirmfotos aufgefallen, nicht
|
|
beim Lesen der Ausgabe -- und beide waren wahr und trotzdem
|
|
irrefuehrend.
|
|
|
|
1. Der Satz "diese Runde sind 8 von 12 dran" stand nur, solange
|
|
etwas offen oder faellig war. Im RUHEZUSTAND -- dem, den man am
|
|
haeufigsten sieht -- fehlte er. Wer dann acht Fragen zaehlt, wo
|
|
zwoelf im Katalog stehen, haelt vier fuer verschwunden.
|
|
|
|
2. "Das war deine Antwort beim letzten Mal" stand an JEDER Frage,
|
|
unmittelbar nachdem man sie beantwortet und die Runde
|
|
abgeschlossen hatte. Technisch richtig (die Antwort ist vor dem
|
|
Rundenabschluss gesetzt), als Auskunft Unsinn.
|
|
============================================================== */
|
|
ok(/von \d+ Fragen dran/.test(stand.taktText),
|
|
`der Satz erklaert die kurze Runde auch im Ruhezustand: `
|
|
+ `„…${(stand.taktText.match(/Diese Runde[^.]*\./) || [""])[0].slice(0, 60)}…“`);
|
|
const ruhig = await seite.evaluate(() => ({
|
|
alt: document.querySelectorAll('.e-punkt[data-alt="ja"]').length,
|
|
hinweise: document.querySelectorAll('.e-punkt__alt').length,
|
|
kernmarken: document.querySelectorAll('.e-punkt__marke').length,
|
|
}));
|
|
ok(ruhig.alt === 0 && ruhig.hinweise === 0,
|
|
`direkt nach einer Runde steht nirgends „vom letzten Mal“ (${ruhig.hinweise})`);
|
|
ok(ruhig.kernmarken === kernAnzahl,
|
|
`die ${kernAnzahl} festen Fragen sind als solche gekennzeichnet (${ruhig.kernmarken})`);
|
|
|
|
/* GEGENPROBE: Ist eine Runde wirklich faellig, MUSS der Hinweis da
|
|
sein -- sonst hiesse die Zeile oben nur, dass er nie erscheint.
|
|
Die Runde wird dafuer um mehr als den Rhythmus zurueckdatiert;
|
|
die Uhr laesst sich nicht vorstellen. */
|
|
{
|
|
/* ALLE Runden verschieben, nicht nur die letzte.
|
|
|
|
Erster Anlauf datierte nur die juengste zurueck -- danach war
|
|
die ZWEITGROESSTE die juengste, und die lag weiterhin auf heute.
|
|
Die Seite meldete folgerichtig "ruhig", und die Gegenprobe war
|
|
rot, ohne dass an der Sache etwas falsch war.
|
|
|
|
In JavaScript gerechnet und nicht in SQL: `datetime(runde, '-15
|
|
days')` gibt ein anderes Format zurueck (Leerzeichen statt T,
|
|
ohne Z) -- Date.parse liest das als ORTSZEIT, und die Rechnung
|
|
waere danach um zwei Stunden daneben. */
|
|
const versatz = (modul.RHYTHMUS_TAGE + 1) * 86400000;
|
|
const dd = oeffne();
|
|
const runden = dd.prepare(
|
|
"SELECT DISTINCT runde FROM befinden_verlauf WHERE person_id = ?").all(idFrida)
|
|
.map((z) => z.runde);
|
|
const setzen = dd.prepare(
|
|
"UPDATE befinden_verlauf SET runde = ?, am = ? WHERE person_id = ? AND runde = ?");
|
|
const alteStaende = runden.map((r) => [r, new Date(Date.parse(r) - versatz).toISOString()]);
|
|
for (const [vorher, nachher] of alteStaende) setzen.run(nachher, nachher, idFrida, vorher);
|
|
|
|
/* UND DIE ANTWORTEN GLEICH MIT. Im Betrieb werden sie gesetzt und
|
|
kurz darauf die Runde abgeschlossen -- sie sind also IMMER etwas
|
|
aelter als die Runde. Nur die Runden zurueckzudatieren erzeugte
|
|
einen Zustand, den es nie gibt: Antworten von heute unter einer
|
|
Runde von vor zwei Wochen. Die Seite hielt sie folgerichtig fuer
|
|
frisch, und die Gegenprobe war rot an der falschen Stelle. */
|
|
const antworten = dd.prepare(
|
|
`SELECT schluessel, am FROM entwicklung_stand
|
|
WHERE person_id = ? AND von_id = ?`).all(idFrida, idFrida);
|
|
const setzenA = dd.prepare(
|
|
`UPDATE entwicklung_stand SET am = ?
|
|
WHERE person_id = ? AND von_id = ? AND schluessel = ?`);
|
|
const alteAntworten = antworten.map((z) =>
|
|
[z.schluessel, z.am, new Date(Date.parse(z.am) - versatz).toISOString()]);
|
|
for (const [k, , nachher] of alteAntworten) setzenA.run(nachher, idFrida, idFrida, k);
|
|
dd.close();
|
|
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(900);
|
|
const faellig = await seite.evaluate(() => ({
|
|
lage: document.getElementById('takt')?.dataset.lage || '',
|
|
alt: document.querySelectorAll('.e-punkt[data-alt="ja"]').length,
|
|
hinweise: document.querySelectorAll('.e-punkt__alt').length,
|
|
fragen: document.querySelectorAll('.e-punkt--meins').length,
|
|
knopf: !document.getElementById('takt-knopf')?.hidden,
|
|
}));
|
|
ok(faellig.lage === "faellig", `jetzt ist eine Runde fällig (${faellig.lage})`);
|
|
ok(faellig.hinweise > 0 && faellig.alt > 0,
|
|
`und die alten Antworten sind als solche gekennzeichnet (${faellig.hinweise} von ${faellig.fragen})`);
|
|
/* UND DER KNOPF IST WEG, solange nichts bestaetigt ist. Genau das
|
|
war der Fehler: Er stand da und schloss eine leere Runde ab. */
|
|
ok(faellig.knopf === false,
|
|
"der Knopf zum Abschließen ist weg, solange nichts bestätigt ist");
|
|
|
|
/* ZURUECKDREHEN. Ohne das liefe der Rest dieser Datei auf einem
|
|
Stand, den die Pruefung selbst verbogen hat -- Abschnitt 7 fand
|
|
danach ein "besser" statt eines Musters und meldete fuenf
|
|
Fehler, die keine waren. Genau die Sorte Befund, die man
|
|
stundenlang im falschen Code sucht. */
|
|
const dz = oeffne();
|
|
const zurueckSetzen = dz.prepare(
|
|
"UPDATE befinden_verlauf SET runde = ?, am = ? WHERE person_id = ? AND runde = ?");
|
|
for (const [vorher, nachher] of alteStaende) zurueckSetzen.run(vorher, vorher, idFrida, nachher);
|
|
const zurueckA = dz.prepare(
|
|
`UPDATE entwicklung_stand SET am = ?
|
|
WHERE person_id = ? AND von_id = ? AND schluessel = ?`);
|
|
for (const [k, vorher] of alteAntworten) zurueckA.run(vorher, idFrida, idFrida, k);
|
|
dz.close();
|
|
const zurueck = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida }));
|
|
ok(zurueck.rhythmus?.letzte === runden.sort().at(-1)
|
|
&& zurueck.rhythmus?.faellig === false,
|
|
"der Stand ist danach wieder so, wie er war");
|
|
}
|
|
|
|
/* GEGENPROBE mit einem zweiten Menschen: DogFather hat keine einzige
|
|
Runde. Bei ihm muss derselbe Block WEG sein -- sonst hiesse „er
|
|
erscheint ab der zweiten Runde" nur, dass er immer erscheint. */
|
|
const zweiter = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
await zweiter.addCookies(kDogi.split("; ").filter(Boolean).map((teil) => {
|
|
const i = teil.indexOf("=");
|
|
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite2 = await zweiter.newPage();
|
|
await seite2.goto(`http://127.0.0.1:${PORT}/workspace/befinden.html`, { waitUntil: "networkidle" });
|
|
await seite2.waitForTimeout(800);
|
|
const ohne = await seite2.evaluate(() => ({
|
|
fragen: document.querySelectorAll('.e-punkt--meins').length,
|
|
verlauf: document.getElementById('verlauf-block')?.hidden === false,
|
|
knopf: document.getElementById('takt-knopf')?.hidden === false,
|
|
}));
|
|
ok(ohne.fragen >= 5, `auch bei ihm stehen die Fragen da (${ohne.fragen})`);
|
|
ok(ohne.verlauf === false, "aber kein Verlauf — er hat noch keine Runde");
|
|
ok(ohne.knopf === false,
|
|
"und kein Knopf zum Abschließen, solange nichts beantwortet ist");
|
|
|
|
ok(konsole.length === 0,
|
|
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser");
|
|
await browser.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. WAS DEIN VERLAUF SAGT
|
|
|
|
Filipe: „mach mir auch eine analyse, mach eine perfektion draus. eine
|
|
professionelle auch mit infos und so aus aller welt was tiktok
|
|
angeht."
|
|
|
|
Eine Auswertung über das eigene Befinden ist das Empfindlichste, was
|
|
dieses Haus rechnet. Also dieselben drei Fragen wie beim Verlauf:
|
|
Sagt sie das Richtige? Sagt sie es nur IHR? Und sagt sie gar nichts,
|
|
wenn sie noch nichts sagen kann?
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Die Analyse ===");
|
|
{
|
|
const holen = async (keks, host) =>
|
|
json(await ruf("/workspace/api/befinden", host, "GET", { cookie: keks }));
|
|
|
|
/* Stand nach Abschnitt 3: zwei Runden. In der zweiten hakt der erste
|
|
Punkt, vorher lief er -- das ist „neu", noch kein Muster. */
|
|
const d = await holen(kFrida, CREW);
|
|
const erste = d.punkte[0].schluessel;
|
|
ok(d.analyse?.moeglich === true, `mit ${d.analyse?.runden} Runden gibt es eine Analyse`);
|
|
ok(d.analyse.zeilen.length === anzahlFragen,
|
|
`sie geht alle ${d.analyse.zeilen.length} Fragen des Katalogs durch, `
|
|
+ `nicht nur die ${d.punkte.length} dieser Runde und nicht nur die schlechten`);
|
|
const z1 = d.analyse.zeilen.find((z) => z.schluessel === erste);
|
|
ok(z1.art === "neu", `der frisch hakende Punkt gilt als neu, nicht als Muster (${z1.art})`);
|
|
ok(/noch kein Muster/i.test(d.analyse.satz),
|
|
`und der Satz oben sagt genau das: „${d.analyse.satz.slice(0, 60)}…“`);
|
|
|
|
/* DIE EINORDNUNG IST DER TEIL, DER „AUS ALLER WELT" HEISST. Sie steht
|
|
bei JEDER Zeile -- auch bei denen, die laufen. */
|
|
ok(d.analyse.zeilen.every((z) => z.welt && z.welt.length > 40),
|
|
`zu jeder der ${anzahlFragen} Fragen steht ein belegter Befund`);
|
|
ok(new Set(d.analyse.zeilen.map((z) => z.welt)).size === anzahlFragen,
|
|
"und jeder ist ein anderer — kein Satz, der zu allen passen würde");
|
|
/* „Was hilft" NUR dort, wo es etwas zu tun gibt. */
|
|
ok(d.analyse.zeilen.filter((z) => z.hilft).length === 1,
|
|
`ein Rat steht nur an der Stelle, an der es hakt (${d.analyse.zeilen.filter((z) => z.hilft).length})`);
|
|
|
|
/* ---- Und jetzt dasselbe ein zweites Mal: daraus wird ein Muster ---- */
|
|
for (const p of d.punkte) {
|
|
await ruf(`/workspace/api/entwicklung/mich/${p.schluessel}`, CREW, "PUT",
|
|
{ cookie: kFrida }, p.schluessel === erste
|
|
? { stufe: "hakt", anlass: "Immer noch dasselbe." } : { stufe: "laeuft" });
|
|
}
|
|
const dritte = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {});
|
|
ok(dritte.code === 201, `eine dritte Runde (${dritte.code})`);
|
|
|
|
const n = await holen(kFrida, CREW);
|
|
const z2 = n.analyse.zeilen.find((z) => z.schluessel === erste);
|
|
ok(z2.art === "dauerhaft", `jetzt ist es ein Muster (${z2.art})`);
|
|
/* "MALE", NICHT "RUNDEN": Mit der Rotation ist eine Frage nicht in
|
|
jeder Runde dran -- gezaehlt wird, wie oft sie gestellt wurde. */
|
|
ok(z2.male === modul.MUSTER_AB, `über ${z2.male} Male hintereinander gleich`);
|
|
ok(z2.gefragt >= z2.male,
|
|
`und sie war insgesamt ${z2.gefragt}x dran — die Zahl gibt es wirklich`);
|
|
ok(/kein schlechter Tag/i.test(n.analyse.satz),
|
|
`und der Satz sagt, worauf es ankommt: „${n.analyse.satz.slice(0, 64)}…“`);
|
|
/* DIE REIHENFOLGE MACHT DIE AUSSAGE -- das Muster gehört nach oben.
|
|
Sortiert wird im Browser; hier wird geprüft, dass die Angabe dafür
|
|
überhaupt mitkommt. */
|
|
ok(n.analyse.zahlen.dauerhaft === 1 && n.analyse.zahlen.neu === 0,
|
|
`die Zahlen daneben stimmen (${JSON.stringify(n.analyse.zahlen)})`);
|
|
|
|
/* ---- Sie gehört ihr allein ---- */
|
|
const seine = await holen(kDogi, CREW);
|
|
ok(seine.analyse?.moeglich === false,
|
|
"DogFather bekommt seine eigene — und die ist leer");
|
|
ok(!JSON.stringify(seine).includes("Immer noch dasselbe"),
|
|
"nichts von ihr steht darin");
|
|
|
|
/* ---- Und ohne zweite Runde gibt es sie gar nicht ---- */
|
|
ok(seine.analyse.ab === modul.MUSTER_AB,
|
|
`sie sagt, ab wann es sie gibt (ab ${seine.analyse.ab} Runden)`);
|
|
ok(Array.isArray(seine.analyse.zeilen) && seine.analyse.zeilen.length === 0,
|
|
"und liefert bis dahin keine einzige Zeile");
|
|
|
|
/* ---- Im Protokoll steht weiterhin nichts davon ---- */
|
|
const db = oeffne();
|
|
const spuren = db.prepare("SELECT aktion, detail FROM protokoll").all();
|
|
db.close();
|
|
ok(!spuren.some((x) => /analyse/i.test(x.aktion || "")),
|
|
"eine Analyse ist kein Vorgang — sie steht in keinem Protokoll");
|
|
ok(!spuren.some((x) => /Immer noch dasselbe/.test(x.detail || "")),
|
|
"und ihr Anlass erst recht nicht");
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. DIE ANALYSE AUF DEM BILDSCHIRM
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 8. Die Analyse im Browser ===");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
await kontext.addCookies(kFrida.split("; ").filter(Boolean).map((teil) => {
|
|
const i = teil.indexOf("=");
|
|
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/befinden.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1000);
|
|
|
|
const a = await seite.evaluate(() => ({
|
|
da: document.getElementById("analyse-block")?.hidden === false,
|
|
satz: document.getElementById("analyse-satz")?.textContent.trim() || "",
|
|
zeilen: document.querySelectorAll(".b-analyse__zeile").length,
|
|
ersteMarke: document.querySelector(".b-analyse__zeile")?.dataset.ton || "",
|
|
welt: document.querySelectorAll(".b-analyse__welt").length,
|
|
hilft: document.querySelectorAll(".b-analyse__hilft").length,
|
|
quellen: [...document.querySelectorAll(".b-analyse__quelle-hin")].map((x) => x.href),
|
|
blank: [...document.querySelectorAll(".b-analyse__quelle-hin")]
|
|
.every((x) => x.rel.includes("noopener")),
|
|
}));
|
|
ok(a.da, "die Analyse steht auf der Seite");
|
|
ok(a.zeilen === anzahlFragen,
|
|
`mit allen ${anzahlFragen} Fragen (${a.zeilen})`);
|
|
/* DAS MUSTER STEHT OBEN. Alphabetisch oder in Katalogreihenfolge
|
|
müsste man es suchen -- und wer sucht, liest es beim zweiten Mal
|
|
nicht mehr. */
|
|
ok(a.ersteMarke === "hakt",
|
|
`und das, was seit mehreren Runden hakt, steht zuoberst (${a.ersteMarke})`);
|
|
ok(a.welt === anzahlFragen,
|
|
`zu jeder der ${anzahlFragen} Zeilen die Einordnung (${a.welt})`);
|
|
ok(a.hilft === 1, `ein Rat, genau dort, wo es hakt (${a.hilft})`);
|
|
ok(a.quellen.length === 3, `${a.quellen.length} Quellen stehen darunter`);
|
|
ok(a.quellen.some((x) => /tiktok/i.test(x)),
|
|
"darunter eine zu TikTok — danach hatte Filipe ausdrücklich gefragt");
|
|
ok(a.blank, "die Verweise öffnen ohne Zugriff aufs eigene Fenster (noopener)");
|
|
|
|
/* ---- DIE FELDER UND DIE EINE ZUSAMMENFASSUNG (20.09.2026) --------
|
|
|
|
Filipe: „viel mehr fragen aber kategorisiert … und am ende EINE
|
|
gesamte zusammenfassung."
|
|
|
|
Gemessen wird hier NICHT, ob ein Text dasteht, sondern:
|
|
* dass die Fragen unter Überschriften stehen -- und zwar unter
|
|
MEHREREN, sonst ist es eine Überschrift über allem und keine
|
|
Gliederung;
|
|
* dass jede Frage unter einer steht und keine darüber hängt;
|
|
* dass die Zusammenfassung GENAU EINMAL vorkommt. Eine je
|
|
Kategorie war ausdrücklich nicht gewünscht, und eine Prüfung,
|
|
die nur "mindestens eine" verlangt, würde sechs durchlassen. */
|
|
const g = await seite.evaluate(() => {
|
|
const felder = [...document.querySelectorAll("#fragen .e-feld")];
|
|
const fragen = [...document.querySelectorAll("#fragen .e-punkt")];
|
|
/* Wie viele Fragen stehen VOR der ersten Überschrift? Jede davon
|
|
hängt ohne Thema in der Luft. */
|
|
const ersteFeldPos = felder.length
|
|
? [...document.getElementById("fragen").children].indexOf(felder[0]) : -1;
|
|
const kinder = [...document.getElementById("fragen").children];
|
|
const heimatlos = kinder.slice(0, ersteFeldPos < 0 ? kinder.length : ersteFeldPos)
|
|
.filter((k) => k.classList.contains("e-punkt")).length;
|
|
const fazit = document.getElementById("fazit-block");
|
|
return {
|
|
felder: felder.length,
|
|
feldNamen: felder.map((f) => f.querySelector(".e-feld__wort")?.textContent.trim()),
|
|
fragen: fragen.length,
|
|
heimatlos,
|
|
fazitDa: fazit && fazit.hidden === false,
|
|
fazitAnzahl: document.querySelectorAll(".b-fazit").length,
|
|
kopf: document.getElementById("fazit-kopf")?.textContent.trim() || "",
|
|
traegt: document.querySelectorAll("#fazit-traegt-liste li").length,
|
|
schluss: document.getElementById("fazit-schluss")?.textContent.trim() || "",
|
|
zahlen: document.getElementById("fazit-zahlen")?.textContent.trim() || "",
|
|
};
|
|
});
|
|
ok(g.felder >= 3,
|
|
`die Fragen stehen unter ${g.felder} Feldüberschriften — nicht in einer Reihe`);
|
|
ok(g.heimatlos === 0,
|
|
`keine Frage hängt über der ersten Überschrift (${g.heimatlos})`);
|
|
ok(g.feldNamen.every((x) => x && x.length > 3),
|
|
`jede Überschrift hat einen Namen (${g.feldNamen.join(", ")})`);
|
|
ok(g.fazitDa === true, "die Zusammenfassung steht am Ende der Fragen");
|
|
ok(g.fazitAnzahl === 1,
|
|
`und zwar GENAU EINMAL (${g.fazitAnzahl}) — eine je Kategorie war ausdrücklich nicht gewünscht`);
|
|
ok(g.kopf.length > 30, `mit einem ganzen Satz („${g.kopf.slice(0, 52)}…")`);
|
|
ok(g.schluss.length > 20, `und einem Schlusssatz („${g.schluss.slice(0, 46)}…")`);
|
|
ok(/\d+ Fragen aus \d+ Feldern/.test(g.zahlen),
|
|
`die Belegzahlen stehen darunter („${g.zahlen}")`);
|
|
|
|
/* ---- KEIN TEXT AUF TEXT, AUCH AM HANDY (20.09.2026) --------------
|
|
|
|
Gefunden hat das ein Bildschirmfoto bei 430 px: Die Unterzeile
|
|
„Deine Antworten liest niemand ausser dir" lag 18 px UNTER dem
|
|
Absatz darunter -- Buchstaben in Buchstaben, ausgerechnet in dem
|
|
Satz, in dem Vertrauen entsteht.
|
|
|
|
Ursache war eine feste Zahl: `-18px` Abstand, gerechnet gegen die
|
|
26 px, die das Element darueber mitbrachte. In der Kopfzeile sind
|
|
es aber 0 px. Zwei fuer sich richtige Regeln, und dazwischen ein
|
|
Schaden, den keine von beiden verursacht.
|
|
|
|
Warum das hier stehen bleibt: Es ist kein Fehler, den man beim
|
|
Lesen sieht -- beide Regeln wirken einzeln vernuenftig. Nur eine
|
|
Messung findet ihn wieder, wenn jemand an einem der beiden
|
|
Abstaende dreht. */
|
|
const enge = await seite.setViewportSize({ width: 430, height: 900 })
|
|
.then(() => seite.waitForTimeout(500))
|
|
.then(() => seite.evaluate(() => {
|
|
const kandidaten = [...document.querySelectorAll("body *")].filter((e) => {
|
|
const r = e.getBoundingClientRect();
|
|
return r.width > 20 && r.height > 6
|
|
&& /P|H1|H2|H3|LI/.test(e.tagName)
|
|
&& e.textContent.trim().length > 12;
|
|
});
|
|
const treffer = [];
|
|
for (let i = 0; i < kandidaten.length; i++) {
|
|
for (let j = i + 1; j < kandidaten.length; j++) {
|
|
const x = kandidaten[i], y = kandidaten[j];
|
|
if (x.contains(y) || y.contains(x)) continue;
|
|
const a2 = x.getBoundingClientRect(), b2 = y.getBoundingClientRect();
|
|
const ux = Math.min(a2.right, b2.right) - Math.max(a2.left, b2.left);
|
|
const uy = Math.min(a2.bottom, b2.bottom) - Math.max(a2.top, b2.top);
|
|
if (ux > 8 && uy > 6) {
|
|
treffer.push(`${x.className.split(" ")[0]} über ${y.className.split(" ")[0]} (${Math.round(uy)} px)`);
|
|
}
|
|
}
|
|
}
|
|
return { treffer, geprueft: kandidaten.length };
|
|
}));
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Faende der Wahlausdruck nichts,
|
|
waere `treffer` leer und die Pruefung gruen, ohne etwas angesehen
|
|
zu haben -- genau der Fehler vom 01.09. mit `every()` auf einer
|
|
leeren Liste. */
|
|
ok(enge.geprueft > 20 && enge.treffer.length === 0,
|
|
enge.treffer.length
|
|
? `Text liegt auf Text: ${enge.treffer.slice(0, 2).join("; ")}`
|
|
: `bei 430 px liegt kein Text auf Text (${enge.geprueft} Absätze verglichen)`);
|
|
await seite.setViewportSize({ width: 1280, height: 1000 });
|
|
if (process.env.SCHIRM) {
|
|
await seite.locator("#analyse-block").screenshot(
|
|
{ path: "C:/Users/qciga/Documents/Obelix/DogiHompage/blick.png" });
|
|
}
|
|
ok(konsole.length === 0,
|
|
konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser");
|
|
await browser.close();
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- */
|
|
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);
|