Der Einzel-Import kennt den dritten Ausgang jetzt auch

Nach dem Umbau von heute Nachmittag habe ich nachgesehen, WER
tagAusZelle() sonst noch ruft -- und genau dort die naechste Luecke
gefunden, bevor sie jemanden getroffen hat.

DIE FUNKTION HAT SEIT HEUTE DREI AUSGAENGE (Tag, Zeitraum, Grund).
Gekannt hat den dritten nur der Backstage-Weg. Im Einzel-Import stand
weiterhin `gelesen.tag` -- bei einem Zeitraum `undefined`:

  VORSCHAU:    `undefined > heute` ist false, die Zeile faellt durch
               und landet mit `tag: undefined` in der Liste.
  SCHREIBWEG:  `if (!tag)` greift, die Zeile wird still uebersprungen.

Die Vorschau haette also eine Zeile versprochen, die danach nirgends
steht. Zwei Aussagen ueber dieselbe Datei, die einander widersprechen,
und beide sehen fuer sich plausibel aus -- das ist der Unterschied, den
niemand bemerkt.

Das ist an diesem Tag die FUENFTE Wiederholung derselben Sache: eine
Regel, mehrere Aufrufer, und einer kennt sie nicht. Ein dritter Ausgang
taugt nur, wenn ihn ALLE Aufrufer kennen.

Jetzt legt auch der Einzel-Import einen Zeitraum in leistung_zeitraum
ab -- dieselbe Tabelle, dieselbe Regel, dieselbe Anzeige. Und die
Dauer-Einheit gilt dort ebenfalls; sie fehlte in der Vorschau noch.

DIE MELDUNG SAGT JETZT, WAS ANGEKOMMEN IST: "1 Zeitraum uebernommen"
statt "1 Tage uebernommen". Bei einer Backstage-Ausgabe ueber zwei
Wochen haette man sie sonst in der Tagesliste gesucht und nicht
gefunden.

pruef-backstage-import 106 -> 118. Die Pruefung, auf die es ankommt,
vergleicht VORSCHAU UND ERGEBNIS: Was die Vorschau verspricht, muss
danach dastehen -- genau die Aussage, die vorher falsch gewesen waere.
Dazu, dass in der Vorschau die Spanne steht und kein leerer Tag, und
die Gegenprobe, dass eine echte Tagesdatei weiterhin als Tage durchgeht.

ZWEI EIGENE FEHLER DABEI: Ich habe `.length` auf eine Zahl angewendet
(`fehlerhaft` ist eine Anzahl, keine Liste) -- immer `undefined`, immer
rot. Und zum zweiten Mal heute den Absolutwert gemessen, wo die
Veraenderung gehoert: Lumi hatte aus einem frueheren Teil der Pruefung
schon eine Tageszeile in derselben Spanne.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-14 16:34:42 +02:00
co-authored by Claude Opus 5
parent e82bf45f0d
commit 0cce22c08c
32 changed files with 507 additions and 350 deletions
+87
View File
@@ -452,6 +452,93 @@ for (const [rolle, code, wer] of [
}
{
/* ---- DER EINZEL-IMPORT KENNT DEN DRITTEN AUSGANG (14.09.2026) ----
Als tagAusZelle() den Zeitraum dazubekam, kannte ihn nur der
Backstage-Weg. Im Einzel-Import stand weiterhin `gelesen.tag` --
bei einem Zeitraum `undefined`. Folge: Die VORSCHAU haette die
Zeile gezaehlt, der SCHREIBWEG sie still uebersprungen.
Zwei Aussagen ueber dieselbe Datei, die einander widersprechen --
das ist der Unterschied, den niemand bemerkt, weil beide Seiten
fuer sich plausibel aussehen.
GEPRUEFT WIRD DESHALB DER ABGLEICH: Was die Vorschau verspricht,
muss danach dastehen. */
const datei = [
"Datum;Diamanten;LIVE-Dauer",
"2026-09-01 ~ 2026-09-11;53.679;86Std. 10Min. 52Sek.",
].join("\n");
/* WIE VIELE TAGESZEILEN LUMI SCHON HAT. Zum zweiten Mal heute derselbe
Griff: Der Backstage-Import weiter oben hat ihr bereits einen Tag
geschrieben, der in dieselbe Spanne faellt. Gemessen wird die
VERAENDERUNG, nicht der Absolutwert. */
const tageVorLumi = (() => {
const d0 = new DatabaseSync(DB, { readOnly: true });
const n = d0.prepare(
"SELECT COUNT(*) AS n FROM leistung WHERE creator_id = ? AND tag >= '2026-09-01' AND tag <= '2026-09-11'")
.get(idLumi).n;
d0.close();
return n;
})();
const v = await ruf(`/workspace/api/leistung/${idLumi}/import/vorschau`,
keksDogi, { text: datei });
ok(v.status === 200, `die Vorschau nimmt den Zeitraum an (HTTP ${v.status})`);
const versprochen = (v.daten?.neu ?? 0) + (v.daten?.ersetzt ?? 0);
ok(versprochen === 1, `sie verspricht genau eine Zeile (${versprochen})`);
/* `fehlerhaft` ist eine ZAHL, keine Liste -- nachgesehen in der
Antwort, nicht geraten. Beim ersten Anlauf stand hier `.length`
auf einer Zahl: immer `undefined`, immer ungleich 0, immer rot. */
ok((v.daten?.fehlerhaft ?? -1) === 0,
`und meldet keinen Fehler (${v.daten?.fehlerhaft})`);
/* KEIN `undefined` IN DER VORSCHAU. Der alte Weg haette hier eine
Zeile mit `tag: undefined` gezeigt -- sichtbar als leere Spalte. */
const beispiel = (v.daten?.beispiele || [])[0]
|| (v.daten?.ersetzt_beispiele || [])[0] || {};
ok(beispiel.von === "2026-09-01" && beispiel.bis === "2026-09-11",
`und nennt die Spanne statt eines leeren Tages (${beispiel.von}–${beispiel.bis})`);
ok(beispiel.tage === 11, `mit der Zahl der Tage (${beispiel.tage})`);
const s = await ruf(`/workspace/api/leistung/${idLumi}/import`,
keksDogi, { text: datei });
ok(s.status === 200, `der Import laeuft (HTTP ${s.status})`);
const angekommen = (s.daten?.geschrieben ?? 0) + (s.daten?.zeitraeume ?? 0);
ok(angekommen === versprochen,
`es kommt an, was die Vorschau versprochen hat (${versprochen} -> ${angekommen})`);
ok(s.daten?.zeitraeume === 1 && s.daten?.geschrieben === 0,
`und zwar als Zeitraum, nicht als Tag (${s.daten?.zeitraeume} / ${s.daten?.geschrieben})`);
{
const d2 = new DatabaseSync(DB, { readOnly: true });
const z = d2.prepare(
"SELECT * FROM leistung_zeitraum WHERE creator_id = ?").get(idLumi);
const tage = d2.prepare(
"SELECT COUNT(*) AS n FROM leistung WHERE creator_id = ? AND tag >= '2026-09-01' AND tag <= '2026-09-11'")
.get(idLumi).n;
d2.close();
ok(z?.tage === 11 && z?.diamanten === 53679,
`er steht in der Datenbank (${z?.tage} Tage, ${z?.diamanten} Diamanten)`);
ok(z?.dauer_min === 5171, `mit der Dauer in Minuten (${z?.dauer_min})`);
ok(tage === tageVorLumi,
`und KEINE Tageszeile kam hinzu (${tageVorLumi} -> ${tage})`);
}
/* GEGENPROBE: Eine Datei mit echten Tagen geht weiterhin als Tage
durch -- sonst hiesse alles oben nur, dass jetzt alles ein
Zeitraum ist. */
const tagesdatei = [
"Datum;Diamanten",
`${gestern};1.111`,
].join("\n");
const t = await ruf(`/workspace/api/leistung/${idLumi}/import`,
keksDogi, { text: tagesdatei });
ok(t.daten?.geschrieben === 1 && (t.daten?.zeitraeume ?? 0) === 0,
`eine Tagesdatei bleibt eine Tagesdatei (${t.daten?.geschrieben} Tag, `
+ `${t.daten?.zeitraeume} Zeitraeume)`);
}
/* =======================================================================
7. Und die Seite selbst
======================================================================= */
+67 -4
View File
@@ -1244,15 +1244,52 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import/vorschau", gleich
const fehlerhaft = [];
for (let i = 1; i < zeilen.length; i++) {
const z = zeilen[i];
/* DER DRITTE AUSGANG MUSS HIER GENAUSO ANKOMMEN wie im
Backstage-Weg (14.09.2026).
Als tagAusZelle() den Zeitraum bekam, stand hier weiterhin
nur `gelesen.tag`. Bei einem Zeitraum war der `undefined`,
der Vergleich mit heute wurde still falsch, und die Zeile
landete mit `tag: undefined` in der Vorschau -- waehrend der
Schreibweg daneben sie kommentarlos uebersprang. Vorschau
und Ergebnis haetten sich widersprochen, und das ist der
Unterschied, den niemand bemerkt.
Ein dritter Ausgang taugt nur, wenn ihn ALLE Aufrufer
kennen. Das ist an diesem Tag die fuenfte Wiederholung
derselben Sache -- gefunden, bevor sie jemanden getroffen
hat, weil ich nach dem Umbau nachgesehen habe, wer die
Funktion sonst noch ruft. */
const gelesen = tagAusZelle(z[zu.tag]);
if (gelesen.fehler) { fehlerhaft.push({ zeile: i + 1, grund: gelesen.fehler }); continue; }
if (gelesen.zeitraum) {
const { von, bis } = gelesen.zeitraum;
if (bis > tagVor(0)) {
fehlerhaft.push({ zeile: i + 1, grund: `${bis} liegt in der Zukunft` });
continue;
}
const wertez = { von, bis, tage: tageImZeitraum(von, bis) };
for (const [feld, spalte] of Object.entries(zu)) {
if (feld === "tag" || spalte === undefined) continue;
wertez[feld] = feld === "dauer_min" ? dauerLesen(z[spalte]) : zahlLesen(z[spalte]);
}
const daz = db().prepare(
"SELECT 1 FROM leistung_zeitraum WHERE creator_id = ? AND von = ? AND bis = ?")
.get(creatorId, von, bis);
(daz ? ersetzt : neu).push(wertez);
continue;
}
const tag = gelesen.tag;
if (tag > tagVor(0)) { fehlerhaft.push({ zeile: i + 1, grund: `${tag} liegt in der Zukunft` }); continue; }
const werte = { tag };
for (const [feld, spalte] of Object.entries(zu)) {
if (feld === "tag" || spalte === undefined) continue;
werte[feld] = zahlLesen(z[spalte]);
/* Die Dauer hat eine Einheit, die uebrigen Felder nicht --
dieselbe Unterscheidung wie im Backstage-Weg. */
werte[feld] = feld === "dauer_min" ? dauerLesen(z[spalte]) : zahlLesen(z[spalte]);
}
const vorhanden = db().prepare(
"SELECT 1 FROM leistung WHERE creator_id = ? AND tag = ?").get(creatorId, tag);
@@ -1301,15 +1338,41 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft
(SELECT notiz FROM leistung WHERE creator_id = ? AND tag = ?),
?,?, 'import')`);
let geschrieben = 0, uebersprungen = 0;
/* Der zweite Weg, wie im Backstage-Import: Ein Zeitraum geht in
seine eigene Tabelle. */
const spanneSchreiben = d.prepare(`
INSERT OR REPLACE INTO leistung_zeitraum
(creator_id, von, bis, tage, diamanten, dauer_min, gueltige_tage,
zuschauer_avg, zuschauer_max, verweildauer_s, schenker, follower_neu,
erfasst, erfasst_von)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
let geschrieben = 0, uebersprungen = 0, spannen = 0;
d.exec("BEGIN");
try {
for (let i = 1; i < zeilen.length; i++) {
const z = zeilen[i];
/* Dieselbe Regel wie in der Vorschau -- sonst zeigt die
Vorschau etwas anderes an, als geschrieben wird. Genau das
ist der Unterschied, den niemand bemerkt. */
ist der Unterschied, den niemand bemerkt.
UND DERSELBE DRITTE AUSGANG: Ohne diesen Zweig wuerde ein
Zeitraum hier still uebersprungen, waehrend die Vorschau
ihn zaehlt. */
const gelesen = tagAusZelle(z[zu.tag]);
const wz = (feld) => (zu[feld] === undefined ? null
: (feld === "dauer_min" ? dauerLesen(z[zu[feld]]) : zahlLesen(z[zu[feld]])));
if (gelesen.zeitraum) {
const { von, bis } = gelesen.zeitraum;
if (bis > tagVor(0)) { uebersprungen++; continue; }
spanneSchreiben.run(creatorId, von, bis, tageImZeitraum(von, bis),
wz("diamanten"), wz("dauer_min"), wz("gueltige_tage"),
wz("zuschauer_avg"), wz("zuschauer_max"), wz("verweildauer_s"),
wz("schenker"), wz("follower_neu"),
jetzt(), req.person.id);
spannen++;
continue;
}
const tag = gelesen.tag;
if (!tag || tag > tagVor(0)) { uebersprungen++; continue; }
const w = (feld) => (zu[feld] === undefined ? null : zahlLesen(z[zu[feld]]));
@@ -1338,7 +1401,7 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `Creator #${creatorId}: ${geschrieben} Tage`,
});
res.json({ ok: true, geschrieben, uebersprungen });
res.json({ ok: true, geschrieben, zeitraeume: spannen, uebersprungen });
} catch (fehler) {
console.error("[leistung] Import:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });