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
+179 -8
View File
@@ -252,6 +252,31 @@ leistungRouter.get("/workspace/api/leistung/:creatorId", (req, res) => {
FROM leistung_zeitraum WHERE creator_id = ?
ORDER BY bis DESC, von DESC LIMIT 6`).all(creatorId);
/* ALLES, WAS IN DER DATEI STAND -- IN EINER EINZIGEN ABFRAGE.
Filipe: "wenn ich dir runter lade soll alles notiert und
angezeigt werden." Notiert allein reicht nicht; was niemand
sieht, ist so gut wie nicht da.
EINE Abfrage fuer alle sechs Zeitraeume statt sechs einzelner --
dieselbe Regel wie bei den Chat-Reaktionen. Sortiert nach `nr`,
also in der Reihenfolge der Datei: Backstage gruppiert seine
Spalten bereits sinnvoll (dieser Zeitraum, letzter Monat,
Prozente, Matches, Fanclub). Eine eigene Gruppierung waere eine
erfundene Ordnung, die beim naechsten Backstage-Update falsch
ist. */
if (zeitraeume.length) {
const nach = new Map(zeitraeume.map((z) => [z.von + "|" + z.bis, z]));
for (const z of zeitraeume) z.felder = [];
for (const f of db().prepare(`
SELECT von, bis, nr, name, text, zahl
FROM leistung_zeitraum_feld WHERE creator_id = ?
ORDER BY nr`).all(creatorId)) {
nach.get(f.von + "|" + f.bis)?.felder.push(
{ nr: f.nr, name: f.name, text: f.text, zahl: f.zahl });
}
}
res.json({
zeitraeume,
creator_id: creatorId,
@@ -586,6 +611,59 @@ export function zahlLesen(roh) {
return Number.isFinite(n) ? n : null;
}
/* =====================================================================
ALLES, WAS IN DER ZEILE STAND (16.09.2026)
Filipe: "ich will dass alles von der excel datei genommen wird ...
wenn ich dir runter lade soll alles notiert und angezeigt werden."
Seine echte Ausgabe hat 41 Spalten; `spaltenRaten` kennt acht davon.
Die restlichen 33 fielen beim Einlesen weg -- lautlos, ohne dass
irgendwo stand, dass es sie gab: Zahlen vom letzten Monat, fuenf
Prozentwerte, Matches, Multi-Gast-LIVEs, Fanclub, Graduierungs- und
Stufenstatus.
DIESE FUNKTION DEUTET NICHTS. Sie nimmt Ueberschrift und Zelle,
so wie sie dastehen. Das ist Absicht: Was "Diamanten - Prozent
erreicht" genau bedeutet, weiss TikTok, nicht ich -- und eine
erfundene Bedeutung waere schlimmer als gar keine. Nachgerechnet
habe ich es trotzdem einmal (13.210 / 29.488 = 44,79 %, und dasselbe
Verhaeltnis stimmt bei allen fuenf Groessen), aber eine Herleitung
aus EINER Zeile ist kein Beweis, und sie steht deshalb nicht im Code.
WAS UEBERSPRUNGEN WIRD, und warum genau das:
* Spalten OHNE Ueberschrift -- ohne Namen gibt es keinen
Schluessel, und "Spalte 37" ist keine Auskunft.
* DOPPELTE Ueberschriften -- die erste gilt. Der Schluessel in der
Datenbank ist der Name; zwei gleiche wuerden einander
ueberschreiben, und dann haenge es von der Reihenfolge ab, welche
gewinnt.
* LEERE Zellen -- eine leere Zelle ist kein Wert. In Filipes Datei
ist "Status" leer; eine Zeile "Status: (nichts)" waere eine
Auskunft, die die Datei gar nicht gibt.
BEIDES WIRD BEHALTEN: der Text woertlich und, falls lesbar, die
Zahl. Nur die Zahl hiesse "Nicht graduiert" wegzuwerfen; nur den
Text hiesse, nicht rechnen zu koennen. */
export function felderAus(kopf, zeile) {
const felder = [];
const gesehen = new Set();
for (let i = 0; i < kopf.length; i++) {
const name = String(kopf[i] ?? "").trim();
if (!name || gesehen.has(name)) continue;
gesehen.add(name);
const text = String(zeile[i] ?? "").trim();
if (!text) continue;
felder.push({
nr: i,
name: name.slice(0, 120),
text: text.slice(0, 300),
zahl: zahlLesen(text),
});
}
return felder;
}
/** Macht aus "86Std. 10Min. 52Sek." Minuten.
*
* GEMESSEN AN DER ECHTEN BACKSTAGE-AUSGABE (14.09.2026): Die Spalte
@@ -874,9 +952,48 @@ function handleSchluessel(roh) {
* erst das Handle (eindeutig), dann der Name (mehrdeutig, aber
* besser als nichts). */
function personSpaltenRaten(kopf) {
const handle = kopf.findIndex((n) => /handle|username|user.?name|tiktok.?id|@/i.test(String(n)));
const name = kopf.findIndex((n, i) => i !== handle
&& /nick|anzeige|display|name|creator/i.test(String(n)));
/* AN SEINER ECHTEN AUSGABE GEMESSEN (16.09.2026) -- und sie ist hier
durchgefallen, bevor eine einzige Zahl ankam.
"Creator_innendaten" hat DREI Spalten, die nach Person aussehen:
Creator*in-ID 700001
Creator*innen-Anmeldename nova_live
Agent <Scout>
Die alte Regel nahm die ERSTE, die das Wort "creator" enthaelt --
also die ID. Danach wurde nach einem Creator namens "700001"
gesucht, und den gibt es nicht. Ergebnis: die ganze Datei
unzugeordnet, mit der Meldung "steht bei keinem Creator im Feld
TikTok". Der Hinweis war richtig formuliert und zeigte in die
voellig falsche Richtung.
ZWEI AENDERUNGEN, und beide sind Ausschluesse, keine Zusaetze:
1. EINE ID IST KEIN NAME. Was wie eine Kennnummer heisst, kommt
fuer beide Spalten nicht in Frage. Eine Nummer trifft nie einen
Namen -- sie kann also nur schaden.
2. "ANMELDENAME" IST DER HANDLE. So heisst bei TikTok der Name
mit dem @. Damit wird die richtige Spalte zum Handle, statt gar
keine zu finden.
UND "creator" ALLEIN REICHT NICHT MEHR als Namensspalte. In
derselben Datei steht weiter hinten "Neue*r LIVE-Creator*innen"
mit dem Wert "Nein" -- die alte Regel haette nach dem Ausschluss
der ID genau dort weitergesucht und einen Creator namens "Nein"
gesucht. Ein Spaltenname zaehlt nur noch, wenn er WIRKLICH nach
Name klingt (nick, anzeige, display, name) oder fuer sich allein
"Creator" heisst -- so wie in den aelteren Ausgaben.
KEINE BACKSLASHES IM MUSTER, mit Absicht: Diese Datei wird oefter
ueber Textwerkzeuge bearbeitet, und ein verschlucktes Zeichen in
einer Regex faellt erst auf, wenn Daten fehlen. */
const istKennung = /(^|[^a-z])id([^a-z]|$)|nummer/i;
const handle = kopf.findIndex((n) => !istKennung.test(String(n))
&& /handle|username|user.?name|anmeldename|anmelde.?name|login|@/i.test(String(n)));
const name = kopf.findIndex((n, i) => i !== handle && !istKennung.test(String(n))
&& (/nick|anzeige|display|name/i.test(String(n))
|| /^[\s]*creator[\s*:in]*[\s]*$/i.test(String(n))));
return { handle, name };
}
@@ -974,7 +1091,12 @@ function netzwerkLesen(text, person, tagVorgabe) {
const werte = spanne
? { creator_id: p.id, name: p.name, von: spanne.von, bis: spanne.bis,
tage: tageImZeitraum(spanne.von, spanne.bis) }
tage: tageImZeitraum(spanne.von, spanne.bis),
/* ALLE Spalten der Zeile, nicht nur die acht erkannten.
Nur beim Zeitraum: Die Tageszeilen haben ihre eigene
Tabelle, und ein halb gebautes Feld dort waere Datenhaltung
ohne Anzeige -- also nichts, was jemand je saehe. */
felder: felderAus(kopf, z) }
: { creator_id: p.id, name: p.name, tag };
let hatZahl = false;
for (const feld of messwerte) {
@@ -1039,6 +1161,18 @@ leistungRouter.post("/workspace/api/leistung/netzwerk-import/vorschau", gleicheH
tage: e.treffer.find((t) => t.von).tage }
: null,
zugeordnet: e.treffer.length, neu, ersetzt,
/* WIE VIELE SPALTEN LANDEN WIRKLICH IN DER ABLAGE?
Filipes Sorge war genau die: "ich will dass alles von der
excel datei genommen wird." Das gehoert VOR das Bestaetigen,
nicht hinterher -- sonst muesste er der Zusage glauben,
statt die Zahl zu sehen.
Gemessen am ERSTEN Zeitraum der Datei, nicht ueber alle
summiert: Alle Zeilen haben dieselben Spalten, eine Summe
ueber 40 Creator waere eine grosse Zahl ohne Aussage. */
spalten_gesamt: (e.kopf || []).length,
felder: (e.treffer.find((t) => t.felder) || {}).felder?.length ?? 0,
unbekannt: e.unbekannt, probleme: e.schlecht.slice(0, 12),
probleme_gesamt: e.schlecht.length,
creator_gesamt: e.creator_gesamt,
@@ -1082,12 +1216,28 @@ leistungRouter.post("/workspace/api/leistung/netzwerk-import", gleicheHerkunft,
erfasst, erfasst_von)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
/* ERST WEGRAEUMEN, DANN SCHREIBEN. Ohne das Loeschen bliebe eine
Spalte, die beim ersten Mal gefuellt war und beim zweiten leer
ist, mit ihrem ALTEN Wert stehen -- ein Wert aus einer Datei,
die es nicht mehr gibt, und niemand koennte ihn von einem
frischen unterscheiden. */
const felderWeg = d.prepare(`DELETE FROM leistung_zeitraum_feld
WHERE creator_id = ? AND von = ? AND bis = ?`);
const feldSchreiben = d.prepare(`INSERT OR REPLACE INTO leistung_zeitraum_feld
(creator_id, von, bis, nr, name, text, zahl) VALUES (?,?,?,?,?,?,?)`);
let geschrieben = 0;
let spannen = 0;
let felder = 0;
d.exec("BEGIN");
try {
for (const t of e.treffer) {
if (t.von) {
felderWeg.run(t.creator_id, t.von, t.bis);
for (const f of (t.felder || [])) {
feldSchreiben.run(t.creator_id, t.von, t.bis, f.nr, f.name, f.text, f.zahl);
felder++;
}
spanneSchreiben.run(t.creator_id, t.von, t.bis, t.tage,
t.diamanten ?? null, t.dauer_min ?? null, t.gueltige_tage ?? null,
t.zuschauer_avg ?? null, t.zuschauer_max ?? null,
@@ -1113,10 +1263,10 @@ leistungRouter.post("/workspace/api/leistung/netzwerk-import", gleicheHerkunft,
protokolliere("leistung_netzwerk_import", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `${geschrieben} Tage, ${spannen} Zeitraeume, `
detail: `${geschrieben} Tage, ${spannen} Zeitraeume, ${felder} Felder, `
+ `${e.unbekannt.length} unbekannt`.slice(0, 120),
});
res.json({ geschrieben, zeitraeume: spannen,
res.json({ geschrieben, zeitraeume: spannen, felder,
unbekannt: e.unbekannt, probleme: e.schlecht.length });
} catch (fehler) {
console.error("[leistung] Netzwerk-Import:", fehler?.message);
@@ -1368,6 +1518,22 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft
erfasst, erfasst_von)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
/* UND ALLE UEBRIGEN SPALTEN -- WORTGLEICH WIE IM ANDEREN WEG.
Es gibt zwei Wege in dieses System (eine Datei je Creator und
eine Tabelle fuer alle), und sie sind in diesem Modul schon
dreimal auseinandergelaufen: beim Zeitraumschutz, bei der
Dauer-Einheit und beim dritten Ausgang. Jedes Mal, weil eine
Sache an zwei Stellen geschrieben stand.
Deshalb kommt die Liste der Felder hier NICHT aus einer zweiten
Rechnung, sondern aus derselben Funktion `felderAus`. */
const felderWeg = d.prepare(`DELETE FROM leistung_zeitraum_feld
WHERE creator_id = ? AND von = ? AND bis = ?`);
const feldSchreiben = d.prepare(`INSERT OR REPLACE INTO leistung_zeitraum_feld
(creator_id, von, bis, nr, name, text, zahl) VALUES (?,?,?,?,?,?,?)`);
let felder = 0;
let geschrieben = 0, uebersprungen = 0, spannen = 0;
d.exec("BEGIN");
try {
@@ -1391,6 +1557,11 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft
wz("zuschauer_avg"), wz("zuschauer_max"), wz("verweildauer_s"),
wz("schenker"), wz("follower_neu"),
jetzt(), req.person.id);
felderWeg.run(creatorId, von, bis);
for (const f of felderAus(zeilen[0], z)) {
feldSchreiben.run(creatorId, von, bis, f.nr, f.name, f.text, f.zahl);
felder++;
}
spannen++;
continue;
}
@@ -1420,9 +1591,9 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft
protokolliere("leistung_import", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `Creator #${creatorId}: ${geschrieben} Tage`,
detail: `Creator #${creatorId}: ${geschrieben} Tage, ${spannen} Zeitraeume, ${felder} Felder`,
});
res.json({ ok: true, geschrieben, zeitraeume: spannen, uebersprungen });
res.json({ ok: true, geschrieben, zeitraeume: spannen, felder, uebersprungen });
} catch (fehler) {
console.error("[leistung] Import:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });