/* ===================================================================== DER BACKSTAGE-IMPORT — eine Tabelle, alle Creator auf einmal. Filipe (10.09.2026): "wie sehr wir diese seite hier mit dem backstage verbinden könnten von tiktok. ohne probleme und ganz schnell." Die Recherche ergab: Backstage hat keine Schnittstelle und keinen Export. Was es hat, ist eine Tabelle im Browser. Also: markieren, kopieren, einfügen -- und der Workspace ordnet die Zeilen den Creators zu. DIE ZUORDNUNG IST DIE GANZE ARBEIT, und sie ist auch die ganze Gefahr: Eine falsch zugeordnete Zahl ist schlimmer als eine fehlende, weil die fehlende auffällt. Deshalb prüft diese Datei vor allem, was NICHT passiert: - eine unbekannte Person wird gemeldet, nicht geraten - eine Zeile ohne Zahlen wird übersprungen, nicht als Null geschrieben (sie würde sonst einen echten Tag überschreiben) - ein Datum in der Zukunft wird abgelehnt - ein Scout kommt gar nicht erst an diesen Weg Aufruf: node server/pruef-backstage-import.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; import { DatabaseSync } from "node:sqlite"; import { scryptSync, randomBytes } from "node:crypto"; import { notbremse } from "./helfer-notbremse.mjs"; notbremse(240_000, "pruef-backstage-import"); const HIER = dirname(fileURLToPath(import.meta.url)); const PORT = 4389; const BASIS = `http://127.0.0.1:${PORT}`; const ordner = mkdtempSync(join(tmpdir(), "ws-backstage-")); const DB = join(ordner, "workspace.db"); const jetzt = new Date().toISOString(); const heute = new Date().toISOString().slice(0, 10); const gestern = new Date(Date.now() - 86400000).toISOString().slice(0, 10); const morgen = new Date(Date.now() + 86400000).toISOString().slice(0, 10); let gemacht = 0, fehler = 0; const ok = (b, text) => { gemacht++; if (b) console.log(` ok ${text}`); else { fehler++; console.log(` FEHL ${text}`); } }; let kind = null; async function starten() { kind = spawn(process.execPath, [join(HIER, "index.js")], { env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT), SITE_ACCESS_SECRET: "lokaler-test", SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" }, stdio: ["ignore", "pipe", "pipe"] }); let aus = ""; kind.stdout.on("data", (d) => { aus += d; }); kind.stderr.on("data", (d) => { aus += d; }); for (let i = 0; i < 80; i++) { await new Promise((r) => setTimeout(r, 250)); if (kind.exitCode !== null) break; try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); if (a.status < 500) return; } catch { /* noch nicht oben */ } } throw new Error("Server startete nicht:\n" + aus.slice(-1500)); } await starten(); kind.kill(); await new Promise((r) => setTimeout(r, 600)); let idNova, idKiro, idLumi; { const d = new DatabaseSync(DB); const anlegen = (name, rolle, code, tiktok) => { 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, tiktok) VALUES (?,?,?,?,?,1,?,?)") .run(name, rolle, hash, salz, 32768, jetzt, tiktok ?? null); return d.prepare("SELECT last_insert_rowid() AS id").get().id; }; anlegen("Filipe", "admin", "CODE-DOGI-0001"); anlegen("Sam", "scout", "CODE-SAM-00001"); /* Nova hat ein Handle, Kiro NICHT -- er wird ueber den Namen gefunden. Lumi hat ein Handle mit anderer Schreibweise als in der Tabelle: Genau daran zeigt sich, ob normalisiert wird. */ idNova = anlegen("Nova", "creator", "CODE-NOVA-0001", "nova_live"); idKiro = anlegen("Kiro", "creator", "CODE-KIRO-0001", null); idLumi = anlegen("Lumi", "creator", "CODE-LUMI-0001", "LumiStreamt"); d.close(); } await starten(); const anmelden = async (rolle, code) => { const a = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle, code }) }); const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`); return keks; }; const keksDogi = await anmelden("admin", "CODE-DOGI-0001"); const keksScout = await anmelden("scout", "CODE-SAM-00001"); const ruf = async (weg, keks, koerper) => { const a = await fetch(BASIS + weg, { method: "POST", headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS }, body: JSON.stringify(koerper) }); let daten = null; try { daten = await a.json(); } catch { /* leer erlaubt */ } return { status: a.status, daten }; }; /* EINE TABELLE, WIE SIE AUS DEM BROWSER KOMMT: mit TABULATOREN. Genau das ist der Fall, den der alte Zerleger nicht kannte -- er suchte nur Komma und Semikolon. */ const TABELLE = [ "Creator\tHandle\tDiamanten\tLIVE-Dauer (Minuten)\tØ Zuschauer\tPeak Zuschauer\tNeue Follower", `Nova\t@Nova_Live\t12.480\t184\t42\t310\t23`, `Kiro\t\t3.900\t95\t18\t74\t6`, `Lumi\t@lumistreamt\t22.010\t240\t88\t512\t41`, `Fremde Person\t@niemand_hier\t500\t30\t5\t12\t1`, ].join("\n"); /* ======================================================================= 1. Die Vorschau ordnet zu -- und raet nicht ======================================================================= */ console.log("=== Die Vorschau ==="); { const { status, daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", keksDogi, { text: TABELLE, tag: gestern }); ok(status === 200, `die Tabelle wird gelesen (HTTP ${status})`); /* DER TABULATOR IST DER PUNKT: Ohne ihn stuende alles in Spalte 1 und es waere nichts erkannt worden. */ ok(daten?.zugeordnet === 3, `drei von vier Zeilen zugeordnet (${daten?.zugeordnet})`); ok(daten?.unbekannt?.length === 1 && /niemand_hier/.test(daten.unbekannt[0]), `die vierte wird beim Namen genannt (${(daten?.unbekannt || []).join(", ")})`); ok(daten?.neu === 3 && daten?.ersetzt === 0, `drei neu, keine ersetzt (${daten?.neu}/${daten?.ersetzt})`); const sp = Object.keys(daten?.spalten || {}); ok(sp.includes("diamanten") && sp.includes("dauer_min") && sp.includes("zuschauer_avg") && sp.includes("zuschauer_max") && sp.includes("follower_neu"), `fuenf Zahlenspalten erkannt (${sp.join(", ")})`); /* "12.480" ist deutsch geschrieben und muss 12480 sein, nicht 12,48. */ const nova = (daten?.beispiele || []).find((b) => b.name === "Nova"); ok(nova?.diamanten === 12480, `"12.480" wurde zu 12480 (${nova?.diamanten})`); } /* ======================================================================= 2. Ohne Datum in der Tabelle UND ohne Vorgabe: eine Frage, kein Unsinn ======================================================================= */ console.log("\n=== Wenn der Tag fehlt ==="); { const { status, daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", keksDogi, { text: TABELLE }); ok(status === 400 && daten?.braucht_tag === true, `es wird nach dem Tag gefragt statt geraten (HTTP ${status})`); } /* ======================================================================= 3. Uebernehmen -- und was dabei NICHT passiert ======================================================================= */ console.log("\n=== Übernehmen ==="); { const { status, daten } = await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: TABELLE, tag: gestern }); ok(status === 200, `die Übernahme läuft (HTTP ${status})`); ok(daten?.geschrieben === 3, `drei Zeilen geschrieben (${daten?.geschrieben})`); const d = new DatabaseSync(DB); const z = d.prepare("SELECT creator_id, diamanten, dauer_min, gueltiger_tag, quelle FROM leistung WHERE tag = ? ORDER BY creator_id").all(gestern); ok(z.length === 3, `drei Zeilen stehen in der Datenbank (${z.length})`); ok(z.every((x) => x.quelle === "import"), "alle mit Quelle 'import' -- von Hand getippte sind unterscheidbar"); ok(z.every((x) => x.gueltiger_tag === 1), "alle als gültiger Tag gezählt, weil LIVE-Dauer vorhanden ist"); const lumi = z.find((x) => x.creator_id === idLumi); ok(lumi?.diamanten === 22010, `Lumi wurde über "@lumistreamt" gegen "LumiStreamt" gefunden (${lumi?.diamanten})`); const kiro = z.find((x) => x.creator_id === idKiro); ok(kiro?.diamanten === 3900, `Kiro ohne Handle über den Namen (${kiro?.diamanten})`); /* DIE FREMDE PERSON DARF NICHT ENTSTANDEN SEIN. Ein Import, der nebenbei Leute anlegt, waere die schlimmste Variante von "hilfreich". */ const wieViele = d.prepare("SELECT COUNT(*) AS n FROM personen WHERE rolle = 'creator'").get().n; ok(wieViele === 3, `es wurde niemand neu angelegt (${wieViele} Creator)`); d.close(); } /* ======================================================================= 4. Noch einmal dieselbe Tabelle: ersetzen statt verdoppeln ======================================================================= */ console.log("\n=== Dieselbe Tabelle ein zweites Mal ==="); { const { daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", keksDogi, { text: TABELLE, tag: gestern }); ok(daten?.ersetzt === 3 && daten?.neu === 0, `jetzt drei ersetzt, keine neu (${daten?.neu}/${daten?.ersetzt})`); const { status } = await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: TABELLE, tag: gestern }); ok(status === 200, "und sie laeuft anstandslos durch"); const d = new DatabaseSync(DB); const n = d.prepare("SELECT COUNT(*) AS n FROM leistung WHERE tag = ?").get(gestern).n; ok(n === 3, `es sind immer noch drei Zeilen, keine sechs (${n})`); d.close(); } /* ======================================================================= 5. Eine von Hand geschriebene Notiz ueberlebt ======================================================================= */ console.log("\n=== Was von Hand kam, bleibt ==="); { const d = new DatabaseSync(DB); d.prepare("UPDATE leistung SET notiz = ? WHERE creator_id = ? AND tag = ?") .run("Technikprobleme ab 22 Uhr", idNova, gestern); d.close(); await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: TABELLE, tag: gestern }); const d2 = new DatabaseSync(DB); const n = d2.prepare("SELECT notiz FROM leistung WHERE creator_id = ? AND tag = ?") .get(idNova, gestern); ok(n?.notiz === "Technikprobleme ab 22 Uhr", `die Notiz steht nach dem Import noch da ("${n?.notiz}")`); d2.close(); } /* ======================================================================= 6. Gegenproben -- was abgelehnt wird ======================================================================= */ console.log("\n=== Gegenproben ==="); { const { status } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", keksDogi, { text: TABELLE, tag: morgen }); /* Ein Tag in der Zukunft laesst die Zeilen durchfallen -- die Vorschau antwortet zwar 200, ordnet aber nichts zu. */ const { daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", keksDogi, { text: TABELLE, tag: morgen }); ok(status === 200 && daten?.zugeordnet === 0 && daten?.probleme_gesamt >= 3, `ein Datum in der Zukunft wird abgelehnt (${daten?.zugeordnet} zugeordnet, ` + `${daten?.probleme_gesamt} Probleme)`); } { const ohneZahlen = ["Creator\tHandle\tDiamanten", "Nova\t@Nova_Live\t"].join("\n"); const { daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", keksDogi, { text: ohneZahlen, tag: gestern }); ok(daten?.zugeordnet === 0, "eine Zeile ganz ohne Zahl wird uebersprungen statt als Null geschrieben"); } { const ohneNamen = ["Datum\tDiamanten", `${gestern}\t100`].join("\n"); const { status, daten } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", keksDogi, { text: ohneNamen }); ok(status === 400 && /Name oder Handle/.test(daten?.fehler || ""), `ohne Personenspalte wird es abgelehnt ("${daten?.fehler}")`); } { const { status } = await ruf("/workspace/api/leistung/netzwerk-import", keksScout, { text: TABELLE, tag: gestern }); ok(status === 403, `ein Scout kommt an diesen Weg nicht (HTTP ${status})`); } { /* GEGENPROBE ZUR GEGENPROBE: Das Richtige geht weiterhin durch. Ohne sie hiesse "alles wird abgelehnt" auch "alles wird abgelehnt". */ const { status } = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", keksDogi, { text: TABELLE, tag: heute }); ok(status === 200, `heute als Tag geht dagegen durch (HTTP ${status})`); } /* ======================================================================= 7. Und die Seite selbst ======================================================================= */ console.log("\n=== Die Seite ==="); { 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: 1400, height: 1000 } }); const seite = await ctx.newPage(); const skriptfehler = []; seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140))); await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); 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: 25000 }); await seite.goto(BASIS + "/workspace/leistung.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(1800); ok(skriptfehler.length === 0, `kein Skriptfehler (${skriptfehler.join(" | ") || "keiner"})`); const sichtbar = await seite.$eval("#netz-oeffnen", (e) => !e.hidden).catch(() => false); ok(sichtbar === true, "DogFather sieht den Knopf „Backstage-Tabelle einfügen\""); /* Einfuegen, Vorschau abwarten, Meldung lesen -- der ganze Weg. */ await seite.click("#netz-oeffnen"); await seite.waitForTimeout(400); await seite.fill("#netz-tag", gestern); await seite.fill("#netz-text", TABELLE); await seite.dispatchEvent("#netz-text", "change"); await seite.waitForTimeout(1200); const vorschau = await seite.$eval("#netz-vorschau", (e) => e.textContent).catch(() => ""); ok(/3 von 4 Zeilen zugeordnet/.test(vorschau), `die Vorschau nennt die Zahl (${vorschau.slice(0, 60)}…)`); ok(/niemand_hier/.test(vorschau), "und den Namen, der nicht zugeordnet wurde"); const losFrei = await seite.$eval("#netz-los", (e) => !e.disabled); ok(losFrei === true, "der Übernehmen-Knopf ist danach frei"); await browser.close(); } kind.kill(); await new Promise((r) => setTimeout(r, 400)); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ } console.log(`\n${gemacht} Pruefungen`); console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`); process.exit(fehler ? 1 : 0);