diff --git a/server/pruef-backstage-import.mjs b/server/pruef-backstage-import.mjs index a1c6a26d..a9c10f43 100644 --- a/server/pruef-backstage-import.mjs +++ b/server/pruef-backstage-import.mjs @@ -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 ======================================================================= */ diff --git a/server/workspace-leistung.js b/server/workspace-leistung.js index 991c7a4d..acc96fd3 100644 --- a/server/workspace-leistung.js +++ b/server/workspace-leistung.js @@ -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" }); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index e6c81009..fa1df43b 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609141622", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609141634", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609141622", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609141634", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609141622", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609141634", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609141622", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609141634", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/js/leistung.js b/workspace/assets/js/leistung.js index 6b796674..dd69bc7e 100644 --- a/workspace/assets/js/leistung.js +++ b/workspace/assets/js/leistung.js @@ -626,7 +626,14 @@ $('datei').value = ''; importText = ''; await allesLaden(); - melde(`${r.geschrieben} Tage übernommen.`); + /* WAS GENAU ANGEKOMMEN IST -- Tage und Zeitraeume getrennt. + "2 Tage uebernommen" waere bei einer Backstage-Ausgabe ueber + zwei Wochen schlicht falsch; man wuerde sie in der Tagesliste + suchen und nicht finden. */ + const teile = []; + if (r.geschrieben) teile.push(`${r.geschrieben} Tag${r.geschrieben === 1 ? '' : 'e'}`); + if (r.zeitraeume) teile.push(`${r.zeitraeume} Zeitraum${r.zeitraeume === 1 ? '' : 'e'}`); + melde(teile.length ? `${teile.join(' und ')} übernommen.` : 'Nichts zu übernehmen.'); setTimeout(() => melde(''), 5000); } catch { /* umgeleitet */ } finally { knopf.disabled = false; } } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index e8ca170c..ec1ac400 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -252,10 +252,10 @@ - - - - - + + + + +