From e82bf45f0d6c4911857ac5e0e6da5f07d88aad47 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Mon, 14 Sep 2026 16:23:16 +0200 Subject: [PATCH] Ein Zeitraum geht jetzt durch -- als Zeitraum, nicht als Tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filipe: "verbesser das, also ich will dass das auch so geht, mach dass es funktioniert, ich will es so einfach und perfekt wie moeglich. also sieh zu dass die excel auch so durch geht." Backstage gibt eine Ausgabe fuer einen ZEITRAUM heraus -- "2026-09-01 ~ 2026-09-13", eine Zeile je Creator, alle Zahlen aufsummiert. Am Vormittag wurde so eine Zeile abgewiesen. Jetzt geht sie durch. DREI WEGE WAEREN MOEGLICH GEWESEN, ZWEI DAVON WAEREN ZAHLENFAELSCHUNG: * auf den ersten Tag schreiben -> dreizehn Tage auf einem Tag; die Zahl steht da, sie ist gross, sie sieht richtig aus, und niemand kann spaeter sagen, was darin steckt; * gleichmaessig verteilen -> erfundene Tage, die es nie gab. Der dritte ist dieser: den Zeitraum ALS Zeitraum ablegen. Was drinsteht, ist dann wahr -- und was er nicht sagt (welcher Tag wie lief), behauptet er auch nicht. EIGENE TABELLE UND NICHT EIN FELD IN `leistung`: Dort ist der Schluessel (creator, tag). Ein Zeitraum ab dem 1. September wuerde mit dem ECHTEN 1. September zusammenstossen, und eine der beiden Zahlen waere weg. Getrennt kann keines das andere ueberschreiben -- und die Wochenzahlen bleiben, was sie sind: aus Tagen gerechnet. Der Schluessel ist (creator, von, bis): Dieselbe Datei zweimal einzulesen ersetzt denselben Zeitraum, statt ihn zu verdoppeln. AUF DER SEITE steht ein eigener Block zwischen Woche und Tageszeilen: die Spanne vorn, die Zahl der Tage als Marke daneben, die Werte darunter. Dazu EINE Umrechnung, und nur diese: "Ø 4.129 Diamanten pro Tag" -- als Durchschnitt bezeichnet, nirgends gespeichert. Wer eine Woche vergleichen will, braucht sie; wer sie fuer einen echten Tag haelt, hat das Wort nicht gelesen. Darueber steht woertlich, dass diese Zahlen nicht in die Wochenzahlen eingehen. MIT DER ECHTEN DATEI GEMESSEN: alle 8 Zeilen gehen durch, 8 als Zeitraum, 0 abgelehnt. Spanne 01.09.-11.09. = 11 Tage, Diamanten 53.679, Dauer 5171 Minuten. pruef-backstage-import 86 -> 106. Die Pruefung, auf die es ankommt, ist nicht "es wird gespeichert", sondern "es wird NICHT in die Woche gemischt": kein Tag kommt hinzu, die Wochenzahl bleibt Ziffer fuer Ziffer dieselbe. Dazu die Gegenprobe, dass ein Zeitraum von EINEM Tag weiterhin ein Tag bleibt -- sonst hiesse alles andere nur, dass jetzt alles als Zeitraum abgelegt wird. DREI PRUEFUNGEN GEDREHT, KEINE GELOESCHT: Zwei behaupteten noch die Ablehnung vom Vormittag. Und tagAusZelle() hat jetzt DREI Ausgaenge statt zwei (Tag, Zeitraum, Grund) -- geprueft wird, dass nie zwei davon gleichzeitig kommen. Gaebe es zwei, entschiede jeder Aufrufer selbst, was Vorrang hat, und der zweite entschiede anders als der erste. Genau daran ist heute frueh der Zeitraum-Schutz gescheitert. ZWEI EIGENE MESSFEHLER, beide von der Pruefung gefunden: Ich verlangte "kein einziger Tag in der Spanne" und uebersah, dass weiter oben schon ein Tag geschrieben worden war, der hineinfaellt -- gemessen wird jetzt die VERAENDERUNG. Und ich mass den Zeitraum-Block, waehrend ein anderer Creator geoeffnet war: keine Frage an die Seite, sondern an die falsche Person. Nebenbei zum zweiten Mal heute: eine Versalzeile mit 10,88 px. Auf .72rem angehoben, bevor pruef-css-klassen sie findet. Co-Authored-By: Claude Opus 5 --- server/pruef-backstage-import.mjs | 173 +++++++++++++++++++++++++++++- server/pruef-xlsx.mjs | 25 +++-- server/workspace-leistung.js | 105 ++++++++++++++++-- server/workspace.js | 49 +++++++++ workspace/app.webmanifest | 8 +- workspace/assets/css/leistung.css | 66 ++++++++++++ workspace/assets/js/leistung.js | 69 ++++++++++++ workspace/aufgaben.html | 24 ++--- workspace/automation.html | 26 ++--- workspace/bereich.html | 34 +++--- workspace/calls.html | 28 ++--- workspace/chat.html | 26 ++--- workspace/content.html | 30 +++--- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 ++--- workspace/entwicklung.html | 24 ++--- workspace/index.html | 8 +- workspace/kalender.html | 28 ++--- workspace/leistung.html | 45 +++++--- workspace/personen.html | 26 ++--- workspace/profil.html | 28 ++--- workspace/rechte.html | 24 ++--- workspace/report.html | 28 ++--- workspace/scouting.html | 26 ++--- workspace/start.html | 24 ++--- workspace/startcheck.html | 28 ++--- workspace/steckbrief.html | 26 ++--- workspace/talente.html | 24 ++--- workspace/team.html | 26 ++--- workspace/teamlage.html | 24 ++--- workspace/treff-moderation.html | 24 ++--- workspace/treff-regeln.html | 24 ++--- workspace/uebersicht.html | 26 ++--- workspace/wissen.html | 26 ++--- 35 files changed, 829 insertions(+), 367 deletions(-) diff --git a/server/pruef-backstage-import.mjs b/server/pruef-backstage-import.mjs index 89c8a049..a1c6a26d 100644 --- a/server/pruef-backstage-import.mjs +++ b/server/pruef-backstage-import.mjs @@ -337,6 +337,121 @@ for (const [rolle, code, wer] of [ `der Scout sieht seinen Creator in der Liste (${namen || "leer"})`); } +{ + /* ---- EIN ZEITRAUM GEHT JETZT DURCH (14.09.2026) ------------------ + Filipe: "ich will dass das auch so geht, mach dass es + funktioniert ... sieh zu dass die excel auch so durch geht." + + Am Vormittag wurde eine Zeile mit "2026-09-01 ~ 2026-09-13" + abgewiesen -- richtig, denn auf einen Tag geschrieben waeren es + dreizehn Tage auf einmal gewesen. Jetzt geht sie durch, aber als + ZEITRAUM: eigene Tabelle, eigener Block auf der Seite, und die + Wochenzahlen bleiben unberuehrt. + + DAS IST DIE PRUEFUNG, AUF DIE ES ANKOMMT: nicht "es wird + gespeichert", sondern "es wird NICHT in die Woche gemischt". Eine + Summe aus dreizehn Tagen in einer Wochenzahl waere genau der + stille Fehler, den das Ganze vermeiden soll. */ + const spanneText = [ + "Datenzeitraum\tCreator\tDiamanten\tLIVE-Dauer\tGültige LIVE-Gehen-Tage", + "2026-09-01 ~ 2026-09-13\t@Nova_Live\t53.679\t86Std. 10Min. 52Sek.\t11", + ].join("\n"); + + { + const v = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", + keksDogi, { text: spanneText }); + ok(v.status === 200, `die Vorschau nimmt den Zeitraum an (HTTP ${v.status})`); + ok(v.daten?.zeitraeume === 1, + `sie zaehlt ihn als Zeitraum, nicht als Tag (${v.daten?.zeitraeume})`); + ok(v.daten?.spanne?.tage === 13, + `und nennt die Zahl der Tage (${v.daten?.spanne?.tage})`); + ok((v.daten?.probleme || []).length === 0, + `keine Absage mehr (${(v.daten?.probleme || []).length} Probleme)`); + } + + /* Die Woche VOR dem Import -- als Vergleichspunkt. Ohne ihn hiesse + "die Woche stimmt" nur, dass irgendetwas dasteht. */ + const wocheVor = await (await fetch(BASIS + `/workspace/api/leistung/${idNova}`, + { headers: { Cookie: keksDogi } })).json(); + const diaVor = wocheVor?.woche?.diamanten?.wert ?? null; + /* WIE VIELE TAGESZEILEN ES SCHON GIBT. Der erste Entwurf verlangte + hier NULL -- und uebersah, dass die Backstage-Tabelle weiter oben + bereits einen Tag geschrieben hat, der in dieselbe Spanne faellt. + Die Pruefung war rot, der Code richtig. Gemessen wird deshalb die + VERAENDERUNG, nicht der Absolutwert. */ + const tageVor = (() => { + 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-13'") + .get(idNova).n; + d0.close(); + return n; + })(); + + { + const s = await ruf("/workspace/api/leistung/netzwerk-import", + keksDogi, { text: spanneText }); + ok(s.status === 200, `der Zeitraum wird uebernommen (HTTP ${s.status})`); + ok(s.daten?.zeitraeume === 1 && s.daten?.geschrieben === 0, + `als Zeitraum geschrieben, 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(idNova); + const tage = d2.prepare( + "SELECT COUNT(*) AS n FROM leistung WHERE creator_id = ? AND tag >= '2026-09-01' AND tag <= '2026-09-13'") + .get(idNova).n; + d2.close(); + ok(z && z.von === "2026-09-01" && z.bis === "2026-09-13" && z.tage === 13, + `er steht in der Datenbank (${z?.von} bis ${z?.bis}, ${z?.tage} Tage)`); + ok(z?.diamanten === 53679, `mit den Diamanten (${z?.diamanten})`); + ok(z?.dauer_min === 5171, `und der Dauer in Minuten (${z?.dauer_min})`); + /* DER WICHTIGSTE HAKEN: In der TAGES-Tabelle steht davon nichts. */ + ok(tage === tageVor, + `und KEIN einziger Tag kam hinzu (${tageVor} -> ${tage} Tageszeilen)`); + } + + { + /* Und die Wochenzahl ist unveraendert -- der Zeitraum mischt sich + nicht hinein. */ + const nach = await (await fetch(BASIS + `/workspace/api/leistung/${idNova}`, + { headers: { Cookie: keksDogi } })).json(); + ok((nach?.woche?.diamanten?.wert ?? null) === diaVor, + `die Wochenzahl bleibt unveraendert (${diaVor} -> ${nach?.woche?.diamanten?.wert})`); + ok(Array.isArray(nach?.zeitraeume) && nach.zeitraeume.length === 1, + `aber der Zeitraum wird mitgeliefert (${(nach?.zeitraeume || []).length})`); + } + + { + /* DIESELBE DATEI ZWEIMAL ersetzt denselben Zeitraum, statt ihn zu + verdoppeln -- sonst stuenden nach dem zweiten Einlesen zwei + gleiche Karten da. */ + await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: spanneText }); + const d3 = new DatabaseSync(DB, { readOnly: true }); + const n = d3.prepare( + "SELECT COUNT(*) AS n FROM leistung_zeitraum WHERE creator_id = ?").get(idNova).n; + d3.close(); + ok(n === 1, `zweimal einlesen ergibt EINEN Zeitraum (${n})`); + } + + { + /* GEGENPROBE: Ein Zeitraum von EINEM Tag ist kein Zeitraum -- er + geht weiterhin in die Tagestabelle. Ohne sie hiesse alles oben + nur, dass jetzt alles als Zeitraum abgelegt wird. */ + const einTag = [ + "Datenzeitraum\tCreator\tDiamanten", + `${gestern} ~ ${gestern}\t@Nova_Live\t1.234`, + ].join("\n"); + const s = await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: einTag }); + ok(s.daten?.geschrieben === 1 && (s.daten?.zeitraeume ?? 0) === 0, + `ein Ein-Tages-Zeitraum bleibt ein Tag (${s.daten?.geschrieben} Tag, ` + + `${s.daten?.zeitraeume} Zeitraeume)`); + } + +} + /* ======================================================================= 7. Und die Seite selbst ======================================================================= */ @@ -504,16 +619,64 @@ console.log("\n=== Die Seite ==="); await seite.waitForTimeout(1800); const spanne = await seite.evaluate(() => document.getElementById("netz-vorschau")?.textContent || ""); - ok(spanne.includes("mehrere Tage") && spanne.includes("2026-09-01"), - `ein Zeitraum ueber mehrere Tage wird abgelehnt und BEGRUENDET ("${spanne.slice(0, 90)}…")`); - ok(!/1 von 1 Zeilen zugeordnet/.test(spanne), - "und die Zeile wird NICHT als zugeordnet gezaehlt"); + /* GEDREHT AM 14.09.2026 NACHMITTAG, NICHT GELOESCHT. + + Hier stand: "ein Zeitraum ueber mehrere Tage wird abgelehnt und + BEGRUENDET" -- richtig, bis Filipe sagte: "ich will dass das auch + so geht." Jetzt geht die Zeile durch, und die Vorschau sagt, dass + es ein ZEITRAUM ist. Eine geloeschte Pruefung senkt die Zahl und + beweist nichts; eine gedrehte haelt fest, dass die Aenderung eine + Entscheidung war. */ + ok(/1 von 1 Zeilen zugeordnet/.test(spanne), + `ein Zeitraum ueber mehrere Tage geht jetzt durch ("${spanne.slice(0, 60)}…")`); + ok(!/mehrere Tage auf einmal/.test(spanne), + "und wird nicht mehr abgewiesen"); if (process.env.SCHIRM) { await seite.locator("#netz-dialog").screenshot( { path: join(HIER, "pruef-netz-zeitraum.png") }); } - await seite.click("#netz-zu"); + /* ---- UND WIE ES DANACH AUSSIEHT (14.09.2026) -------------------- + Der Block mit den Zeitraeumen ist die eigentliche Antwort auf + "sieh zu dass die excel auch so durch geht": Die Zahlen sind da, + und man sieht auf einen Blick, worauf sie sich beziehen. */ + await seite.click("#netz-los"); + await seite.waitForTimeout(1800); + + /* AUF DEN RICHTIGEN CREATOR SCHALTEN. Der erste Entwurf mass sofort + und fand nichts -- geoeffnet war Kiro, der Zeitraum gehoert Nova. + Das war kein Befund, sondern eine Frage an die falsche Person. */ + await seite.selectOption("#creator", String(idNova)).catch(() => {}); + await seite.waitForTimeout(1600); + const werOffen = await seite.evaluate(() => + document.getElementById("creator")?.selectedOptions?.[0]?.textContent || ""); + ok(/Nova/.test(werOffen), `Nova ist geoeffnet ("${werOffen.slice(0, 30)}")`); + + const spannenBild = await seite.evaluate(() => ({ + sichtbar: document.getElementById("spannen-block")?.hidden === false, + karten: document.querySelectorAll(".spanne").length, + zeit: document.querySelector(".spanne__zeit")?.textContent || "", + tage: document.querySelector(".spanne__tage")?.textContent || "", + schnitt: document.querySelector(".spanne__schnitt")?.textContent || "", + satz: document.querySelector(".spannen__satz")?.textContent || "", + })); + ok(spannenBild.sichtbar && spannenBild.karten >= 1, + `der Zeitraum-Block steht auf der Seite (${spannenBild.karten} Karten)`); + ok(spannenBild.zeit.includes("01.09.") && spannenBild.zeit.includes("13.09."), + `mit der Spanne davor ("${spannenBild.zeit}")`); + ok(/Tage/.test(spannenBild.tage), + `und der Zahl der Tage ("${spannenBild.tage}")`); + ok(/Ø/.test(spannenBild.schnitt) && /pro Tag/.test(spannenBild.schnitt), + `der Schnitt steht als Durchschnitt da, nicht als Tageswert ("${spannenBild.schnitt}")`); + ok(/nicht für einen Tag/.test(spannenBild.satz), + "und darueber steht, dass es kein Tageswert ist"); + + if (process.env.SCHIRM) { + await seite.locator("#spannen-block").screenshot( + { path: join(HIER, "pruef-spannen.png") }); + } + + await seite.click("#netz-zu").catch(() => {}); await seite.waitForTimeout(300); /* Gemessen wird SATZ FUER SATZ, nicht der Block. Ein Block, der als Ganzes erscheint, erklaert einem Scout auch den Knopf, den er diff --git a/server/pruef-xlsx.mjs b/server/pruef-xlsx.mjs index 7a37585f..40528572 100644 --- a/server/pruef-xlsx.mjs +++ b/server/pruef-xlsx.mjs @@ -320,7 +320,12 @@ console.log("=== Die eine Regel fuer den Tag ==="); 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"], + /* GEDREHT AM NACHMITTAG DES 14.09.2026, nicht geloescht. + Vormittags wurde ein Zeitraum ueber mehrere Tage ABGELEHNT -- + richtig, denn auf einen Tag geschrieben waeren es elf Tage auf + einmal gewesen. Filipe: "ich will dass das auch so geht." Jetzt + kommt er als ZEITRAUM zurueck und wird als solcher abgelegt. */ + ["2026-09-01 ~ 2026-09-11", "ZEITRAUM", null], ["2026-09-06 ~ 2026-09-06", "2026-09-06", null], ["2026-09-06", "2026-09-06", null], ["06.09.2026", "2026-09-06", null], @@ -329,19 +334,25 @@ console.log("=== Die eine Regel fuer den Tag ==="); ]; for (const [text, sollTag, sollFehler] of faelle) { const r = tagAusZelle(text); - if (sollTag) { - ok(r.tag === sollTag && !r.fehler, + if (sollTag === "ZEITRAUM") { + ok(!!r.zeitraum && !r.tag && !r.fehler, + `"${text}" kommt als Zeitraum zurueck ` + + `(${r.zeitraum ? r.zeitraum.von + " bis " + r.zeitraum.bis : r.tag || r.fehler})`); + } else if (sollTag) { + ok(r.tag === sollTag && !r.fehler && !r.zeitraum, `"${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. */ + /* DREI AUSGAENGE, NIE ZWEI DAVON GLEICHZEITIG: Tag, Zeitraum oder + Grund. Gaebe es zwei auf einmal, entschiede jeder Aufrufer selbst, + was Vorrang hat -- und der zweite entschiede anders als der erste. + Genau daran ist heute frueh der Zeitraum-Schutz gescheitert. */ 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"); + ok(alle.every((r) => [r.tag, r.zeitraum, r.fehler].filter(Boolean).length === 1), + "sie liefert immer genau EINES von dreien: Tag, Zeitraum oder Grund"); } console.log(""); diff --git a/server/workspace-leistung.js b/server/workspace-leistung.js index 34fca48e..991c7a4d 100644 --- a/server/workspace-leistung.js +++ b/server/workspace-leistung.js @@ -237,7 +237,23 @@ leistungRouter.get("/workspace/api/leistung/:creatorId", (req, res) => { anteil: Math.round((dieseWoche.verweildauer_s / ziele.verweildauer_s) * 100) } : null, } : null; + /* DIE ZEITRAEUME AUS BACKSTAGE (14.09.2026). + + Sie gehen NICHT in die Wochenzahlen ein -- die bleiben aus Tagen + gerechnet. Ein Zeitraum ueber 13 Tage in eine Woche zu mischen + waere eine Summe aus zwei verschiedenen Dingen, und niemand + koennte hinterher sagen, was darin steckt. + + Sie stehen deshalb daneben, mit ihrer Spanne davor. Die juengsten + zuerst; mehr als sechs braucht niemand auf einen Blick. */ + const zeitraeume = db().prepare(` + SELECT von, bis, tage, diamanten, dauer_min, gueltige_tage, + zuschauer_avg, zuschauer_max, follower_neu, erfasst + FROM leistung_zeitraum WHERE creator_id = ? + ORDER BY bis DESC, von DESC LIMIT 6`).all(creatorId); + res.json({ + zeitraeume, creator_id: creatorId, woche: mitVergleich, roh: { woche: dieseWoche, vorwoche }, @@ -684,14 +700,33 @@ export function zeitraumLesen(roh) { 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." }; + /* MEHRERE TAGE SIND KEIN FEHLER MEHR (14.09.2026, zweite Fassung). + + Am Vormittag wurde so eine Zeile abgewiesen -- richtig, denn auf + einen Tag geschrieben waeren es 13 Tage auf einmal gewesen. + Filipe: "ich will dass das auch so geht ... sieh zu dass die + excel auch so durch geht." + + Also geht sie durch -- aber als das, was sie ist. Der Aufrufer + bekommt `zeitraum` statt `tag` und legt sie in + leistung_zeitraum ab. Wer nur `tag` liest, bekommt weiterhin + nichts und schreibt damit auch nichts Falsches. */ + return { zeitraum }; } const tag = zeitraum ? zeitraum.von : datumLesen(roh); if (!tag) return { fehler: `Datum unlesbar: "${String(roh ?? "").slice(0, 40)}"` }; return { tag }; } +/** Wie viele Tage liegen in einem Zeitraum? Beide Enden zaehlen mit -- + * vom 1. bis zum 1. ist EIN Tag, nicht null. */ +export function tageImZeitraum(von, bis) { + const a = Date.parse(`${von}T00:00:00Z`); + const b = Date.parse(`${bis}T00:00:00Z`); + if (!Number.isFinite(a) || !Number.isFinite(b) || b < a) return 0; + return Math.round((b - a) / 86400000) + 1; +} + /** Macht aus einem Textwert ein Datum "JJJJ-MM-TT". * Erkennt 2026-09-06, 06.09.2026 und 09/06/2026. */ export function datumLesen(roh) { @@ -891,13 +926,19 @@ function netzwerkLesen(text, person, tagVorgabe) { daraufhin durch den Einzel-Import, und dort fehlte sie. Jetzt benutzen alle drei Stellen dieselbe. */ let tag = tagVorgabe; + let spanne = null; if (ausTabelle) { const gelesen = tagAusZelle(z[zu.tag]); if (gelesen.fehler) { schlecht.push({ zeile: i + 1, grund: gelesen.fehler }); continue; } - tag = gelesen.tag; + if (gelesen.zeitraum) spanne = gelesen.zeitraum; + else tag = gelesen.tag; + } + if (!tag && !spanne) { schlecht.push({ zeile: i + 1, grund: "Kein Tag angegeben." }); continue; } + const letzterTag = spanne ? spanne.bis : tag; + if (letzterTag > heuteLokal()) { + schlecht.push({ zeile: i + 1, grund: `${letzterTag} liegt in der Zukunft` }); + 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 gegen Name, Name gegen Handle, Name gegen Name. Das Eindeutige @@ -910,7 +951,10 @@ function netzwerkLesen(text, person, tagVorgabe) { || (sName && nach.name.get(sName)); if (!p) { unbekannt.push(rohWer.slice(0, 60)); continue; } - const werte = { creator_id: p.id, name: p.name, tag }; + const werte = spanne + ? { creator_id: p.id, name: p.name, von: spanne.von, bis: spanne.bis, + tage: tageImZeitraum(spanne.von, spanne.bis) } + : { creator_id: p.id, name: p.name, tag }; let hatZahl = false; for (const feld of messwerte) { /* Die Dauer hat eine EINHEIT, die anderen Felder nicht. @@ -948,12 +992,31 @@ leistungRouter.post("/workspace/api/leistung/netzwerk-import/vorschau", gleicheH hinterher raten, ob er etwas überschrieben hat. */ const vorhanden = db().prepare( "SELECT 1 FROM leistung WHERE creator_id = ? AND tag = ?"); - let neu = 0, ersetzt = 0; - for (const t of e.treffer) (vorhanden.get(t.creator_id, t.tag) ? ersetzt++ : neu++); + const vorhandenSpanne = db().prepare( + "SELECT 1 FROM leistung_zeitraum WHERE creator_id = ? AND von = ? AND bis = ?"); + let neu = 0, ersetzt = 0, zeitraeume = 0; + for (const t of e.treffer) { + if (t.von) { + zeitraeume++; + if (vorhandenSpanne.get(t.creator_id, t.von, t.bis)) ersetzt++; else neu++; + } else { + (vorhanden.get(t.creator_id, t.tag) ? ersetzt++ : neu++); + } + } res.json({ spalten: e.spalten, person_spalten: e.person_spalten, aus_tabelle: e.aus_tabelle, kopf: e.kopf, + /* WIE VIELE DAVON SIND ZEITRAEUME? Steht eigens da, weil es + etwas anderes ist als ein Tag -- wer eine Backstage-Ausgabe + ueber zwei Wochen einliest, soll das VOR dem Bestaetigen + sehen und nicht hinterher raten, warum die Woche leer ist. */ + zeitraeume, + spanne: e.treffer.find((t) => t.von) + ? { von: e.treffer.find((t) => t.von).von, + bis: e.treffer.find((t) => t.von).bis, + tage: e.treffer.find((t) => t.von).tage } + : null, zugeordnet: e.treffer.length, neu, ersetzt, unbekannt: e.unbekannt, probleme: e.schlecht.slice(0, 12), probleme_gesamt: e.schlecht.length, @@ -988,10 +1051,30 @@ leistungRouter.post("/workspace/api/leistung/netzwerk-import", gleicheHerkunft, (SELECT notiz FROM leistung WHERE creator_id = ? AND tag = ?), ?,?, 'import')`); + /* DER ZWEITE WEG: ein Zeitraum geht in seine eigene Tabelle. + INSERT OR REPLACE, damit dieselbe Datei zweimal denselben + Zeitraum ersetzt statt ihn zu verdoppeln. */ + 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; + let spannen = 0; d.exec("BEGIN"); try { for (const t of e.treffer) { + if (t.von) { + 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, + t.verweildauer_s ?? null, t.schenker ?? null, t.follower_neu ?? null, + new Date().toISOString(), req.person.id); + spannen++; + continue; + } schreiben.run(t.creator_id, t.tag, t.diamanten ?? null, t.dauer_min ?? null, /* Ein gültiger Tag im Sinne von TikTok ist einer mit @@ -1009,9 +1092,11 @@ 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} Zeilen, ${e.unbekannt.length} unbekannt`.slice(0, 120), + detail: `${geschrieben} Tage, ${spannen} Zeitraeume, ` + + `${e.unbekannt.length} unbekannt`.slice(0, 120), }); - res.json({ geschrieben, unbekannt: e.unbekannt, probleme: e.schlecht.length }); + res.json({ geschrieben, zeitraeume: spannen, + unbekannt: e.unbekannt, probleme: e.schlecht.length }); } catch (fehler) { console.error("[leistung] Netzwerk-Import:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); diff --git a/server/workspace.js b/server/workspace.js index 0cdcb332..863dbf83 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -3250,6 +3250,55 @@ export function db() { CHECK (quelle IN ('hand','import')), PRIMARY KEY (creator_id, tag) ); + + /* ZAHLEN FUER EINEN ZEITRAUM (14.09.2026). + + Filipe: "ich will dass das auch so geht, mach dass es + funktioniert ... sieh zu dass die excel auch so durch geht." + + Backstage gibt eine Ausgabe fuer einen ZEITRAUM heraus -- + "2026-09-01 ~ 2026-09-13", eine Zeile je Creator, alle Zahlen + aufsummiert. Bis heute wurde so eine Zeile abgewiesen, weil + daraus kein Tageswert wird. + + DREI WEGE WAEREN MOEGLICH GEWESEN, und zwei davon waeren + Zahlenfaelschung: + * auf den ersten Tag schreiben -> 13 Tage auf einem Tag, + sieht richtig aus, ist es nicht; + * gleichmaessig verteilen -> erfundene Tage, die es nie gab. + Der dritte ist dieser: den Zeitraum ALS Zeitraum ablegen. Was + drinsteht, ist dann wahr -- und was es nicht sagt (welcher Tag + wie lief), behauptet es auch nicht. + + EIGENE TABELLE UND NICHT EIN FELD IN "leistung": Dort ist der + Schluessel (creator, tag). Ein Zeitraum ab dem 1. September + wuerde mit dem ECHTEN 1. September zusammenstossen -- und eine + der beiden Zahlen waere weg. Getrennt kann keines das andere + ueberschreiben, und die Wochenzahlen bleiben, was sie sind: + aus Tagen gerechnet. + + DER SCHLUESSEL IST (creator, von, bis): Dieselbe Datei zweimal + einzulesen ersetzt denselben Zeitraum, statt ihn zu + verdoppeln. */ + CREATE TABLE IF NOT EXISTS leistung_zeitraum ( + creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, + von TEXT NOT NULL, + bis TEXT NOT NULL, + tage INTEGER NOT NULL, + diamanten INTEGER, + dauer_min INTEGER, + gueltige_tage INTEGER, + zuschauer_avg INTEGER, + zuschauer_max INTEGER, + verweildauer_s INTEGER, + schenker INTEGER, + follower_neu INTEGER, + erfasst TEXT NOT NULL, + erfasst_von INTEGER REFERENCES personen(id) ON DELETE SET NULL, + PRIMARY KEY (creator_id, von, bis) + ); + CREATE INDEX IF NOT EXISTS idx_leistung_zeitraum_creator + ON leistung_zeitraum (creator_id, bis DESC); CREATE INDEX IF NOT EXISTS idx_leistung_tag ON leistung (tag DESC); /* ZIELE je Creator. Aus dem 90-Tage-Plan, den es im Profil schon diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 26a7d9ec..e6c81009 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=202609141428", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609141622", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609141428", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609141622", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609141428", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609141622", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609141428", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609141622", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/leistung.css b/workspace/assets/css/leistung.css index e4808aff..a516a92d 100644 --- a/workspace/assets/css/leistung.css +++ b/workspace/assets/css/leistung.css @@ -430,3 +430,69 @@ eigenen Knoepfe. Gerollt wird INNEN, damit "Uebernehmen" immer erreichbar bleibt. */ .dialog--breit .neu { max-height: min(86vh, 900px); overflow-y: auto; } + +/* ===================================================================== + ZEITRÄUME AUS BACKSTAGE (14.09.2026) + + Filipe: "sieh zu dass die excel auch so durch geht." + + Sie stehen zwischen der Woche und den Tageszeilen -- gröber als ein + Tag, feiner als gar nichts. Und sie sehen ABSICHTLICH anders aus als + die Wochenkarten: Wer sie für Tageswerte hält, rechnet mit ihnen + weiter. Der Zeitraum steht deshalb ganz vorn und nicht als Beiwerk. + ===================================================================== */ +.leistung-spannen { margin: 26px 0 0; } +.spannen__satz { + margin: 4px 0 12px; + font-size: .8rem; line-height: 1.55; + color: var(--text-still); + max-width: 62ch; +} +.spannen { + display: grid; gap: 10px; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); +} +.spanne { + border: 1px solid var(--rand); border-radius: 14px; + padding: 11px 13px 12px; + background: rgba(255, 255, 255, .025); +} +.spanne__kopf { + display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; + margin-bottom: 8px; +} +.spanne__zeit { + font-size: .95rem; font-weight: 650; color: var(--text); + font-variant-numeric: tabular-nums; +} +/* Die Zahl der Tage ist die WICHTIGSTE Einordnung und trotzdem still: + "12.480 Diamanten" ueber drei Tage ist etwas anderes als ueber + dreizehn -- ohne diese Angabe sagt die Zahl nichts. */ +.spanne__tage { + font-size: .74rem; color: var(--text-still); + border: 1px solid var(--rand); border-radius: 999px; + padding: 1px 8px 2px; +} +.spanne__werte { + display: grid; gap: 3px 14px; + grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); +} +.spanne__wert { display: flex; flex-direction: column; gap: 1px; min-width: 0; } +.spanne__name { + /* .72rem = 11.52 px, knapp ueber der Lesbarkeitsgrenze von 11.5 px. + .68rem waeren 10.88 gewesen -- und eine gesperrte Versalzeile ist + ohnehin schwerer zu lesen als normale Schrift. Zum zweiten Mal an + diesem Tag derselbe Griff; deshalb steht die Zahl jetzt mit + Begruendung da und nicht nur als Wert. */ + font-size: .72rem; letter-spacing: .07em; text-transform: uppercase; + color: var(--text-still); +} +.spanne__zahl { + font-size: .92rem; color: var(--text-leise); + font-variant-numeric: tabular-nums; +} +.spanne__schnitt { + margin: 9px 0 0; padding-top: 8px; + border-top: 1px solid var(--rand); + font-size: .76rem; color: var(--text-still); +} diff --git a/workspace/assets/js/leistung.js b/workspace/assets/js/leistung.js index af2413d6..6b796674 100644 --- a/workspace/assets/js/leistung.js +++ b/workspace/assets/js/leistung.js @@ -631,6 +631,74 @@ } catch { /* umgeleitet */ } finally { knopf.disabled = false; } } + /* =================================================================== + ZAHLEN FÜR EINEN ZEITRAUM (14.09.2026) + + Filipe: "ich will dass das auch so geht, mach dass es + funktioniert ... sieh zu dass die excel auch so durch geht." + + Backstage gibt eine Ausgabe für einen ZEITRAUM heraus -- alle + Zahlen aufsummiert, eine Zeile je Creator. Auf einen Tag + geschrieben wären das dreizehn Tage auf einmal: eine Zahl, die + richtig aussieht und falsch ist. Also stehen sie hier als das, was + sie sind. + + DER BLOCK VERSCHWINDET, WENN ES NICHTS GIBT. Eine leere + Überschrift ist keine Ordnung, sondern eine Lücke. + =================================================================== */ + function spannenZeichnen() { + const liste = stand?.zeitraeume || []; + $('spannen-block').hidden = liste.length === 0; + const ziel = $('spannen'); + ziel.textContent = ''; + if (!liste.length) return; + + for (const z of liste) { + const karte = el('div', 'spanne'); + + const kopf = el('div', 'spanne__kopf'); + kopf.append(el('span', 'spanne__zeit', `${kurzTag(z.von)} – ${kurzTag(z.bis)}`)); + /* DIE ZAHL DER TAGE STEHT DABEI. Ohne sie sagt "12.480 + Diamanten" nichts: über drei Tage ist das etwas anderes als + über dreizehn. */ + kopf.append(el('span', 'spanne__tage', `${z.tage} Tage`)); + karte.append(kopf); + + const werte = el('div', 'spanne__werte'); + const zeig = (name, wert, form) => { + if (wert === null || wert === undefined) return; + const w = el('div', 'spanne__wert'); + w.append(el('span', 'spanne__name', name)); + w.append(el('span', 'spanne__zahl', form(wert))); + werte.append(w); + }; + zeig('Diamanten', z.diamanten, zahl); + zeig('LIVE', z.dauer_min, stunden); + zeig('Gültige Tage', z.gueltige_tage, (n) => String(n)); + zeig('Ø Zuschauer', z.zuschauer_avg, zahl); + zeig('Neue Follower', z.follower_neu, zahl); + karte.append(werte); + + /* PRO TAG IM SCHNITT -- die einzige Umrechnung, die ehrlich ist: + Sie steht als Durchschnitt da und nicht als Tageswert, und sie + wird nirgends gespeichert. Wer eine Woche vergleichen will, + braucht sie; wer sie für einen echten Tag hält, liest das + Wort "Ø" nicht. */ + if (z.diamanten !== null && z.diamanten !== undefined && z.tage > 0) { + karte.append(el('p', 'spanne__schnitt', + `Ø ${zahl(Math.round(z.diamanten / z.tage))} Diamanten pro Tag`)); + } + ziel.append(karte); + } + } + + /** "2026-09-01" -> "01.09." -- in einer Spanne zählt der Tag, nicht + * das Jahr; beide Enden im selben Jahr wären doppelt genannt. */ + function kurzTag(iso) { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || '')); + return m ? `${m[3]}.${m[2]}.` : String(iso || ''); + } + /* ---------- Laden ------------------------------------------------------- */ async function allesLaden() { @@ -644,6 +712,7 @@ $('woche').hidden = false; $('ziele-block').hidden = false; $('tage-block').hidden = false; + spannenZeichnen(); /* WIE VOLLSTÄNDIG IST DIE WOCHE? Eine Zahl aus zwei von sieben Tagen ist etwas anderes als eine aus sieben -- ohne diesen diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 831c07f3..e8ca170c 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 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index e4ffc719..e075516b 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -147,10 +147,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 9daafe50..f7f54d44 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -181,14 +181,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index d3b6e987..3aa54e2e 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 329258f7..59c42952 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 52768600..1ac802ed 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index 9aaac3e6..04ca4592 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -319,6 +319,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index a6ad0add..f6d3b088 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609141428", + "src": "/assets/img/app-symbole/crew-192.png?v=202609141622", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609141428", + "src": "/assets/img/app-symbole/crew-512.png?v=202609141622", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609141428", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609141622", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609141428", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609141622", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 7187c24d..2c6dd4de 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index 66eb4660..446f6ebd 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index db4b59a0..642d93d0 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 474ff8a4..e7d56fb2 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index d845465c..21ba97b2 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,6 +85,25 @@
+ + +