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]>
260 lines
12 KiB
JavaScript
260 lines
12 KiB
JavaScript
/* =====================================================================
|
|
DIE KANALZEILE (17.09.2026) -- Schritt 3 aus dem Video-Plan
|
|
|
|
Filipe: "fuer meinen Account, dogfather, dann dogfather clips und
|
|
hasidog."
|
|
|
|
Geprueft wird nicht nur, DASS die Zeile da ist:
|
|
|
|
* Bei weniger als zwei Kanaelen darf sie NICHT erscheinen. Ein
|
|
Filter, der nichts zu filtern hat, macht misstrauisch -- und
|
|
wer einmal einen Knopf gedrueckt hat, bei dem nichts passiert,
|
|
drueckt den naechsten nicht mehr.
|
|
* Ein Klick muss wirklich weniger Karten uebrig lassen, und zwar
|
|
die richtigen.
|
|
* Ein zweiter Klick auf denselben Knopf hebt den Filter auf.
|
|
* Kanal- und Art-Filter wirken ZUSAMMEN -- das war der Grund, den
|
|
Kanal als zweite Dimension zu bauen statt als weiteren Wert in
|
|
derselben Leiste.
|
|
|
|
Aufrufen mit: node server/pruef-kanalzeile.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createServer } from "node:http";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-kz-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "das Bildwerkzeug");
|
|
const TIKTOK = await eigenerPort(import.meta, "nachgebautes TikTok", 1);
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
process.env.TIKTOK_OEMBED_BASIS = `http://127.0.0.1:${TIKTOK}/oembed`;
|
|
|
|
const PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
|
"base64");
|
|
const tiktok = createServer((q, a) => {
|
|
const u = new URL(q.url, `http://127.0.0.1:${TIKTOK}`);
|
|
if (u.pathname === "/oembed") {
|
|
const ziel = u.searchParams.get("url") || "";
|
|
const wer = (ziel.match(/@([^/?]+)/) || [])[1] || "";
|
|
if (!wer) { a.writeHead(404); a.end("{}"); return; }
|
|
const titel = { "dogfather0804": "Casper klaut die Fernbedienung 😄",
|
|
"hasidog00804": "Casper schläft im Wäschekorb",
|
|
"dogis.modi.gang": "Der Chat entscheidet: Horror oder Koop?" }[wer] || "Video";
|
|
a.writeHead(200, { "content-type": "application/json" });
|
|
a.end(JSON.stringify({
|
|
version: "1.0", type: "video", title: titel,
|
|
author_url: `https://www.tiktok.com/@${wer}`,
|
|
thumbnail_url: `http://127.0.0.1:${TIKTOK}/cover.jpg`,
|
|
thumbnail_width: 576, thumbnail_height: 1024,
|
|
}));
|
|
return;
|
|
}
|
|
if (u.pathname === "/cover.jpg") {
|
|
a.writeHead(200, { "content-type": "image/jpeg" }); a.end(PNG); return;
|
|
}
|
|
a.writeHead(404); a.end("{}");
|
|
});
|
|
await new Promise((r) => tiktok.listen(TIKTOK, "127.0.0.1", r));
|
|
|
|
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 }); };
|
|
await import("./index.js");
|
|
await new Promise((r) => setTimeout(r, 1200));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
for (let i = 0; i < 60; i++) {
|
|
try {
|
|
const t = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { t.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { t.close(); }
|
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
}
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const j = 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, j);
|
|
d.close();
|
|
|
|
const an = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
|
|
});
|
|
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
|
|
async function video(handle, nr) {
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS },
|
|
body: JSON.stringify({ url: `http://127.0.0.1:${TIKTOK}/@${handle}/video/${nr}` }),
|
|
});
|
|
return a.status;
|
|
}
|
|
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1200, height: 900 }, deviceScaleFactor: 2 });
|
|
await ctx.addCookies(keks.split("; ").map((k) => {
|
|
const [name, ...rest] = k.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await ctx.newPage();
|
|
const konsolenfehler = [];
|
|
seite.on("console", (m) => { if (m.type() === "error") konsolenfehler.push(m.text()); });
|
|
seite.on("pageerror", (f) => konsolenfehler.push(String(f)));
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const zeileDa = async () => !!(await seite.$(".filter__kanaele"));
|
|
/* DER SELEKTOR HEISST .eintrag-karte -- nachgesehen in bereich.js
|
|
(el("article", "eintrag-karte")), nicht geraten. Mein erster
|
|
Versuch traf nichts, und die Messung meldete brav "0 -> 0": Das
|
|
sah aus wie ein Ergebnis und war keins.
|
|
|
|
BEIM BERICHTIGEN bin ich dann in die naechste Falle gelaufen und
|
|
habe daraus "undefined -> undefined" gemacht: Das Patchskript
|
|
benutzte String.replace, und dort bedeutet "$$" im ERSATZTEXT ein
|
|
einzelnes "$". Aus seite.$$(...) wurde seite.$(...). Der Fehler
|
|
steht woertlich in meinen eigenen Regeln -- und hat trotzdem
|
|
zugeschlagen. Aufgefallen ist er nur, weil die Ausgabe gelesen
|
|
wurde: "undefined" ist wenigstens ehrlich, "0" waere es nicht
|
|
gewesen. */
|
|
const karten = async () => (await seite.$$(".eintrag-karte")).length;
|
|
|
|
/* ---- 1. EIN Kanal: die Zeile darf NICHT da sein --------------------- */
|
|
console.log("\n=== 1. Mit nur einem Kanal ===");
|
|
console.log(" Video 1:", await video("dogfather0804", "101"));
|
|
await seite.goto(BASIS + "/workspace/bereich.html?b=highlight", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1500);
|
|
ok(!(await zeileDa()), "bei einem einzigen Kanal steht dort KEINE Filterzeile");
|
|
|
|
/* ---- 2. Drei Kanäle -------------------------------------------------- */
|
|
console.log("\n=== 2. Mit drei Kanaelen ===");
|
|
console.log(" Video 2:", await video("hasidog00804", "102"));
|
|
console.log(" Video 3:", await video("dogis.modi.gang", "103"));
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1800);
|
|
ok(await zeileDa(), "sobald drei Kanaele da sind, erscheint sie");
|
|
const knoepfe = await seite.$$eval(".filter__kanal", (ns) => ns.map((n) => ({
|
|
text: n.textContent.trim().replace(/\s+/g, " "),
|
|
kanal: n.dataset.kanal || "(alle)",
|
|
gewaehlt: n.getAttribute("aria-pressed"),
|
|
})));
|
|
for (const k of knoepfe) console.log(` ${k.gewaehlt === "true" ? "[x]" : "[ ]"} ${k.text}`);
|
|
ok(knoepfe.length === 4, `vier Knoepfe: Alle Kanaele und die drei (${knoepfe.length})`);
|
|
ok(knoepfe[0]?.gewaehlt === "true", "und „Alle Kanaele“ ist die Vorgabe");
|
|
|
|
const zeile = await seite.$(".filter__kanaele");
|
|
if (zeile) {
|
|
const leiste = await seite.$("#filter");
|
|
await (leiste || zeile).scrollIntoViewIfNeeded();
|
|
await seite.waitForTimeout(400);
|
|
await (leiste || zeile).screenshot({ path: "server/bild-kanalzeile.png" });
|
|
}
|
|
|
|
/* ---- 3. Ein Klick filtert wirklich ----------------------------------- */
|
|
console.log("\n=== 3. Ein Klick ===");
|
|
const vorher = await karten();
|
|
ok(vorher === 3, `ungefiltert liegen drei Videos da (${vorher})`);
|
|
|
|
const hasi = await seite.$('.filter__kanal[data-kanal="hasidog"]');
|
|
ok(!!hasi, "der HasiDog-Knopf ist da");
|
|
if (hasi) {
|
|
await hasi.click();
|
|
await seite.waitForTimeout(1500);
|
|
const nachher = await karten();
|
|
ok(nachher === 1, `nach einem Klick auf HasiDog bleibt eines uebrig (${nachher})`);
|
|
|
|
/* UND ZWAR DAS RICHTIGE. "Weniger" allein waere auch erfuellt, wenn
|
|
der Filter irgendetwas wegwirft -- das ist der Unterschied
|
|
zwischen filtern und kaputt sein. */
|
|
const titel = await seite.$$eval(".eintrag-karte",
|
|
/* /\s+/ und nicht /s+/ -- beim Erzeugen dieser Datei ist der
|
|
Backslash durch eine Escaping-Ebene verloren gegangen, und der
|
|
Ausdruck ersetzte danach JEDES "s" durch ein Leerzeichen. In der
|
|
Ausgabe stand "Ca per chläft im Wä chekorb". Das war kein Fehler
|
|
im Filter, sondern einer in der Pruefung -- und er hat sie rot
|
|
gemacht, was genau richtig ist: Eine Pruefung, die ihren eigenen
|
|
Text zerstoert, darf nicht gruen sein. */
|
|
(ns) => ns.map((n) => n.textContent.replace(/\s+/g, " ").trim()));
|
|
for (const t of titel) console.log(" " + t.slice(0, 60));
|
|
ok(titel.length === 1 && /Wäschekorb/.test(titel[0]),
|
|
"und es ist wirklich das HasiDog-Video");
|
|
|
|
const leiste2 = await seite.$("#filter");
|
|
if (leiste2) await leiste2.screenshot({ path: "server/bild-kanalzeile-gefiltert.png" });
|
|
|
|
/* Der gewaehlte Knopf sagt es auch. */
|
|
const gedrueckt = await seite.$$eval(".filter__kanal",
|
|
(ns) => ns.filter((n) => n.getAttribute("aria-pressed") === "true")
|
|
.map((n) => n.dataset.kanal || "(alle)"));
|
|
ok(gedrueckt.length === 1 && gedrueckt[0] === "hasidog",
|
|
`genau ein Knopf ist gewaehlt (${gedrueckt.join(", ")})`);
|
|
|
|
/* ---- Und ein zweiter Klick hebt ihn auf ---------------------------
|
|
Ohne das sucht man den Weg zurueck zu "Alle Kanaele" und findet
|
|
ihn nicht, weil man nicht mehr weiss, dass man gefiltert hat. */
|
|
const nochmal = await seite.$('.filter__kanal[data-kanal="hasidog"]');
|
|
await nochmal.click();
|
|
await seite.waitForTimeout(1500);
|
|
ok((await karten()) === vorher,
|
|
`ein zweiter Klick hebt den Filter auf (wieder ${await karten()})`);
|
|
}
|
|
|
|
/* ---- 4. Kanal UND Art zusammen --------------------------------------
|
|
Der eigentliche Grund fuer zwei getrennte Reihen. Waere der Kanal
|
|
nur ein weiterer Wert in derselben Leiste, koennte man das hier
|
|
nicht fragen -- und die Pruefung waere trotzdem gruen geblieben. */
|
|
console.log("\n=== 4. Beide Filter zusammen ===");
|
|
{
|
|
const df = await seite.$('.filter__kanal[data-kanal="dogfather"]');
|
|
await df.click();
|
|
await seite.waitForTimeout(1200);
|
|
const nurKanal = await karten();
|
|
ok(nurKanal === 1, `nur DogFather: ${nurKanal}`);
|
|
|
|
/* Jetzt zusaetzlich eine Art, die dieses Video NICHT hat. */
|
|
const arten = await seite.$$eval("#filter > .schritt",
|
|
(ns) => ns.map((n) => n.textContent.trim()));
|
|
console.log(" Arten in der ersten Reihe: " + arten.join(", "));
|
|
const bild = (await seite.$$("#filter > .schritt"))
|
|
.find ? null : null;
|
|
const alleKnoepfe = await seite.$$("#filter > .schritt");
|
|
let getroffen = false;
|
|
for (const k of alleKnoepfe) {
|
|
const t = (await k.textContent()).trim();
|
|
if (t !== "Bild") continue;
|
|
await k.click();
|
|
await seite.waitForTimeout(1200);
|
|
getroffen = true;
|
|
ok((await karten()) === 0,
|
|
`DogFather + Art "Bild" ergibt nichts (${await karten()}) -- beide Filter wirken`);
|
|
break;
|
|
}
|
|
ok(getroffen, "die Art „Bild“ war zum Gegenprobieren da");
|
|
void bild;
|
|
}
|
|
|
|
console.log("");
|
|
ok(konsolenfehler.length === 0,
|
|
konsolenfehler.length ? konsolenfehler.join(" | ") : "keine Fehler in der Browserkonsole");
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
await browser.close();
|
|
await new Promise((r) => tiktok.close(r));
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
|
process.exit(fehler ? 1 : 0);
|