diff --git a/server/pruef-backstage-import.mjs b/server/pruef-backstage-import.mjs index 1175b8d2..89c8a049 100644 --- a/server/pruef-backstage-import.mjs +++ b/server/pruef-backstage-import.mjs @@ -537,10 +537,14 @@ console.log("\n=== Die Seite ==="); erste Entwurf schickte zehn Bytes "PK…" und war gruen, weil der Server "ZIP-Ende fehlt" meldete -- das haette auch bei kaputtem Leser gestimmt. */ + /* EINE Person, ZWEI Tage -- dafuer ist dieser Weg da. Der erste + Entwurf hatte hier zwei verschiedene Creator; dass die neue Sperre + ihn abgewiesen hat, war ihr erster echter Treffer. */ + const vortag = new Date(Date.now() - 2 * 86400000).toISOString().slice(0, 10); const xlsxDatei = xlsxBauen([ ["Datum", "Creator", "Diamanten", "LIVE-Dauer"], [gestern, "@Nova_Live", "12.480", "3Std. 4Min."], - [gestern, "Kiro", "3.900", "1Std. 35Min."], + [vortag, "@Nova_Live", "3.900", "1Std. 35Min."], ]); await seite.click("#import-oeffnen"); await seite.waitForTimeout(300); @@ -561,6 +565,57 @@ console.log("\n=== Die Seite ==="); ok(xlsx.text.includes("Diamanten") && xlsx.text.includes("LIVE-Dauer"), `die Vorschau nennt die erkannten Spalten ("${xlsx.text.slice(0, 55)}…")`); + /* ---- EINE DATEI MIT ALLEN CREATORN GEHOERT WOANDERS HIN --------- + (14.09.2026) Genau das ist Filipe passiert: Er hat seine + Backstage-Ausgabe -- eine Tabelle mit allen Creator:innen -- hier + hochgeladen. Dieser Weg schreibt auf EINEN Creator. Herausgekommen + ist eine Zeile mit lauter Nullen auf einem Tag, den er nicht mehr + sah; eine Fehlermeldung gab es nicht. + + "Es passiert nichts Sichtbares" ist die schlechteste aller + Antworten. Jetzt sagt der Weg, WAS los ist und WOHIN die Datei + gehoert. */ + const vieleLeute = xlsxBauen([ + ["Datenzeitraum", "Creator*innen-Anmeldename", "Diamanten"], + [gestern, "nova_live", "12.480"], + [gestern, "kiro_on", "3.900"], + [gestern, "lumistreamt", "22.010"], + ]); + await seite.setInputFiles("#datei", { + name: "alle.xlsx", + mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + buffer: vieleLeute, + }); + await seite.waitForTimeout(1400); + const viele = await seite.evaluate(() => ({ + fehler: document.getElementById("import-fehler")?.textContent || "", + vorschau: document.getElementById("vorschau")?.hidden, + })); + ok(viele.fehler.includes("3 verschiedene Creator"), + `die Datei mit drei Creatorn wird erkannt ("${viele.fehler.slice(0, 55)}…")`); + ok(viele.fehler.includes("Backstage-Tabelle"), + "und der richtige Knopf wird genannt"); + ok(viele.vorschau === true, "es entsteht KEINE Vorschau -- geschrieben wird nichts"); + + /* GEGENPROBE: EIN Creator in der Datei geht weiterhin durch. Sonst + hiesse das oben nur, dass jede Datei mit Namensspalte abgewiesen + wird. */ + await seite.setInputFiles("#datei", { + name: "einer.xlsx", + mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + buffer: xlsxBauen([ + ["Datenzeitraum", "Creator*innen-Anmeldename", "Diamanten"], + [gestern, "nova_live", "12.480"], + ]), + }); + await seite.waitForTimeout(1400); + const einer = await seite.evaluate(() => ({ + fehler: document.getElementById("import-fehler")?.textContent || "", + vorschau: document.getElementById("vorschau")?.hidden, + })); + ok(einer.vorschau === false && einer.fehler === "", + `eine Datei mit EINEM Creator kommt durch ("${einer.fehler.slice(0, 40)}")`); + /* DIE ALTE .xls IST ETWAS ANDERES -- kein ZIP, sondern BIFF. Sie bekommt weiterhin eine Absage, und die muss WEITERHELFEN. */ await seite.setInputFiles("#datei", { diff --git a/server/pruef-xlsx.mjs b/server/pruef-xlsx.mjs index f78b11a5..7a37585f 100644 --- a/server/pruef-xlsx.mjs +++ b/server/pruef-xlsx.mjs @@ -303,6 +303,47 @@ console.log("\n=== Der dritte Ausgang ==="); zwei Werte, die jeder nachschlagen kann. Verrutscht die Basis um einen Tag, faellt es hier auf und nicht erst in einer Tabelle. ======================================================================= */ +console.log(""); +console.log("=== Die eine Regel fuer den Tag ==="); +{ + /* DIE LUECKE, DIE FILIPE GEFUNDEN HAT (14.09.2026). + + Der Zeitraum-Schutz stand am Vormittag nur im Backstage-Weg. Er + schickte seine echte Datei durch den EINZEL-Import -- dort fehlte + er, und es entstand eine Zeile mit lauter Nullen auf dem + 1. September. Sichtbar war davon nichts: Der Tag lag 13 Tage + zurueck, und die Liste zeigt sieben. + + Dieselbe Regel, zweimal aufgeschrieben, und die zweite Abschrift + war die unvollstaendige -- zum dritten Mal an einem Tag. Jetzt + steht sie EINMAL in tagAusZelle(), und diese Pruefung haelt fest, + dass sie beides kann: ablehnen UND durchlassen. */ + const { tagAusZelle } = await import("./workspace-leistung.js"); + const faelle = [ + ["2026-09-01 ~ 2026-09-11", null, "mehrere Tage"], + ["2026-09-06 ~ 2026-09-06", "2026-09-06", null], + ["2026-09-06", "2026-09-06", null], + ["06.09.2026", "2026-09-06", null], + ["65", null, "unlesbar"], + ["", null, "unlesbar"], + ]; + for (const [text, sollTag, sollFehler] of faelle) { + const r = tagAusZelle(text); + if (sollTag) { + ok(r.tag === sollTag && !r.fehler, + `"${text}" wird zum Tag ${sollTag} (${r.tag || r.fehler})`); + } else { + ok(!r.tag && typeof r.fehler === "string" && r.fehler.includes(sollFehler), + `"${text}" wird abgelehnt und begruendet ("${(r.fehler || "").slice(0, 42)}…")`); + } + } + /* Und die Regel gibt NIE beides und nie keines zurueck -- sonst + entscheidet jeder Aufrufer selbst, was Vorrang hat. */ + const alle = faelle.map(([t]) => tagAusZelle(t)); + ok(alle.every((r) => (!!r.tag) !== (!!r.fehler)), + "sie liefert immer genau eines von beidem: Tag oder Grund"); +} + console.log(""); console.log("=== Der Nullpunkt der Excel-Zeitrechnung ==="); { diff --git a/server/workspace-leistung.js b/server/workspace-leistung.js index 7532b32d..34fca48e 100644 --- a/server/workspace-leistung.js +++ b/server/workspace-leistung.js @@ -666,6 +666,32 @@ export function zeitraumLesen(roh) { return { von, bis }; } +/** Der Tag aus einer Zelle -- MIT dem Zeitraum-Schutz. + * + * (14.09.2026, nachgetragen.) Den Schutz gab es seit heute frueh nur + * im Backstage-Weg. Filipe hat daraufhin seine echte Datei durch den + * EINZEL-Import geschickt, und genau das ist passiert, wovor der + * Schutz bewahren sollte: eine Zeile auf den 1. September, aus einem + * Zeitraum vom 1. bis 11. + * + * Dass es an zwei Stellen stand und an einer fehlte, ist derselbe + * Fehler wie an diesem Tag schon zweimal: eine Regel, zweimal + * aufgeschrieben, und die zweite Abschrift ist die unvollstaendige. + * Deshalb steht sie jetzt EINMAL hier und wird dreimal benutzt. + * + * Rueckgabe: { tag } oder { fehler } -- nie beides, nie keines. + */ +export function tagAusZelle(roh) { + const zeitraum = zeitraumLesen(roh); + if (zeitraum && zeitraum.von !== zeitraum.bis) { + return { fehler: `Zeitraum ${zeitraum.von} bis ${zeitraum.bis} – das sind mehrere ` + + "Tage auf einmal. Stell in Backstage den Zeitraum auf EINEN Tag." }; + } + const tag = zeitraum ? zeitraum.von : datumLesen(roh); + if (!tag) return { fehler: `Datum unlesbar: "${String(roh ?? "").slice(0, 40)}"` }; + return { tag }; +} + /** Macht aus einem Textwert ein Datum "JJJJ-MM-TT". * Erkennt 2026-09-06, 06.09.2026 und 09/06/2026. */ export function datumLesen(roh) { @@ -860,29 +886,17 @@ function netzwerkLesen(text, person, tagVorgabe) { const rohWer = String(rohHandle).trim() || String(rohName).trim(); if (!rohWer) { schlecht.push({ zeile: i + 1, grund: "Keine Person in der Zeile." }); continue; } - /* EIN ZEITRAUM IST KEIN TAG (14.09.2026). - - Backstage schreibt in "Datenzeitraum" `2026-09-01 ~ 2026-09-11`. - `datumLesen` griff sich davon den ersten Tag -- elf Tage - Diamanten waeren auf den 1. September geschrieben worden. Die - Zahl steht da, sie ist gross, sie sieht richtig aus, und niemand - kann spaeter sagen, dass elf Tage darin stecken. - - Gemessen am echten Export vom 12.09.2026, nicht vermutet. - - Ein Zeitraum von EINEM Tag geht durch -- dann ist die Datei - genau das, was hier gebraucht wird. */ - const zeitraum = ausTabelle ? zeitraumLesen(z[zu.tag]) : null; - if (zeitraum && zeitraum.von !== zeitraum.bis) { - schlecht.push({ zeile: i + 1, - grund: `Zeitraum ${zeitraum.von} bis ${zeitraum.bis} – das sind mehrere Tage ` - + "auf einmal. Stell in Backstage den Zeitraum auf EINEN Tag." }); - continue; + /* EIN ZEITRAUM IST KEIN TAG -- die Regel steht in tagAusZelle(). + Sie stand am 14.09. frueh nur hier; Filipe schickte seine Datei + daraufhin durch den Einzel-Import, und dort fehlte sie. Jetzt + benutzen alle drei Stellen dieselbe. */ + let tag = tagVorgabe; + if (ausTabelle) { + const gelesen = tagAusZelle(z[zu.tag]); + if (gelesen.fehler) { schlecht.push({ zeile: i + 1, grund: gelesen.fehler }); continue; } + tag = gelesen.tag; } - const tag = ausTabelle - ? (zeitraum ? zeitraum.von : datumLesen(z[zu.tag])) - : tagVorgabe; - if (!tag) { schlecht.push({ zeile: i + 1, grund: `Datum unlesbar: "${z[zu.tag]}"` }); continue; } + if (!tag) { schlecht.push({ zeile: i + 1, grund: "Kein Tag angegeben." }); continue; } if (tag > heuteLokal()) { schlecht.push({ zeile: i + 1, grund: `${tag} liegt in der Zukunft` }); continue; } /* Vier Versuche, in dieser Reihenfolge: Handle gegen Handle, Handle @@ -1111,13 +1125,43 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import/vorschau", gleich }); } + /* DIESE DATEI GEHOERT WOANDERS HIN (14.09.2026). + + Filipe hat seine Backstage-Ausgabe hier hochgeladen -- eine + Tabelle mit ALLEN Creator:innen. Dieser Weg schreibt aber auf + EINEN Creator; herausgekommen ist eine Zeile mit lauter Nullen + auf einem Tag, den er nicht mehr sah. + + "Es passiert nichts Sichtbares" ist die schlechteste aller + Antworten. Erkannt wird es an einer Namensspalte mit MEHREREN + verschiedenen Eintraegen -- das ist keine Tagesliste, das ist + eine Personenliste. */ + const werSpalte = kopf.findIndex((n) => + /creator|nickname|anmeldename|benutzer|username|handle|tiktok/i.test(String(n))); + if (werSpalte >= 0) { + const leute = new Set(); + for (let i = 1; i < zeilen.length; i++) { + const w = String(zeilen[i][werSpalte] ?? "").trim().toLowerCase(); + if (w) leute.add(w); + } + if (leute.size > 1) { + return res.status(409).json({ + fehler: `In dieser Datei stehen ${leute.size} verschiedene Creator ` + + `(Spalte „${kopf[werSpalte]}“). Dieser Weg schreibt auf EINE Person. ` + + "Nimm „Backstage-Tabelle einfügen“ – der ordnet jede Zeile selbst zu.", + kopf, spalten: zu, mehrere: leute.size, + }); + } + } + const neu = []; const ersetzt = []; const fehlerhaft = []; for (let i = 1; i < zeilen.length; i++) { const z = zeilen[i]; - const tag = datumLesen(z[zu.tag]); - if (!tag) { fehlerhaft.push({ zeile: i + 1, grund: `Datum unlesbar: "${z[zu.tag]}"` }); continue; } + const gelesen = tagAusZelle(z[zu.tag]); + if (gelesen.fehler) { fehlerhaft.push({ zeile: i + 1, grund: gelesen.fehler }); continue; } + const tag = gelesen.tag; if (tag > tagVor(0)) { fehlerhaft.push({ zeile: i + 1, grund: `${tag} liegt in der Zukunft` }); continue; } const werte = { tag }; @@ -1177,10 +1221,16 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft try { for (let i = 1; i < zeilen.length; i++) { const z = zeilen[i]; - const tag = datumLesen(z[zu.tag]); + /* Dieselbe Regel wie in der Vorschau -- sonst zeigt die + Vorschau etwas anderes an, als geschrieben wird. Genau das + ist der Unterschied, den niemand bemerkt. */ + const gelesen = tagAusZelle(z[zu.tag]); + const tag = gelesen.tag; if (!tag || tag > tagVor(0)) { uebersprungen++; continue; } const w = (feld) => (zu[feld] === undefined ? null : zahlLesen(z[zu[feld]])); - const dauer = w("dauer_min"); + /* Die Dauer hat eine EINHEIT ("86Std. 10Min. 52Sek."), die + uebrigen Felder nicht. */ + const dauer = zu.dauer_min === undefined ? null : dauerLesen(z[zu.dauer_min]); schreiben.run( creatorId, tag, w("diamanten"), dauer, /* Ein gültiger Tag ist ein Tag mit LIVE-Zeit. Steht im diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index d2b4e034..f336b67a 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=202609141135", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609141201", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609141135", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609141201", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609141135", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609141201", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609141135", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609141201", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 14d7df1a..e083d0bc 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 @@ - - - - - + + + + +