/* Prueft "Neu & wichtig" in der Bibliothek: 1. Es steht nur JE EINER da -- der wichtigste und der neueste. 2. Der Rest kommt auf Knopfdruck und geht wieder weg. 3. "Wichtig" laeuft nach 48 Stunden ab, genau wie "neu". Punkt 3 ist der, den man leicht falsch prueft: Mit frischen Daten ist ALLES neu, und ein abgelaufener Eintrag kommt nie vor. Deshalb werden hier ausdruecklich Eintraege mit einem Zeitstempel von vor drei Tagen angelegt -- sonst waere die Pruefung gruen, ohne den Fall je gesehen zu haben. */ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-wissen-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); /* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026). Ohne diese Zeile startet der eigene Server bei belegtem Port STILL nicht -- und alles Folgende misst gegen einen fremden Stand. Das ist der dritte Ausgang: belegt ist etwas anderes als kaputt. */ const PORT = await eigenerPort(import.meta, "pruef-wissen-neu"); process.env.PORT = `${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"; import { eigenerPort } from "./helfer-port.mjs"; await import("./index.js"); /* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort zwei Auffangnetze ein, die einen Absturz nur protokollieren -- richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ notbremse(480_000, "pruef-wissen-neu"); await new Promise((r) => setTimeout(r, 700)); const BASIS = `http://127.0.0.1:${PORT}`; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); for (let i = 0; i < 60; i++) { try { const p = new DatabaseSync(process.env.WORKSPACE_DB); try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } } catch { await new Promise((r) => setTimeout(r, 250)); } } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const salt = randomBytes(16).toString("hex"); const hash = scryptSync("CODE-DOGI-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") .run("Filipe", "admin", hash, salt, 32768, jetzt); /* Die Dateien muessen auf der Platte liegen, sonst zeigt die Seite sie zwar an, aber der Server koennte sie nicht ausliefern. */ const wissenOrdner = join(ordner, "wissen"); mkdirSync(wissenOrdner, { recursive: true }); const vorDreiTagen = new Date(Date.now() - 3 * 86400_000).toISOString(); const vorEinerStunde = new Date(Date.now() - 3600_000).toISOString(); /* Vier Faelle, und der dritte ist der eigentliche Punkt. */ const EINTRAEGE = [ ["Frisch und wichtig", 1, vorEinerStunde], // muss oben stehen ["Frisch, nicht wichtig", 0, vorEinerStunde], // muss oben stehen ["ALT aber wichtig", 1, vorDreiTagen], // darf NICHT mehr oben stehen ["Alt und unwichtig", 0, vorDreiTagen], // darf nicht oben stehen ["Noch einer frisch", 0, vorEinerStunde], // landet im Aufklappteil ]; for (const [titel, wichtig, wann] of EINTRAEGE) { const datei = randomBytes(8).toString("hex") + ".pdf"; writeFileSync(join(wissenOrdner, datei), "%PDF-1.4\n%%EOF\n"); d.prepare(`INSERT INTO wissen (titel, beschreibung, kategorie, stufe, geraet, name_datei, name_original, groesse, wichtig, veroeffentlicht, hochgeladen, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`) .run(titel, "Beschreibung", "tiktok-grundlagen", "einsteiger", "egal", datei, titel + ".pdf", 1024, wichtig, wann.slice(0, 10), wann, 1); } d.close(); /* ---------- Was der Server liefert --------------------------------------- */ async function anmelden() { const a = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }) }); return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); } const keks = await anmelden(); /* ===================================================================== WIE DIE PDF AUSGELIEFERT WIRD (02.10.2026) Am 02.10. kam heraus, dass zehn Stellen im Haus Dateien ohne Teilanfragen hinausgaben — Anlass war eine Sprachnachricht, die Miss auf ihrem iPhone nicht hoeren konnte. Alle zehn gehen jetzt ueber `liefereDatei()`. NEUN DAVON HATTEN EINE PRUEFUNG, DIESE NICHT. Der Weg zur PDF war der einzige, den keine einzige Pruefung je abgerufen hat — und ein Weg ohne Pruefung ist genau der, bei dem eine Umstellung unbemerkt danebengeht. Deshalb steht er jetzt hier. Fuer einen PDF-Betrachter ist das mehr als eine Formalie: Mit Teilanfragen springt er zu einer Seite, ohne die ganze Datei zu holen. =================================================================== */ console.log("\n=== Der Weg zur PDF"); { const liste = await (await fetch(BASIS + "/workspace/api/wissen", { headers: { Cookie: keks } })).json(); const id = (liste.neu || [])[0]?.id; ok(Number.isInteger(id), `ein Eintrag zum Abrufen da (#${id})`); const ganz = await fetch(`${BASIS}/workspace/api/wissen/${id}/datei?modus=ansehen`, { headers: { Cookie: keks } }); const bytes = Buffer.from(await ganz.arrayBuffer()); ok(ganz.status === 200 && bytes.length > 0, `die PDF kommt (HTTP ${ganz.status}, ${bytes.length} Bytes)`); ok(ganz.headers.get("content-type") === "application/pdf", `zum Ansehen mit dem richtigen Typ (${ganz.headers.get("content-type")})`); ok(ganz.headers.get("accept-ranges") === "bytes" && Number(ganz.headers.get("content-length")) === bytes.length, `mit Accept-Ranges und Laenge (${ganz.headers.get("accept-ranges")},` + ` ${ganz.headers.get("content-length")})`); const teil = await fetch(`${BASIS}/workspace/api/wissen/${id}/datei?modus=ansehen`, { headers: { Cookie: keks, Range: "bytes=0-3" } }); const stueck = Buffer.from(await teil.arrayBuffer()); ok(teil.status === 206 && teil.headers.get("content-range") === `bytes 0-3/${bytes.length}` && stueck.length === 4 && stueck.toString("latin1") === "%PDF", `ein Abschnitt kommt als 206 mit genau diesen Bytes` + ` (${teil.headers.get("content-range")}, "${stueck.toString("latin1")}")`); /* GEGENPROBE: Ohne `modus=ansehen` ist es ein Download, und der Transport bleibt derselbe. Ein Weg, der nur in einer von zwei Spielarten richtig ausliefert, waere die haesslichste Sorte Fehler — er faellt erst dem auf, der zufaellig die andere nimmt. */ const laden = await fetch(`${BASIS}/workspace/api/wissen/${id}/datei`, { headers: { Cookie: keks, Range: "bytes=1-2" } }); const zwei = Buffer.from(await laden.arrayBuffer()); ok(laden.status === 206 && zwei.toString("latin1") === "PD" && /^attachment/.test(laden.headers.get("content-disposition") || ""), `auch als Download (HTTP ${laden.status}, "${zwei.toString("latin1")}",` + ` ${laden.headers.get("content-disposition")})`); } console.log("\n=== Was oben stehen darf"); { const a = await fetch(BASIS + "/workspace/api/wissen", { headers: { Cookie: keks } }); const { neu } = await a.json(); const namen = neu.map((e) => e.titel); console.log(" oben:", namen.join(" · ")); ok(namen.includes("Frisch und wichtig"), "das frische Wichtige steht oben"); ok(namen.includes("Frisch, nicht wichtig"), "das frisch Eingestellte auch"); /* DER EIGENTLICHE PUNKT: Vor dem 01.09.2026 blieb "wichtig" ewig stehen. Ein drei Tage alter, als wichtig markierter Eintrag muss jetzt verschwunden sein. */ ok(!namen.includes("ALT aber wichtig"), `"wichtig" laeuft nach 48 Stunden ab (${namen.includes("ALT aber wichtig") ? "steht noch da" : "weg"})`); ok(!namen.includes("Alt und unwichtig"), "und Altes ohne Marke erst recht"); /* Wichtiges zuerst -- sonst steht der Scheinwerfer an zweiter Stelle. */ ok(neu[0]?.titel === "Frisch und wichtig", `Wichtiges steht an erster Stelle (${neu[0]?.titel})`); ok(neu.every((e) => typeof e.ist_wichtig === "boolean"), "jeder Eintrag sagt, ob er wichtig ist"); } /* ---------- Was die Seite zeigt ------------------------------------------ */ console.log("\n=== Was die Seite zeigt"); 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 konsole = []; const seite = await kontext.newPage(); seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message)); await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); await seite.click('.rolle[data-rolle="admin"]'); await seite.fill("#code", "CODE-DOGI-0001"); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 15000 }); await seite.goto(BASIS + "/workspace/wissen.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(900); const lesen = () => seite.evaluate(() => { const block = [...document.querySelectorAll(".block")] .find((b) => b.querySelector(".block__titel")?.textContent === "Neu & wichtig"); if (!block) return null; /* Die Karten heissen .pdf -- der erste Anlauf suchte nach ".karte" und zaehlte dabei die UMSCHLIESSENDEN Container mit. */ const sichtbar = [...block.querySelectorAll(".pdf")] .filter((k) => k.offsetParent !== null); return { sichtbar: sichtbar.length, titel: sichtbar.map((k) => k.querySelector(".pdf__titel, h3, strong")?.textContent?.trim()) .filter(Boolean), restVerborgen: document.getElementById("neu-rest")?.hidden, restAnzahl: document.getElementById("neu-rest")?.children.length ?? 0, knopf: block.querySelector(".knopf-still")?.textContent?.trim() || null, }; }); const r = await lesen(); console.log(" sichtbar:", r?.titel.join(" · "), "| Knopf:", r?.knopf); ok(r !== null, "der Block \"Neu & wichtig\" ist da"); /* GENAU ZWEI: einer wichtig, einer neu. */ ok(r.sichtbar === 2, `es stehen genau zwei da (${r.sichtbar})`); ok(r.restVerborgen === true, `der Rest ist zugeklappt (${r.restVerborgen})`); ok(r.restAnzahl > 0, `und es gibt ueberhaupt einen Rest (${r.restAnzahl})`); ok(/Alle \d+ zeigen/.test(r.knopf || ""), `der Knopf nennt die Zahl ("${r.knopf}")`); /* Aufklappen muss WIRKLICH etwas zeigen -- ein Knopf, der nur seine Beschriftung aendert, ist keiner. */ await seite.click(".block .knopf-still"); await seite.waitForTimeout(350); const auf = await lesen(); ok(auf.sichtbar > r.sichtbar, `aufgeklappt stehen mehr da (${r.sichtbar} -> ${auf.sichtbar})`); ok(auf.restVerborgen === false, "der Rest ist sichtbar"); ok(!/Alle \d+ zeigen/.test(auf.knopf || ""), `und der Knopf bietet das Zuklappen an ("${auf.knopf}")`); /* Und wieder zu. */ await seite.click(".block .knopf-still"); await seite.waitForTimeout(350); const zu = await lesen(); ok(zu.sichtbar === 2, `zugeklappt sind es wieder zwei (${zu.sichtbar})`); ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`); await kontext.close(); } await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung."); process.exit(fehler ? 1 : 0);