Bisher waren die acht Bretter acht Silos. Ein Wunsch wurde angenommen,
irgendwann passierte etwas im Stream, irgendwann lag ein Clip bei den
Highlights -- und nichts davon wusste voneinander. Wer den Wunsch
geschrieben hatte, erfuhr NIE, dass er erfuellt wurde.
Wunsch -> Termin bei "Was ansteht" -> Clip bei "Highlights"
Das ist der Unterschied zwischen einem Briefkasten und einer Community.
Jemand schreibt einen Wunsch und sieht ihn vier Wochen spaeter als Clip
wieder -- mit seinem Namen daneben.
ZUM DRITTEN MAL LAG DER PLAN DANEBEN. Dort stand "`aus_eintrag_id`
gibt es in der Tabelle bereits". Gibt es -- an den AUFGABEN. Die
Eintraege hatten nichts dergleichen. Dreimal an einem Tag, und jedes
Mal hat es das Messen gefunden, nicht das Nachdenken.
EINE HANDLUNG, KEIN AUSWAHLFELD. Der Zusammenhang entsteht durch
"daraus wird ein Termin" -- als Knopf an dem Wunsch, um den es geht.
Wer ein Formular ausfuellt, denkt nicht an den Wunsch von letzter
Woche, und ein Auswahlfeld mit vierzig Eintraegen benutzt niemand.
Die Kette steht auf beiden Karten: "Aus Wunschliste: ... von Lena"
(ruhig, es ist Herkunft) und "Daraus wurde: Highlights ..." (gruen, das
ist die Nachricht, auf die jemand gewartet hat).
ON DELETE SET NULL, nicht CASCADE: Wird der Wunsch geloescht, bleibt
der Clip. Er ist ja trotzdem passiert.
UND DIE PRUEFUNG VON STUFE 2 HAT STUFE 5 ERWISCHT.
Der Startkatalog stand ueber dem Countdown und schob die Antwort auf
"wann ist der naechste Stream" von 458 px auf 1488 px -- aus dem
ersten Handybildschirm heraus. Gefunden hat das nicht das Auge,
sondern das Abnahmekriterium aus §7 des Plans, das seit heute Vormittag
in pruef-countdown steht.
DIE REGEL DARAUS: Ein WERKZEUG (fuer das Team) draengt nie eine
ANTWORT (fuer alle) nach unten.
Der Countdown steht jetzt ganz oben, noch vor den Filtern -- gemessen
342 px statt 458.
NEU: pruef-kreislauf (23 Pruefungen)
Lena schreibt einen Wunsch, daraus wird ein Termin, daraus ein
Highlight. Geprueft: Der Titel wandert mit, der NAME wandert mit, beide
Enden sehen die Kette, Lena sieht sie auch (ein Kreislauf, den nur das
Team sieht, ist keiner), zweimal derselbe Weg verdoppelt nichts -- und
die Gegenprobe, dass ein ANDERER Weg vom selben Wunsch sehr wohl geht.
Und wohin sie NICHT fuehrt: nicht in die Content-Planung eines
Creators. Dort steht Arbeit, die die Community nichts angeht.
pruef-countdown 22, pruef-treff-start 27, pruef-wunschliste 30,
pruef-galerie 26, pruef-treff 66, pruef-bremse 16, pruef-css-klassen:
alle gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
272 lines
12 KiB
JavaScript
272 lines
12 KiB
JavaScript
/* =====================================================================
|
|
DER KREISLAUF (17.09.2026) -- Stufe 8 des Plans im Vault
|
|
|
|
Bisher waren die acht Bretter acht Silos. Ein Wunsch wurde
|
|
angenommen, irgendwann passierte etwas im Stream, irgendwann lag ein
|
|
Clip bei den Highlights -- und nichts davon wusste voneinander. Wer
|
|
den Wunsch geschrieben hatte, erfuhr nie, dass er erfuellt wurde.
|
|
|
|
Wunsch -> Termin bei "Was ansteht" -> Clip bei "Highlights"
|
|
|
|
DAS IST DER UNTERSCHIED ZWISCHEN EINEM BRIEFKASTEN UND EINER
|
|
COMMUNITY. Jemand schreibt einen Wunsch und sieht ihn vier Wochen
|
|
spaeter als Clip wieder, mit seinem Namen daneben.
|
|
|
|
---------------------------------------------------------------------
|
|
UND DER PLAN LAG ZUM DRITTEN MAL DANEBEN
|
|
|
|
Dort stand: "`aus_eintrag_id` gibt es in der Tabelle bereits". Gibt
|
|
es -- an den AUFGABEN. Die Eintraege hatten nichts dergleichen.
|
|
Auch ein Plan von heute Nacht ist eine Bestandsliste und altert;
|
|
dreimal an einem Tag hat er sich geirrt, und jedes Mal hat es das
|
|
Messen gefunden, nicht das Nachdenken.
|
|
|
|
---------------------------------------------------------------------
|
|
VIER FRAGEN
|
|
|
|
1. Entsteht die Kette -- und traegt der neue Eintrag den Namen
|
|
dessen, der den Wunsch geschrieben hat?
|
|
2. Sieht man sie von BEIDEN Enden?
|
|
3. Zweimal derselbe Weg darf nichts verdoppeln.
|
|
4. Und sie bleibt in der Community: ein Weg in die Content-Planung
|
|
eines Creators waere ein Leck.
|
|
|
|
Aufrufen mit: node server/pruef-kreislauf.mjs
|
|
Bild: SCHIRM=1 node server/pruef-kreislauf.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { portMussFreiSein } from "./helfer-port.mjs";
|
|
import { heuteLokal } from "./helfer-tag.mjs";
|
|
|
|
const PORT = await portMussFreiSein(4491, "pruef-kreislauf");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-kreis-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
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 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-kreislauf");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
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 { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const 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);
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen"
|
|
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
|
}
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Lena", "modi", "CODE-LENA-0001");
|
|
const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name);
|
|
d.close();
|
|
|
|
async function anmelden(kachel, code) {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: kachel, code }),
|
|
});
|
|
return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
const dogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const lena = await anmelden("creator", "CODE-LENA-0001");
|
|
const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS });
|
|
|
|
async function liste(bereich, keks = dogi) {
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/${bereich}`, { headers: { cookie: keks } });
|
|
return (await a.json().catch(() => null))?.eintraege || [];
|
|
}
|
|
async function daraus(vonBereich, id, nach, keks = dogi) {
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/${vonBereich}/${id}/daraus`, {
|
|
method: "POST", headers: kopf(keks), body: JSON.stringify({ nach }),
|
|
});
|
|
return { status: a.status, daten: await a.json().catch(() => null) };
|
|
}
|
|
|
|
/* ---------- LENA schreibt einen Wunsch ------------------------------- */
|
|
const w = await fetch(`${BASIS}/workspace/api/bereich/wunsch`, {
|
|
method: "POST", headers: kopf(lena),
|
|
body: JSON.stringify({ art: "gemeinsam",
|
|
titel: "Ein Abend, an dem wir zusammen zocken", datum: heuteLokal() }),
|
|
});
|
|
const wunschId = (await w.json().catch(() => null))?.id || 0;
|
|
|
|
/* =======================================================================
|
|
0. DER PLATZ DAFUER
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 0. Die Spalte ===");
|
|
ok(spalten.includes("aus_eintrag_id"),
|
|
`die Eintraege haben 'aus_eintrag_id' (${spalten.length} Spalten)`);
|
|
ok(wunschId > 0, `Lena hat einen Wunsch geschrieben (#${wunschId})`);
|
|
|
|
/* =======================================================================
|
|
1. AUS DEM WUNSCH WIRD EIN TERMIN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Wunsch wird Termin ===");
|
|
let terminId = 0;
|
|
{
|
|
const a = await daraus("wunsch", wunschId, "ansteht");
|
|
ok(a.status === 201, `daraus wird ein Termin (${a.status} ${a.daten?.fehler || ""})`);
|
|
terminId = a.daten?.id || 0;
|
|
|
|
const termine = await liste("ansteht");
|
|
const t = termine.find((x) => x.id === terminId);
|
|
ok(t?.titel === "Ein Abend, an dem wir zusammen zocken",
|
|
`der Titel wandert mit ("${t?.titel}")`);
|
|
|
|
/* DER NAME IST DER GANZE PUNKT. Ein Termin, unter dem steht "Aus
|
|
Wunschliste -- von Lena", ist etwas anderes als ein Termin. */
|
|
ok(t?.aus?.bereich === "wunsch", `und er weiss, woher er kam (${t?.aus?.bereich})`);
|
|
ok(t?.aus?.von_name === "Lena",
|
|
`mit dem Namen dessen, der den Wunsch schrieb ("${t?.aus?.von_name}")`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. VON BEIDEN ENDEN SICHTBAR
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Beide Enden ===");
|
|
{
|
|
/* OHNE DIESEN ABSCHNITT waere die Kette eine Einbahnstrasse: Der
|
|
Termin wuesste vom Wunsch, aber wer den Wunsch ansieht, erfuehre
|
|
nie, dass etwas daraus wurde -- und genau das ist der Mangel, um
|
|
den es hier geht. */
|
|
const wuensche = await liste("wunsch");
|
|
const wu = wuensche.find((x) => x.id === wunschId);
|
|
ok((wu?.wurde || []).length === 1, `am Wunsch steht, was daraus wurde (${(wu?.wurde || []).length})`);
|
|
ok(wu?.wurde?.[0]?.bereich === "ansteht",
|
|
`und wohin (${wu?.wurde?.[0]?.bereich}: ${wu?.wurde?.[0]?.titel})`);
|
|
|
|
/* AUCH LENA SIEHT ES. Ein Kreislauf, den nur das Team sieht, ist
|
|
keiner -- der Sinn ist, dass die Person es erfaehrt. */
|
|
const beiLena = (await liste("wunsch", lena)).find((x) => x.id === wunschId);
|
|
ok((beiLena?.wurde || []).length === 1,
|
|
`und Lena sieht es auch (${(beiLena?.wurde || []).length})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. DIE KETTE GEHT WEITER -- UND NICHT ZWEIMAL
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Weiter zum Highlight ===");
|
|
{
|
|
const a = await daraus("ansteht", terminId, "highlight");
|
|
ok(a.status === 201, `aus dem Termin wird ein Highlight (${a.status})`);
|
|
|
|
const h = (await liste("highlight")).find((x) => x.id === a.daten?.id);
|
|
ok(h?.aus?.bereich === "ansteht", `es kennt seinen Termin (${h?.aus?.bereich})`);
|
|
|
|
/* ZWEIMAL DERSELBE WEG DARF NICHTS VERDOPPELN. Zwei Termine aus
|
|
einem Wunsch waeren ein Versehen, kein Wunsch. */
|
|
const z = await daraus("wunsch", wunschId, "ansteht");
|
|
ok(z.status === 200 && z.daten?.schon === true,
|
|
`derselbe Weg noch einmal legt nichts nach (${z.status}, schon=${z.daten?.schon})`);
|
|
ok(z.daten?.id === terminId, "und verweist auf den vorhandenen");
|
|
|
|
/* GEGENPROBE: Ein ANDERER Weg vom selben Wunsch geht sehr wohl --
|
|
sonst hiesse "schon" nur, dass ab dem zweiten Mal nichts mehr
|
|
geht. */
|
|
const g = await daraus("wunsch", wunschId, "highlight");
|
|
ok(g.status === 201, `ein anderer Weg vom selben Wunsch geht (${g.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. DIE KETTE BLEIBT IN DER COMMUNITY
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Wohin sie NICHT fuehrt ===");
|
|
{
|
|
/* Ein Weg in die Content-Planung eines Creators waere ein Leck:
|
|
Dort steht Arbeit, die die Community nichts angeht. */
|
|
const a = await daraus("wunsch", wunschId, "content");
|
|
ok(a.status === 404, `nicht in die Content-Planung (${a.status})`);
|
|
const b = await daraus("content", 1, "wunsch");
|
|
ok(b.status === 404, `und nicht von dort hierher (${b.status})`);
|
|
|
|
/* UND EINE ERFUNDENE NUMMER ist kein Sonderfall. */
|
|
const c = await daraus("wunsch", 999999, "ansteht");
|
|
ok(c.status === 404, `eine erfundene Quelle ebenfalls nicht (${c.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. IM BROWSER
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Was man sieht ===");
|
|
{
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1100, height: 950 } });
|
|
await kontext.addCookies(dogi.split("; ").map((k) => {
|
|
const [name, ...rest] = k.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
|
|
await seite.goto(`${BASIS}/workspace/bereich.html?b=ansteht`, { waitUntil: "networkidle" });
|
|
await seite.waitForSelector(".kette", { timeout: 15_000 });
|
|
await seite.waitForTimeout(400);
|
|
|
|
const m = await seite.evaluate(() => ({
|
|
her: [...document.querySelectorAll(".kette--her")].map((z) => z.textContent.trim()),
|
|
hin: [...document.querySelectorAll(".kette--hin")].map((z) => z.textContent.trim()),
|
|
wege: [...document.querySelectorAll(".kette__weg[href]")].length,
|
|
}));
|
|
ok(m.her.length >= 1, `die Herkunft steht auf der Karte ("${m.her[0] || ""}")`);
|
|
ok(/Lena/.test(m.her[0] || ""), "mit dem Namen");
|
|
ok(/Wunschliste/.test(m.her[0] || ""), "und dem Brett, aus dem es kam");
|
|
ok(m.hin.length >= 1, `und was daraus wurde ("${m.hin[0] || ""}")`);
|
|
ok(m.wege >= 2, `beide sind anklickbar (${m.wege} Wege)`);
|
|
ok(konsole.length === 0,
|
|
konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole");
|
|
|
|
if (process.env.SCHIRM) {
|
|
await seite.screenshot({ path: fileURLToPath(new URL("pruef-kreislauf.png",
|
|
import.meta.url)), fullPage: false });
|
|
melde(" -- Bildschirmfoto: server/pruef-kreislauf.png");
|
|
}
|
|
await kontext.close();
|
|
} finally {
|
|
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);
|