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 @@ - - - - - + + + + +