diff --git a/server/pruef-backstage-import.mjs b/server/pruef-backstage-import.mjs index 3f27e73d..e0acff0e 100644 --- a/server/pruef-backstage-import.mjs +++ b/server/pruef-backstage-import.mjs @@ -469,6 +469,219 @@ for (const [rolle, code, wer] of [ } +/* ======================================================================= + ALLE 41 SPALTEN -- nichts faellt mehr weg (16.09.2026) + ======================================================================= + + Filipe: "ich will dass alles von der excel datei genommen wird. + perfektionnier das, aber wenn ich dir runter lade soll alles notiert + und angezeigt werden." + + --------------------------------------------------------------------- + DIE VORLAGE IST SEINE ECHTE AUSGABE vom 14.09.2026 ("Creator:innen- + daten"). Sie hat EINUNDVIERZIG Spalten; erkannt und gedeutet werden + acht. Die restlichen 33 wurden beim Einlesen weggeworfen, ohne dass + irgendwo stand, dass es sie gab. + + DIE ECHTE DATEI STEHT NICHT IM REPO -- in ihr stehen Name, ID und + Agent seiner Creator. Was hier steht, sind ihre UEBERSCHRIFTEN (die + gehoeren TikTok, nicht ihm) mit erfundenen Werten. Die echte Datei + hat die Form belegt; diese Pruefung haelt sie fest. + + WAS GEMESSEN WIRD, und jedes davon hat einen Grund: + * Alle 41 Spalten kommen an -- gezaehlt, nicht gehofft. + * Eine Spalte, die es in KEINEM Muster gibt ("Graduierungsstatus"), + kommt mit ihrem TEXT an. Waere nur die Zahl gespeichert, stuende + dort nichts. + * Eine LEERE Zelle erzeugt keine Zeile -- sonst stuende auf der + Karte "Status: (nichts)", eine Auskunft, die die Datei nicht gibt. + * Eine ERFUNDENE Spalte kommt genauso durch. Das ist die + eigentliche Gegenprobe: Sie beweist, dass hier keine Liste + gepflegt wird, sondern die Datei entscheidet -- also auch eine + Spalte durchkommt, die TikTok erst naechstes Jahr hinzufuegt. + * Ein ZWEITER Durchgang mit weniger Spalten laesst keine Leichen + zurueck. Ohne das Wegraeumen vorher stuende ein Wert aus einer + Datei da, die es nicht mehr gibt. + ======================================================================= */ +console.log(""); +console.log("=== Alle Spalten der echten Ausgabe ==="); +{ + /* Die 41 Ueberschriften, wortgleich aus seiner Ausgabe. */ + const KOPF = [ + "Datenzeitraum", "Creator*in-ID", "Creator*innen-Anmeldename", "Gruppe", + "Agent", "Beitrittszeit", "Tage seit dem Beitritt", "Diamanten", + "LIVE-Dauer", "Gültige LIVE-Gehen-Tage", "Neue Follower*innen", "Livestreams", + "Diamanten im letzten Monat", "LIVE-Dauer (in Std.) im letzten Monat", + "Gültige LIVE-Gehen-Tage im letzten Monat", + "Neue Follower*innen im letzten Monat", "Livestreams im letzten Monat", + "Diamanten - Prozent erreicht", "LIVE-Dauer - Prozent erreicht", + "Gültige LIVE-Gehen-Tage - Prozent erreicht", + "Neue Follower*innen - Prozent erreicht", "Livestreams - Prozent erreicht", + "Diamanten - im Vergleich zu letztem Monat", + "LIVE-Dauer - im Vergleich zu letztem Monat", + "Gültige LIVE-Gehen-Tage - im Vergleich zu letztem Monat", + "Neue Follower*innen - im Vergleich zu letztem Monat", + "Livestreams - im Vergleich zu letztem Monat", + "Matches", "Diamanten aus Matches", "Neue*r LIVE-Creator*innen", + "Diamanten von Multi-Gast-LIVEs", "Diamanten von Multi-Gast-LIVEs (als Host)", + "Diamanten von Multi-Gast-LIVEs (als Gäst*in)", + "Graduierungsstatus", "Stufen-Status", "Neue Fans", + "Gesamte Diamanten des Fanclubs", "Beitrag durch Fans in %", + "Fans insgesamt", "Aktive Fans vom Fanclub", "Status", + ]; + ok(KOPF.length === 41, `die Vorlage hat ${KOPF.length} Spalten`); + + /* Erfundene Werte in der FORM der echten -- mit allen Eigenheiten, + die dort vorkommen: Prozent mit englischem Punkt, eine Dauer mit + Einheiten, ein Bruch, der trotz "%" im Namen KEINER ist, Text + statt Zahl, und eine leere Zelle am Ende. */ + const WERT = [ + "2026-08-01 ~ 2026-08-31", "700001", "@Nova_Live", "Dogfather", + "Scout Sam", "2026-07-09 19:30:57 (UTC+0)", "68", "13210", + "6Std. 24Min. 7Sek.", "5", "97", "5", + "29488", "13Std. 22Min. 40Sek.", "9", "126", "16", + "44.79%", "47.85%", "55.55%", "76.98%", "31.25%", + "98.55%", "83.68%", "150.00%", "361.90%", "0.00%", + "13", "8665", "Nein", + "26", "26", "0", + "Nicht graduiert", "Nicht beibehalten", "17", + "13128", "0.99379", "1182", "75", "", + ]; + ok(WERT.length === KOPF.length, + `und genauso viele Werte (${WERT.length})`); + + const alles = [KOPF.join("\t"), WERT.join("\t")].join("\n"); + const v = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", + keksDogi, { text: alles }); + ok(v.daten?.spalten_gesamt === 41, + `die Vorschau nennt alle Spalten der Datei (${v.daten?.spalten_gesamt})`); + ok(v.daten?.felder === 40, + `und wie viele davon einen Wert tragen (${v.daten?.felder} -- "Status" ist leer)`); + + const a = await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: alles }); + ok(a.daten?.felder === 40, `uebernommen werden ${a.daten?.felder} Werte`); + + const d4 = new DatabaseSync(DB, { readOnly: true }); + const felder = d4.prepare(`SELECT nr, name, text, zahl FROM leistung_zeitraum_feld + WHERE creator_id = ? AND von = ? AND bis = ? ORDER BY nr`) + .all(idNova, "2026-08-01", "2026-08-31"); + d4.close(); + ok(felder.length === 40, + `in der Datenbank stehen ${felder.length} Felder (41 Spalten, eine leer)`); + + const nach = new Map(felder.map((f) => [f.name, f])); + ok(felder.length > 0 && !nach.has("Status"), + "die leere Zelle erzeugt KEINE Zeile -- sonst staende dort \"Status: nichts\""); + + /* ---- WER IST DAS UEBERHAUPT? (16.09.2026) -------------------------- + DER TEUERSTE FEHLER DIESES TAGES, und er stand nicht im Verdacht. + + Seine Ausgabe hat DREI Spalten, die nach Person aussehen: + "Creator*in-ID", "Creator*innen-Anmeldename" und "Agent". Die alte + Regel nahm die erste mit dem Wort "creator" darin -- die ID. Danach + wurde nach einem Creator namens "700001" gesucht, und die ganze + Datei kam als "nicht zugeordnet" zurueck, mit einem Hinweis, der + in die voellig falsche Richtung zeigte. + + Die Pruefung oben faellt darauf herein wie er: Ohne Zuordnung gibt + es keinen Zeitraum, also auch keine Felder. Deshalb steht es hier + noch einmal ausdruecklich -- damit beim naechsten Mal die MELDUNG + sagt, was los ist, und nicht nur eine Zahl fehlt. */ + ok(nach.get("Creator*innen-Anmeldename")?.text === "@Nova_Live", + "die Zeile wurde ueber den Anmeldenamen zugeordnet, nicht ueber die ID"); + ok(nach.get("Creator*in-ID")?.text === "700001", + "und die ID steht trotzdem in der Ablage -- sie ist eine Auskunft, kein Schluessel"); + { + /* GEGENPROBE: Eine Tabelle, in der NUR eine Kennnummer steht, darf + niemanden zuordnen -- und muss das sagen, statt stillschweigend + nichts zu tun. */ + const nurId = [ + ["Datenzeitraum", "Creator*in-ID", "Diamanten"].join("\t"), + ["2026-08-01 ~ 2026-08-31", "700001", "500"].join("\t"), + ].join("\n"); + const r = await ruf("/workspace/api/leistung/netzwerk-import/vorschau", + keksDogi, { text: nurId }); + ok(r.status === 400 && /Name oder Handle/.test(r.daten?.fehler || ""), + `eine Tabelle mit nur einer Kennnummer wird begruendet abgelehnt ` + + `(HTTP ${r.status}: "${String(r.daten?.fehler || "").slice(0, 40)}…")`); + } + + /* Eine Spalte, die KEIN Muster kennt, und ihr Wert ist TEXT. */ + ok(nach.get("Graduierungsstatus")?.text === "Nicht graduiert", + `eine Spalte ohne Muster kommt woertlich an ("${nach.get("Graduierungsstatus")?.text}")`); + ok(nach.get("Graduierungsstatus")?.zahl === null, + "und traegt keine erfundene Zahl"); + + /* Prozent mit englischem Punkt -- die Falle, an der eine deutsche + Zahlenlesung 4479 statt 44,79 liefern wuerde. */ + ok(nach.get("Diamanten - Prozent erreicht")?.zahl === 44.79, + `"44.79%" wird zu ${nach.get("Diamanten - Prozent erreicht")?.zahl}, nicht zu 4479`); + /* Und der Bruch, der trotz "%" im Spaltennamen keiner ist. */ + ok(nach.get("Beitrag durch Fans in %")?.zahl === 0.99379, + `"0.99379" bleibt ${nach.get("Beitrag durch Fans in %")?.zahl} ` + + "-- der Spaltenname sagt Prozent, der Wert ist ein Bruch"); + /* Die Reihenfolge der Datei bleibt erhalten. */ + ok(felder[0].name === "Datenzeitraum" && felder[felder.length - 1].name === "Aktive Fans vom Fanclub", + `die Reihenfolge der Datei bleibt (${felder[0].name} … ${felder[felder.length - 1].name})`); + + /* ---- DIE EIGENTLICHE GEGENPROBE ------------------------------------ + Eine Spalte, die es bei TikTok (noch) gar nicht gibt. Kommt sie + durch, wird hier keine Liste gepflegt -- sondern die Datei + entscheidet. Genau das soll die Bauart leisten. */ + { + const erfunden = [ + [...KOPF, "Sternenstaub pro Woche"].join("\t"), + [...WERT, "4711"].join("\t"), + ].join("\n"); + await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: erfunden }); + const d5 = new DatabaseSync(DB, { readOnly: true }); + const f = d5.prepare(`SELECT text, zahl FROM leistung_zeitraum_feld + WHERE creator_id = ? AND von = ? AND bis = ? AND name = ?`) + .get(idNova, "2026-08-01", "2026-08-31", "Sternenstaub pro Woche"); + d5.close(); + ok(f?.zahl === 4711, + `auch eine Spalte, die es heute nicht gibt, kommt an (${f?.zahl})`); + } + + /* ---- UND KEINE LEICHEN --------------------------------------------- + Dieselbe Datei noch einmal, diesmal OHNE die erfundene Spalte. Sie + muss verschwinden. Ohne das Wegraeumen vor dem Schreiben stuende + dort ein Wert aus einer Datei, die es nicht mehr gibt -- und + niemand koennte ihn von einem frischen unterscheiden. */ + { + await ruf("/workspace/api/leistung/netzwerk-import", keksDogi, { text: alles }); + const d6 = new DatabaseSync(DB, { readOnly: true }); + const n = d6.prepare(`SELECT COUNT(*) AS n FROM leistung_zeitraum_feld + WHERE creator_id = ? AND von = ? AND bis = ?`) + .get(idNova, "2026-08-01", "2026-08-31").n; + const weg = d6.prepare(`SELECT 1 FROM leistung_zeitraum_feld + WHERE creator_id = ? AND name = ?`).get(idNova, "Sternenstaub pro Woche"); + d6.close(); + ok(!weg && n === 40, + `beim zweiten Einlesen bleibt keine Leiche zurueck (${n} Felder)`); + } + + /* ---- UND DIE SEITE BEKOMMT SIE AUCH ------------------------------ */ + { + const antwort = await (await fetch(BASIS + `/workspace/api/leistung/${idNova}`, + { headers: { Cookie: keksDogi } })).json(); + /* GEZIELT DEN AUGUST, nicht "den ersten". Nova hat inzwischen zwei + Zeitraeume; `[0]` waere der jüngere und die Pruefung haette ohne + Vorwarnung etwas anderes gemessen als gemeint. Genau dieser + Fehler ist mir am 14.09. schon einmal unterlaufen. */ + const z = (antwort?.zeitraeume || []).find((x) => x.von === "2026-08-01"); + ok(Array.isArray(z?.felder) && z.felder.length === 40, + `die Seite bekommt alle Werte mitgeliefert (${z?.felder?.length})`); + ok(z?.felder?.[0]?.name === "Datenzeitraum", + "und zwar in der Reihenfolge der Datei"); + /* Und der ANDERE Zeitraum hat seine eigenen -- die Felder haengen + am Zeitraum, nicht am Creator. */ + const sept = (antwort?.zeitraeume || []).find((x) => x.von === "2026-09-01"); + ok((sept?.felder || []).length === 5, + `der andere Zeitraum hat seine eigenen ${(sept?.felder || []).length} Werte`); + } +} + { /* ---- DER EINZEL-IMPORT KENNT DEN DRITTEN AUSGANG (14.09.2026) ---- Als tagAusZelle() den Zeitraum dazubekam, kannte ihn nur der @@ -903,6 +1116,88 @@ console.log("\n=== Die Seite ==="); { path: join(HIER, "pruef-spannen.png") }); } + /* ---- "ALLES ANGEZEIGT" HEISST: MAN KANN ES SEHEN (16.09.2026) ---- + Filipe: "wenn ich dir runter lade soll alles notiert und angezeigt + werden." Dass die Werte in der Datenbank stehen, ist oben belegt. + Ob sie auf dem Bildschirm ankommen, ist eine ANDERE Frage -- und + genau die Sorte, die hier schon einmal keiner gestellt hat (die + Teilnehmerwahl im Kalender, 05.09.). + + GEMESSEN WIRD MIT `checkVisibility()`, UND DAS IST KEINE + GESCHMACKSFRAGE (16.09.2026, an einem Miniaturfall nachgemessen): + + zugeklappt getBoundingClientRect().height 72 + aufgeklappt getBoundingClientRect().height 72 + + Dieselbe Zahl. Auch `offsetHeight`, `offsetParent` und + `getClientRects()` sind in beiden Zustaenden identisch -- Chromium + verbirgt den Inhalt eines geschlossenen
mit + `content-visibility: hidden`, und das behaelt die letzte + Ausmessung. Wer die Hoehe misst, bekommt also fuer "unsichtbar" + und "sichtbar" dieselbe Antwort und merkt es nicht. + + Genau daran ist mein erster Anlauf gescheitert: Er meldete + "zugeklappt 141 px hoch" -- im Bildschirmfoto war an derselben + Stelle nichts zu sehen. Die Messung log, nicht die Seite. + + `checkVisibility()` unterscheidet beides sauber (false / true) und + ist das einzige der fuenf Verfahren, das es kann. */ + { + const deckel = await seite.$$(".spanne__alles-kopf"); + ok(deckel.length >= 1, `es gibt ${deckel.length} Deckel "Alle Werte aus Backstage"`); + const zuVorher = await seite.evaluate(() => + document.querySelector(".spanne__alles-liste")?.checkVisibility() ?? null); + ok(zuVorher === false, `zugeklappt ist die Liste nicht zu sehen (${zuVorher})`); + + /* Der LETZTE Deckel gehoert zum August -- dem Zeitraum mit allen + 41 Spalten. Der erste haette nur fuenf und die Pruefung waere + gruen, ohne das Gemeinte gemessen zu haben. */ + await deckel[deckel.length - 1].click(); + await seite.waitForTimeout(350); + const auf = await seite.evaluate(() => { + const offen = document.querySelector(".spanne__alles[open]"); + if (!offen) return null; + const namen = [...offen.querySelectorAll(".spanne__alles-name")]; + const werte = [...offen.querySelectorAll(".spanne__alles-wert")]; + const liste = offen.querySelector(".spanne__alles-liste"); + return { + namen: namen.length, werte: werte.length, + /* SICHTBAR, nicht "hat eine Hoehe" -- siehe die Begruendung + oben. Die Hoehe steht daneben, aber nur als Auskunft. */ + sichtbar: liste.checkVisibility(), + hoehe: Math.round(liste.getBoundingClientRect().height), + erster: namen[0]?.textContent || "", + letzter: namen[namen.length - 1]?.textContent || "", + /* Steht ein Wert, den es OHNE diese Liste nirgends gaebe? */ + graduierung: namen.some((e) => e.textContent === "Graduierungsstatus") + ? werte[namen.findIndex((e) => e.textContent === "Graduierungsstatus")]?.textContent + : "", + kleinste: Math.min(...[...namen, ...werte].map((e) => + parseFloat(getComputedStyle(e).fontSize))), + }; + }); + ok(auf && auf.namen === 40 && auf.werte === 40, + `aufgeklappt stehen ${auf?.namen} Namen und ${auf?.werte} Werte da`); + ok(auf && auf.sichtbar === true, + `und sie sind jetzt wirklich sichtbar (${auf?.sichtbar}, ${auf?.hoehe} px hoch)`); + ok(auf && auf.erster === "Datenzeitraum", + `in der Reihenfolge der Datei (zuerst "${auf?.erster}")`); + ok(auf && auf.graduierung === "Nicht graduiert", + `ein Wert, den es sonst NIRGENDS gaebe, steht da ("${auf?.graduierung}")`); + ok(auf && auf.kleinste >= 11.5, + `und nichts darin ist kleiner als 11,5 px (${auf?.kleinste} px)`); + + if (process.env.SCHIRM) { + await seite.locator(".spanne__alles[open]").screenshot( + { path: join(HIER, "pruef-spannen-alles.png") }); + } + /* Wieder zu -- das Bild der Tafel oben zeigt den Normalfall, und + die Kontrastmessung danach misst den Kopf, nicht eine offene + Liste, die die Seite verschoben hat. */ + await deckel[deckel.length - 1].click(); + await seite.waitForTimeout(250); + } + /* GEGENPROBE -- und sie ist hier das Wichtigste. "Null rote Bildpunkte" koennte auch heissen, dass gar kein Foto @@ -966,8 +1261,13 @@ console.log("\n=== Die Seite ==="); `keine einzige sichtbare Null mehr (${ohneTage.nullen})`); ok(/kein einziger Tag erfasst/.test(ohneTage.satz), `sie sagt, WAS los ist ("${ohneTage.satz.slice(0, 46)}…")`); + /* MIT DEM, WAS SIE GESEHEN HAT. Diese Zeile scheiterte am + 16.09.2026 und sagte nur "nennt den Zeitraum nicht" -- wo der + Fehler lag, war daran nicht zu erkennen. Eine Pruefung, die ihren + Befund nicht zeigt, kostet genau die Zeit, die sie sparen soll. */ ok(/01\.09\./.test(ohneTage.satz) && /13 Tage/.test(ohneTage.satz), - "und nennt den Zeitraum, der stattdessen da ist"); + `und nennt den Zeitraum, der stattdessen da ist ` + + `("${ohneTage.satz.replace(/\s+/g, " ").slice(0, 150)}")`); ok(/EINEN Tag/.test(ohneTage.satz), "und den Weg, wie man die Wochenzahlen doch fuellt"); diff --git a/server/workspace-leistung.js b/server/workspace-leistung.js index 54f46c1d..a69b7409 100644 --- a/server/workspace-leistung.js +++ b/server/workspace-leistung.js @@ -252,6 +252,31 @@ leistungRouter.get("/workspace/api/leistung/:creatorId", (req, res) => { FROM leistung_zeitraum WHERE creator_id = ? ORDER BY bis DESC, von DESC LIMIT 6`).all(creatorId); + /* ALLES, WAS IN DER DATEI STAND -- IN EINER EINZIGEN ABFRAGE. + + Filipe: "wenn ich dir runter lade soll alles notiert und + angezeigt werden." Notiert allein reicht nicht; was niemand + sieht, ist so gut wie nicht da. + + EINE Abfrage fuer alle sechs Zeitraeume statt sechs einzelner -- + dieselbe Regel wie bei den Chat-Reaktionen. Sortiert nach `nr`, + also in der Reihenfolge der Datei: Backstage gruppiert seine + Spalten bereits sinnvoll (dieser Zeitraum, letzter Monat, + Prozente, Matches, Fanclub). Eine eigene Gruppierung waere eine + erfundene Ordnung, die beim naechsten Backstage-Update falsch + ist. */ + if (zeitraeume.length) { + const nach = new Map(zeitraeume.map((z) => [z.von + "|" + z.bis, z])); + for (const z of zeitraeume) z.felder = []; + for (const f of db().prepare(` + SELECT von, bis, nr, name, text, zahl + FROM leistung_zeitraum_feld WHERE creator_id = ? + ORDER BY nr`).all(creatorId)) { + nach.get(f.von + "|" + f.bis)?.felder.push( + { nr: f.nr, name: f.name, text: f.text, zahl: f.zahl }); + } + } + res.json({ zeitraeume, creator_id: creatorId, @@ -586,6 +611,59 @@ export function zahlLesen(roh) { return Number.isFinite(n) ? n : null; } +/* ===================================================================== + ALLES, WAS IN DER ZEILE STAND (16.09.2026) + + Filipe: "ich will dass alles von der excel datei genommen wird ... + wenn ich dir runter lade soll alles notiert und angezeigt werden." + + Seine echte Ausgabe hat 41 Spalten; `spaltenRaten` kennt acht davon. + Die restlichen 33 fielen beim Einlesen weg -- lautlos, ohne dass + irgendwo stand, dass es sie gab: Zahlen vom letzten Monat, fuenf + Prozentwerte, Matches, Multi-Gast-LIVEs, Fanclub, Graduierungs- und + Stufenstatus. + + DIESE FUNKTION DEUTET NICHTS. Sie nimmt Ueberschrift und Zelle, + so wie sie dastehen. Das ist Absicht: Was "Diamanten - Prozent + erreicht" genau bedeutet, weiss TikTok, nicht ich -- und eine + erfundene Bedeutung waere schlimmer als gar keine. Nachgerechnet + habe ich es trotzdem einmal (13.210 / 29.488 = 44,79 %, und dasselbe + Verhaeltnis stimmt bei allen fuenf Groessen), aber eine Herleitung + aus EINER Zeile ist kein Beweis, und sie steht deshalb nicht im Code. + + WAS UEBERSPRUNGEN WIRD, und warum genau das: + * Spalten OHNE Ueberschrift -- ohne Namen gibt es keinen + Schluessel, und "Spalte 37" ist keine Auskunft. + * DOPPELTE Ueberschriften -- die erste gilt. Der Schluessel in der + Datenbank ist der Name; zwei gleiche wuerden einander + ueberschreiben, und dann haenge es von der Reihenfolge ab, welche + gewinnt. + * LEERE Zellen -- eine leere Zelle ist kein Wert. In Filipes Datei + ist "Status" leer; eine Zeile "Status: (nichts)" waere eine + Auskunft, die die Datei gar nicht gibt. + + BEIDES WIRD BEHALTEN: der Text woertlich und, falls lesbar, die + Zahl. Nur die Zahl hiesse "Nicht graduiert" wegzuwerfen; nur den + Text hiesse, nicht rechnen zu koennen. */ +export function felderAus(kopf, zeile) { + const felder = []; + const gesehen = new Set(); + for (let i = 0; i < kopf.length; i++) { + const name = String(kopf[i] ?? "").trim(); + if (!name || gesehen.has(name)) continue; + gesehen.add(name); + const text = String(zeile[i] ?? "").trim(); + if (!text) continue; + felder.push({ + nr: i, + name: name.slice(0, 120), + text: text.slice(0, 300), + zahl: zahlLesen(text), + }); + } + return felder; +} + /** Macht aus "86Std. 10Min. 52Sek." Minuten. * * GEMESSEN AN DER ECHTEN BACKSTAGE-AUSGABE (14.09.2026): Die Spalte @@ -874,9 +952,48 @@ function handleSchluessel(roh) { * erst das Handle (eindeutig), dann der Name (mehrdeutig, aber * besser als nichts). */ function personSpaltenRaten(kopf) { - const handle = kopf.findIndex((n) => /handle|username|user.?name|tiktok.?id|@/i.test(String(n))); - const name = kopf.findIndex((n, i) => i !== handle - && /nick|anzeige|display|name|creator/i.test(String(n))); + /* AN SEINER ECHTEN AUSGABE GEMESSEN (16.09.2026) -- und sie ist hier + durchgefallen, bevor eine einzige Zahl ankam. + + "Creator_innendaten" hat DREI Spalten, die nach Person aussehen: + + Creator*in-ID 700001 + Creator*innen-Anmeldename nova_live + Agent + + Die alte Regel nahm die ERSTE, die das Wort "creator" enthaelt -- + also die ID. Danach wurde nach einem Creator namens "700001" + gesucht, und den gibt es nicht. Ergebnis: die ganze Datei + unzugeordnet, mit der Meldung "steht bei keinem Creator im Feld + TikTok". Der Hinweis war richtig formuliert und zeigte in die + voellig falsche Richtung. + + ZWEI AENDERUNGEN, und beide sind Ausschluesse, keine Zusaetze: + + 1. EINE ID IST KEIN NAME. Was wie eine Kennnummer heisst, kommt + fuer beide Spalten nicht in Frage. Eine Nummer trifft nie einen + Namen -- sie kann also nur schaden. + 2. "ANMELDENAME" IST DER HANDLE. So heisst bei TikTok der Name + mit dem @. Damit wird die richtige Spalte zum Handle, statt gar + keine zu finden. + + UND "creator" ALLEIN REICHT NICHT MEHR als Namensspalte. In + derselben Datei steht weiter hinten "Neue*r LIVE-Creator*innen" + mit dem Wert "Nein" -- die alte Regel haette nach dem Ausschluss + der ID genau dort weitergesucht und einen Creator namens "Nein" + gesucht. Ein Spaltenname zaehlt nur noch, wenn er WIRKLICH nach + Name klingt (nick, anzeige, display, name) oder fuer sich allein + "Creator" heisst -- so wie in den aelteren Ausgaben. + + KEINE BACKSLASHES IM MUSTER, mit Absicht: Diese Datei wird oefter + ueber Textwerkzeuge bearbeitet, und ein verschlucktes Zeichen in + einer Regex faellt erst auf, wenn Daten fehlen. */ + const istKennung = /(^|[^a-z])id([^a-z]|$)|nummer/i; + const handle = kopf.findIndex((n) => !istKennung.test(String(n)) + && /handle|username|user.?name|anmeldename|anmelde.?name|login|@/i.test(String(n))); + const name = kopf.findIndex((n, i) => i !== handle && !istKennung.test(String(n)) + && (/nick|anzeige|display|name/i.test(String(n)) + || /^[\s]*creator[\s*:in]*[\s]*$/i.test(String(n)))); return { handle, name }; } @@ -974,7 +1091,12 @@ function netzwerkLesen(text, person, tagVorgabe) { const werte = spanne ? { creator_id: p.id, name: p.name, von: spanne.von, bis: spanne.bis, - tage: tageImZeitraum(spanne.von, spanne.bis) } + tage: tageImZeitraum(spanne.von, spanne.bis), + /* ALLE Spalten der Zeile, nicht nur die acht erkannten. + Nur beim Zeitraum: Die Tageszeilen haben ihre eigene + Tabelle, und ein halb gebautes Feld dort waere Datenhaltung + ohne Anzeige -- also nichts, was jemand je saehe. */ + felder: felderAus(kopf, z) } : { creator_id: p.id, name: p.name, tag }; let hatZahl = false; for (const feld of messwerte) { @@ -1039,6 +1161,18 @@ leistungRouter.post("/workspace/api/leistung/netzwerk-import/vorschau", gleicheH tage: e.treffer.find((t) => t.von).tage } : null, zugeordnet: e.treffer.length, neu, ersetzt, + /* WIE VIELE SPALTEN LANDEN WIRKLICH IN DER ABLAGE? + + Filipes Sorge war genau die: "ich will dass alles von der + excel datei genommen wird." Das gehoert VOR das Bestaetigen, + nicht hinterher -- sonst muesste er der Zusage glauben, + statt die Zahl zu sehen. + + Gemessen am ERSTEN Zeitraum der Datei, nicht ueber alle + summiert: Alle Zeilen haben dieselben Spalten, eine Summe + ueber 40 Creator waere eine grosse Zahl ohne Aussage. */ + spalten_gesamt: (e.kopf || []).length, + felder: (e.treffer.find((t) => t.felder) || {}).felder?.length ?? 0, unbekannt: e.unbekannt, probleme: e.schlecht.slice(0, 12), probleme_gesamt: e.schlecht.length, creator_gesamt: e.creator_gesamt, @@ -1082,12 +1216,28 @@ leistungRouter.post("/workspace/api/leistung/netzwerk-import", gleicheHerkunft, erfasst, erfasst_von) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`); + /* ERST WEGRAEUMEN, DANN SCHREIBEN. Ohne das Loeschen bliebe eine + Spalte, die beim ersten Mal gefuellt war und beim zweiten leer + ist, mit ihrem ALTEN Wert stehen -- ein Wert aus einer Datei, + die es nicht mehr gibt, und niemand koennte ihn von einem + frischen unterscheiden. */ + const felderWeg = d.prepare(`DELETE FROM leistung_zeitraum_feld + WHERE creator_id = ? AND von = ? AND bis = ?`); + const feldSchreiben = d.prepare(`INSERT OR REPLACE INTO leistung_zeitraum_feld + (creator_id, von, bis, nr, name, text, zahl) VALUES (?,?,?,?,?,?,?)`); + let geschrieben = 0; let spannen = 0; + let felder = 0; d.exec("BEGIN"); try { for (const t of e.treffer) { if (t.von) { + felderWeg.run(t.creator_id, t.von, t.bis); + for (const f of (t.felder || [])) { + feldSchreiben.run(t.creator_id, t.von, t.bis, f.nr, f.name, f.text, f.zahl); + felder++; + } 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, @@ -1113,10 +1263,10 @@ 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} Tage, ${spannen} Zeitraeume, ` + detail: `${geschrieben} Tage, ${spannen} Zeitraeume, ${felder} Felder, ` + `${e.unbekannt.length} unbekannt`.slice(0, 120), }); - res.json({ geschrieben, zeitraeume: spannen, + res.json({ geschrieben, zeitraeume: spannen, felder, unbekannt: e.unbekannt, probleme: e.schlecht.length }); } catch (fehler) { console.error("[leistung] Netzwerk-Import:", fehler?.message); @@ -1368,6 +1518,22 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft erfasst, erfasst_von) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`); + /* UND ALLE UEBRIGEN SPALTEN -- WORTGLEICH WIE IM ANDEREN WEG. + + Es gibt zwei Wege in dieses System (eine Datei je Creator und + eine Tabelle fuer alle), und sie sind in diesem Modul schon + dreimal auseinandergelaufen: beim Zeitraumschutz, bei der + Dauer-Einheit und beim dritten Ausgang. Jedes Mal, weil eine + Sache an zwei Stellen geschrieben stand. + + Deshalb kommt die Liste der Felder hier NICHT aus einer zweiten + Rechnung, sondern aus derselben Funktion `felderAus`. */ + const felderWeg = d.prepare(`DELETE FROM leistung_zeitraum_feld + WHERE creator_id = ? AND von = ? AND bis = ?`); + const feldSchreiben = d.prepare(`INSERT OR REPLACE INTO leistung_zeitraum_feld + (creator_id, von, bis, nr, name, text, zahl) VALUES (?,?,?,?,?,?,?)`); + let felder = 0; + let geschrieben = 0, uebersprungen = 0, spannen = 0; d.exec("BEGIN"); try { @@ -1391,6 +1557,11 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft wz("zuschauer_avg"), wz("zuschauer_max"), wz("verweildauer_s"), wz("schenker"), wz("follower_neu"), jetzt(), req.person.id); + felderWeg.run(creatorId, von, bis); + for (const f of felderAus(zeilen[0], z)) { + feldSchreiben.run(creatorId, von, bis, f.nr, f.name, f.text, f.zahl); + felder++; + } spannen++; continue; } @@ -1420,9 +1591,9 @@ leistungRouter.post("/workspace/api/leistung/:creatorId/import", gleicheHerkunft protokolliere("leistung_import", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), - detail: `Creator #${creatorId}: ${geschrieben} Tage`, + detail: `Creator #${creatorId}: ${geschrieben} Tage, ${spannen} Zeitraeume, ${felder} Felder`, }); - res.json({ ok: true, geschrieben, zeitraeume: spannen, uebersprungen }); + res.json({ ok: true, geschrieben, zeitraeume: spannen, felder, uebersprungen }); } catch (fehler) { console.error("[leistung] Import:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); diff --git a/server/workspace.js b/server/workspace.js index 0633e7c9..5789560a 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -3415,6 +3415,52 @@ export function db() { ON leistung_zeitraum (creator_id, bis DESC); CREATE INDEX IF NOT EXISTS idx_leistung_tag ON leistung (tag DESC); + /* ALLES, WAS IN DER DATEI STAND (16.09.2026) + + Filipe: "ich will dass alles von der excel datei genommen wird + ... wenn ich dir runter lade soll alles notiert und angezeigt + werden." + + Seine echte Backstage-Ausgabe hat 41 SPALTEN. Die Tabelle + darueber nimmt acht davon. Dreiunddreissig Spalten -- Zahlen + vom letzten Monat, Prozentwerte, Matches, Multi-Gast-LIVEs, + Fanclub, Graduierungs- und Stufenstatus -- wurden beim Einlesen + weggeworfen, ohne dass irgendwo stand, dass es sie gab. + + WARUM NICHT 33 WEITERE SPALTEN OBEN? Weil das eine ABGESCHRIEBENE + LISTE waere, und die hat in diesem Haus schon zweimal Daten + gekostet (siehe die Notiz zur Agentur-Umstellung). Sie waere + ausserdem beim naechsten Backstage-Update falsch: TikTok + benennt Spalten um und fuegt welche hinzu, ohne jemanden zu + fragen. + + EINE ZEILE JE SPALTE, und der NAME ist der Schluessel. Damit + gilt: Was in der Datei steht, steht danach in der Datenbank -- + auch eine Spalte, die es heute noch gar nicht gibt. Nichts zu + pflegen, nichts zu vergessen. + + Die Spalte nr IST DIE REIHENFOLGE DER DATEI, nicht die Identitaet. Wer + die Reihenfolge zum Schluessel machte, verwechselte zwei + Spalten, sobald Backstage eine dazwischenschiebt. + + BEIDES WIRD GESPEICHERT: der Text woertlich, wie er dastand + (daraus kann man immer noch alles herleiten), und die Zahl, wenn + es eine ist. Nur die Zahl zu behalten hiesse, "Nicht graduiert" + und "6Std. 24Min. 7Sek." zu verlieren; nur den Text zu behalten + hiesse, nicht rechnen zu koennen. */ + CREATE TABLE IF NOT EXISTS leistung_zeitraum_feld ( + creator_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, + von TEXT NOT NULL, + bis TEXT NOT NULL, + nr INTEGER NOT NULL, + name TEXT NOT NULL, + text TEXT, + zahl REAL, + PRIMARY KEY (creator_id, von, bis, name) + ); + CREATE INDEX IF NOT EXISTS idx_leistung_zfeld + ON leistung_zeitraum_feld (creator_id, von, bis, nr); + /* ZIELE je Creator. Aus dem 90-Tage-Plan, den es im Profil schon als FREITEXT gibt -- daraus wird hier eine pruefbare Groesse. "Mehr Reichweite" ist kein Ziel, "4 LIVE-Tage pro Woche" ist diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 780bc775..7d8274e8 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=202609160059", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609160146", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609160059", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609160146", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609160059", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609160146", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609160059", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609160146", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/leistung.css b/workspace/assets/css/leistung.css index b1b335a4..1dfe9d07 100644 --- a/workspace/assets/css/leistung.css +++ b/workspace/assets/css/leistung.css @@ -779,6 +779,69 @@ font-variant-numeric: tabular-nums; } +/* ---------- Alles, was in der Datei stand ------------------------------ */ +/* Filipe: "wenn ich dir runter lade soll alles notiert und angezeigt + werden." Achtunddreissig Werte je Zeitraum -- offen waeren sie eine + Wand, weg waeren sie eine Luecke. Also ein Deckel, der die ZAHL + nennt: Man sieht ohne Oeffnen, dass nichts fehlt. + + ZWEI SPALTEN, NICHT ZWEI ZEILEN. Name links, Wert rechts, mit einer + gepunkteten Fuehrung dazwischen -- so liest man 38 Paare von oben + nach unten, ohne die Zeile zu verlieren. Untereinander waeren es 76 + Zeilen. */ +.spanne__alles { + margin-top: 12px; + border-top: 1px solid rgba(150, 186, 220, .13); + padding-top: 10px; +} +.spanne__alles-kopf { + cursor: pointer; + list-style: none; + display: flex; align-items: center; gap: 7px; + font-size: .76rem; letter-spacing: .04em; + color: var(--text-leise); +} +.spanne__alles-kopf::-webkit-details-marker { display: none; } +/* Ein eigener Pfeil statt des Standarddreiecks -- das sieht auf jedem + Gerät anders aus, und hier soll alles gleich aussehen. */ +.spanne__alles-kopf::before { + content: ""; + width: 7px; height: 7px; flex: none; + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + transform: rotate(-45deg); + transition: transform var(--tempo, .2s); +} +.spanne__alles[open] > .spanne__alles-kopf::before { transform: rotate(45deg); } +.spanne__alles-kopf:hover { color: var(--text); } + +.spanne__alles-liste { + margin: 10px 0 0; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0 12px; + align-items: baseline; +} +.spanne__alles-name { + margin: 0; padding: 4px 0; + font-size: .74rem; color: var(--text-still); + min-width: 0; + /* Die gepunktete Fuehrung liegt auf dem NAMEN, nicht als eigenes + Element: ein Unterrand, der genau so breit ist wie der Text. */ + border-bottom: 1px dotted rgba(150, 186, 220, .16); +} +.spanne__alles-wert { + margin: 0; padding: 4px 0; + font-size: .78rem; color: var(--text); + text-align: right; white-space: nowrap; + font-variant-numeric: tabular-nums; + border-bottom: 1px dotted rgba(150, 186, 220, .16); +} + +@media (prefers-reduced-motion: reduce) { + .spanne__alles-kopf::before { transition: none; } +} + @media (max-width: 620px) { .spannen-tafel { --sp-fase: 12px; } .spannen-tafel__kopf { padding: 12px 14px; } diff --git a/workspace/assets/js/leistung.js b/workspace/assets/js/leistung.js index ec49bc6c..6abcbe9e 100644 --- a/workspace/assets/js/leistung.js +++ b/workspace/assets/js/leistung.js @@ -633,6 +633,7 @@ 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'}`); + if (r.felder) teile.push(`${r.felder} Werte aus der Datei`); melde(teile.length ? `${teile.join(' und ')} übernommen.` : 'Nichts zu übernehmen.'); setTimeout(() => melde(''), 5000); } catch { /* umgeleitet */ } finally { knopf.disabled = false; } @@ -746,6 +747,38 @@ zeig('Neue Follower', z.follower_neu, zahl); if (werte.children.length) karte.append(werte); + /* ---- UND ALLES ANDERE, WAS IN DER DATEI STAND (16.09.2026) ---- + Filipe: "ich will dass alles von der excel datei genommen wird. + ... wenn ich dir runter lade soll alles notiert und angezeigt + werden." + + Seine Ausgabe hat 41 Spalten; oben stehen die sechs, die das + System deutet. Der Rest -- letzter Monat, fünf Prozentwerte, + Matches, Multi-Gast-LIVEs, Fanclub, Graduierungs- und + Stufenstatus -- steht hier, WÖRTLICH wie in der Datei. + + IN DER REIHENFOLGE DER DATEI, nicht in einer eigenen: Backstage + gruppiert seine Spalten schon sinnvoll, und jede Ordnung, die + ich erfinde, ist beim nächsten Backstage-Update falsch. + + ZUGEKLAPPT, ABER MIT DER ZAHL IM DECKEL. Achtunddreißig Zeilen + offen unter jedem Zeitraum wären die Karte nicht mehr lesbar -- + und genau die Lesbarkeit war gestern sein Thema. Im Deckel + steht, wie viele es sind; damit sieht man ohne Öffnen, dass + nichts fehlt. */ + if (Array.isArray(z.felder) && z.felder.length) { + const alles = el('details', 'spanne__alles'); + alles.append(el('summary', 'spanne__alles-kopf', + `Alle ${z.felder.length} Werte aus Backstage`)); + const liste = el('dl', 'spanne__alles-liste'); + for (const f of z.felder) { + liste.append(el('dt', 'spanne__alles-name', f.name)); + liste.append(el('dd', 'spanne__alles-wert', f.text)); + } + alles.append(liste); + karte.append(alles); + } + ziel.append(karte); } } @@ -873,6 +906,20 @@ zeile(`Erkannte Spalten: ${Object.keys(d.spalten).join(', ') || 'keine'}` + (d.aus_tabelle ? ' · Datum aus der Tabelle' : ' · Datum aus dem Feld oben')); + /* WAS MIT DEM REST PASSIERT -- und das ist seit dem 16.09. die + Zeile, auf die es Filipe ankommt: "ich will dass alles von der + excel datei genommen wird." + + Die Zeile darueber nennt nur die ACHT Spalten, die das System + deutet. Seine echte Ausgabe hat 41. Ohne diese Zeile sieht es + aus, als fielen 33 davon weg -- sie werden aber vollstaendig + mitgeschrieben und stehen danach auf der Karte. */ + if (d.spalten_gesamt) { + zeile(`Alle ${d.spalten_gesamt} Spalten der Datei werden übernommen` + + (d.felder ? ` · ${d.felder} davon tragen einen Wert` : '') + + ' · sie stehen danach unter „Alle Werte aus Backstage“', 'gut'); + } + /* DIE UNBEKANNTEN SIND DER WICHTIGE TEIL. Wer sie übersieht, hat stillschweigend Leute ausgelassen -- und merkt es erst, wenn die Zahlen nicht stimmen. Deshalb stehen sie mit Namen da und nicht @@ -889,7 +936,10 @@ } for (const b of d.beispiele) { const zahlen = Object.entries(b) - .filter(([k]) => !['creator_id', 'name', 'tag'].includes(k)) + /* `felder` ist eine LISTE und gehoert nicht in diese Zeile -- + ohne diesen Ausschluss stand hier woertlich + "felder [object Object],[object Object]". */ + .filter(([k]) => !['creator_id', 'name', 'tag', 'felder'].includes(k)) .filter(([, w]) => w !== null && w !== undefined) .map(([k, w]) => `${k} ${w}`).join(' · '); zeile(`${b.name} · ${b.tag} · ${zahlen}`, 'still'); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 2bb833a0..43260a6f 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. --> - - - - - - + + + + + + - + @@ -282,10 +282,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 21e3f2ea..813f0fee 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. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/befinden.html b/workspace/befinden.html index 8d777661..a0cb0525 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 12132c98..264fc074 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 4e03eb2a..ba4c43ad 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 112b5d31..7c8de7d4 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 a5acf6e4..6860740c 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 e522dc51..6d4eaadd 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 61e6c25d..881681bc 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=202609160059", + "src": "/assets/img/app-symbole/crew-192.png?v=202609160146", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609160059", + "src": "/assets/img/app-symbole/crew-512.png?v=202609160146", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609160059", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609160146", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609160059", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609160146", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 7f39f524..67401beb 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 294cff03..10738752 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -102,9 +102,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 0c01645c..81a97679 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 40e06277..67b55e24 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 bd82d185..59eaf0d6 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. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index beb8e9b1..497c728f 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 151be0e8..9ab15347 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index b8b3ecb7..a4b4247d 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index bc823e16..13c9bf94 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 33134d47..87206890 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 05d736a6..4cf15668 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index e50772ad..b4821b9a 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 23be98d9..6ac04f96 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index 2dbacdc3..8421bcf4 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index 6127f0a3..eb427b3f 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 6bd5107f..2b64a75a 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index a3959018..05c97662 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -110,9 +110,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index c35d15c9..d83bcfca 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -210,10 +210,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index ebebcc4a..a50fa056 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index fa555d2c..9e305b8c 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +