Files
DogFatherGitandClaude Opus 5 9a71f3a242 Eine Null zu viel -- HasiDog-Videos waeren abgelehnt worden
In KANAELE stand "@hasidog00804". Der Account heisst "@hasidog0804".
Nachgemessen bei TikTok selbst: der richtige hat 65 Follower, der
andere liefert "Couldn't find this account".

Das war nicht kosmetisch. `kanalVonHandle` ordnet ein hochgeladenes
Video ueber genau diesen Text einem Kanal zu. Ein echtes
HasiDog-Video waere mit 403 abgelehnt worden -- und die Begruendung
haette gelautet: "ist keiner deiner Kanaele. Moeglich sind:
@dogfather0804, @hasidog00804, @dogis.modi.gang." Eine Sackgasse, die
zusaetzlich eine Adresse nennt, die es nicht gibt.

WARUM ES KEINE PRUEFUNG GEFUNDEN HAT: pruef-kanalzeile und
pruef-teilen hatten denselben Handle abgeschrieben. Zwei Abschriften
desselben Tippfehlers bestaetigen sich gegenseitig -- beide waren
gruen. Sie leiten ihn jetzt aus KANAELE ab und koennen ihn damit gar
nicht mehr eigenstaendig falsch haben.

Gefunden hat es ein Vergleich: Die oeffentliche Website verlinkt
dieselben Accounts und hatte die richtige Schreibweise. Genau das ist
jetzt Abschnitt 7 von pruef-kanaele -- alle Handles muessen auch
draussen vorkommen, mit Gegenprobe, dass eine falsche Adresse
durchfaellt. Ohne Netz, damit eine Stoerung bei TikTok nicht zu einem
roten Alarm im Haus wird.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 11:51:06 +02:00

275 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`;
/* DIE HANDLES WERDEN ABGELEITET, NICHT ABGESCHRIEBEN (21.09.2026).
Hier standen sie als Text im Pruefcode -- und einer davon war falsch:
"@hasidog00804" mit einer Null zu viel. Er stand genauso falsch in
KANAELE, weshalb die Pruefung gruen war: Zwei Abschriften desselben
Tippfehlers bestaetigen sich gegenseitig. Nachgemessen bei TikTok
selbst gibt es nur "@hasidog0804" (65 Follower); die andere Adresse
liefert "Couldn't find this account".
Jetzt kommen sie aus KANAELE. Damit kann diese Datei den Wert gar
nicht mehr eigenstaendig falsch haben -- sie misst den echten. */
const { KANAELE } = await import("./workspace.js");
const HANDLES = KANAELE.map((k) => k.handle.replace(/^@/, ""));
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 = ["Casper klaut die Fernbedienung 😄",
"Casper schläft im Wäschekorb",
"Der Chat entscheidet: Horror oder Koop?"];
const titel = TITEL[HANDLES.indexOf(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(HANDLES[1], "102"));
console.log(" Video 3:", await video(HANDLES[2], "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);