Files
DogFatherGitandClaude Opus 5 7dc5356c80 Datum: der Tag kommt aus der Ortszeit, nicht aus UTC
GEFUNDEN UM 01:22, von pruef-kreislauf -- und nur, weil nachts
gearbeitet wurde.

Die Pruefung legt einen Wunsch mit dem HEUTIGEN Datum an und macht
daraus einen Termin. Gemessen:

    geschickt:   datum = 2026-10-01   (heuteLokal auf dem Server)
    gespeichert: datum = 2026-09-30

Der Termin lag also in der Vergangenheit. Auf „Was ansteht" sortiert
er sich damit in den Abschnitt „Vorbei" ein -- und der ist mit
Absicht zugeklappt. Beim Wunsch stand „Daraus wurde ein Termin", auf
dem Brett war er nicht zu sehen. Zwei Stunden jede Nacht (im Winter
eine), genau in den Stunden, in denen hier gearbeitet wird.

URSACHE

    const jetzt = () => new Date().toISOString();   // UTC
    ... jetzt().slice(0, 10) ...                     // UTC-Tag

ES WAR NICHT EINE STELLE. Nachgemessen: SECHZEHN in vierzehn Modulen,
davon ZEHN, die den falschen Tag in die Datenbank schreiben --
Termine, Wuensche, Highlights, Talente, Leads, Videos, Vorlagen --
und sechs, die „heute" vergleichen (ueberfaellige Aufgaben, Berichte,
die Frist einer Entwicklungsaufgabe).

NICHT ANGEFASST, WEIL RICHTIG: Rechnungen auf einem Datumstext mit
fester Uhrzeit (`Date.parse(tag + "T12:00:00Z") + n * 86400000`). Die
bleiben in jeder Zeitzone am selben Kalendertag -- kalender, teamlage
und serien machen es so, und das bleibt.

WARUM ES NIEMAND GEMERKT HAT

pruef-struktur sucht dieses Muster seit dem 06.09.2026. Aber:
  - sie sah NUR in die `pruef-*.mjs`, nie in die Anwendung
  - sie kannte die Schreibweise ueber eine FUNKTION nicht
    (`const jetzt = () => ...` statt `const jetzt = ...`)

Die Wache stand vor den Pruefungen, nicht vor dem Haus -- derselbe
Fehler wie heute Nacht bei den Messports: eine Sicherung, die nur die
halbe Menge kennt, faellt in der anderen Haelfte aus, und zwar
lautlos, denn sie meldet ja „nichts gefunden".

Jetzt sieht sie in beides und kennt beide Schreibweisen. Beim ersten
scharfen Lauf fand sie sofort 23 weitere Stellen in den Pruefdateien
selbst -- dieselben Zeitbomben, gegen die sie gebaut worden war.

EINE ZWEITE WACHE, WEIL ICH SELBST HINEINGELAUFEN BIN

Mein Umbauwerkzeug hat in elf Modulen `heuteLokal()` eingesetzt und
die Einfuhr weggelassen: Es hat erst ersetzt und DANN gefragt, ob der
Name schon in der Datei steht -- da stand er, mein eigener Aufruf.
`node --check` sagt dazu nichts, „Laedt jedes Server-Modul?" auch
nicht: Die Datei ist syntaktisch tadellos. Erst der Aufruf faellt um
mit `ReferenceError: heuteLokal is not defined`. Gefunden hat es
pruef-video, zufaellig. Die anderen zehn waeren durchgerutscht.

Deshalb neu: „Ruft ein Modul etwas, das es nie eingefuehrt hat?" --
die Namen des Hauses aus den export-Zeilen gelesen, nicht
aufgezaehlt. 460 Aufrufe in 350 Dateien, alle mit Einfuhr.

pruef-kreislauf STELLT JETZT DIE RICHTIGE FRAGE

Sie war rot und hat den Fehler dabei nur gestreift: „`.kette` wird
nicht sichtbar", Zeitsperre nach 15 s. Das klingt nach der Anzeige
und schickt einen zur falschen Stelle. Neu:
  - eine Zeile fragt das DATUM (ohne Browser, nennt den Fehler beim
    Namen)
  - der Browserteil klappt zu, was zu ist, und misst dann die Kette;
    „gar nicht da" wird von „da und unsichtbar" unterschieden

NEBENBEFUND IN pruef-ics

Die Probe „fast richtig" war `echt.slice(0, -1) + "A"`. Der
Schluessel ist base64url; sein letztes Zeichen ist eines von
sechzehn. Endet er auf „A", IST die Probe der echte Schluessel, der
Server antwortet zu Recht mit 200, und die Pruefung meldet ein Loch,
das es nicht gibt -- einmal je sechzehn Laeufe. Heute Nacht zweimal
hintereinander, und die Suche ging eine halbe Stunde in eine
Aenderung, die damit nichts zu tun hatte.

GEPRUEFT

  pruef-struktur   44 -> 59 Pruefungen, 0 Fehler
  pruef-ics        37 -> 38, 0 Fehler
  pruef-kreislauf  Absturz bei Nr. 17 -> 25 Pruefungen, 0 Fehler

  und gruen geblieben: treff 85, arten 28, video 74, uebernahme 39,
  entwicklung 79, content 45, vorlagen 24, zuteilung 75,
  scout-zuteilung 37, unterstuetzung 70, aufbewahrung 45,
  bewerbung 91, treff-start 42, uebergang 65, nachwuchs 262,
  auskunft 46, modi-ideen 30, neue-seiten 109, spicy 85,
  wege-nach-draussen 67, aufgabenbrett 49, agentur 62,
  bereiche-lesend 37 -- beide Haeuser

GEGENPROBEN, DIE WIRKLICH ROT WERDEN

  - den UTC-Tag im `daraus`-Weg wieder eingebaut: pruef-kreislauf
    meldet „er liegt HEUTE, nicht gestern (2026-09-30, heute ist
    2026-10-01)", 25 Pruefungen, 1 Fehler -- und der Browserteil
    bleibt gruen, weil er jetzt aufklappt. Jede Frage bei ihrer
    eigenen Pruefung.
  - eine Einfuhr aus workspace-video.js entfernt: die neue Wache
    meldet „workspace-video.js: heuteLokal() (aus helfer-tag.mjs)"
  - beide Erkennungen je gegen einen gebauten Rueckschritt geprueft
    (Funktion, Variable, zwei Schritte, Date.now-Rechnung) und gegen
    das, was NICHT anschlagen darf (UTC-Mittag, voller Zeitstempel,
    fremdes Date, Name im Kommentar, Eigenschaft am Objekt)

EIN FEHLER BEIM UMBAU, HIER FESTGEHALTEN: Mein erster Lauf ueber die
Pruefdateien hat stumpf ersetzt und dabei KOMMENTARE umgeschrieben --
in sieben Dateien stand die alte Schreibweise als Beleg in der
Begruendung, und daraus wurde das Gegenteil. Bemerkt hat es der
Vergleich der Zahlen (34 Stellen statt der gemessenen 24), nicht die
Absicht. Zurueckgenommen und mit Schutz fuer Kommentare und
Zeichenketten wiederholt.

Datenbank vorher gesichert. Keine Schemaaenderung.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 01:45:16 +02:00

362 lines
17 KiB
JavaScript

/* =====================================================================
Aus einer Idee wird Arbeit (15.09.2026)
Stufe 9: „Idee → Aufgabe". Das ganze Haus steht auf dem Satz *„Was
hier nicht steht, passiert nicht."* Ein Ideenbrett, aus dem keine
Aufgabe werden kann, ist der eine Ort, an dem er nicht gilt.
---------------------------------------------------------------------
DREI FRAGEN, UND ALLE DREI HABEN EINE GEGENPROBE
1. Wird aus einer Idee eine Aufgabe? ... und steht wirklich alles
Richtige darin?
2. Genau EINMAL? ... und was passiert beim
zweiten Versuch?
3. Aus JEDEM Brett? ... nein — und genau das ist
der Punkt: Ein Beitrag
aus dem Treff gehört
einem Menschen aus der
Community.
Dazu die Frage, die das Loeschkonzept aufwirft: Was passiert mit der
Aufgabe, wenn die Idee geloescht wird? (Sie bleibt — mit `SET NULL`.)
Aufrufen mit: node server/pruef-uebernahme.mjs
===================================================================== */
import { mkdtempSync, rmSync } 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-uebernahme");
const ordner = mkdtempSync(join(tmpdir(), "ws-ueb-"));
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";
import { heuteLokal } from "./helfer-tag.mjs";
await import("./index.js");
notbremse(180_000, "pruef-uebernahme");
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";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const modul = await import("./workspace-uebernahme.js");
const { BEREICHE } = await import("./workspace-bereiche.js");
/* Welche Bretter dem Treff gehoeren, sagt workspace.js -- nicht
ein Merkmal, das ich mir an BEREICHE vorgestellt habe. Der erste
Anlauf fragte nach `BEREICHE[b].treff` und fand null Bretter: eine
Gegenprobe, die nichts gegenprueft. */
const { TREFF_BRETTER } = await import("./workspace.js");
const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
let idDogi, idLuna, idIdee, idTreff, idIdeeHoch;
{
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");
idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
anlegen("Kessi", "gast", "CODE-GAST-0001");
const rein = d.prepare(`INSERT INTO eintraege
(bereich, art, titel, text, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von)
VALUES (?,?,?,?,?,?,'offen',?,?,?)`);
rein.run("ideen", "content", "Reihe über Technik", "Drei Teile, je fünf Minuten.",
heuteLokal(), "mittel", idLuna, jetzt, idDogi);
idIdee = d.prepare("SELECT last_insert_rowid() AS id").get().id;
rein.run("ideen", "content", "Dringende Idee", "Muss diese Woche.",
heuteLokal(), "hoch", null, jetzt, idDogi);
idIdeeHoch = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)
VALUES ('treff','hallo','Beitrag aus dem Rudel','Hallo zusammen',?,?,?)`)
.run(heuteLokal(), jetzt, idDogi);
idTreff = d.prepare("SELECT last_insert_rowid() AS id").get().id;
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 === "gast" ? { rolle, code, alter_ok: true } : { rolle, code });
return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
}
const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS);
const kGast = await anmelden("gast", "CODE-GAST-0001", CREW);
ok(!!kDogi && !!kGast, "DogFather und ein Community-Mitglied sind angemeldet");
/* =======================================================================
1. DIE LISTE DER BRETTER IST NICHT ERFUNDEN
======================================================================= */
melde("");
melde("=== 1. Aus welchen Brettern ===");
{
const echte = Object.keys(BEREICHE);
ok(echte.length > 5, `${echte.length} Bretter gibt es im Haus`);
const erfunden = modul.AUS_BRETTERN.filter((b) => !echte.includes(b));
ok(erfunden.length === 0,
erfunden.length ? `GIBT ES NICHT: ${erfunden.join(", ")}`
: `alle ${modul.AUS_BRETTERN.length} genannten Bretter gibt es wirklich`);
/* DIE TREFF-BRETTER STEHEN NICHT DRIN, und das ist der Punkt. */
const treffBretter = TREFF_BRETTER.filter((b) => echte.includes(b));
ok(treffBretter.length > 0, `${treffBretter.length} Bretter gehören dem Rudel`);
const drin = treffBretter.filter((b) => modul.AUS_BRETTERN.includes(b));
ok(drin.length === 0,
drin.length ? `TREFF-BRETT DABEI: ${drin.join(", ")}`
: "keines davon ist dabei — was jemand aus der Community schreibt, gehört ihm");
}
/* =======================================================================
2. AUS EINER IDEE WIRD EINE AUFGABE
======================================================================= */
melde("");
melde("=== 2. Die Übernahme ===");
{
const a = await ruf(`/workspace/api/eintrag/${idIdee}/zur-aufgabe`, HAUS, "POST",
{ cookie: kDogi }, {});
ok(a.code === 201, `aus der Idee wird eine Aufgabe (${a.code})`);
const neu = json(a);
ok(Number.isInteger(neu.aufgabe_id), `mit einer Nummer (${neu.aufgabe_id})`);
const d = oeffne();
const auf = d.prepare("SELECT * FROM aufgaben WHERE id = ?").get(neu.aufgabe_id);
const idee = d.prepare("SELECT * FROM eintraege WHERE id = ?").get(idIdee);
d.close();
ok(auf.titel === "Reihe über Technik", `der Titel wandert mit (${auf.titel})`);
ok(/fünf Minuten/.test(auf.beschreibung || ""), "der Text auch");
ok(auf.status === "offen", `sie beginnt offen (${auf.status})`);
ok(auf.aus_eintrag_id === idIdee, `und weiß, woher sie kam (${auf.aus_eintrag_id})`);
ok(auf.creator_id === idLuna,
`der Creator-Bezug bleibt erhalten (${auf.creator_id === idLuna ? "Luna" : auf.creator_id})`);
ok(auf.verantwortlich_id === idDogi,
"zuständig ist, wer übernommen hat — nicht, wer die Idee aufgeschrieben hat");
/* WAS ABSICHTLICH NICHT MITWANDERT. */
ok(auf.frist === null, "eine Frist wird NICHT erfunden");
/* DIE IDEE BLEIBT STEHEN. Sie wird nicht verschoben und nicht
geloescht -- wer nachsieht, was aus einem Gedanken geworden ist,
soll den Gedanken noch finden. */
ok(!!idee && idee.bereich === "ideen",
`und die Idee steht unverändert im Brett (${idee?.bereich})`);
}
/* =======================================================================
3. GENAU EINMAL
======================================================================= */
melde("");
melde("=== 3. Genau einmal ===");
{
const zweite = await ruf(`/workspace/api/eintrag/${idIdee}/zur-aufgabe`, HAUS, "POST",
{ cookie: kDogi }, {});
ok(zweite.code === 409, `ein zweiter Versuch wird abgelehnt (${zweite.code})`);
ok(json(zweite).fehler === "schon_uebernommen", "mit einem Grund, den die Seite versteht");
ok(Number.isInteger(json(zweite).aufgabe_id),
`und dem Verweis auf die vorhandene Aufgabe (${json(zweite).aufgabe_id})`);
const d = oeffne();
const wieviele = d.prepare(
"SELECT COUNT(*) AS n FROM aufgaben WHERE aus_eintrag_id = ?").get(idIdee).n;
d.close();
ok(wieviele === 1, `es gibt genau eine Aufgabe dazu (${wieviele})`);
}
/* =======================================================================
4. NICHT AUS JEDEM BRETT
======================================================================= */
melde("");
melde("=== 4. Nicht aus dem Rudel ===");
{
const a = await ruf(`/workspace/api/eintrag/${idTreff}/zur-aufgabe`, HAUS, "POST",
{ cookie: kDogi }, {});
ok(a.code === 400, `aus einem Rudel-Beitrag wird keine Aufgabe (${a.code})`);
ok(/keine Aufgabe/.test(json(a).fehler || ""), `und die Absage sagt warum (${json(a).fehler})`);
const d = oeffne();
const n = d.prepare("SELECT COUNT(*) AS n FROM aufgaben WHERE aus_eintrag_id = ?").get(idTreff).n;
d.close();
ok(n === 0, "es ist auch wirklich keine entstanden");
const erfunden = await ruf("/workspace/api/eintrag/999999/zur-aufgabe", HAUS, "POST",
{ cookie: kDogi }, {});
ok(erfunden.code === 404, `eine erfundene Nummer ergibt 404 (${erfunden.code})`);
const ohne = await ruf(`/workspace/api/eintrag/${idIdeeHoch}/zur-aufgabe`, HAUS, "POST", {}, {});
ok(ohne.code === 401, `ohne Anmeldung gar nichts (${ohne.code})`);
}
/* =======================================================================
5. DIE DRINGLICHKEIT WIRD ZUR PRIORITÄT
======================================================================= */
melde("");
melde("=== 5. Dringlichkeit ===");
{
const a = await ruf(`/workspace/api/eintrag/${idIdeeHoch}/zur-aufgabe`, HAUS, "POST",
{ cookie: kDogi }, {});
ok(a.code === 201, `die dringende Idee wird übernommen (${a.code})`);
ok(json(a).prioritaet === "hoch", `und behält ihre Dringlichkeit (${json(a).prioritaet})`);
/* GEGENPROBE -- ohne sie beweist die Zeile darueber nichts: Die
andere Idee stand auf "mittel" und muss dort geblieben sein. */
const d = oeffne();
const andere = d.prepare(
"SELECT prioritaet FROM aufgaben WHERE aus_eintrag_id = ?").get(idIdee);
d.close();
ok(andere?.prioritaet === "mittel",
`die andere steht weiterhin auf mittel (${andere?.prioritaet}) — es wird nicht alles „hoch“`);
}
/* =======================================================================
6. WAS DAS BRETT DAZU SAGT
======================================================================= */
melde("");
melde("=== 6. Die Liste am Brett ===");
{
const a = json(await ruf("/workspace/api/bereich/ideen/uebernahmen", HAUS, "GET", { cookie: kDogi }));
ok(a.moeglich === true, "das Ideenbrett kennt Übernahmen");
ok(Object.keys(a.uebernahmen || {}).length === 2,
`zwei Einträge sind übernommen (${Object.keys(a.uebernahmen || {}).join(", ")})`);
ok(a.uebernahmen[idIdee]?.status === "offen",
`und der Stand der Aufgabe steht dabei (${a.uebernahmen[idIdee]?.status})`);
/* GEGENPROBE: Ein Treff-Brett sagt „nicht moeglich" -- daran
entscheidet die Oberflaeche, ob der Knopf ueberhaupt hingehoert. */
const treff = json(await ruf("/workspace/api/bereich/treff/uebernahmen", HAUS, "GET", { cookie: kDogi }));
ok(treff.moeglich === false, "ein Rudel-Brett kennt keine");
ok(Object.keys(treff.uebernahmen || {}).length === 0, "und liefert auch keine");
}
/* =======================================================================
7. WIRD DIE IDEE GELÖSCHT, BLEIBT DIE ARBEIT
Die Frage, die das Loeschkonzept aufwirft. `ON DELETE SET NULL` und
nicht CASCADE: Die Aufgabe ist inzwischen etwas Eigenes.
======================================================================= */
melde("");
melde("=== 7. Wenn die Idee verschwindet ===");
{
const d = oeffne();
const vorher = d.prepare("SELECT id FROM aufgaben WHERE aus_eintrag_id = ?").get(idIdee);
d.prepare("PRAGMA foreign_keys = ON").run();
d.prepare("DELETE FROM eintraege WHERE id = ?").run(idIdee);
const nachher = d.prepare("SELECT id, aus_eintrag_id, titel FROM aufgaben WHERE id = ?")
.get(vorher.id);
d.close();
ok(!!nachher, "die Aufgabe gibt es noch, obwohl die Idee weg ist");
ok(nachher.aus_eintrag_id === null,
`und der Verweis steht auf null statt ins Leere (${nachher.aus_eintrag_id})`);
ok(nachher.titel === "Reihe über Technik", "mit unverändertem Titel");
}
/* =======================================================================
8. DER KNOPF STEHT IM BROWSER — UND WIRKT
======================================================================= */
melde("");
melde("=== 8. 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: 900 } });
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));
/* Eine frische Idee, damit es etwas zu uebernehmen gibt. */
const d = oeffne();
d.prepare(`INSERT INTO eintraege
(bereich, art, titel, text, datum, dringlichkeit, status, erstellt, erstellt_von)
VALUES ('ideen','content','BILDSCHIRM-IDEE','Zum Anklicken.',?,'mittel','offen',?,?)`)
.run(heuteLokal(), jetzt, idDogi);
d.close();
await seite.goto(`http://127.0.0.1:${PORT}/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: 20000 });
await seite.goto(`http://127.0.0.1:${PORT}/workspace/bereich.html?b=ideen`,
{ waitUntil: "networkidle" });
await seite.waitForTimeout(900);
const knoepfe = await seite.evaluate(() =>
[...document.querySelectorAll("button")].filter((b) => /zur Aufgabe/i.test(b.textContent)).length);
ok(knoepfe >= 1, `${knoepfe} Knopf/Knöpfe „zur Aufgabe“ auf dem Ideenbrett`);
const verweise = await seite.evaluate(() =>
document.querySelectorAll(".eintrag__aufgabe").length);
ok(verweise >= 1,
`${verweise} Einträge zeigen stattdessen den Verweis auf ihre Aufgabe`);
/* DER KNOPF MUSS WIRKEN -- gemessen an der Datenbank, nicht am Text. */
const vorher = (() => { const dd = oeffne();
const n = dd.prepare("SELECT COUNT(*) AS n FROM aufgaben").get().n; dd.close(); return n; })();
await seite.evaluate(() => {
const b = [...document.querySelectorAll("button")].find((x) => /zur Aufgabe/i.test(x.textContent));
b?.click();
});
await seite.waitForTimeout(1200);
const nachher = (() => { const dd = oeffne();
const n = dd.prepare("SELECT COUNT(*) AS n FROM aufgaben").get().n; dd.close(); return n; })();
ok(nachher === vorher + 1, `nach dem Klick gibt es eine Aufgabe mehr (${vorher} → ${nachher})`);
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);