Kampagnen-Kacheln: gegliederte Texte, Geschenke mit Preis, aufklappbar

Filipe, mit einem Bildschirmfoto des Regelblocks: "man soll die titel
in den texten besser erkennen und sehen was zu was passt. ich will
auch dass man sieht welche geschenke bei dem event zaehlen und so.
ich will alle informationen die moeglich sind wenn man den link
hochlaedt. ... auch wenn man teilt soll es einfach perfekt sein. man
soll alle infos sehen die man braucht. die kacheln von den events
soll auch nicht so lang sein, die soll man aufklappen koennen. genau
wie eine trennung fuer heute, letzte woche letzten monat und so."

DER FEHLER LAG NICHT IM TEXT, SONDERN IN SEINER ANZEIGE. Im Feld
stand die ganze Zeit eine Gliederung -- Hinweise mit "* ", eine
Ueberschrift mit Untertitel hinter einem Gedankenstrich, Absaetze
ohne Zeichen, Aufzaehlungen mit "- ". Die Kachel hat daraus mit
punkteAusText() EINE flache Liste gemacht: jede Zeile derselbe
Strich, alle gleich gross.

  gliederung()      liest die Gliederung, die schon dasteht, statt
                    den Text umzuschreiben. Die beiden Kampagnen, die
                    heute in der Datenbank liegen, sehen dadurch
                    sofort richtig aus -- ohne Umstellung.
  Geschenke         kommen aus der gelesenen LISTE, nicht aus dem
                    Text, und stehen OHNE Aufklappen auf der Kachel.
  Aufklappen        alles Uebrige hinter einem Druck: gemessen 555 px
                    weniger je Kachel bei Gipfelstuermer.
  Gruppen           je Spalte laeuft / kommt / vorbei, Vergangenes
                    nach diese Woche, dieser Monat, aelter.

NEU GELESEN (stand in allen vier gespeicherten Seiten und wurde nie
angesehen): die Rangliste mit ihren Gruppen ("Superstars" bis
"5. Liga", bei Glow Up "Team Glow"/"Team Shine"), die Beschreibung je
Missionsgruppe, der Steckbrief (Kurznummer, Markt, Sprache, Zone),
der zweite Bildbaustein, und Ueberschriften INNERHALB der Klapptexte
-- "WELCHE LIGA BIN ICH?" hat jetzt sechs eingerueckte Unterpunkte
statt zwoelf gleichrangiger Zeilen.

EVENT_TEXT_MAX 6000 -> 20000. Der Regeltext von Gipfelstuermer ist
7679 Zeichen lang; es fielen bisher jedes Mal rund 1600 Zeichen
echter Inhalt weg. Die Zahl steht jetzt an EINER Stelle.

TEILEN: Was die KAMPAGNE sagt, geht hinaus (Aufgaben, Geschenke,
Regeln) -- wer WIR sind, nicht (Name, Rolle, Haus, Kampagnennummer,
Steckbrief). Und keine Hinweiszeile: "Die Preise konnten nicht
gelesen werden -- bitte nachtragen" ist eine Notiz an DogFather und
hat auf einer Seite, die er in Discord postet, nichts zu suchen.

BERICHTIGT, WEIL NACHGEMESSEN:
  - Number(null) ist 0 und 0 ist endlich -- "Feuerwerk" bekam an drei
    Stellen "0 Coins" angeschrieben, also eine falsche Preisangabe.
  - Ein Kommentar im Leser behauptete, der Wertungszeitraum weiche
    vom Kampagnenzeitraum ab. Nachgemessen ist er in allen drei
    Seiten mit Rangliste auf die Millisekunde gleich; ich hatte zwei
    verschiedene Kampagnen verglichen.
  - bild-kampagne.mjs verlangte "jede Kachel traegt den Ton ihrer
    Seite" und war damit seit dem 07.10. rot, weil an dem Tag bewusst
    das Gegenteil entschieden wurde (Zustandston, Seitenkante). Nicht
    aufgefallen, weil nach dem Lauf nur die Zahl der MESSUNGEN notiert
    wurde, nicht die der FEHLER.
  - .kv-seite > .eintrag-karte wird .kv-seite .eintrag-karte: Die
    Karten haengen durch die Gruppen eine Ebene tiefer. Das waere der
    fuenfte Direktkind-Selektor gewesen, der nach einem Umbau still
    ausfaellt -- diesmal vorher gesehen.

GEPRUEFT: 186 (lesen, +58), 148 (Route/Teilen, +25), 158 (Kachel,
+64), 67 (Agentur, +5), 67 Messungen (Bild, +14), dazu struktur,
css-klassen, zeichen, deutsche-texte, tippziele, lesbarkeit und
haus-trennung (107, unveraendert). Sechs Gegenproben: alle sechs
Sabotagen werden bemerkt, alle Dateien byte-gleich wiederhergestellt.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-07 22:39:03 +02:00
co-authored by Claude Opus 5
parent d7720a3687
commit cadab24a5f
58 changed files with 3950 additions and 798 deletions
+570 -9
View File
@@ -98,11 +98,19 @@ const tag = (v) => tagLokal(v);
der Agentur. Die Pruefung braucht beide Seiten gefuellt -- sonst
misst sie eine Trennung, bei der eine Haelfte leer ist, und
„zwei nebeneinander" waere dann zu Recht rot. */
function evt(titel, von, bis, auf, reg, kampagneId = null) {
/* `daten` ist der Auszug, den der Leser beim Einlesen ablegt
(07.10.2026). Daraus baut der Server die Geschenke mit
Coinpreis, den Steckbrief und die Beschreibung je
Missionsgruppe. Ohne ihn waeren diese drei Dinge hier gar
nicht pruefbar -- und ein Feld, das keine Pruefung ansieht,
ist ein Feld, das beim naechsten Umbau still ausfaellt. */
function evt(titel, von, bis, auf, reg, kampagneId = null, daten = null) {
d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum, event_ende,"
+ " event_aufgaben, event_regeln, dringlichkeit, erstellt, erstellt_von, kampagne_id)"
+ " VALUES (?,?,'agentur','kampagne','offen',?,?,?,?,'mittel',?,?,?)")
.run(titel, "Beschreibung zum Test.", von, bis, auf, reg, jetzt, idDogi, kampagneId);
+ " event_aufgaben, event_regeln, dringlichkeit, erstellt, erstellt_von, kampagne_id,"
+ " kampagne_daten)"
+ " VALUES (?,?,'agentur','kampagne','offen',?,?,?,?,'mittel',?,?,?,?)")
.run(titel, "Beschreibung zum Test.", von, bis, auf, reg, jetzt, idDogi, kampagneId,
daten === null ? null : JSON.stringify(daten));
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
/* Die drei Zustaende, die eventStand kennt -- jeder einzeln, nicht als
@@ -116,6 +124,66 @@ const kommend = evt("November Challenge", tag(20), tag(45),
"· 30 Live-Stunden", "· Nur eigene Konten");
const vorbei = evt("September Rückblick", tag(-40), tag(-12),
"· 20 Live-Stunden", "· Nur eigene Konten");
/* ==== EIN EVENT MIT ECHTER GLIEDERUNG (07.10.2026) ==========
Der Regeltext ist ZEICHENGLEICH mit dem, was am 07.10.2026
wirklich in der Datenbank steht (Eintrag 89, „LIVE Glow Up")
-- nur gekuerzt. Erfundene Testdaten haetten hier genau das
verdeckt, worum es geht: Die Hinweise mit `* ` davor, die
Ueberschrift mit Untertitel hinter einem Gedankenstrich, der
Absatz ohne Zeichen und die Aufzaehlung mit `· `. Jede dieser
vier Zeilensorten sah vorher gleich aus.
ZWEI GESCHENKE, UND EINES OHNE PREIS. Das ist die Gegenprobe
zu einem Fehler, der am 07.10.2026 beim ersten Lauf wirklich
auftrat: `Number(null)` ist 0, und 0 ist endlich -- „Feuerwerk"
bekam dadurch „0 Coins" angeschrieben, also eine falsche
Preisangabe. Mit nur einem Geschenk MIT Preis waere das nie
aufgefallen.
ES LAEUFT UND KOMMT VON TIKTOK: Damit liegt es links in der
Gruppe „Laeuft gerade", die offen steht -- sonst waere alles
darin zugeklappt und nicht messbar. */
const struktText = [
"* Diese Kampagne hat keinen Einleitungstext — der Inhalt steht in den Klapptexten.",
"* Diese Kampagne hat keine Preisliste auf der Seite.",
"",
"SAMMLE DEIN FEUERWERK — Erhalte doppelte Punkte für die Rangliste",
"· Feuerwerk (Sammle Feuerwerk in deinem LIVE!)",
"· Ahorn-Welt — 12000 Coins",
"",
"INFOS ZUR KAMPAGNE",
"LIVE Glow Up ist eine exklusive 4-wöchige Wachstum Challenge.",
"Wofür brauche ich das?",
"Damit du stärkere LIVE-Gewohnheiten aufbaust.",
"",
"### Superstars",
"Du kannst teilnehmen, wenn du eine Einladung hast.",
"",
"KAMPAGNENZEITPLAN",
"· Woche 1: 5. - 11.10.",
"",
"TEILNAHME",
"Länder: DE, CH, AT, LI, LU",
].join("\n");
const strukt = evt("Glow Up Struktur", tag(-2), tag(20),
"Tägliche Mission\nWöchentliche Mission", struktText,
"7690927903777262344", {
geschenke: [
{ id: "1", name: "Feuerwerk", preis: null, hinweis: "Sammle Feuerwerk in deinem LIVE!" },
{ id: "2", name: "Ahorn-Welt", preis: 12000 },
],
aufgabenGruppen: [
{ titel: "Tägliche Mission", beschreibung: "Erfülle sie jeden Tag neu.",
von_tag: "2026-10-05", bis_tag: "2026-10-11" },
],
steckbrief: {
nummer: "7690927903777262344", kurznummer: "1139900164",
region: "EU-TTP2", sprache: "de-DE", verbund: null,
zeitzone: "Europe/Berlin", anmeldung_ab: null, ergebnis_bis: null,
},
});
/* Ein gewoehnlicher Eintrag auf demselben Brett: Er darf sich NICHT
mitverwandeln. Ohne ihn wuerde "nur Events sind betroffen" hier gar
nicht geprueft. */
@@ -249,8 +317,102 @@ melde("\n=== 1. Die Punkte kommen als Liste vom Server ===");
`mit Leerzeichen getippt -> gleich sauber: ${JSON.stringify(p[2]?.text)}`);
ok(p.every((x) => /^[0-9a-f]{16}$/.test(x.schluessel || "")) && p.length > 0,
"jeder Punkt hat einen Schluessel (16 Hexzeichen)");
ok((e?.event_regelpunkte || []).length === 2,
`2 Regelpunkte (${(e?.event_regelpunkte || []).length})`);
/* GEGLIEDERT STATT FLACH (07.10.2026). Hier stand
`event_regelpunkte` -- eine Liste gleichrangiger Zeilen. Der
Regeltext dieses Events hat keine Ueberschrift, also ist es
EIN Abschnitt ohne Titel mit zwei Punkten. Genau das wird
gemessen: nicht „es kommt irgendetwas", sondern die Form. */
const ab = e?.event_regelabschnitte || [];
ok(ab.length === 1 && ab[0].titel === null && ab[0].zeilen.length === 2,
`ein Abschnitt ohne Titel mit 2 Zeilen: ${JSON.stringify(ab)}`);
ok(ab[0]?.zeilen.every((z) => z.art === "punkt"),
"beide Zeilen sind Punkte, keine Absaetze");
/* DER ROHTEXT KOMMT WEITERHIN MIT -- er ist der dritte Ausgang,
wenn die Gliederung einmal ausfaellt. */
ok(typeof e?.event_regeln === "string" && e.event_regeln.includes("eigenen Konto"),
"der Rohtext steht als Rueckfall weiterhin in der Antwort");
/* ---- Das gegliederte Event: Geschenke, Steckbrief, Gruppen -- */
const s = (daten.eintraege || []).find((x) => x.titel === "Glow Up Struktur");
ok(!!s, "das gegliederte Event ist auf dem Brett");
const sab = s?.event_regelabschnitte || [];
const titel = sab.map((a) => a.titel);
ok(titel.includes("SAMMLE DEIN FEUERWERK") && titel.includes("INFOS ZUR KAMPAGNE")
&& titel.includes("KAMPAGNENZEITPLAN") && titel.includes("TEILNAHME"),
`die vier Ueberschriften sind erkannt: ${JSON.stringify(titel)}`);
const feuer = sab.find((a) => a.titel === "SAMMLE DEIN FEUERWERK");
ok(feuer?.unter === "Erhalte doppelte Punkte für die Rangliste",
`der Untertitel steht getrennt: ${JSON.stringify(feuer?.unter)}`);
ok(feuer?.art === "geschenke", `der Abschnitt ist als Geschenke erkannt (${feuer?.art})`);
/* DIE GEGENPROBE ZUM 0-COINS-FEHLER. `preis: null` darf NICHT
als 0 herauskommen -- „0 Coins" ist eine Preisangabe, und
zwar eine falsche. */
const gaben = (feuer?.zeilen || []).filter((z) => z.art === "geschenk");
ok(gaben.length === 2, `zwei Geschenke (${gaben.length})`);
const ohne = gaben.find((z) => z.text === "Feuerwerk");
const mit = gaben.find((z) => z.text === "Ahorn-Welt");
ok(ohne && (ohne.coins === null || ohne.coins === undefined),
`ein Geschenk ohne Preis hat KEINEN Preis, nicht 0: ${JSON.stringify(ohne)}`);
ok(mit?.coins === 12000, `und eines mit Preis hat ihn: ${JSON.stringify(mit?.coins)}`);
ok(ohne?.zusatz === "Sammle Feuerwerk in deinem LIVE!",
`der Hinweis aus der Geschenkliste kommt mit: ${JSON.stringify(ohne?.zusatz)}`);
/* Die Unterstufe aus einem fremden Text. */
const supers = sab.find((a) => a.titel === "Superstars");
ok(supers?.stufe === 3, `eine Unterueberschrift ist Stufe 3 (${supers?.stufe})`);
ok(sab.find((a) => a.titel === "INFOS ZUR KAMPAGNE")?.stufe === 2,
"und ein eigener Abschnitt Stufe 2");
/* Die Frage mit Antwort darunter. */
const infos = sab.find((a) => a.titel === "INFOS ZUR KAMPAGNE");
ok((infos?.zeilen || []).some((z) => z.art === "frage" && z.text === "Wofür brauche ich das?"),
`die Frage mit Antwort darunter ist erkannt: ${JSON.stringify(infos?.zeilen)}`);
/* GEGENPROBE: Eine Zeile aus nichts als Laendercodes waere
gross geschrieben und wuerde zur Ueberschrift. `regelnText`
schreibt deshalb „Länder: ..." -- und genau das muss ein
ABSATZ bleiben, kein Titel. */
const teil = sab.find((a) => a.titel === "TEILNAHME");
ok((teil?.zeilen || []).some((z) => z.art === "text" && z.text.startsWith("Länder:")),
`„Länder: DE, CH, ..." bleibt ein Absatz: ${JSON.stringify(teil?.zeilen)}`);
ok(!titel.includes("DE, CH, AT, LI, LU"),
"und wird nicht zur Ueberschrift");
/* Die Hinweise stehen in einem eigenen Abschnitt ohne Titel --
nicht zwischen den Regeln. Das war der Mangel im
Bildschirmfoto. */
const hin = sab.find((a) => a.art === "hinweise");
ok(hin && hin.titel === null && hin.zeilen.length === 2
&& hin.zeilen.every((z) => z.art === "hinweis"),
`die zwei Hinweise stehen fuer sich: ${JSON.stringify(hin)}`);
/* Steckbrief und Aufgabenbeschreibung. */
ok(s?.event_steckbrief?.kurznummer === "1139900164",
`der Steckbrief kommt mit: ${JSON.stringify(s?.event_steckbrief)}`);
ok(s?.event_steckbrief?.verbund === undefined,
"ein leeres Feld faellt weg, statt als Leerstelle zu erscheinen");
const tagMission = (s?.event_punkte || []).find((x) => x.text === "Tägliche Mission");
ok(tagMission?.beschreibung === "Erfülle sie jeden Tag neu.",
`die Beschreibung haengt an der Aufgabe: ${JSON.stringify(tagMission)}`);
ok(tagMission?.von_tag === "2026-10-05" && tagMission?.bis_tag === "2026-10-11",
"und ihr Zeitraum dazu");
const woche = (s?.event_punkte || []).find((x) => x.text === "Wöchentliche Mission");
ok(woche && woche.beschreibung === undefined,
"eine Aufgabe ohne Gruppe bekommt kein leeres Feld");
/* GEGENPROBE: Die drei Events OHNE Kampagnendaten bekommen
weder Geschenke noch Steckbrief. Ohne sie waere unbelegt,
dass die Felder aus `kampagne_daten` kommen und nicht
irgendwo erfunden werden. */
ok(e?.event_steckbrief === undefined,
"ein Event ohne Kampagnendaten hat keinen Steckbrief");
ok(!(e?.event_regelabschnitte || []).some((a) => a.art === "geschenke"),
"und keinen Geschenkabschnitt");
ok(e?.kampagne_daten === undefined,
"die Rohspalte selbst geht NICHT an die Oberflaeche");
ok(s?.kampagne_daten === undefined,
"auch nicht beim Event, das sie hat");
ok(e?.event_geschafft === 0, `noch nichts abgehakt (${e?.event_geschafft})`);
/* GEGENPROBE: Ein Eintrag, der KEIN Event ist, bekommt keine Punkte.
@@ -443,8 +605,16 @@ async function alsRolle(rolle, code, breite = 1280) {
{
const { ctx, seite } = await alsRolle("admin", "CODE-DOGI-0001", 1280);
/* VIER SEIT DEM 07.10.2026: dazu kam „Glow Up Struktur" mit
echter Gliederung. Die Zahl steht hier als Bedingung und
nicht nur im Meldetext -- ein stiller Rueckgang waere sonst
unsichtbar (CLAUDE.md, 28.08.2026).
ZUGEKLAPPTE GRUPPEN ZAEHLEN MIT: „Vorbei" steht von sich aus
zu, aber `hidden` entfernt ein Element nicht aus dem Baum.
Waere das anders, muesste hier erst aufgeklappt werden. */
const karten = await seite.$$('.eintrag-karte[data-eventkarte="ja"]');
ok(karten.length === 3, `3 Eventkarten (${karten.length}) -- die Schulung ist keine`);
ok(karten.length === 4, `4 Eventkarten (${karten.length}) -- die Schulung ist keine`);
/* GEGENPROBE: Die Schulung bleibt eine gewoehnliche Karte. Ohne sie
waere "nur Events sind umgebaut" unbelegt. */
@@ -844,6 +1014,362 @@ async function alsRolle(rolle, code, breite = 1280) {
await ctx.close();
}
/* =======================================================================
4b. SIEHT MAN DIE GLIEDERUNG? (07.10.2026)
Das ist der Abschnitt, der Filipes Bildschirmfoto beantwortet. Und
zwar MESSEND: „es gibt ein h5" beweist nichts -- ein h5, das
genauso aussieht wie der Absatz darunter, ist genau der Zustand,
der beanstandet wurde. Gemessen werden deshalb die berechneten
Werte und ihr UNTERSCHIED.
EIN GRUENER HAKEN, DER NICHTS MISST, IST DAS SCHLIMMERE VON ZWEIEN
(CLAUDE.md, 01.09.2026): Jede Bedingung hier enthaelt die Zahl der
tatsaechlich untersuchten Dinge, und zu jeder Aussage „so sieht es
aus" gehoert eine, die „und so NICHT" sagt.
======================================================================= */
melde("\n=== 4b. Die Gliederung auf dem Bildschirm ===");
{
const { ctx, seite } = await alsRolle("admin", "CODE-DOGI-0001", 1280);
/* ---- Die Kachel ist zu, und das ist der Zweck --------------- */
const zuerst = await seite.evaluate(() => {
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.find((x) => x.textContent.includes("Glow Up Struktur"));
if (!k) return { fehlt: true };
const mehr = k.querySelector(".ev-mehr__inhalt");
const knopf = k.querySelector(".ev-mehr__knopf");
return {
hoeheZu: Math.round(k.getBoundingClientRect().height),
verborgen: !!mehr?.hidden,
gedrueckt: knopf?.getAttribute("aria-expanded"),
zahl: k.querySelector(".ev-mehr__zahl")?.textContent.trim() || null,
wort: k.querySelector(".ev-mehr__wort")?.textContent.trim() || null,
/* Was TROTZ zugeklapptem Kasten dasteht. */
gaben: [...k.querySelectorAll(".ev-block--gaben .ev-gabe")].map((g) => ({
name: g.querySelector(".ev-gabe__name")?.textContent.trim(),
coins: g.querySelector(".ev-gabe__coins")?.textContent.trim() || null,
zusatz: g.querySelector(".ev-gabe__zusatz")?.textContent.trim() || null,
})),
hinweise: [...k.querySelectorAll(".ev-hinweise .ev-hinweis")]
.map((h) => h.textContent.trim()),
/* Die Hinweise duerfen NICHT in der Abschnittsliste stehen --
genau das war der Mangel. */
hinweisInRegeln: k.querySelectorAll(".ev-abschnitte .ev-hinweis").length,
zusatz: k.querySelector(".ev-punkt__zusatz")?.textContent.trim() || null,
};
});
melde(" -- zu: " + JSON.stringify(zuerst));
ok(!zuerst.fehlt, "die gegliederte Kachel steht auf dem Brett");
ok(zuerst.verborgen === true && zuerst.gedrueckt === "false",
`der Infokasten ist von sich aus zu (hidden=${zuerst.verborgen}, `
+ `aria-expanded=${zuerst.gedrueckt})`);
ok(zuerst.wort === "Alle Infos" && /Abschnitt/.test(zuerst.zahl || ""),
`und der Knopf sagt, was dahinter steht: "${zuerst.wort} / ${zuerst.zahl}"`);
/* ---- Die Geschenke stehen OHNE Aufklappen da ---------------- */
ok(zuerst.gaben.length === 2,
`zwei Geschenke sind sofort sichtbar (${zuerst.gaben.length})`);
const ahorn = zuerst.gaben.find((g) => g.name === "Ahorn-Welt");
const feuer = zuerst.gaben.find((g) => g.name === "Feuerwerk");
ok(/12\.000/.test(ahorn?.coins || "") && /Coins/.test(ahorn?.coins || ""),
`der Coinpreis steht mit Tausenderpunkt da: ${JSON.stringify(ahorn?.coins)}`);
/* DIE GEGENPROBE ZUM 0-COINS-FEHLER, diesmal auf dem Bildschirm:
Ein Kampagnengeschenk ohne Preis darf KEINE Preisplakette
bekommen. „0 Coins" waere eine falsche Preisangabe. */
ok(feuer && feuer.coins === null,
`ein Geschenk ohne Preis zeigt keinen Preis: ${JSON.stringify(feuer)}`);
ok(feuer?.zusatz === "Sammle Feuerwerk in deinem LIVE!",
`stattdessen steht da, wie man es bekommt: ${JSON.stringify(feuer?.zusatz)}`);
/* ---- Die Hinweise stehen fuer sich ------------------------- */
ok(zuerst.hinweise.length === 2,
`die zwei Hinweise stehen sichtbar in ihrem eigenen Kasten (${zuerst.hinweise.length})`);
ok(zuerst.hinweisInRegeln === 0,
`und KEINER zwischen den Regeln (${zuerst.hinweisInRegeln}) -- das war der Mangel`);
/* ---- Die Beschreibung haengt unter der Aufgabe -------------- */
ok(/Erfülle sie jeden Tag neu/.test(zuerst.zusatz || "")
&& /05\.10\.2026/.test(zuerst.zusatz || ""),
`unter der Aufgabe stehen Beschreibung und Zeitraum: ${JSON.stringify(zuerst.zusatz)}`);
/* ---- Aufklappen: wird die Kachel laenger? ------------------- */
/* DIE RICHTIGE KACHEL (berichtigt 07.10.2026).
Hier stand `.eintrag-karte[data-eventkarte="ja"] .ev-mehr__knopf`
-- das traf die ERSTE Eventkarte („Oktober Event"), nicht die
gegliederte. Gemessen wurde danach eine Kachel, deren Kasten
weiterhin zu war: „spart 0 px", „0 px eingerueckt",
„Untertitel null". Drei rote Zeilen, kein Schaden am Code.
Das ist die Sorte Fehlalarm, die Vertrauen kostet (CLAUDE.md,
26.09.2026) -- deshalb wird die Kachel ueber ihren TITEL
gesucht, nicht ueber ihre Position. */
const knopfNr = await seite.evaluate(() => {
const alle = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')];
const i = alle.findIndex((x) => x.textContent.includes("Glow Up Struktur"));
return i;
});
ok(knopfNr >= 0, `die gegliederte Kachel ist Karte Nr. ${knopfNr}`);
await seite.evaluate(() => {
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.find((x) => x.textContent.includes("Glow Up Struktur"));
k.querySelector(".ev-mehr__knopf").click();
});
await seite.waitForTimeout(300);
const danach = await seite.evaluate(() => {
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.find((x) => x.textContent.includes("Glow Up Struktur"));
const mehr = k.querySelector(".ev-mehr__inhalt");
const lies = (w) => {
const s = getComputedStyle(w);
return {
groesse: Math.round(parseFloat(s.fontSize) * 100) / 100,
gewicht: Number(s.fontWeight),
sperrung: s.letterSpacing,
gross: s.textTransform,
farbe: s.color,
text: w.textContent.trim().slice(0, 40),
};
};
const k2 = k.querySelector('.ev-abschnitt[data-stufe="2"] > .ev-abschnitt__titel');
const k3 = k.querySelector('.ev-abschnitt[data-stufe="3"] > .ev-abschnitt__titel');
const abs = k.querySelector(".ev-abschnitt .ev-absatz");
const frage = k.querySelector(".ev-abschnitt .ev-frage");
return {
hoeheAuf: Math.round(k.getBoundingClientRect().height),
verborgen: !!mehr?.hidden,
titel: [...k.querySelectorAll(".ev-abschnitt__titel")].map((h) => h.textContent.trim()),
stufe2: k2 ? lies(k2) : null,
stufe3: k3 ? lies(k3) : null,
absatz: abs ? lies(abs) : null,
frage: frage ? lies(frage) : null,
/* DER UNTERTITEL DES GESCHENKABSCHNITTS STEHT IM
GESCHENKBLOCK (berichtigt 07.10.2026).
Hier stand `.ev-abschnitt__unter`. „SAMMLE DEIN FEUERWERK
— Erhalte doppelte Punkte" ist aber genau der Abschnitt,
der aus den Regeln HERAUSGENOMMEN und nach oben gezogen
wird (damit die Geschenke ohne Aufklappen sichtbar sind).
Sein Untertitel heisst dort `.ev-gaben__unter`. Die
Pruefung suchte ihn an der Stelle, an der er absichtlich
nicht mehr steht.
BEIDE WERDEN JETZT GEMESSEN: `unter` fuer einen
gewoehnlichen Abschnitt (es gibt hier keinen mit
Untertitel, also null -- das ist in Ordnung) und
`gabenWoher`/`gabenUnter` fuer den, um den es geht. */
unter: k.querySelector(".ev-gaben__unter")?.textContent.trim() || null,
gabenWoher: (() => {
const p = k.querySelector(".ev-gaben__woher");
if (!p) return null;
const u = p.querySelector(".ev-gaben__unter");
const ganz = p.textContent.trim();
return u ? ganz.slice(0, ganz.length - u.textContent.trim().length).trim() : ganz;
})(),
/* DIE EINRUECKUNG WIRD AM TEXT GEMESSEN, NICHT AM KASTEN
(berichtigt 07.10.2026).
Hier stand der Abstand der beiden ABSCHNITTE. Der ist 0,
und das ist richtig: Beide sind volle Kinder derselben
Spalte, die Einrueckung entsteht durch `padding-left` --
sie verschiebt den INHALT, nicht das Rechteck. Gemessen
kam „0 px eingerueckt" heraus, obwohl der Text sichtbar
eingerueckt war.
Was man sehen soll, ist der Text. Also wird der gemessen. */
einzug: (() => {
const h3 = k.querySelector('.ev-abschnitt[data-stufe="3"] > .ev-abschnitt__titel');
const h2 = k.querySelector('.ev-abschnitt[data-stufe="2"] > .ev-abschnitt__titel');
return h3 && h2
? Math.round(h3.getBoundingClientRect().x - h2.getBoundingClientRect().x)
: null;
})(),
steck: [...k.querySelectorAll(".ev-steck__name")].map((x) => x.textContent.trim()),
steckWerte: [...k.querySelectorAll(".ev-steck__wert")].map((x) => x.textContent.trim()),
};
});
melde(" -- auf: " + JSON.stringify(danach).slice(0, 700));
ok(danach.verborgen === false,
"ein Druck macht den Infokasten auf");
/* DAS IST DIE ANTWORT AUF „die kacheln sollen nicht so lang sein":
nicht „sie ist kurz", sondern „sie ist MESSBAR kuerzer als mit
allem offen". Eine Kachel, die zugeklappt genauso hoch ist, hat
nichts gespart. */
ok(danach.hoeheAuf > zuerst.hoeheZu + 80,
`zugeklappt spart die Kachel ${danach.hoeheAuf - zuerst.hoeheZu} px `
+ `(${zuerst.hoeheZu} statt ${danach.hoeheAuf})`);
/* ---- Und jetzt: SIEHT man die Ueberschrift? ----------------- */
ok(danach.titel.length >= 4,
`${danach.titel.length} Ueberschriften stehen da: ${JSON.stringify(danach.titel.slice(0, 6))}`);
ok(!!danach.stufe2 && !!danach.absatz,
"Ueberschrift und Absatz sind beide da (sonst misst der Vergleich nichts)");
/* DREI UNTERSCHIEDE, NICHT EINER. Ein einziger (etwa nur die
Farbe) wuerde bei einem anderen Farbschema wieder verschwinden. */
const u2 = danach.stufe2 || {};
const ua = danach.absatz || {};
const unterschiede = [
["Gewicht", u2.gewicht, ua.gewicht],
["Sperrung", u2.sperrung, ua.sperrung],
["Grossschreibung", u2.gross, ua.gross],
["Farbe", u2.farbe, ua.farbe],
].filter(([, a, b]) => a !== b);
ok(unterschiede.length >= 3,
`die Ueberschrift unterscheidet sich in ${unterschiede.length} von 4 Merkmalen `
+ `vom Absatz: ${JSON.stringify(unterschiede)}`);
ok(u2.gewicht >= 700 && ua.gewicht < 700,
`sie ist fett, der Absatz nicht (${u2.gewicht} gegen ${ua.gewicht})`);
ok(u2.gross === "uppercase" && ua.gross === "none",
`sie steht in Grossbuchstaben, der Absatz nicht (${u2.gross} / ${ua.gross})`);
/* Die Unterstufe sieht anders aus als die Oberstufe UND anders
als ein Absatz -- sonst waere die zweite Stufe keine. */
ok(!!danach.stufe3, "eine Unterueberschrift ist da");
const u3 = danach.stufe3 || {};
ok(u3.gross === "none" && u3.gewicht >= 600,
`die Unterstufe ist halbfett und NICHT in Grossbuchstaben `
+ `(${u3.gewicht}, ${u3.gross})`);
ok(u3.gewicht > ua.gewicht,
`aber immer noch kraeftiger als ein Absatz (${u3.gewicht} gegen ${ua.gewicht})`);
ok(danach.einzug >= 8,
`und sie ist ${danach.einzug} px eingerueckt (Grenze 8)`);
/* Der Untertitel hinter dem Gedankenstrich steht getrennt -- und
die Ueberschrift der Kampagne bleibt erhalten. „Sammle dein
Feuerwerk" sagt, WARUM diese Geschenke zaehlen; ein selbst
erfundenes „Geschenke" daneben haette das verschluckt. */
ok(danach.gabenWoher === "SAMMLE DEIN FEUERWERK",
`die Ueberschrift der Kampagne steht ueber den Geschenken: `
+ `${JSON.stringify(danach.gabenWoher)}`);
ok(danach.unter === "Erhalte doppelte Punkte für die Rangliste",
`der Untertitel steht als eigene Zeile: ${JSON.stringify(danach.unter)}`);
/* Die Frage ist kraeftiger als der Absatz unter ihr. */
ok(!!danach.frage && danach.frage.gewicht > ua.gewicht,
`eine Frage mit Antwort darunter steht kraeftiger da `
+ `(${danach.frage?.gewicht} gegen ${ua.gewicht}): ${JSON.stringify(danach.frage?.text)}`);
/* Der Steckbrief -- deutsche Namen, keine Rohfelder. */
ok(danach.steck.includes("Kurznummer") && danach.steck.includes("Markt"),
`der Steckbrief nennt die Felder auf Deutsch: ${JSON.stringify(danach.steck)}`);
ok(danach.steckWerte.includes("1139900164"),
`und zeigt die Werte: ${JSON.stringify(danach.steckWerte)}`);
ok(!danach.steck.some((n) => /_|activity|flag/i.test(n)),
"kein Rohfeldname steht auf der Kachel");
/* ---- Die eigene Wahl haelt das Neuladen aus ---------------- */
await seite.reload({ waitUntil: "networkidle" });
await seite.waitForSelector(".ev-mehr__knopf", { timeout: 15000 });
await seite.waitForTimeout(600);
const nachher = await seite.evaluate(() => {
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.find((x) => x.textContent.includes("Glow Up Struktur"));
return { verborgen: !!k?.querySelector(".ev-mehr__inhalt")?.hidden };
});
/* WARUM DAS WICHTIG IST: Das Brett baut sich bei jeder
Aktualisierung neu auf. Eine Kachel, die dabei zuklappt,
waehrend man liest, ist aergerlicher als eine lange. */
ok(nachher.verborgen === false,
"aufgeklappt bleibt aufgeklappt, auch nach dem Neuladen");
/* ---- Die Gruppen der Spalten ------------------------------- */
const gruppen = await seite.evaluate(() => {
const seiten = [...document.querySelectorAll(".kv-seite")].map((s) => ({
seite: s.dataset.seite,
gruppen: [...s.querySelectorAll(".ev-gruppe__kopf")].map((g) => ({
name: g.dataset.gruppe,
wort: g.querySelector(".ev-gruppe__wort")?.textContent.trim(),
zahl: g.querySelector(".ev-gruppe__zahl")?.textContent.trim(),
zu: g.nextElementSibling?.hidden === true,
})),
}));
return { seiten };
});
melde(" -- Gruppen: " + JSON.stringify(gruppen));
const alleGruppen = gruppen.seiten.flatMap((s) => s.gruppen);
ok(alleGruppen.length >= 3,
`${alleGruppen.length} Gruppen stehen in den Spalten`);
const laeuft = alleGruppen.filter((g) => g.name === "laeuft");
const vorbei = alleGruppen.filter((g) => g.name.startsWith("vorbei"));
ok(laeuft.length >= 1 && laeuft.every((g) => !g.zu),
`„Laeuft gerade" steht offen (${laeuft.length})`);
ok(vorbei.length >= 1 && vorbei.every((g) => g.zu),
`und Vergangenes zu (${vorbei.length}: `
+ `${vorbei.map((g) => g.wort).join(", ")})`);
/* KEINE LEERE GRUPPE. Fuenf Ueberschriften ueber nichts waeren
schlimmer als eine lange Spalte. */
ok(alleGruppen.every((g) => Number(g.zahl) > 0),
`jede Gruppe hat Eintraege: ${JSON.stringify(alleGruppen.map((g) => g.zahl))}`);
/* UND DIE SUMME STIMMT: Keine Karte faellt zwischen zwei Gruppen
durch. Ohne diese Zeile waere „es gibt Gruppen" vereinbar damit,
dass ein Event in keiner davon auftaucht. */
const summe = alleGruppen.reduce((s, g) => s + Number(g.zahl), 0);
ok(summe === 4, `alle 4 Events stehen in genau einer Gruppe (Summe ${summe})`);
/* GEGENPROBE: Links und rechts teilen sich den Zustand NICHT.
Dieser Fehler war bei den Zeitstufen schon einmal da.
ERST DRUECKEN, DANN NACHSEHEN (berichtigt 07.10.2026). Vorher
stand hier `... || merker.length === 0` -- und weil nie
jemand eine Gruppe angefasst hatte, war die Liste leer und
die Zeile gruen, ohne etwas gemessen zu haben. Genau die
Sorte Haken, die immer bestaetigt (CLAUDE.md, 01.09.2026).
Jetzt wird eine Gruppe LINKS zugeklappt und danach geprueft,
dass (1) ein Merker entstanden ist, (2) er die Seite nennt
und (3) die gleichnamige Gruppe RECHTS davon unberuehrt ist. */
const vorher = await seite.evaluate(() => {
const links = document.querySelector('.kv-seite[data-seite="tiktok"] .ev-gruppe__kopf');
const name = links?.dataset.gruppe;
const rechts = [...document.querySelectorAll('.kv-seite[data-seite="agentur"] .ev-gruppe__kopf')]
.find((g) => g.dataset.gruppe === name);
links?.click();
return {
name,
linksZu: links?.nextElementSibling?.hidden === true,
rechtsDa: !!rechts,
rechtsZu: rechts ? rechts.nextElementSibling?.hidden === true : null,
};
});
const merker = await seite.evaluate(() => Object.keys(localStorage)
.filter((s) => s.startsWith("abschnitt:agentur:")));
melde(" -- nach dem Druck: " + JSON.stringify({ ...vorher, merker }));
ok(merker.length > 0 && merker.every((s) => /:(tiktok|agentur):/.test(s)),
`der Merker nennt die Seite: ${JSON.stringify(merker)}`);
/* DIESE ZEILE WAR SELBST FALSCH (berichtigt 07.10.2026).
Hier stand `!s.includes(":agentur:")`. Der Merker heisst aber
`abschnitt:agentur:tiktok:laeuft` -- und „agentur" darin ist der
BRETTNAME, nicht die Seite. Die Zeile war damit nie erfuellbar.
Zwei Dinge heissen gleich und sind verschieden; genau dieser
Fehler stand am 07.10.2026 schon einmal im Projekt (das Feld
„Gilt für" nennt das HAUS, nicht das Brett).
Gemessen wird jetzt die Form des Schluessels GANZ: genau einer,
und er nennt die angefasste Seite. Das ist die Aussage, auf die
es ankommt -- der Zustand ist je Seite abgelegt, also kann ein
Druck links rechts nichts bewirken. */
ok(merker.length === 1 && merker[0] === `abschnitt:agentur:tiktok:${vorher.name}`,
`genau ein Merker, fuer die angefasste Seite: ${JSON.stringify(merker)}`);
/* UND DIE ANDERE SEITE STEHT NOCH SO DA WIE VORHER. Gemessen an
der Gruppe, die es dort gibt -- „Kommt noch" war offen und muss
offen bleiben, obwohl links gerade zugeklappt wurde. */
const andereSeite = await seite.evaluate(() => {
const g = [...document.querySelectorAll('.kv-seite[data-seite="agentur"] .ev-gruppe__kopf')]
.map((x) => ({ name: x.dataset.gruppe, zu: x.nextElementSibling?.hidden === true }));
return g;
});
const kommt = andereSeite.find((g) => g.name === "kommt");
ok(!!kommt && kommt.zu === false,
`rechts ist nichts mitgeklappt: ${JSON.stringify(andereSeite)}`);
ok(vorher.linksZu === true,
`die angefasste Gruppe links ist zu (${vorher.name})`);
await ctx.close();
}
melde("\n=== 5. Abhaken im Browser, mit Neuladen ===");
{
const { ctx, seite } = await alsRolle("creator", "CODE-CREA-0001", 1280);
@@ -1037,11 +1563,42 @@ melde("\n=== 7. Auf dem Handy (412 px) ===");
199 px breit. Dass die Regel UMSCHALTET, muss gemessen werden;
sonst beweist „zwei nebeneinander" am Rechner nur, dass es dort
passt, und nichts darueber, was auf dem Telefon passiert. */
/* GEMESSEN WIRD, WAS MAN SIEHT (berichtigt 07.10.2026).
Hier standen ALLE Eventkarten. Seit die Spalten nach Zustand
und Alter gegliedert sind, steht „Vorbei — aelter" von sich
aus zugeklappt -- und eine Karte in einem `hidden`-Kasten hat
ein Rechteck von 0x0 an Position 0,0. Gemessen kam
„379, 379, 379, 379, 0 px" heraus, und die Pruefung meldete
eine zu schmale Karte, obwohl keine zu schmal war.
ZWEI SCHAEDEN, NICHT EINER: Die Paarprobe daneben haette bei
ZWEI zugeklappten Karten „zwei auf gleicher Hoehe" gemeldet --
beide liegen auf y=0. Das waere ein Fehlalarm, der erst
auftritt, wenn ein zweites Event vorbeigelaufen ist, also
irgendwann von selbst. Solche Fehlalarme kosten das Vertrauen
in den ganzen Satz (CLAUDE.md, 26.09.2026).
UND DAS VERSCHWINDEN WIRD SELBST GEPRUEFT: Eine unsichtbare
Karte darf nur deshalb unsichtbar sein, weil ihre Gruppe zu
ist. Ohne diese zweite Haelfte haette „wir messen nur die
sichtbaren" genau den Fall weggeschaut, in dem eine Karte
wirklich fehlt. */
const schmal = await seite.evaluate(() => {
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.map((x) => { const r = x.getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y), b: Math.round(r.width) }; });
const alle = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.map((x) => {
const r = x.getBoundingClientRect();
const gruppe = x.closest(".ev-gruppe__inhalt");
return {
x: Math.round(r.x), y: Math.round(r.y), b: Math.round(r.width),
sichtbar: r.width > 0 && r.height > 0,
inZu: !!(gruppe && gruppe.hidden),
};
});
const k = alle.filter((a) => a.sichtbar);
return {
karten: k,
versteckt: alle.filter((a) => !a.sichtbar),
/* Stehen zwei auf gleicher Hoehe? Dann waere nicht umgeschaltet. */
paar: k.some((a, i) => k.slice(i + 1).some((b2) => Math.abs(a.y - b2.y) < 5)),
quer: document.documentElement.scrollWidth - document.documentElement.clientWidth,
@@ -1051,6 +1608,10 @@ melde("\n=== 7. Auf dem Handy (412 px) ===");
`auf 412 px steht eine Karte je Reihe (${schmal.karten.map((a) => `${a.b}px@${a.y}`).join(", ")})`);
ok(schmal.karten.every((a) => a.b > 330),
`und sie ist breit genug zum Bedienen (${schmal.karten.map((a) => a.b).join(", ")} px)`);
ok(schmal.versteckt.every((a) => a.inZu),
`was man nicht sieht, steckt in einer zugeklappten Gruppe `
+ `(${schmal.versteckt.length} verborgen, davon `
+ `${schmal.versteckt.filter((a) => a.inZu).length} erklaert)`);
ok(schmal.quer <= 1,
`die Seite laesst sich nicht seitlich schieben (${schmal.quer} px)`);
/* UND WENN DOCH, DANN WELCHES ELEMENT? Eine Zahl allein schickt