From cf6c72b3d8600699750a746bf08cc69455648921 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sun, 27 Sep 2026 18:34:10 +0200 Subject: [PATCH] Beim Laden stand "SPICY MEDIA - Zentrale" auf jeder Startseite WAS DAS BILD GEZEIGT HAT Beim Durchsehen der Einzelbilder einer Videoaufnahme -- vier Sekunden lang, gleich nach dem Laden: ---- SPICY MEDIA ---- Zentrale WIRD GELADEN Und zwar auf der Startseite eines COMMUNITY-MITGLIEDS. In start.html steht die Vorgabe des Agenturhauses. Das Teamhaus bekommt eigene Worte ("Team Dogi" / "Die IrrenAnstalt"), aber erst wenn /api/ich geantwortet hat. Bis dahin las jeder Modi und jedes Community-Mitglied die Marke der Agentur in der groessten Schrift der Seite. Auf einem langsamen Telefon sekundenlang, bei jedem Aufruf. Das ist kein Schoenheitsfehler. Spicy Media ist der Betrieb hinter der Agentur, und seit dem 24.09. sind die beiden Haeuser getrennt. Genau diese Zeile hat die Trennung bei jedem Seitenaufruf kurz aufgehoben -- und niemandem ist es aufgefallen, weil die Seite DANACH richtig aussah. Wer hinsieht, sieht den Endzustand. GELOEST OHNE SPRUNG `data-wartet` macht die zwei Zeilen durchsichtig, nicht leer -- der Platz bleibt stehen, es ruckt nichts. start.js nimmt das Merkmal weg, sobald es weiss, in welchem Haus es ist; gemessen dauert das 351 ms. Eine Notbremse in der Seite nimmt es nach vier Sekunden notfalls selbst weg. Ohne sie waere die groesste Schrift der Seite fuer immer unsichtbar, falls start.js gar nicht erst laeuft -- und "unsichtbar" waere schlimmer als das falsche Wort. DIE PRUEFUNG DAZU -- UND ZWEI MESSFEHLER DARIN pruef-start-ansicht misst ab jetzt beide Richtungen: nach dem Laden muessen beide Zeilen sichtbar sein, mit gesetztem `data-wartet` unsichtbar. Ohne die zweite Haelfte koennte die Regel spurlos verschwinden, ohne dass etwas rot wird. Die Pruefung selbst hat mich zweimal getaeuscht, und beide Male lehrreich: 1. Sie mass 900 ms nach der Anmeldung -- eine feste Pause. Ob /api/ich in dieser Zeit geantwortet hat, haengt vom Rechner ab. Dieselbe Pruefung war einmal gruen und einmal rot, bei unveraendertem Code. Gewartet wird jetzt auf das Merkmal selbst, und wie lange es gedauert hat, steht im Meldetext. 2. `opacity` hat einen Uebergang von 180 ms, und getComputedStyle liefert waehrenddessen den laufenden Zwischenwert statt des Ziels. Erst meldete die Gegenprobe "nicht verdeckt", dann die Messung davor "nicht sichtbar" -- beide Male war die Regel in Ordnung und nur die Animation im Weg. Fuer die Messung wird der Uebergang jetzt abgeschaltet. DAS VIDEOWERKZEUG server/tiktok-videos.mjs nimmt die vier Clips auf. Sechs Aenderungen, jede aus einem Einzelbild: - DIE NACHTRUHE HAT DIE VORBEREITUNG MITBLOCKIERT. Video 2 zeigte einen leeren Chat. Gemeldet wurde "gefuellt", weil nur geprueft war, dass es den RAUM gibt. Jetzt werden die Nachrichten zurueckgelesen und gezaehlt; unter acht wird nicht gefilmt. - DOGI-MEDIA WAR LEER -- ein Video ueber Material zum Mitnehmen, und auf dem Bildschirm stand "Gerade ist nichts frei". Sechs echte Marken-Bilder werden eingestellt, zwei davon schon genommen, damit die Aussage des Films auch im Bild steht. - "WILL ICH AUCH - 0" an jedem Wunsch, waehrend der Untertitel sagte "die anderen sehen, wer mitwill". Ein Versprechen, das das Bild gleich wieder einkassiert, ist schlimmer als ein leeres Brett. Jetzt wird ueber die echte Route gestimmt, absteigend 6/5/4 -- unter zehn Stimmen wird nicht gefilmt. - DER WAECHTER FUER VIDEO 2 verglich zwei feste Zahlen (TREFF_NACHT_AB === "0"). Das ist die Abschrift einer Einstellung und keine Frage nach dem Zustand: Jedes andere Fenster, das den Chat genauso schlafen legt, wurde abgewiesen. Gefragt wird jetzt `istNachtruhe()` -- dieselbe Funktion, die auch die Seite befragt. Mit 12 bis 6 steht im Bild "Ab 06:00 Uhr geht es weiter" statt "Ab 24:00 Uhr", und das ergibt fuer einen Zuschauer ueberhaupt erst Sinn. - ROLLEN IM KASTEN STATT IN DER SEITE. `window.scrollBy` bewegt die Seite; im Chat rollt aber der Verlauf in seinem eigenen Kasten. Drei Bilder hintereinander sahen gleich aus. - EINE FESTE PIXELZAHL TRAF DEN FALSCHEN BLOCK. Video 4 filmte den Katalog der fertigen Vorschlaege statt der Wuensche, weil "480 nach unten" zufaellig dort endete. `b.zu(wahl)` misst, wo das Element steht. Dazu: Umlaute in allen Bildtexten ("Tueren" stand im Video), der Abspann bleibt am Ende stehen (vorher sah man die letzte Sekunde wieder die App), und die Dateigroessen im Regal sind echt. KEIN LINK, UND ZWAR GEMESSEN Nach jedem Seitenwechsel wird der SICHTBARE Text nach Adressen abgesucht -- dogfather-universe, https://, www., jeder Domainname. Bei einem Fund bricht die Aufnahme ab, und es wird keine Datei geschrieben. Das mit dem Auge zu pruefen waere genau die Sorte Kontrolle, die beim vierten Video nachlaesst. Mit PROBE_LECK=ja schiebt die Suche selbst eine Adresse ins Bild -- nachgefahren, Rueckgabewert 2. Eine Suche, die nur "nichts gefunden" sagen kann, hat nichts bewiesen. Gemessen: pruef-start-ansicht 160/0 (vorher 157), pruef-buehne 242/0, pruef-handy 177/0, pruef-kachelraster 24/0, pruef-css-klassen 33/0. --- server/pruef-start-ansicht.mjs | 65 ++++++++ server/tiktok-videos.mjs | 259 +++++++++++++++++++++++++++----- tools/.commit-video.txt | 114 ++++++++++++++ workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/start.css | 25 +++ workspace/assets/js/start.js | 15 ++ workspace/aufgaben.html | 38 ++--- workspace/automation.html | 32 ++-- workspace/befinden.html | 34 ++--- workspace/bereich.html | 44 +++--- workspace/bewerben.html | 38 ++--- workspace/bewerbungen.html | 36 ++--- workspace/calls.html | 34 ++--- workspace/chat.html | 40 ++--- workspace/content.html | 34 ++--- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 ++--- workspace/entwicklung.html | 36 ++--- workspace/hilfe.html | 30 ++-- workspace/index.html | 10 +- workspace/kalender.html | 34 ++--- workspace/leistung.html | 32 ++-- workspace/material.html | 32 ++-- workspace/notizen.html | 28 ++-- workspace/personen.html | 32 ++-- workspace/profil.html | 36 ++--- workspace/rechte.html | 34 ++--- workspace/report.html | 36 ++--- workspace/scouting.html | 32 ++-- workspace/start.html | 68 ++++++--- workspace/startcheck.html | 34 ++--- workspace/steckbrief.html | 36 ++--- workspace/support.html | 28 ++-- workspace/talente.html | 36 ++--- workspace/team.html | 32 ++-- workspace/teamlage.html | 34 ++--- workspace/teilen.html | 30 ++-- workspace/treff-moderation.html | 32 ++-- workspace/treff-regeln.html | 32 ++-- workspace/uebersicht.html | 32 ++-- workspace/unsere-seiten.html | 30 ++-- workspace/unterstuetzen.html | 32 ++-- workspace/werdegang.html | 32 ++-- workspace/willkommen.html | 30 ++-- workspace/wissen.html | 34 ++--- 47 files changed, 1118 insertions(+), 686 deletions(-) create mode 100644 tools/.commit-video.txt diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index b2a09ea9..134fbfb7 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -1163,6 +1163,71 @@ console.log("\n=== Ohne feinen Zeiger bleibt es aus"); await kontext.close(); } +/* ======================================================================= + MARKE UND TITEL VERRATEN NICHT DAS FALSCHE HAUS (27.09.2026) + + In start.html steht die Vorgabe des AGENTURHAUSES: "Spicy Media" und + "Zentrale". Das Teamhaus bekommt eigene Worte -- aber erst, wenn + /api/ich geantwortet hat. Bis dahin stand bei jedem Modi und jedem + Community-Mitglied die Marke der Agentur in der groessten Schrift + der Seite. + + Gefunden hat das keine Pruefung, sondern ein Einzelbild aus einer + Videoaufnahme -- die Seite sah danach ja richtig aus, und genau so + lange hat niemand hingesehen. Deshalb steht es ab jetzt hier. + + GEMESSEN WIRD IN BEIDE RICHTUNGEN: Am Ende muessen beide Zeilen + sichtbar sein (sonst haette die Reparatur die groesste Schrift der + Seite dauerhaft weggenommen), und mit gesetztem `data-wartet` + muessen sie unsichtbar sein (sonst tut die Regel gar nichts, und + die Pruefung darueber waere wertlos). */ +console.log("\n=== Marke und Titel warten, bis das Haus bekannt ist"); +{ + const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } }); + const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001"); + /* Gewartet wird auf das Merkmal, nicht auf die Uhr: Wie lange + /api/ich braucht, haengt vom Rechner ab. Mit einer festen Pause + war dieselbe Pruefung mal gruen und mal rot, ohne dass sich + etwas geaendert hatte. */ + const t0 = Date.now(); + await seite.waitForFunction( + () => !document.querySelector(".zmarke[data-wartet], #ztitel[data-wartet]"), + null, { timeout: 5000 }).catch(() => {}); + const dauerte = Date.now() - t0; + const r = await seite.evaluate(() => { + const m = document.querySelector(".zmarke"); + const t = document.getElementById("ztitel"); + const lies = (e) => (e ? Number(getComputedStyle(e).opacity) : -1); + /* OHNE UEBERGANG MESSEN, UND ZWAR VOR DER ERSTEN ABLESUNG. + `opacity` blendet in 180 ms, und getComputedStyle liefert + waehrenddessen den laufenden Zwischenwert. Das hat diese + Pruefung zweimal getaeuscht: einmal meldete die Gegenprobe + "nicht verdeckt", einmal die Messung davor "nicht sichtbar" -- + beide Male war die Regel in Ordnung und nur die Uhr im Weg. + Ohne Uebergang steht sofort der Zielwert da. */ + for (const e of [m, t]) if (e) e.style.transition = "none"; + const jetzt = { marke: lies(m), titel: lies(t) }; + m?.setAttribute("data-wartet", "ja"); + t?.setAttribute("data-wartet", "ja"); + const gedeckt = { marke: lies(m), titel: lies(t) }; + m?.removeAttribute("data-wartet"); + t?.removeAttribute("data-wartet"); + for (const e of [m, t]) if (e) e.style.transition = ""; + return { jetzt, gedeckt, breit: (t?.getBoundingClientRect().width || 0) }; + }); + ok(r.jetzt.marke === 1 && r.jetzt.titel === 1, + "nach dem Laden sind beide sichtbar " + + `(Marke ${r.jetzt.marke}, Titel ${r.jetzt.titel}, nach ${dauerte} ms)`); + ok(r.gedeckt.marke === 0 && r.gedeckt.titel === 0, + "Gegenprobe: mit data-wartet sind beide verdeckt " + + `(Marke ${r.gedeckt.marke}, Titel ${r.gedeckt.titel})`); + /* Der Platz muss auch im Wartezustand stehen -- sonst springt die + halbe Kachel, sobald das Wort ankommt. `opacity` tut das, + `display: none` taete es nicht. */ + ok(r.breit > 40, `der Titel nimmt Platz ein und rueckt nichts (${Math.round(r.breit)} px)`); + await kontext.close(); +} + await browser.close(); console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } diff --git a/server/tiktok-videos.mjs b/server/tiktok-videos.mjs index d4e81c59..45f523a2 100644 --- a/server/tiktok-videos.mjs +++ b/server/tiktok-videos.mjs @@ -26,7 +26,7 @@ node server/tiktok-videos.mjs alle vier node server/tiktok-videos.mjs 2 nur das zweite Für Video 2 (die Nachtruhe) muss der Chat schlafen: - TREFF_NACHT_AB=0 TREFF_NACHT_BIS=24 node server/tiktok-videos.mjs 2 + TREFF_NACHT_AB=12 TREFF_NACHT_BIS=6 node server/tiktok-videos.mjs 2 Das Ergebnis liegt in videos/ und ist git-ignoriert. @@ -143,6 +143,7 @@ function roh(pfad, { method = "GET", keks = "", rumpf = null } = {}) { /* ---------- Leute anlegen --------------------------------------------- Erfundene Namen, erfundene Beiträge. Nichts davon stammt aus dem echten Haus, und nichts davon geht dorthin zurück. */ +const { istNachtruhe } = await import("./workspace-treffchat.js"); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); @@ -233,7 +234,7 @@ console.log("Fuelle die Bretter …"); soll, muss vorher drei Wuensche geschrieben haben -- sonst gilt dieselbe Sperre wie beim ersten Versuch. */ await eintrag("DogFather", "anschlag", "ansage", - "Freitag 20 Uhr — der grosse Stream", "Bringt Zeit mit. Es gibt was zu feiern."); + "Freitag 20 Uhr — der große Stream", "Bringt Zeit mit. Es gibt was zu feiern."); await eintrag("DogFather", "anschlag", "regel", "Keine Handynummern im Chat", "Auch nicht privat. Wir passen hier aufeinander auf."); await eintrag("VanVan", "anschlag", "danke", @@ -243,7 +244,7 @@ await eintrag("DogFather", "ansteht", "stream", "Freitag 20:00 — Rudel-Abend", "Der lange. Mit allem."); await eintrag("DogFather", "ansteht", "event", "Sonntag 18:00 — Fragerunde", "Ihr fragt, ich antworte. Ohne Filter."); -await eintrag("Miss Kira", "ansteht", "turnier", "Mittwoch — Turnier, 16 Plaetze", +await eintrag("Miss Kira", "ansteht", "turnier", "Mittwoch — Turnier, 16 Plätze", "Anmelden im Chat. Wer zuerst kommt."); await eintrag("Mo", "wunsch", "stream", "Nochmal die Horror-Nacht", @@ -252,33 +253,66 @@ await eintrag("Mo", "wunsch", "gemeinsam", "Gemeinsames Kochen im Live", "Jeder kocht mit, am Ende zeigen alle den Teller."); await eintrag("Mo", "wunsch", "technik", "Bitte eine zweite Kamera", "Man sieht Casper nie. Casper ist wichtig."); -await eintrag("Nachtfalter", "wunsch", "stream", "Rueckblick auf das erste Jahr", +await eintrag("Nachtfalter", "wunsch", "stream", "Rückblick auf das erste Jahr", "Mit den alten Clips. Ich heul dann halt."); await eintrag("Nachtfalter", "wunsch", "gemeinsam", "Spieleabend mit dem Rudel", "Zehn Leute, ein Spiel, offene Kamera."); -await eintrag("Nachtfalter", "wunsch", "sonstiges", "Merch in Groesse XXL", - "Ich bin gross. Wir sind mehr, als ihr denkt."); +await eintrag("Nachtfalter", "wunsch", "sonstiges", "Merch in Größe XXL", + "Ich bin groß. Wir sind mehr, als ihr denkt."); await eintrag("Sanja", "wunsch", "stream", "Ein Stream nur mit Musik", "Einfach reden und Platten auflegen."); -await eintrag("Sanja", "wunsch", "technik", "Untertitel fuer die Aufzeichnungen", - "Meine Schwester hoert schlecht und schaut trotzdem jede Folge."); +await eintrag("Sanja", "wunsch", "technik", "Untertitel für die Aufzeichnungen", + "Meine Schwester hört schlecht und schaut trotzdem jede Folge."); await eintrag("Sanja", "wunsch", "gemeinsam", "Rudel-Treffen in echt", "Einmal im Jahr. Irgendwo in der Mitte."); +/* ---------- „Will ich auch" --------------------------------------------- + + Video 4 sagt: „Und die anderen sehen, wer mitwill." Im ersten Lauf + stand an jedem Wunsch „Will ich auch · 0" -- der Untertitel + behauptete etwas, das auf dem Bildschirm widerlegt wurde. Das ist + schlimmer als ein leeres Brett: Es ist ein Versprechen, das das + Bild gleich wieder einkassiert. + + Gestimmt wird ueber die echte Route, mit echten Zugaengen. Wie + viele mitwollen, ist je Wunsch verschieden -- alle gleich waere + sichtbar gebaut. */ +{ + const brett = await roh("/workspace/api/bereich/wunsch", { keks: keks.DogFather }); + const liste = brett.j?.eintraege || brett.j?.liste || []; + const WAEHLER = ["Nachtfalter", "Lenny", "Sanja", "Tobi", "Mo", "Elif", "Miss Kira"]; + let stimmen = 0; + for (const [i, e] of liste.entries()) { + /* 6, 5, 4 … absteigend: Die Liste sortiert nach Zuspruch, und eine + Reihenfolge, die man versteht, sieht echter aus als Zufall. */ + const wie_viele = Math.max(0, 6 - i); + for (const wer of WAEHLER.slice(0, wie_viele)) { + const a = await roh(`/workspace/api/treff/eintrag/${e.id}/willich`, + { method: "PUT", keks: keks[wer], rumpf: {} }); + if (a.code === 200) stimmen++; + } + } + console.log(`Will ich auch: ${stimmen} Stimmen auf ${liste.length} Wuensche`); + if (stimmen < 10) { + console.error("Zu wenige Stimmen -- der Untertitel wuerde luegen. Es wird nicht gefilmt."); + process.exit(7); + } +} + /* Jetzt reichen die drei fuer „Stamm" -- und duerfen Highlights. */ await eintrag("Mo", "highlight", "clip", "Der Moment, als der Ton wegflog", "Zwanzig Sekunden Stille und dann alle am Lachen."); await eintrag("Nachtfalter", "highlight", "moment", "1000 Follower — live erlebt", - "Wir haben alle mitgezaehlt."); + "Wir haben alle mitgezählt."); await eintrag("Sanja", "highlight", "fanart", "Gezeichnet: Casper", "Zwei Abende gebraucht. Er guckt genau so."); await eintrag("DogFather", "mitmachen", "gesucht", "Wir suchen zwei Modis", - "Du musst nichts koennen. Du musst da sein wollen."); -await eintrag("Mo", "mitmachen", "lust", "Ich haette Lust auf Moderation", + "Du musst nichts können. Du musst da sein wollen."); +await eintrag("Mo", "mitmachen", "lust", "Ich hätte Lust auf Moderation", "Bin fast jeden Abend da. Traue mich langsam."); await eintrag("Sanja", "mitmachen", "frage", "Wie viel Zeit kostet das?", - "Ich wuerde gern, hab aber Schichtdienst."); + "Ich würde gern, hab aber Schichtdienst."); if (schiefgegangen) { console.error(`${schiefgegangen} Beitraege sind nicht entstanden. Es wird nicht gefilmt.`); @@ -303,15 +337,103 @@ const CHAT = [ ["Tobi", "Kann jemand den Link zur Fragerunde... ach nee, steht ja unter Was ansteht"], ["Nachtfalter", "Genau deswegen liebe ich das hier"], ]; +let imChat = 0; +let imRegal = 0; if (treffRaum) { - for (const [wer, text] of CHAT) { - await roh(`/workspace/api/chat/raeume/${treffRaum}/nachrichten`, { - method: "POST", keks: keks[wer], rumpf: { text }, - }); - await new Promise((r) => setTimeout(r, 60)); - } + /* GESCHRIEBEN WIRD AN DER SCHNITTSTELLE VORBEI -- UND ZWAR ABSICHTLICH + (27.09.2026). + + Fuer Video 2 laeuft die Nachtruhe. Sie verbietet das Schreiben + ALLEN, auch dieser Vorbereitung -- der erste Lauf hat deshalb + einen leeren Chat gefilmt. Ein Sonderweg in der Nachtruhe waere + genau das Loch, das der Film bewirbt; also wird hier nichts + aufgeweicht, sondern die Buehne direkt aufgebaut. Dieselbe Sorte + Eingriff wie das Anlegen der Statisten ein paar Zeilen weiter + oben, und ebenfalls nur in der Wegwerf-Datenbank. + + Dass es angekommen ist, wird gleich danach an der Schnittstelle + NACHGELESEN -- lesen darf man auch nachts. */ + const db = new DatabaseSync(process.env.WORKSPACE_DB); + const wannBasis = Date.now() - CHAT.length * 90_000; + let letzte = null; + CHAT.forEach(([wer, text], i) => { + const wann = new Date(wannBasis + i * 90_000).toISOString(); + db.prepare("INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)") + .run(treffRaum, ID[wer], text, wann); + letzte = wann; + }); + db.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(letzte, treffRaum); + db.close(); + /* NACHGEZAEHLT STATT ANGENOMMEN (27.09.2026). + Der erste Lauf von Video 2 hat einen LEEREN Chat gefilmt. Die + Nachtruhe war fuer diesen Film eingeschaltet -- und sie verbietet + nicht nur den Zuschauern das Schreiben, sondern auch dieser + Vorbereitung. Geschrieben wurde nichts, gemeldet wurde + „gefuellt", weil nur geprueft war, dass es den Raum GIBT. + Gelesen werden darf immer; deshalb faellt es hier auf. */ + const nach = await roh(`/workspace/api/chat/raeume/${treffRaum}/nachrichten`, + { keks: keks.Nachtfalter }); + imChat = (nach.j?.nachrichten || nach.j?.liste || []).length; +} +console.log(`Chat: ${treffRaum ? `${imChat} von ${CHAT.length} Nachrichten` : "Raum nicht gefunden"}`); + +/* ---------- Dogi-Media --------------------------------------------------- + + Video 3 handelt von Material zum Mitnehmen. Der erste Lauf filmte + "Gerade ist nichts frei. Schau spaeter noch einmal rein." -- eine + leere Seite, drei mal vier Sekunden lang. Das Brett war gefuellt, + das Medienregal nicht, und niemand hatte danach gefragt. + + Eingestellt wird an der Schnittstelle vorbei, aus demselben Grund + wie beim Chat: Das Hochladen erwartet einen Rumpf aus Bytes mit + passendem Typ, und die Dateien liegen ohnehin schon im Haus. Die + Namen sind echte Marken-Bilder -- kein Platzhalter, der im Video + wie ein Platzhalter aussieht. */ +{ + const { mkdirSync: mk, copyFileSync, statSync } = await import("node:fs"); + const mOrdner = join(ordner, "material"); + mk(mOrdner, { recursive: true }); + const STUECKE = [ + ["dogfather-husky.png", "Dogi-Kopf freigestellt", "Für Storys und Kommentare. Hintergrund frei."], + ["hasidog-hase.png", "Hasi Dog freigestellt", "Passt zu allem, was mit dem Rudel zu tun hat."], + ["lounge.png", "Lounge als Hintergrund", "Für eigene Clips, hochkant beschnitten."], + ["logo.png", "Team-Dogi-Logo", "Wenn du das Rudel erwähnst."], + ["gate-spicy-dogfather.jpeg", "Studio-Standbild", "Aus dem letzten Stream. Freigegeben zum Posten."], + ["logo-kopf-frei.png", "Kopf klein für die Ecke", "Klein, transparent, für die Bildecke."], + ]; + /* DIE TABELLE GIBT ES ERST NACH DEM ERSTEN ZUGRIFF. Material legt + sein Schema traege an -- ein direktes INSERT davor scheitert mit + "no such table: material". Ein Abruf reicht, um sie zu bauen. */ + await roh("/workspace/api/material", { keks: keks.DogFather }); + const db2 = new DatabaseSync(process.env.WORKSPACE_DB); + let n = 0; + STUECKE.forEach(([quelle, name, text], i) => { + const von = join(WURZEL, "assets", "img", "marke", quelle); + const endung = quelle.slice(quelle.lastIndexOf(".")); + const datei = `tk-${i}${endung}`; + let bytes = 0; + try { + copyFileSync(von, join(mOrdner, datei)); + bytes = statSync(join(mOrdner, datei)).size; + } catch { return; } + db2.prepare(`INSERT INTO material (datei, dateiname, typ, art, groesse, text, + von_id, erstellt, haus) VALUES (?,?,?,?,?,?,?,?,?)`) + .run(datei, name + endung, endung === ".jpeg" ? "image/jpeg" : "image/png", + "bild", bytes, text, ID.DogFather, + new Date(Date.now() - (6 - i) * 3600_000).toISOString(), "crew"); + n++; + }); + /* Zwei sind schon weg -- das ist die Aussage des Videos, und sie + muss auf dem Bildschirm zu sehen sein, nicht nur im Untertitel. */ + const genommen = db2.prepare("SELECT id FROM material ORDER BY id LIMIT 2").all(); + for (const g of genommen) { + db2.prepare("UPDATE material SET genommen_von = ?, genommen_am = ? WHERE id = ?") + .run(ID.Lenny, new Date(Date.now() - 7200_000).toISOString(), g.id); + } + db2.close(); + imRegal = n; + console.log(`Dogi-Media: ${n} Stuecke, davon 2 schon genommen`); } -console.log(`Chat: ${treffRaum ? "gefuellt" : "Raum nicht gefunden"}`); /* ---------- NACHGESEHEN, NICHT GEHOFFT -------------------------------- @@ -324,8 +446,9 @@ console.log(`Chat: ${treffRaum ? "gefuellt" : "Raum nicht gefunden"}`); const n = (probe.j?.eintraege || probe.j?.liste || []).length; const ich = await roh("/workspace/api/ich", { keks: keks.Nachtfalter }); console.log(`Gegenprobe: angemeldet als ${ich.j?.name || "?"} (${ich.j?.rolle || "?"}), ` - + `${n} Wuensche auf dem Brett, ${treffRaum ? CHAT.length : 0} Nachrichten im Chat`); - if (ich.code !== 200 || n < 3 || !treffRaum) { + + `${n} Wuensche auf dem Brett, ${imChat} Nachrichten im Chat, ` + + `${imRegal} Stuecke in Dogi-Media`); + if (ich.code !== 200 || n < 3 || !treffRaum || imChat < CHAT.length || imRegal < 4) { console.error("Die Buehne steht nicht. Es wird nicht gefilmt."); process.exit(5); } @@ -452,7 +575,7 @@ function buehneFuer(seite) { return { auf, /** Eine Vollbild-Karte. `` hebt ein Wort hervor. */ - async karte(html, ms = 2000) { + async karte(html, ms = 2000, { bleibt = false } = {}) { await auf(); await seite.evaluate((t) => { const k = document.getElementById("tk-karte"); @@ -460,6 +583,11 @@ function buehneFuer(seite) { k.classList.add("an"); }, html); await warte(ms); + /* `bleibt` ist fuer den Schluss. Ohne das nimmt die Karte sich + nach ihrer Zeit selbst weg -- und die letzte knappe Sekunde + des Videos zeigte wieder die App statt der Aufforderung. + Das Letzte, was haengenbleibt, soll der Satz sein. */ + if (bleibt) return; await seite.evaluate(() => document.getElementById("tk-karte").classList.remove("an")); await warte(320); }, @@ -503,6 +631,41 @@ function buehneFuer(seite) { return true; }, /** Sanft scrollen — in kleinen Schritten, damit es fließt. */ + /* Rollen INNERHALB eines Kastens. + + `window.scrollBy` bewegt die Seite. Im Chat rollt aber nicht die + Seite, sondern der Verlauf in seinem eigenen Kasten -- der + Aufruf lief dort ins Leere, und drei Bilder hintereinander sahen + gleich aus. Gemerkt habe ich es am Kontaktbogen, nicht am + Quelltext. */ + async rollenIn(wahl, px, ms = 1600) { + const schritte = Math.max(8, Math.round(ms / 40)); + for (let i = 0; i < schritte; i++) { + await seite.evaluate(([w, p]) => { + document.querySelector(w)?.scrollBy(0, p); + }, [wahl, px / schritte]); + await warte(ms / schritte); + } + }, + /* Sanft zu einem Element rollen -- und die Antwort messen, statt + eine Pixelzahl zu raten. + + Video 4 hat beim ersten Lauf den KATALOG der fertigen + Vorschlaege gefilmt statt der Wuensche, weil „480 Pixel nach + unten" zufaellig dort endete. Wie hoch ein Block ist, haengt + davon ab, was darin steht -- eine feste Zahl ist eine Rechnung + von gestern. */ + async zu(wahl, ms = 1400, rand = 150) { + const y = await seite.evaluate((w) => { + const e = document.querySelector(w); + if (!e) return null; + return e.getBoundingClientRect().top + window.scrollY; + }, wahl); + if (y === null) return false; + const von = await seite.evaluate(() => window.scrollY); + await this.rollen(Math.round(y - rand - von), ms); + return true; + }, async rollen(px, ms = 1600) { const schritte = Math.max(8, Math.round(ms / 40)); for (let i = 0; i < schritte; i++) { @@ -530,16 +693,16 @@ const FILME = { await b.karte("Also hab ich dem Rudel
ein Zuhause gebaut.", 2100); await seite.goto(`${BASIS}/workspace/start.html`, { waitUntil: "networkidle" }); await warte(800); - await b.satz("Vierzehn Tueren. Kein Discord.", 1900); + await b.satz("Vierzehn Türen. Kein Discord.", 1900); await b.rollen(560, 2100); await b.satz("Chat. Anschlagbrett. Was ansteht.", 1900); await b.rollen(600, 2100); - await b.satz("Eure Clips. Eure Wuensche.", 1900); + await b.satz("Eure Clips. Eure Wünsche.", 1900); await b.rollen(620, 2100); await b.satz("Und ein Weg, wenn was nicht stimmt.", 2000); await b.rollen(420, 1500); await b.karte("Das ist nicht mein Kanal.
Das ist unsers.", 2200); - await b.karte(CTA, 2400); + await b.karte(CTA, 2600, { bleibt: true }); }, }, @@ -552,13 +715,14 @@ const FILME = { await b.karte("Meine schickt euch
ins Bett
.", 2000); await seite.goto(`${BASIS}/workspace/chat.html?raum=treff`, { waitUntil: "networkidle" }); await warte(1600); - await b.satz("Null bis sechs schlaeft der Rudel-Chat.", 2300); - await b.rollen(240, 1300); - await b.satz("Fuer alle. Auch fuer mich.", 2100); + await b.rollenIn(".chat__verlauf", -260, 1200); + await b.satz("Null bis sechs schläft der Rudel-Chat.", 2300); + await b.rollenIn(".chat__verlauf", 200, 1300); + await b.satz("Für alle. Auch für mich.", 2100); await warte(500); await b.satz("Lesen geht. Schreiben nicht.", 2100); await b.karte("Weil ihr morgen auch noch
ein Leben habt.", 2400); - await b.karte(CTA, 2400); + await b.karte(CTA, 2600, { bleibt: true }); }, }, @@ -566,7 +730,7 @@ const FILME = { datei: "3-jedes-nur-einmal", titel: "Jedes nur einmal", async lauf(seite, b) { - await b.karte("Ich hab Clips und Bilder
fuer euch zum Posten.", 2100); + await b.karte("Ich hab Clips und Bilder
für euch zum Posten.", 2100); await b.karte("Aber jeder kriegt
nur eins.", 1900); await seite.goto(`${BASIS}/workspace/material.html`, { waitUntil: "networkidle" }); await warte(1100); @@ -575,7 +739,7 @@ const FILME = { await b.satz("Wer eins genommen hat, sieht es nicht mehr.", 2300); await b.rollen(420, 1700); await b.karte("Damit nicht einer alles nimmt.
Damit alle was haben.", 2400); - await b.karte(CTA, 2400); + await b.karte(CTA, 2600, { bleibt: true }); }, }, @@ -583,20 +747,22 @@ const FILME = { datei: "4-dazugehoeren", titel: "Dazugehoeren", async lauf(seite, b) { - await b.karte("Du wuenschst dir was im Live.", 2000); + await b.karte("Du wünschst dir was im Live.", 2000); await b.karte("Und bis morgen
hab ich es vergessen.", 2000); await seite.goto(`${BASIS}/workspace/bereich.html?b=wunsch`, { waitUntil: "networkidle" }); await warte(1100); + await b.zu(".eintrag-karte", 1700); await b.satz("Jetzt steht es da. Und geht nicht unter.", 2200); - await b.rollen(480, 2100); + await b.rollen(360, 1800); await b.satz("Und die anderen sehen, wer mitwill.", 2100); - await b.rollen(380, 1500); + await b.rollen(340, 1500); await seite.goto(`${BASIS}/workspace/bereich.html?b=mitmachen`, { waitUntil: "networkidle" }); await warte(1000); + await b.zu(".eintrag-karte", 1500); await b.satz("Wer mehr will als zuschauen:", 2000); - await b.rollen(340, 1500); - await b.karte("Du musst nichts koennen.
Du musst da sein wollen.", 2400); - await b.karte(CTA, 2400); + await b.rollen(300, 1400); + await b.karte("Du musst nichts können.
Du musst da sein wollen.", 2400); + await b.karte(CTA, 2600, { bleibt: true }); }, }, }; @@ -619,10 +785,25 @@ const browser = await pw.chromium.launch({ const fertig = []; for (const nr of liste) { const film = FILME[nr]; - if (film.nachtruhe && !(process.env.TREFF_NACHT_AB === "0" && process.env.TREFF_NACHT_BIS === "24")) { + /* GEFRAGT WIRD, OB DER CHAT SCHLAEFT -- NICHT NACH ZWEI ZAHLEN + (27.09.2026). + + Hier stand ein Vergleich auf `TREFF_NACHT_AB === "0" && + TREFF_NACHT_BIS === "24"`. Das ist die Abschrift einer + Einstellung und keine Frage nach dem Zustand: Jedes andere + Fenster, das den Chat genauso schlafen legt, wurde abgewiesen. + `istNachtruhe()` ist dieselbe Funktion, die auch die Seite + befragt -- damit kann die Antwort hier nicht von dem abweichen, + was im Bild steht. + + Empfohlen wird 12 bis 6: Dann sagt das Band auf der Seite "Ab + 06:00 Uhr geht es weiter" -- also genau das, was ein Mensch um + drei Uhr nachts auch lesen wuerde. Mit 0 bis 24 stand dort + "24:00", und das ergibt fuer einen Zuschauer keinen Sinn. */ + if (film.nachtruhe && !istNachtruhe()) { console.log(`\n--- Video ${nr} (${film.titel}) uebersprungen.`); console.log(" Dafuer muss der Chat schlafen. Noch einmal starten mit:"); - console.log(` TREFF_NACHT_AB=0 TREFF_NACHT_BIS=24 node server/tiktok-videos.mjs ${nr}`); + console.log(` TREFF_NACHT_AB=12 TREFF_NACHT_BIS=6 node server/tiktok-videos.mjs ${nr}`); continue; } console.log(`\n--- Video ${nr}: ${film.titel}`); diff --git a/tools/.commit-video.txt b/tools/.commit-video.txt new file mode 100644 index 00000000..c4b26f87 --- /dev/null +++ b/tools/.commit-video.txt @@ -0,0 +1,114 @@ +Beim Laden stand "SPICY MEDIA - Zentrale" auf jeder Startseite + +WAS DAS BILD GEZEIGT HAT + +Beim Durchsehen der Einzelbilder einer Videoaufnahme -- vier +Sekunden lang, gleich nach dem Laden: + + ---- SPICY MEDIA ---- + Zentrale + WIRD GELADEN + +Und zwar auf der Startseite eines COMMUNITY-MITGLIEDS. + +In start.html steht die Vorgabe des Agenturhauses. Das Teamhaus +bekommt eigene Worte ("Team Dogi" / "Die IrrenAnstalt"), aber erst +wenn /api/ich geantwortet hat. Bis dahin las jeder Modi und jedes +Community-Mitglied die Marke der Agentur in der groessten Schrift +der Seite. Auf einem langsamen Telefon sekundenlang, bei jedem +Aufruf. + +Das ist kein Schoenheitsfehler. Spicy Media ist der Betrieb hinter +der Agentur, und seit dem 24.09. sind die beiden Haeuser getrennt. +Genau diese Zeile hat die Trennung bei jedem Seitenaufruf kurz +aufgehoben -- und niemandem ist es aufgefallen, weil die Seite +DANACH richtig aussah. Wer hinsieht, sieht den Endzustand. + +GELOEST OHNE SPRUNG + +`data-wartet` macht die zwei Zeilen durchsichtig, nicht leer -- der +Platz bleibt stehen, es ruckt nichts. start.js nimmt das Merkmal +weg, sobald es weiss, in welchem Haus es ist; gemessen dauert das +351 ms. + +Eine Notbremse in der Seite nimmt es nach vier Sekunden notfalls +selbst weg. Ohne sie waere die groesste Schrift der Seite fuer immer +unsichtbar, falls start.js gar nicht erst laeuft -- und "unsichtbar" +waere schlimmer als das falsche Wort. + +DIE PRUEFUNG DAZU -- UND ZWEI MESSFEHLER DARIN + +pruef-start-ansicht misst ab jetzt beide Richtungen: nach dem Laden +muessen beide Zeilen sichtbar sein, mit gesetztem `data-wartet` +unsichtbar. Ohne die zweite Haelfte koennte die Regel spurlos +verschwinden, ohne dass etwas rot wird. + +Die Pruefung selbst hat mich zweimal getaeuscht, und beide Male +lehrreich: + + 1. Sie mass 900 ms nach der Anmeldung -- eine feste Pause. Ob + /api/ich in dieser Zeit geantwortet hat, haengt vom Rechner ab. + Dieselbe Pruefung war einmal gruen und einmal rot, bei + unveraendertem Code. Gewartet wird jetzt auf das Merkmal selbst, + und wie lange es gedauert hat, steht im Meldetext. + + 2. `opacity` hat einen Uebergang von 180 ms, und getComputedStyle + liefert waehrenddessen den laufenden Zwischenwert statt des + Ziels. Erst meldete die Gegenprobe "nicht verdeckt", dann die + Messung davor "nicht sichtbar" -- beide Male war die Regel in + Ordnung und nur die Animation im Weg. Fuer die Messung wird der + Uebergang jetzt abgeschaltet. + +DAS VIDEOWERKZEUG + +server/tiktok-videos.mjs nimmt die vier Clips auf. Sechs Aenderungen, +jede aus einem Einzelbild: + + - DIE NACHTRUHE HAT DIE VORBEREITUNG MITBLOCKIERT. Video 2 zeigte + einen leeren Chat. Gemeldet wurde "gefuellt", weil nur geprueft + war, dass es den RAUM gibt. Jetzt werden die Nachrichten + zurueckgelesen und gezaehlt; unter acht wird nicht gefilmt. + - DOGI-MEDIA WAR LEER -- ein Video ueber Material zum Mitnehmen, + und auf dem Bildschirm stand "Gerade ist nichts frei". Sechs + echte Marken-Bilder werden eingestellt, zwei davon schon + genommen, damit die Aussage des Films auch im Bild steht. + - "WILL ICH AUCH - 0" an jedem Wunsch, waehrend der Untertitel + sagte "die anderen sehen, wer mitwill". Ein Versprechen, das das + Bild gleich wieder einkassiert, ist schlimmer als ein leeres + Brett. Jetzt wird ueber die echte Route gestimmt, absteigend + 6/5/4 -- unter zehn Stimmen wird nicht gefilmt. + - DER WAECHTER FUER VIDEO 2 verglich zwei feste Zahlen + (TREFF_NACHT_AB === "0"). Das ist die Abschrift einer Einstellung + und keine Frage nach dem Zustand: Jedes andere Fenster, das den + Chat genauso schlafen legt, wurde abgewiesen. Gefragt wird jetzt + `istNachtruhe()` -- dieselbe Funktion, die auch die Seite + befragt. Mit 12 bis 6 steht im Bild "Ab 06:00 Uhr geht es + weiter" statt "Ab 24:00 Uhr", und das ergibt fuer einen + Zuschauer ueberhaupt erst Sinn. + - ROLLEN IM KASTEN STATT IN DER SEITE. `window.scrollBy` bewegt + die Seite; im Chat rollt aber der Verlauf in seinem eigenen + Kasten. Drei Bilder hintereinander sahen gleich aus. + - EINE FESTE PIXELZAHL TRAF DEN FALSCHEN BLOCK. Video 4 filmte + den Katalog der fertigen Vorschlaege statt der Wuensche, weil + "480 nach unten" zufaellig dort endete. `b.zu(wahl)` misst, wo + das Element steht. + +Dazu: Umlaute in allen Bildtexten ("Tueren" stand im Video), der +Abspann bleibt am Ende stehen (vorher sah man die letzte Sekunde +wieder die App), und die Dateigroessen im Regal sind echt. + +KEIN LINK, UND ZWAR GEMESSEN + +Nach jedem Seitenwechsel wird der SICHTBARE Text nach Adressen +abgesucht -- dogfather-universe, https://, www., jeder Domainname. +Bei einem Fund bricht die Aufnahme ab, und es wird keine Datei +geschrieben. Das mit dem Auge zu pruefen waere genau die Sorte +Kontrolle, die beim vierten Video nachlaesst. + +Mit PROBE_LECK=ja schiebt die Suche selbst eine Adresse ins Bild -- +nachgefahren, Rueckgabewert 2. Eine Suche, die nur "nichts gefunden" +sagen kann, hat nichts bewiesen. + +Gemessen: pruef-start-ansicht 160/0 (vorher 157), pruef-buehne +242/0, pruef-handy 177/0, pruef-kachelraster 24/0, +pruef-css-klassen 33/0. diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index c23f447b..191f2208 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +