Alles aus der Excel-Datei -- und der Fund, der alles blockiert haette

Filipe: "ich will dass alles von der excel datei genommen wird.
perfektionnier das, aber wenn ich dir runter lade soll alles notiert
und angezeigt werden." Dazu eine echte Ausgabe als Vorlage.

DER SCHWERSTE FUND STECKTE VOR DEN DATEN, NICHT IN IHNEN.

Seine Ausgabe "Creator_innendaten" hat DREI Spalten, die nach Person
aussehen: "Creator*in-ID", "Creator*innen-Anmeldename" und "Agent".
`personSpaltenRaten` nahm die erste mit dem Wort "creator" darin --
die ID. Danach wurde nach einem Creator namens "700001" gesucht.
Nachgemessen an seiner echten Kopfzeile: handle = KEINE, name =
"Creator*in-ID". Diese Datei haette KEINE EINZIGE Zeile zugeordnet,
mit einem Hinweis ("steht bei keinem Creator im Feld TikTok"), der in
die voellig falsche Richtung zeigt.

Jetzt ist "Anmeldename" der Handle, eine Kennnummer ist fuer beide
Spalten ausgeschlossen, und "creator" allein reicht nicht mehr als
Namensspalte -- sonst haette weiter hinten "Neue*r LIVE-Creator*innen"
(Wert: "Nein") die Stelle uebernommen. An fuenf Kopfzeilen gemessen.

UND DANN: NICHTS FAELLT MEHR WEG.

41 Spalten in der Datei, acht werden gedeutet. Die restlichen 33 --
letzter Monat, fuenf Prozentwerte, Matches, Multi-Gast-LIVEs, Fanclub,
Graduierungs- und Stufenstatus -- wurden lautlos weggeworfen.

KEINE 33 NEUEN SPALTEN, sondern eine Zeile je Spalte mit dem NAMEN als
Schluessel. Eine abgeschriebene Spaltenliste hat in diesem Haus schon
zweimal Daten gekostet und waere beim naechsten Backstage-Update
falsch. Gegenprobe in der Pruefung: eine erfundene Spalte
("Sternenstaub pro Woche") kommt genauso durch -- es wird also keine
Liste gepflegt, die Datei entscheidet.

An SEINER echten Datei gemessen, ohne sie irgendwo hineinzuschreiben:
40 von 41 Spalten gespeichert (die 41. ist leer), Zeitraum 01.09.-
13.09. erkannt, 32 als Zahl, 8 als Text.

UND EIN MESSFEHLER, DER LEHRREICH IST: Meine erste Pruefung meldete
"zugeklappt ist die Liste 141 px hoch", im Bildschirmfoto war dort
nichts. An einem Miniaturfall nachgemessen: getBoundingClientRect,
offsetHeight, offsetParent und getClientRects liefern bei einem
<details> in BEIDEN Zustaenden identische Werte -- Chromium verbirgt
den Inhalt mit content-visibility:hidden, und das behaelt die letzte
Ausmessung. Nur checkVisibility() kann es unterscheiden. Die Messung
log, nicht die Seite.

pruef-backstage-import 160 (war 133), pruef-xlsx 75,
pruef-leistung-optik 59, pruef-leistung, pruef-css-klassen und
pruef-auskunft (46, DSGVO -- die neue Tabelle ist automatisch dabei,
weil die Auskunft ihre Liste aus PRAGMA foreign_key_list ableitet):
alle gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-16 01:46:07 +02:00
co-authored by Claude Opus 5
parent 984f0dd7a9
commit 8fc1ac7a7a
35 changed files with 999 additions and 369 deletions
+301 -1
View File
@@ -469,6 +469,219 @@ for (const [rolle, code, wer] of [
}
/* =======================================================================
ALLE 41 SPALTEN -- nichts faellt mehr weg (16.09.2026)
=======================================================================
Filipe: "ich will dass alles von der excel datei genommen wird.
perfektionnier das, aber wenn ich dir runter lade soll alles notiert
und angezeigt werden."
---------------------------------------------------------------------
DIE VORLAGE IST SEINE ECHTE AUSGABE vom 14.09.2026 ("Creator:innen-
daten"). Sie hat EINUNDVIERZIG Spalten; erkannt und gedeutet werden
acht. Die restlichen 33 wurden beim Einlesen weggeworfen, ohne dass
irgendwo stand, dass es sie gab.
DIE ECHTE DATEI STEHT NICHT IM REPO -- in ihr stehen Name, ID und
Agent seiner Creator. Was hier steht, sind ihre UEBERSCHRIFTEN (die
gehoeren TikTok, nicht ihm) mit erfundenen Werten. Die echte Datei
hat die Form belegt; diese Pruefung haelt sie fest.
WAS GEMESSEN WIRD, und jedes davon hat einen Grund:
* Alle 41 Spalten kommen an -- gezaehlt, nicht gehofft.
* Eine Spalte, die es in KEINEM Muster gibt ("Graduierungsstatus"),
kommt mit ihrem TEXT an. Waere nur die Zahl gespeichert, stuende
dort nichts.
* Eine LEERE Zelle erzeugt keine Zeile -- sonst stuende auf der
Karte "Status: (nichts)", eine Auskunft, die die Datei nicht gibt.
* Eine ERFUNDENE Spalte kommt genauso durch. Das ist die
eigentliche Gegenprobe: Sie beweist, dass hier keine Liste
gepflegt wird, sondern die Datei entscheidet -- also auch eine
Spalte durchkommt, die TikTok erst naechstes Jahr hinzufuegt.
* Ein ZWEITER Durchgang mit weniger Spalten laesst keine Leichen
zurueck. Ohne das Wegraeumen vorher stuende ein Wert aus einer
Datei da, die es nicht mehr gibt.
======================================================================= */
console.log("");
console.log("=== Alle Spalten der echten Ausgabe ===");
{
/* Die 41 Ueberschriften, wortgleich aus seiner Ausgabe. */
const KOPF = [
"Datenzeitraum", "Creator*in-ID", "Creator*innen-Anmeldename", "Gruppe",
"Agent", "Beitrittszeit", "Tage seit dem Beitritt", "Diamanten",
"LIVE-Dauer", "Gültige LIVE-Gehen-Tage", "Neue Follower*innen", "Livestreams",
"Diamanten im letzten Monat", "LIVE-Dauer (in Std.) im letzten Monat",
"Gültige LIVE-Gehen-Tage im letzten Monat",
"Neue Follower*innen im letzten Monat", "Livestreams im letzten Monat",
"Diamanten - Prozent erreicht", "LIVE-Dauer - Prozent erreicht",
"Gültige LIVE-Gehen-Tage - Prozent erreicht",
"Neue Follower*innen - Prozent erreicht", "Livestreams - Prozent erreicht",
"Diamanten - im Vergleich zu letztem Monat",
"LIVE-Dauer - im Vergleich zu letztem Monat",
"Gültige LIVE-Gehen-Tage - im Vergleich zu letztem Monat",
"Neue Follower*innen - im Vergleich zu letztem Monat",
"Livestreams - im Vergleich zu letztem Monat",
"Matches", "Diamanten aus Matches", "Neue*r LIVE-Creator*innen",
"Diamanten von Multi-Gast-LIVEs", "Diamanten von Multi-Gast-LIVEs (als Host)",
"Diamanten von Multi-Gast-LIVEs (als Gäst*in)",
"Graduierungsstatus", "Stufen-Status", "Neue Fans",
"Gesamte Diamanten des Fanclubs", "Beitrag durch Fans in %",
"Fans insgesamt", "Aktive Fans vom Fanclub", "Status",
];
ok(KOPF.length === 41, `die Vorlage hat ${KOPF.length} Spalten`);
/* Erfundene Werte in der FORM der echten -- mit allen Eigenheiten,
die dort vorkommen: Prozent mit englischem Punkt, eine Dauer mit
Einheiten, ein Bruch, der trotz "%" im Namen KEINER ist, Text
statt Zahl, und eine leere Zelle am Ende. */
const WERT = [
"2026-08-01 ~ 2026-08-31", "700001", "@Nova_Live", "Dogfather",
"Scout Sam", "2026-07-09 19:30:57 (UTC+0)", "68", "13210",
"6Std. 24Min. 7Sek.", "5", "97", "5",
"29488", "13Std. 22Min. 40Sek.", "9", "126", "16",
"44.79%", "47.85%", "55.55%", "76.98%", "31.25%",
"98.55%", "83.68%", "150.00%", "361.90%", "0.00%",
"13", "8665", "Nein",
"26", "26", "0",
"Nicht graduiert", "Nicht beibehalten", "17",
"13128", "0.99379", "1182", "75", "",
];
ok(WERT.length === KOPF.length,
`und genauso viele Werte (${WERT.length})`);
const alles = [KOPF.join("\t"), WERT.join("\t")].join("\n");
const v = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
keksDogi, { text: alles });
ok(v.daten?.spalten_gesamt === 41,
`die Vorschau nennt alle Spalten der Datei (${v.daten?.spalten_gesamt})`);
ok(v.daten?.felder === 40,
`und wie viele davon einen Wert tragen (${v.daten?.felder} -- "Status" ist leer)`);
const a = await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: alles });
ok(a.daten?.felder === 40, `uebernommen werden ${a.daten?.felder} Werte`);
const d4 = new DatabaseSync(DB, { readOnly: true });
const felder = d4.prepare(`SELECT nr, name, text, zahl FROM leistung_zeitraum_feld
WHERE creator_id = ? AND von = ? AND bis = ? ORDER BY nr`)
.all(idNova, "2026-08-01", "2026-08-31");
d4.close();
ok(felder.length === 40,
`in der Datenbank stehen ${felder.length} Felder (41 Spalten, eine leer)`);
const nach = new Map(felder.map((f) => [f.name, f]));
ok(felder.length > 0 && !nach.has("Status"),
"die leere Zelle erzeugt KEINE Zeile -- sonst staende dort \"Status: nichts\"");
/* ---- WER IST DAS UEBERHAUPT? (16.09.2026) --------------------------
DER TEUERSTE FEHLER DIESES TAGES, und er stand nicht im Verdacht.
Seine Ausgabe hat DREI Spalten, die nach Person aussehen:
"Creator*in-ID", "Creator*innen-Anmeldename" und "Agent". Die alte
Regel nahm die erste mit dem Wort "creator" darin -- die ID. Danach
wurde nach einem Creator namens "700001" gesucht, und die ganze
Datei kam als "nicht zugeordnet" zurueck, mit einem Hinweis, der
in die voellig falsche Richtung zeigte.
Die Pruefung oben faellt darauf herein wie er: Ohne Zuordnung gibt
es keinen Zeitraum, also auch keine Felder. Deshalb steht es hier
noch einmal ausdruecklich -- damit beim naechsten Mal die MELDUNG
sagt, was los ist, und nicht nur eine Zahl fehlt. */
ok(nach.get("Creator*innen-Anmeldename")?.text === "@Nova_Live",
"die Zeile wurde ueber den Anmeldenamen zugeordnet, nicht ueber die ID");
ok(nach.get("Creator*in-ID")?.text === "700001",
"und die ID steht trotzdem in der Ablage -- sie ist eine Auskunft, kein Schluessel");
{
/* GEGENPROBE: Eine Tabelle, in der NUR eine Kennnummer steht, darf
niemanden zuordnen -- und muss das sagen, statt stillschweigend
nichts zu tun. */
const nurId = [
["Datenzeitraum", "Creator*in-ID", "Diamanten"].join("\t"),
["2026-08-01 ~ 2026-08-31", "700001", "500"].join("\t"),
].join("\n");
const r = await ruf("/workspace/api/leistung/netzwerk-import/vorschau",
keksDogi, { text: nurId });
ok(r.status === 400 && /Name oder Handle/.test(r.daten?.fehler || ""),
`eine Tabelle mit nur einer Kennnummer wird begruendet abgelehnt `
+ `(HTTP ${r.status}: "${String(r.daten?.fehler || "").slice(0, 40)}…")`);
}
/* Eine Spalte, die KEIN Muster kennt, und ihr Wert ist TEXT. */
ok(nach.get("Graduierungsstatus")?.text === "Nicht graduiert",
`eine Spalte ohne Muster kommt woertlich an ("${nach.get("Graduierungsstatus")?.text}")`);
ok(nach.get("Graduierungsstatus")?.zahl === null,
"und traegt keine erfundene Zahl");
/* Prozent mit englischem Punkt -- die Falle, an der eine deutsche
Zahlenlesung 4479 statt 44,79 liefern wuerde. */
ok(nach.get("Diamanten - Prozent erreicht")?.zahl === 44.79,
`"44.79%" wird zu ${nach.get("Diamanten - Prozent erreicht")?.zahl}, nicht zu 4479`);
/* Und der Bruch, der trotz "%" im Spaltennamen keiner ist. */
ok(nach.get("Beitrag durch Fans in %")?.zahl === 0.99379,
`"0.99379" bleibt ${nach.get("Beitrag durch Fans in %")?.zahl} `
+ "-- der Spaltenname sagt Prozent, der Wert ist ein Bruch");
/* Die Reihenfolge der Datei bleibt erhalten. */
ok(felder[0].name === "Datenzeitraum" && felder[felder.length - 1].name === "Aktive Fans vom Fanclub",
`die Reihenfolge der Datei bleibt (${felder[0].name} … ${felder[felder.length - 1].name})`);
/* ---- DIE EIGENTLICHE GEGENPROBE ------------------------------------
Eine Spalte, die es bei TikTok (noch) gar nicht gibt. Kommt sie
durch, wird hier keine Liste gepflegt -- sondern die Datei
entscheidet. Genau das soll die Bauart leisten. */
{
const erfunden = [
[...KOPF, "Sternenstaub pro Woche"].join("\t"),
[...WERT, "4711"].join("\t"),
].join("\n");
await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: erfunden });
const d5 = new DatabaseSync(DB, { readOnly: true });
const f = d5.prepare(`SELECT text, zahl FROM leistung_zeitraum_feld
WHERE creator_id = ? AND von = ? AND bis = ? AND name = ?`)
.get(idNova, "2026-08-01", "2026-08-31", "Sternenstaub pro Woche");
d5.close();
ok(f?.zahl === 4711,
`auch eine Spalte, die es heute nicht gibt, kommt an (${f?.zahl})`);
}
/* ---- UND KEINE LEICHEN ---------------------------------------------
Dieselbe Datei noch einmal, diesmal OHNE die erfundene Spalte. Sie
muss verschwinden. Ohne das Wegraeumen vor dem Schreiben stuende
dort ein Wert aus einer Datei, die es nicht mehr gibt -- und
niemand koennte ihn von einem frischen unterscheiden. */
{
await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: alles });
const d6 = new DatabaseSync(DB, { readOnly: true });
const n = d6.prepare(`SELECT COUNT(*) AS n FROM leistung_zeitraum_feld
WHERE creator_id = ? AND von = ? AND bis = ?`)
.get(idNova, "2026-08-01", "2026-08-31").n;
const weg = d6.prepare(`SELECT 1 FROM leistung_zeitraum_feld
WHERE creator_id = ? AND name = ?`).get(idNova, "Sternenstaub pro Woche");
d6.close();
ok(!weg && n === 40,
`beim zweiten Einlesen bleibt keine Leiche zurueck (${n} Felder)`);
}
/* ---- UND DIE SEITE BEKOMMT SIE AUCH ------------------------------ */
{
const antwort = await (await fetch(BASIS + `/workspace/api/leistung/${idNova}`,
{ headers: { Cookie: keksDogi } })).json();
/* GEZIELT DEN AUGUST, nicht "den ersten". Nova hat inzwischen zwei
Zeitraeume; `[0]` waere der jüngere und die Pruefung haette ohne
Vorwarnung etwas anderes gemessen als gemeint. Genau dieser
Fehler ist mir am 14.09. schon einmal unterlaufen. */
const z = (antwort?.zeitraeume || []).find((x) => x.von === "2026-08-01");
ok(Array.isArray(z?.felder) && z.felder.length === 40,
`die Seite bekommt alle Werte mitgeliefert (${z?.felder?.length})`);
ok(z?.felder?.[0]?.name === "Datenzeitraum",
"und zwar in der Reihenfolge der Datei");
/* Und der ANDERE Zeitraum hat seine eigenen -- die Felder haengen
am Zeitraum, nicht am Creator. */
const sept = (antwort?.zeitraeume || []).find((x) => x.von === "2026-09-01");
ok((sept?.felder || []).length === 5,
`der andere Zeitraum hat seine eigenen ${(sept?.felder || []).length} Werte`);
}
}
{
/* ---- DER EINZEL-IMPORT KENNT DEN DRITTEN AUSGANG (14.09.2026) ----
Als tagAusZelle() den Zeitraum dazubekam, kannte ihn nur der
@@ -903,6 +1116,88 @@ console.log("\n=== Die Seite ===");
{ path: join(HIER, "pruef-spannen.png") });
}
/* ---- "ALLES ANGEZEIGT" HEISST: MAN KANN ES SEHEN (16.09.2026) ----
Filipe: "wenn ich dir runter lade soll alles notiert und angezeigt
werden." Dass die Werte in der Datenbank stehen, ist oben belegt.
Ob sie auf dem Bildschirm ankommen, ist eine ANDERE Frage -- und
genau die Sorte, die hier schon einmal keiner gestellt hat (die
Teilnehmerwahl im Kalender, 05.09.).
GEMESSEN WIRD MIT `checkVisibility()`, UND DAS IST KEINE
GESCHMACKSFRAGE (16.09.2026, an einem Miniaturfall nachgemessen):
zugeklappt getBoundingClientRect().height 72
aufgeklappt getBoundingClientRect().height 72
Dieselbe Zahl. Auch `offsetHeight`, `offsetParent` und
`getClientRects()` sind in beiden Zustaenden identisch -- Chromium
verbirgt den Inhalt eines geschlossenen <details> mit
`content-visibility: hidden`, und das behaelt die letzte
Ausmessung. Wer die Hoehe misst, bekommt also fuer "unsichtbar"
und "sichtbar" dieselbe Antwort und merkt es nicht.
Genau daran ist mein erster Anlauf gescheitert: Er meldete
"zugeklappt 141 px hoch" -- im Bildschirmfoto war an derselben
Stelle nichts zu sehen. Die Messung log, nicht die Seite.
`checkVisibility()` unterscheidet beides sauber (false / true) und
ist das einzige der fuenf Verfahren, das es kann. */
{
const deckel = await seite.$$(".spanne__alles-kopf");
ok(deckel.length >= 1, `es gibt ${deckel.length} Deckel "Alle Werte aus Backstage"`);
const zuVorher = await seite.evaluate(() =>
document.querySelector(".spanne__alles-liste")?.checkVisibility() ?? null);
ok(zuVorher === false, `zugeklappt ist die Liste nicht zu sehen (${zuVorher})`);
/* Der LETZTE Deckel gehoert zum August -- dem Zeitraum mit allen
41 Spalten. Der erste haette nur fuenf und die Pruefung waere
gruen, ohne das Gemeinte gemessen zu haben. */
await deckel[deckel.length - 1].click();
await seite.waitForTimeout(350);
const auf = await seite.evaluate(() => {
const offen = document.querySelector(".spanne__alles[open]");
if (!offen) return null;
const namen = [...offen.querySelectorAll(".spanne__alles-name")];
const werte = [...offen.querySelectorAll(".spanne__alles-wert")];
const liste = offen.querySelector(".spanne__alles-liste");
return {
namen: namen.length, werte: werte.length,
/* SICHTBAR, nicht "hat eine Hoehe" -- siehe die Begruendung
oben. Die Hoehe steht daneben, aber nur als Auskunft. */
sichtbar: liste.checkVisibility(),
hoehe: Math.round(liste.getBoundingClientRect().height),
erster: namen[0]?.textContent || "",
letzter: namen[namen.length - 1]?.textContent || "",
/* Steht ein Wert, den es OHNE diese Liste nirgends gaebe? */
graduierung: namen.some((e) => e.textContent === "Graduierungsstatus")
? werte[namen.findIndex((e) => e.textContent === "Graduierungsstatus")]?.textContent
: "",
kleinste: Math.min(...[...namen, ...werte].map((e) =>
parseFloat(getComputedStyle(e).fontSize))),
};
});
ok(auf && auf.namen === 40 && auf.werte === 40,
`aufgeklappt stehen ${auf?.namen} Namen und ${auf?.werte} Werte da`);
ok(auf && auf.sichtbar === true,
`und sie sind jetzt wirklich sichtbar (${auf?.sichtbar}, ${auf?.hoehe} px hoch)`);
ok(auf && auf.erster === "Datenzeitraum",
`in der Reihenfolge der Datei (zuerst "${auf?.erster}")`);
ok(auf && auf.graduierung === "Nicht graduiert",
`ein Wert, den es sonst NIRGENDS gaebe, steht da ("${auf?.graduierung}")`);
ok(auf && auf.kleinste >= 11.5,
`und nichts darin ist kleiner als 11,5 px (${auf?.kleinste} px)`);
if (process.env.SCHIRM) {
await seite.locator(".spanne__alles[open]").screenshot(
{ path: join(HIER, "pruef-spannen-alles.png") });
}
/* Wieder zu -- das Bild der Tafel oben zeigt den Normalfall, und
die Kontrastmessung danach misst den Kopf, nicht eine offene
Liste, die die Seite verschoben hat. */
await deckel[deckel.length - 1].click();
await seite.waitForTimeout(250);
}
/* GEGENPROBE -- und sie ist hier das Wichtigste.
"Null rote Bildpunkte" koennte auch heissen, dass gar kein Foto
@@ -966,8 +1261,13 @@ console.log("\n=== Die Seite ===");
`keine einzige sichtbare Null mehr (${ohneTage.nullen})`);
ok(/kein einziger Tag erfasst/.test(ohneTage.satz),
`sie sagt, WAS los ist ("${ohneTage.satz.slice(0, 46)}…")`);
/* MIT DEM, WAS SIE GESEHEN HAT. Diese Zeile scheiterte am
16.09.2026 und sagte nur "nennt den Zeitraum nicht" -- wo der
Fehler lag, war daran nicht zu erkennen. Eine Pruefung, die ihren
Befund nicht zeigt, kostet genau die Zeit, die sie sparen soll. */
ok(/01\.09\./.test(ohneTage.satz) && /13 Tage/.test(ohneTage.satz),
"und nennt den Zeitraum, der stattdessen da ist");
`und nennt den Zeitraum, der stattdessen da ist `
+ `("${ohneTage.satz.replace(/\s+/g, " ").slice(0, 150)}")`);
ok(/EINEN Tag/.test(ohneTage.satz),
"und den Weg, wie man die Wochenzahlen doch fuellt");