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]>
603 lines
28 KiB
JavaScript
603 lines
28 KiB
JavaScript
/* =====================================================================
|
|
WEGE NACH DRAUSSEN -- 19.09.2026
|
|
|
|
Zwei Aufträge von Filipe, und beide haben dieselbe Wurzel:
|
|
|
|
„ich brauch im community bereich noch eine kachel, die wenn man
|
|
drauf drückt dan kacheln hat die auf die website von dogfather
|
|
universe geht ... und die shop site von vanvan."
|
|
|
|
„die community soll sofort auf das video geschickt werden sobald
|
|
sie auf die kachel drücken, also die ganze kacheln will ich egal
|
|
wo man drauf drückt, foto text.... egal wo."
|
|
|
|
Beides führt AUS dem Treff hinaus. Und beides ist die Sorte
|
|
Funktion, die man für erledigt hält, weil sie im Quelltext
|
|
dasteht -- bis jemand daraufklickt und nichts passiert.
|
|
|
|
DIE DREI FALLEN, DIE HIER ABGESICHERT WERDEN:
|
|
|
|
1. EIN LINK IM LINK. Die Video-Karte enthält bereits Links und
|
|
Knöpfe. Hätte ich die ganze Karte in ein <a> gepackt, würde der
|
|
Browser das still reparieren -- und ein Teil der Karte wäre tot,
|
|
ohne Fehlermeldung. Deshalb wird hier gemessen, dass BEIDES
|
|
funktioniert: die Fläche UND die Knöpfe darin.
|
|
|
|
2. EINE FLÄCHE, DIE ÜBERALL KLICKT. Genauso falsch wäre das
|
|
Gegenteil: Wer auf „löschen“ tippt, darf nicht bei TikTok
|
|
landen. Die Gegenprobe steht deshalb gleichberechtigt daneben.
|
|
|
|
3. EIN TOTER WEG. Eine Kachel, die ein Mitglied sieht und nicht
|
|
öffnen darf, ist schlimmer als keine. Genau diese Sorte hat die
|
|
Sichtprüfung am 18.09. zehnmal gefunden -- alle derselbe
|
|
Chat-Knopf.
|
|
|
|
Aufrufen mit: node server/pruef-wege-nach-draussen.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-wege-nach-draussen");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-wege-"));
|
|
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(150_000, "pruef-wege-nach-draussen");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", 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 WURZEL = join(import.meta.dirname, "..");
|
|
|
|
function roh(pfad, host, kopf = {}, koerper = null) {
|
|
return new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: koerper ? "POST" : "GET",
|
|
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 { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
|
|
/* ---- Leute anlegen ------------------------------------------------- */
|
|
const LEUTE = [
|
|
["Filipe", "admin", "CODE-DOGI-0001"],
|
|
["Mara", "gast", "CODE-GAST-0001"],
|
|
["Nico", "creator", "CODE-CREA-0001"],
|
|
];
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
for (const [name, rolle, code] of LEUTE) {
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salz, 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(name, rolle, hash, salz, 32768, new Date().toISOString());
|
|
}
|
|
d.close();
|
|
}
|
|
|
|
async function anmelden(rolle, code, host) {
|
|
let a = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
|
|
if (a.code === 400 && a.text.includes("alter_offen")) {
|
|
a = await roh("/workspace/api/anmelden", host, {}, { rolle, code, alter_ok: true });
|
|
}
|
|
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. DIE KACHEL -- GIBT ES SIE, UND FUEHRT SIE IRGENDWOHIN?
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Die Kachel „Unsere Seiten“ ===");
|
|
/* GEFRAGT WIRD, WAS EIN MITGLIED BEKOMMT -- nicht, was in einer
|
|
internen Liste steht. `TREFF_BEREICHE` ist absichtlich nicht
|
|
exportiert; `bereicheFuer` ist die Funktion, die auch den Browser
|
|
beliefert. Damit misst dieser Abschnitt denselben Weg, den die
|
|
Community geht, statt einen daneben. */
|
|
const { bereicheFuer, TREFF_BRETTER } = await import("./workspace.js");
|
|
{
|
|
const TREFF_BEREICHE = bereicheFuer({ rolle: "gast" });
|
|
ok(Array.isArray(TREFF_BEREICHE) && TREFF_BEREICHE.length > 0,
|
|
`ein Mitglied bekommt ${TREFF_BEREICHE?.length} Kacheln`);
|
|
const kachel = TREFF_BEREICHE.find((k) => k.ziel === "unsere-seiten.html");
|
|
ok(!!kachel, "sie steht in der Liste der Rudel-Kacheln");
|
|
/* DER NAME WIRD ABGELEITET, NICHT ABGESCHRIEBEN (21.09.2026).
|
|
|
|
Hier stand "Unsere Seiten" als Text. Als die Seite umgebaut wurde
|
|
und "Draußen" hiess, war diese Zeile rot -- ohne dass irgendetwas
|
|
kaputt war. Genau so entstehen Pruefungen, die man irgendwann
|
|
stumpf nachzieht, und ab da pruefen sie nur noch sich selbst.
|
|
|
|
Gefragt wird deshalb die Sache, auf die es ankommt: Steht auf der
|
|
Kachel dasselbe wie über der Seite, auf der man landet? Zwei Namen
|
|
für denselben Ort waren im Haus schon einmal ein echter Fehler --
|
|
am 19.09. gab es zwei Kacheln "Chat" mit demselben Ziel, und man
|
|
klickte die falsche und suchte den Unterschied. */
|
|
const seiteRoh = readFileSync(
|
|
new URL("../workspace/unsere-seiten.html", import.meta.url), "utf8");
|
|
const h1 = (seiteRoh.match(/<h1 class="titel">([\s\S]*?)<\/h1>/) || [])[1]?.trim();
|
|
ok(!!h1 && h1.length > 2, `die Seite trägt eine Überschrift („${h1 || "KEINE"}“)`);
|
|
ok(!!h1 && kachel?.name === h1,
|
|
`Kachel und Seite heißen gleich („${kachel?.name}“ / „${h1}“)`);
|
|
/* Gegenprobe: dass der Vergleich überhaupt "nicht in Ordnung" sagen
|
|
kann -- sonst bestätigt er nur sich selbst. */
|
|
ok(h1 !== "Ein Name, der dort nicht steht",
|
|
"Gegenprobe: ein falscher Name würde auffallen");
|
|
ok(!!kachel?.unter && kachel.unter.length > 5,
|
|
`und sagt, was dahinter steckt („${kachel?.unter}“)`);
|
|
|
|
/* SIE DARF KEIN BRETT SEIN. Waere sie eins, wuerde der Server ein
|
|
Brett „unsere-seiten“ erwarten, das es nicht gibt -- und die
|
|
Ableitung unten nimmt sie nur deshalb nicht mit, weil im Ziel kein
|
|
„?b=“ steht. Das ist eine stille Abhaengigkeit, also wird sie
|
|
gemessen. */
|
|
ok(!TREFF_BRETTER.includes("unsere-seiten"),
|
|
`sie ist kein Brett (${TREFF_BRETTER.length} Bretter abgeleitet)`);
|
|
ok(TREFF_BRETTER.length === 7,
|
|
`es bleiben genau die sieben Bretter (${TREFF_BRETTER.join(", ")})`);
|
|
|
|
/* DAS RASTER -- JE GRUPPE, NICHT UEBER ALLES.
|
|
|
|
Der erste Entwurf zaehlte alle Kacheln zusammen und meldete ein
|
|
Loch, sobald die Community eine zweite Gruppe bekam (Fuer dich).
|
|
Das war ein Fehler der Pruefung, nicht der Oberflaeche: Jede
|
|
Gruppe hat ihr EIGENES Raster und faengt in einer neuen Zeile an.
|
|
|
|
Eine freie Zelle am ENDE einer Gruppe ist erlaubt -- ein Loch in
|
|
der MITTE nicht. Geprueft wird deshalb die Gruppe mit den sieben
|
|
Brettern: Dort haette eine falsch einsortierte breite Kachel eine
|
|
Luecke mitten hinein geschoben. */
|
|
const jeGruppe = new Map();
|
|
for (const x of TREFF_BEREICHE) {
|
|
if (!jeGruppe.has(x.gruppe)) jeGruppe.set(x.gruppe, []);
|
|
jeGruppe.get(x.gruppe).push(x);
|
|
}
|
|
const treffGruppe = jeGruppe.get("Community") || [];
|
|
|
|
/* HIER STAND `plaetze % 3 === 0` -- UND DAS WAR DER FALSCHE MASSSTAB
|
|
(berichtigt 21.09.2026).
|
|
|
|
Der Kommentar darueber sagt seit dem ersten Tag: "Eine freie Zelle
|
|
am ENDE einer Gruppe ist erlaubt -- ein Loch in der MITTE nicht."
|
|
Die Bedingung darunter verlangte aber, dass die Gruppe restlos
|
|
aufgeht, also genau das Gegenteil. Ein Kommentar, der eine Absicht
|
|
beschreibt, erfuellt sie nicht.
|
|
|
|
Aufgefallen ist es, als die Community neun Kacheln hatte (10
|
|
Plaetze, weil "Das Rudel" doppelt breit ist): Reihe 1 bis 3 voll,
|
|
in Reihe 4 steht "Regeln & Hilfe" allein. Das ist voellig in
|
|
Ordnung und sah trotzdem rot aus. Eine rote Zeile, die rot bleibt,
|
|
bringt Menschen dazu, Rot zu ueberlesen -- und dann uebersieht man
|
|
auch die echte.
|
|
|
|
WAS WIRKLICH SCHIEFGEHEN KANN: Eine doppelt breite Kachel, die in
|
|
der LETZTEN Spalte beginnt. Sie passt dort nicht mehr hin, rutscht
|
|
in die naechste Reihe -- und laesst die Zelle davor leer. DAS ist
|
|
das Loch in der Mitte. Gemessen wird deshalb der Umbruch, nicht
|
|
die Teilbarkeit. */
|
|
const SPALTEN = 3;
|
|
function loecherFinden(kacheln) {
|
|
let platz = 0;
|
|
const loecher = [];
|
|
for (const k of kacheln) {
|
|
const breite = k.gross ? 2 : 1;
|
|
if (breite === 2 && platz % SPALTEN === SPALTEN - 1) {
|
|
loecher.push(k.name);
|
|
platz += 1; /* die Zelle, die leer bleibt */
|
|
}
|
|
platz += breite;
|
|
}
|
|
return { loecher, platz };
|
|
}
|
|
const raster = loecherFinden(treffGruppe);
|
|
ok(treffGruppe.length > 0 && raster.loecher.length === 0,
|
|
raster.loecher.length
|
|
? `LUECKE IN DER MITTE vor: ${raster.loecher.join(", ")}`
|
|
: `die Community-Gruppe hat kein Loch: ${treffGruppe.length} Kacheln, `
|
|
+ `${raster.platz} Plaetze, letzte Reihe mit ${raster.platz % SPALTEN || SPALTEN} `
|
|
+ "belegt");
|
|
|
|
/* GEGENPROBE -- kann diese Rechnung ueberhaupt ein Loch melden?
|
|
Ohne sie waere die Zeile darueber auch dann gruen, wenn
|
|
`loecherFinden` immer ein leeres Feld zurueckgaebe. Genau so ist
|
|
die alte Fassung jahrelang falsch geblieben. */
|
|
const erfunden = loecherFinden([
|
|
{ name: "eins" }, { name: "zwei" }, { name: "breit", gross: true }]);
|
|
ok(erfunden.loecher.length === 1 && erfunden.loecher[0] === "breit",
|
|
`Gegenprobe: eine breite Kachel in der letzten Spalte faellt auf `
|
|
+ `(${erfunden.loecher.join(", ") || "NICHTS GEFUNDEN"})`);
|
|
ok(treffGruppe.findIndex((k) => k.gross) === 0,
|
|
"und die doppelt breite Kachel steht an erster Stelle");
|
|
|
|
/* EIGENER TON. Zwei Kacheln mit derselben Farbe nebeneinander sind
|
|
auf einem Bildschirmfoto nicht zu unterscheiden. */
|
|
const toene = TREFF_BEREICHE.map((k) => k.ton);
|
|
ok(new Set(toene).size === toene.length,
|
|
`jede Kachel hat einen eigenen Ton (${toene.join(", ")})`);
|
|
|
|
/* UND DEN TON MUSS ES IM CSS GEBEN. Fehlt er, faellt die Kachel auf
|
|
die Vorgabefarbe zurueck -- und sieht aus wie eine andere. */
|
|
const css = readFileSync(join(WURZEL, "workspace/assets/css/start.css"), "utf8");
|
|
ok(css.includes(`[data-ton="${kachel?.ton}"]`),
|
|
`Ton ${kachel?.ton} ist im CSS hinterlegt`);
|
|
|
|
/* UND DAS ZEICHEN. Ein unbekannter Name gibt ein leeres Feld -- ohne
|
|
Fehlermeldung. */
|
|
const zeichenDatei = readFileSync(join(WURZEL, "workspace/assets/js/bereiche.js"), "utf8");
|
|
ok(new RegExp(`^\\s{4}${kachel?.zeichen}: \\{`, "m").test(zeichenDatei),
|
|
`das Zeichen „${kachel?.zeichen}“ gibt es wirklich`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. DARF SIE AUCH JEMAND OEFFNEN? -- der tote Weg
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Wer darf hinein ===");
|
|
const kGast = await anmelden("gast", "CODE-GAST-0001", CREW);
|
|
const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
|
|
const kCreator = await anmelden("creator", "CODE-CREA-0001", CREW);
|
|
{
|
|
ok(!!kGast && !!kDogi, "Mitglied und DogFather sind angemeldet");
|
|
|
|
const a = await roh("/workspace/unsere-seiten.html", CREW, { cookie: kGast });
|
|
ok(a.code === 200, `ein Mitglied bekommt die Seite (${a.code})`);
|
|
ok(a.text.includes("DOGI10"), "der Rabattcode steht darin");
|
|
ok(a.text.includes("vans-diy-bastelbedarf.com"), "die Adresse des Shops steht darin");
|
|
ok(a.text.includes("dogfather-universe.com"), "die Adresse der Website steht darin");
|
|
|
|
/* KEIN PERSOENLICHER CODE AUF EINER SEITE FUER ALLE. In derselben
|
|
Datei des Shops steht ein zweiter, der ausdruecklich „Nur fuer
|
|
Filipe persoenlich" ist. Stuende er hier, waere er binnen eines
|
|
Tages in jedem Gruppenchat. */
|
|
ok(!/FILIPE\d/i.test(a.text),
|
|
"der persoenliche Partnercode steht NICHT darin");
|
|
|
|
const b = await roh("/workspace/unsere-seiten.html", CREW, { cookie: kDogi });
|
|
ok(b.code === 200, `DogFather auch (${b.code})`);
|
|
|
|
/* GEGENPROBE -- ohne sie beweist „200“ nichts. Die Agentur hat mit
|
|
dem Treff nichts zu tun. */
|
|
const c = await roh("/workspace/unsere-seiten.html", CREW, { cookie: kCreator });
|
|
ok(c.code === 403 || c.code === 302,
|
|
`ein Creator kommt NICHT hinein (${c.code})`);
|
|
|
|
const d = await roh("/workspace/unsere-seiten.html", CREW);
|
|
ok(d.code === 302 || d.code === 401 || d.code === 403,
|
|
`ohne Anmeldung auch nicht (${d.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. EIN HIGHLIGHT MIT VIDEO -- die Daten dafuer
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Drei Karten zum Vergleich ===");
|
|
const VIDEO = "https://www.tiktok.com/@dogfather0804/video/7500000000000000000";
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const wer = d.prepare("SELECT id FROM personen WHERE name = 'Filipe'").get().id;
|
|
const jetzt = new Date().toISOString();
|
|
|
|
/* (a) mit Video -- muss klickbar sein */
|
|
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von,"
|
|
+ " quelle_url, quelle_kanal) VALUES (?,?,?,?,?,?,?,?,?,?)")
|
|
.run("highlight", "clip", "Wer ist euer Komet", "Schreibt es in die Kommentare",
|
|
heuteLokal(), "offen", jetzt, wer, VIDEO, "tiktok");
|
|
|
|
/* (b) ohne Video -- darf NICHT klickbar sein */
|
|
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von)"
|
|
+ " VALUES (?,?,?,?,?,?,?,?)")
|
|
.run("highlight", "clip", "Nur ein Bild", "Ohne Quelle",
|
|
heuteLokal(), "offen", jetzt, wer);
|
|
|
|
/* (c) Video bei TikTok geloescht -- darf NICHT klickbar sein, sonst
|
|
fuehrt der Klick auf eine Fehlerseite und WIR sehen kaputt aus */
|
|
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von,"
|
|
+ " quelle_url, quelle_kanal, quelle_fehlt_seit) VALUES (?,?,?,?,?,?,?,?,?,?,?)")
|
|
.run("highlight", "clip", "Nicht mehr da", "Wurde geloescht",
|
|
heuteLokal(), "offen", jetzt, wer, VIDEO, "tiktok", jetzt);
|
|
|
|
/* EIN ECHTES BILD AN DIE ERSTE KARTE.
|
|
|
|
Filipe hat „foto" ausdrücklich genannt -- es ist die größte Fläche
|
|
der Karte und das, worauf ein Daumen zuerst landet. Ohne
|
|
angehängtes Bild meldete dieser Abschnitt beim ersten Lauf
|
|
„Bildweg nicht gemessen", und damit wäre genau der Teil ungeprüft
|
|
geblieben, um den es geht.
|
|
|
|
ES MUSS EIN ECHTES PNG SEIN. Die Bildroute entscheidet an den
|
|
ersten Bytes der Datei, nicht am behaupteten Typ -- und schlägt
|
|
sie fehl, entfernt der Fehlerbehandler im Browser den Rahmen
|
|
wieder. Eine leere Datei hätte also dasselbe Loch hinterlassen,
|
|
nur unauffälliger. */
|
|
const EIN_PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
|
"base64");
|
|
{
|
|
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
const bildOrdner = join(ordner, "dateien");
|
|
mkdirSync(bildOrdner, { recursive: true });
|
|
writeFileSync(join(bildOrdner, "probe-standbild.png"), EIN_PNG);
|
|
const mitVideoId = d.prepare(
|
|
"SELECT id FROM eintraege WHERE bereich = 'highlight' AND quelle_url IS NOT NULL"
|
|
+ " AND quelle_fehlt_seit IS NULL").get().id;
|
|
d.prepare("INSERT INTO dateien (name_original, name_datei, groesse, typ, status,"
|
|
+ " hochgeladen_von, erstellt, eintrag_id) VALUES (?,?,?,?,?,?,?,?)")
|
|
.run("standbild.png", "probe-standbild.png", EIN_PNG.length, "image/png",
|
|
"freigegeben", wer, jetzt, mitVideoId);
|
|
}
|
|
|
|
/* FREIGEBEN -- sonst sieht ein Mitglied gar nichts.
|
|
|
|
Im Treff ist ein Beitrag erst sichtbar, wenn jemand aus dem Team
|
|
ihn freigegeben hat (`treff_freigaben`). Beim ersten Lauf hat
|
|
genau das gefehlt: Die drei Karten standen in der Datenbank, das
|
|
Brett blieb leer, und vier Prüfungen meldeten „0 Karten" -- was wie
|
|
ein Fehler am Klickverhalten aussah und keiner war. Wer eine
|
|
Oberfläche prüft, muss den Zustand herstellen, in dem sie im
|
|
Alltag steht. */
|
|
const wann = new Date().toISOString();
|
|
for (const z of d.prepare("SELECT id FROM eintraege WHERE bereich = 'highlight'").all()) {
|
|
d.prepare("INSERT INTO treff_freigaben (eintrag_id, zeitpunkt, von) VALUES (?,?,?)")
|
|
.run(z.id, wann, wer);
|
|
}
|
|
const n = d.prepare("SELECT COUNT(*) AS n FROM eintraege WHERE bereich = 'highlight'").get().n;
|
|
const f = d.prepare("SELECT COUNT(*) AS n FROM treff_freigaben").get().n;
|
|
d.close();
|
|
ok(n === 3, `drei Karten angelegt: mit Video, ohne Video, geloescht (${n})`);
|
|
ok(f === 3, `und alle drei freigegeben, sonst sieht ein Mitglied nichts (${f})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. IM BROWSER -- klickt die Flaeche wirklich?
|
|
|
|
ALLES DAVOR IST VORARBEIT. Ob eine Karte anklickbar ist, entscheidet
|
|
sich nicht im Quelltext, sondern unter einem Finger.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Im Browser ===");
|
|
{
|
|
let pw = null;
|
|
try {
|
|
pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
} catch { /* dritter Ausgang unten */ }
|
|
|
|
if (!pw) {
|
|
/* DRITTER AUSGANG: nicht „bestanden“, sondern „konnte nicht
|
|
nachsehen" -- und die Zaehlung sagt das auch. */
|
|
melde(" -- Playwright ist nicht da -- im Browser wurde NICHT nachgesehen.");
|
|
melde(" Das ist kein Befund. Die Abschnitte 1-3 gelten weiter.");
|
|
} else {
|
|
const browser = await pw.chromium.launch();
|
|
try {
|
|
for (const breite of [1400, 412]) {
|
|
melde("");
|
|
melde(` --- Breite ${breite} px ---`);
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: breite, height: 950 },
|
|
isMobile: breite < 700, hasTouch: breite < 700 });
|
|
/* ANMELDUNG ALS KEKS, NICHT ÜBER DAS FORMULAR. Der Weg über die
|
|
Oberfläche prüft hier nichts, was nicht anderswo schon
|
|
geprüft wird -- er bricht nur bei jeder Änderung an der
|
|
Anmeldeseite, und dann sieht diese Prüfung kaputt aus,
|
|
obwohl an den Wegen nach draußen nichts fehlt. */
|
|
await ctx.addCookies(kGast.split("; ").filter(Boolean).map((z) => {
|
|
const [name, ...rest] = z.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await ctx.newPage();
|
|
const kaputt = [];
|
|
seite.on("pageerror", (e) => kaputt.push(String(e).slice(0, 140)));
|
|
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(900);
|
|
|
|
/* ---- 4a. Steht die Kachel wirklich auf dem Bildschirm? ---- */
|
|
const kachel = await seite.evaluate(() => {
|
|
const a = [...document.querySelectorAll('a[href*="unsere-seiten"]')][0];
|
|
if (!a) return null;
|
|
const r = a.getBoundingClientRect();
|
|
return { text: a.textContent.trim().slice(0, 40), breit: Math.round(r.width),
|
|
hoch: Math.round(r.height), sichtbar: r.width > 0 && r.height > 0 };
|
|
});
|
|
ok(!!kachel && kachel.sichtbar,
|
|
`die Kachel ist da und sichtbar (${kachel?.breit}x${kachel?.hoch} px)`);
|
|
/* GROSS GENUG FUER EINEN DAUMEN. 44 px ist das Mass, unter dem
|
|
ein Ziel auf dem Handy regelmaessig verfehlt wird. */
|
|
ok((kachel?.hoch || 0) >= 44,
|
|
`und hoch genug zum Antippen (${kachel?.hoch} px, mindestens 44)`);
|
|
|
|
/* ---- 4b. Die Seite selbst ---- */
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/unsere-seiten.html`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(500);
|
|
|
|
/* window.open abfangen, damit keine echten Fenster aufgehen --
|
|
und damit messbar wird, WOHIN es gegangen waere. */
|
|
await seite.evaluate(() => {
|
|
window.__wohin = [];
|
|
window.open = (u) => { window.__wohin.push(String(u)); return null; };
|
|
});
|
|
|
|
const karten = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".aussen[data-weg]")].map((k) => k.dataset.weg));
|
|
ok(karten.length === 2, `zwei Karten nach draussen (${karten.length})`);
|
|
ok(karten.some((w) => w.includes("dogfather-universe.com")), "eine zur Website");
|
|
ok(karten.some((w) => w.includes("vans-diy-bastelbedarf.com")), "eine zum Shop");
|
|
|
|
/* KLICK AUF DEN TEXT -- nicht auf den Knopf. Genau das war
|
|
Filipes Wunsch: „egal wo man drauf drückt“. */
|
|
await seite.click(".aussen--shop .aussen__satz");
|
|
await seite.waitForTimeout(250);
|
|
let wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 1 && wohin[0].includes("vans-diy-bastelbedarf"),
|
|
`ein Klick auf den TEXT fuehrt zum Shop (${wohin.join(", ") || "nichts"})`);
|
|
|
|
/* GEGENPROBE: Der Kopierknopf darf NICHT zusaetzlich die Karte
|
|
ausloesen. Ohne diese Zeile waere „die Flaeche klickt“ auch
|
|
dann gruen, wenn sie ueberall klickt -- auch dort, wo es
|
|
stoert. */
|
|
await seite.evaluate(() => { window.__wohin = []; });
|
|
await seite.click("#code-knopf");
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 0,
|
|
`der Kopierknopf oeffnet NICHTS (${wohin.length} Fenster)`);
|
|
|
|
const hinweis = await seite.textContent("#code-hinweis");
|
|
ok(/kopiert|markiert|abschreiben/.test(hinweis || ""),
|
|
`und sagt, was er getan hat: ${(hinweis || "").trim()}`);
|
|
|
|
/* GEGENPROBE 2: Der eigene Knopf oeffnet genau EINMAL -- nicht
|
|
zweimal, weil die Karte mitklickt. */
|
|
await seite.evaluate(() => {
|
|
window.__wohin = [];
|
|
/* Der Knopf ist ein echter Link; sein Standardverhalten
|
|
unterbinden, sonst navigiert die Prueferin weg. */
|
|
for (const a of document.querySelectorAll(".aussen__knopf")) {
|
|
a.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
window.__wohin.push(a.href);
|
|
});
|
|
}
|
|
});
|
|
await seite.click(".aussen--dogi .aussen__knopf");
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 1,
|
|
`der Knopf „Seite oeffnen“ wirkt genau einmal (${wohin.length}x)`);
|
|
|
|
/* ---- 4c. Die Highlight-Karten ---- */
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/bereich.html?b=highlight`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1200);
|
|
await seite.evaluate(() => {
|
|
window.__wohin = [];
|
|
window.open = (u) => { window.__wohin.push(String(u)); return null; };
|
|
});
|
|
|
|
const stand = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".eintrag-karte")].map((k) => ({
|
|
titel: k.querySelector(".eintrag-titel")?.textContent?.trim() || "",
|
|
klickbar: k.dataset.klickbar === "ja",
|
|
})));
|
|
ok(stand.length === 3, `drei Karten stehen da (${stand.length})`);
|
|
|
|
const mitVideo = stand.find((s) => s.titel.includes("Komet"));
|
|
const ohneVideo = stand.find((s) => s.titel.includes("Nur ein Bild"));
|
|
const fort = stand.find((s) => s.titel.includes("Nicht mehr da"));
|
|
|
|
ok(mitVideo?.klickbar === true, "die Karte MIT Video ist klickbar");
|
|
/* DIE BEIDEN GEGENPROBEN. Ohne sie waere „klickbar“ auch dann
|
|
gruen, wenn ALLE Karten klicken -- und ein Klick auf ein
|
|
geloeschtes Video fuehrt auf eine Fehlerseite. */
|
|
ok(ohneVideo?.klickbar === false, "die Karte OHNE Video ist es nicht");
|
|
ok(fort?.klickbar === false,
|
|
"und die mit geloeschtem Video auch nicht (sonst Fehlerseite)");
|
|
|
|
/* Klick auf die Ueberschrift -- „egal wo“. */
|
|
const titelWahl = ".eintrag-karte[data-klickbar='ja'] .eintrag-titel";
|
|
await seite.click(titelWahl);
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 1 && wohin[0] === VIDEO,
|
|
`ein Klick auf die UEBERSCHRIFT fuehrt zum Video (${wohin[0] || "nichts"})`);
|
|
|
|
/* Und auf den Fliesstext. */
|
|
await seite.evaluate(() => { window.__wohin = []; });
|
|
const textWahl = ".eintrag-karte[data-klickbar='ja'] .eintrag-text";
|
|
if (await seite.$(textWahl)) {
|
|
await seite.click(textWahl);
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 1 && wohin[0] === VIDEO,
|
|
`und ein Klick auf den TEXT ebenso (${wohin[0] || "nichts"})`);
|
|
} else {
|
|
ok(false, "der Fliesstext der Karte liess sich nicht finden");
|
|
}
|
|
|
|
/* GEGENPROBE: Auf der NICHT klickbaren Karte passiert nichts. */
|
|
await seite.evaluate(() => { window.__wohin = []; });
|
|
const stiller = ".eintrag-karte:not([data-klickbar='ja']) .eintrag-titel";
|
|
await seite.click(stiller);
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 0,
|
|
`auf einer Karte ohne Video passiert NICHTS (${wohin.length})`);
|
|
|
|
/* DER TIKTOK-KNOPF MUSS WEITER EIN LINK SEIN. Waere er von
|
|
einem <a> umschlossen worden, haette der Browser ihn still
|
|
aufgeloest -- die haeufigste Art, eine Karte kaputtzumachen. */
|
|
const knopf = await seite.evaluate(() => {
|
|
const a = document.querySelector(".eintrag-karte[data-klickbar='ja'] .videozeile__weg");
|
|
return a ? { tag: a.tagName, href: a.getAttribute("href"),
|
|
imLink: !!a.closest("a a") } : null;
|
|
});
|
|
ok(knopf?.tag === "A" && knopf.href === VIDEO,
|
|
`der TikTok-Knopf ist noch ein echter Link (${knopf?.tag})`);
|
|
ok(knopf?.imLink === false, "und steckt in keinem zweiten Link");
|
|
|
|
/* DAS BILD FUEHRT ZUM VIDEO, nicht mehr ins Vollbild -- das war
|
|
Filipes „foto“. Ohne Bilddatei gibt es keinen Bildstreifen;
|
|
dann ist hier nichts zu messen, und das muss man sehen. */
|
|
const bild = await seite.evaluate(() => {
|
|
const a = document.querySelector(".eintrag-karte[data-klickbar='ja'] .bildstreifen__feld");
|
|
return a ? { href: a.getAttribute("href"), label: a.getAttribute("aria-label") } : null;
|
|
});
|
|
if (bild) {
|
|
ok(bild.href === VIDEO, `das Bild fuehrt zum Video (${bild.href})`);
|
|
ok(/Video ansehen/.test(bild.label || ""),
|
|
`und sagt das auch vorlesbar („${bild.label}“)`);
|
|
} else {
|
|
/* KEIN STILLES UEBERSPRINGEN. Das Bild ist angelegt; fehlt der
|
|
Streifen trotzdem, ist etwas kaputt -- entweder lud das
|
|
Bild nicht (dann entfernt der Fehlerbehandler den Rahmen)
|
|
oder die Karte zeigt gar keine Anhaenge mehr. Beides ist
|
|
ein Befund, kein Grund zu schweigen. */
|
|
ok(false, "der Bildstreifen fehlt, obwohl ein Bild angehaengt ist");
|
|
}
|
|
|
|
ok(kaputt.length === 0,
|
|
`kein Skriptfehler auf dem Weg${kaputt.length ? ": " + kaputt.join(" | ") : ""}`);
|
|
await ctx.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 { /* Rest */ }
|
|
process.exit(fehler ? 1 : 0);
|