TikTok liefert die Kampagnenseite nicht mehr aus -- und der Kasten sagt es jetzt

Filipe hat die Kachel benutzt und bekam "Ging nicht." Im Protokoll des
Servers stand der Grund; im Kasten stand er nicht. Beides ist jetzt
behoben -- und beim Nachmessen kam ein groesserer Befund heraus.

DER BEFUND: DIE SEITE KOMMT LEER AN
  Gemessen am 07.10.2026, von zwei Leitungen und auf fuenf Wegen:

    ehrliche Kennung, accept text/html   17 KB, keine Daten
    ehrliche Kennung + Browser-Accept    17 KB, keine Daten
    Browser-Kennung                      17 KB, keine Daten
    Browser-Kennung + Accept + Sprache   17 KB, keine Daten
    ganz ohne eigene Koepfe              17 KB, keine Daten
    zweiter Abruf MIT Keksen             17 KB, keine Daten
    echtes Chromium (headless)           17 KB, LEERE Seite

  Titel "Campaign", leerer Koerper, zwei Skripte (tiktok-environment,
  gfdatav1). Dasselbe fuer Filipes neue Kampagne UND fuer
  "Gipfelstuermer", von meinem Rechner wie vom Server.

  Damit traegt eine Annahme des Bauplans nicht: "HTML 1.100.298
  Zeichen, serverseitig gerendert". Die 1,1-MB-Seiten, an denen der
  Bauplan entwickelt wurde, stammen aus einer echten Browsersitzung.
  Auf einen gewoehnlichen Abruf baut TikTok die Seite nicht mehr auf.

  Ob das voruebergehend ist (Modern.js kann SSR abstufen -- die
  gespeicherten Seiten tragen "renderLevel":2) oder bleibt, laesst
  sich an einem Abend nicht sagen. Der Weg bleibt deshalb eingebaut:
  Kommt die Seite wieder mit Daten, laeuft alles sofort.

WAS DER MENSCH DAVOR JETZT SIEHT
  Der Leser unterscheidet HUELLE von UMBAU. Das sind zwei sehr
  verschiedene Lagen: Bei einem Umbau ist etwas zu reparieren, bei
  einer Huelle kann niemand etwas machen -- und soll das hoeren statt
  zu suchen. Die Meldung sagt die Groesse, sagt "das liegt nicht an
  dir und nicht am Link" und nennt den naechsten Schritt.

  "umgebaut" steht dort ausdruecklich NICHT mehr: Dieser Satz haette
  ihn auf die falsche Suche geschickt.

UND NIE WIEDER "Ging nicht."
  Der Verlust der Meldung liess sich NICHT nachstellen -- 403 und 502
  kommen beide woertlich im Kasten an, jetzt in bild-kampagne
  gemessen (25 Messungen). Ein Ersatztext, der nichts sagt, ist aber
  auch ohne bekannte Ursache falsch: Er laesst raten. Er nennt jetzt
  die Nummer der Antwort und sagt, wo mehr steht.

GEPRUEFT
  pruef-kampagne-lesen 108 -> 112 (Huelle und Umbau getrennt, mit
  Gegenprobe: eine fremde Seite gilt NICHT als Huelle) ·
  pruef-kampagne 99 -> 102 (fuenf verschiedene Saetze fuer fuenf
  Faelle) · bild-kampagne 20 -> 25 (zeigt der Kasten den Grund?) ·
  pruef-struktur, pruef-eventkarte 94, pruef-agentur 62,
  pruef-css-klassen 39, pruef-zeichen 8, pruef-deutsche-texte 12

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-07 18:02:02 +02:00
co-authored by Claude Opus 5
parent 8cde56bd96
commit 015ac7738e
53 changed files with 884 additions and 697 deletions
+83
View File
@@ -254,6 +254,89 @@ await messen(412, 915, "Auf dem Handy, haeufigste Android-Breite");
`keine Bewegung bei prefers-reduced-motion (${ruht.animation}, ${ruht.dauer})`);
}
/* ---- SAGT DER KASTEN, WARUM ES NICHT GING? -------------------------
Am 07.10.2026 stand bei Filipe im Kasten "Ging nicht." -- der
Ersatztext. Im Protokoll des Servers stand dagegen der genaue
Grund. Zwischen beiden liegt die ganze Frage, ob jemand weiss, was
zu tun ist.
GEMESSEN WIRD HIER DER ECHTE WEG: Auf einer Pruefadresse hat die
Sitzung kein Haus, die Route lehnt deshalb mit 403 und einem
ganzen deutschen Satz ab. Genau dieser Satz muss im Kasten
stehen -- nicht "Ging nicht.".
Das ist kein Umweg: Jede Fehlermeldung dieser Route geht
denselben Weg (res.status(...).json({ fehler })). Wer hier
ankommt, kommt ueberall an. */
{
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const seite = await ctx.newPage();
await anmelden(seite, "admin", "CODE-DOGI-0001");
await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" });
await seite.waitForTimeout(1400);
await seite.fill("#kampagne-text",
"Dein Creator Network lädt dich ein: https://www.tiktok.com/t/ZSbqPsFa8/");
await seite.$eval("#kampagne-holen", (b) => b.click());
/* Auf eine ANTWORT warten, nicht auf eine feste Zeit: Ein fester
Wert waere hier dieselbe Rechnung von gestern wie ueberall. */
await seite.waitForFunction(
() => document.getElementById("kampagne-sagt").textContent.trim() !== "Wird geholt …",
{ timeout: 25000 }).catch(() => {});
const gezeigt = await seite.evaluate(() => ({
text: document.getElementById("kampagne-sagt").textContent.trim(),
knopfFrei: !document.getElementById("kampagne-holen").disabled,
}));
console.log("\n=== Was der Kasten nach einem Fehlschlag sagt ===");
console.log(" gezeigt: " + JSON.stringify(gezeigt.text));
ok(gezeigt.text !== "Ging nicht.",
"der Kasten zeigt NICHT den Ersatztext");
ok(gezeigt.text.length > 25 && /[.!]$/.test(gezeigt.text),
`sondern einen ganzen Satz (${gezeigt.text.length} Zeichen)`);
ok(/Agentur|workspace\.dogfather/i.test(gezeigt.text),
"der den Grund nennt und sagt, was zu tun ist");
ok(gezeigt.knopfFrei, "und der Knopf ist wieder frei");
/* UND DER FALL, DER FILIPE GETROFFEN HAT: HTTP 502.
Der 403 oben beweist, dass der Weg grundsaetzlich traegt. Die
Meldung, die bei ihm verschwand, war aber ein 502 -- und ein
502 kommt in diesem Haus durch einen anderen Flaschenhals
(Vorbau, Dienst). Deshalb wird er hier EINZELN nachgestellt:
window.holen gibt eine fertige 502-Antwort zurueck, genau so,
wie der Server sie schickt. Zeigt der Kasten dann den Satz,
liegt es nicht am Browsercode.
Nachgestellt wird die ANTWORT, nicht der Fehler -- damit misst
diese Zeile genau eine Sache und nicht nebenbei TikTok. */
await seite.evaluate(() => {
const echt = window.holen;
window.holen = async (pfad, o) => {
if (!String(pfad).includes("/kampagne/einlesen")) return echt(pfad, o);
return new Response(JSON.stringify({
fehler: "Die Seite konnte nicht gelesen werden: "
+ "__MODERN_ROUTER_DATA__ fehlt in der Seite. "
+ "Vermutlich hat TikTok die Seite umgebaut.",
}), { status: 502, headers: { "Content-Type": "application/json" } });
};
});
await seite.fill("#kampagne-text", "https://www.tiktok.com/t/ZSbqPsFa8/");
await seite.$eval("#kampagne-holen", (b) => b.click());
await seite.waitForFunction(
() => document.getElementById("kampagne-sagt").textContent.trim() !== "Wird geholt …",
{ timeout: 20000 }).catch(() => {});
const beiFuenfNullZwei = (await seite.evaluate(
() => document.getElementById("kampagne-sagt").textContent.trim()));
console.log(" bei HTTP 502: " + JSON.stringify(beiFuenfNullZwei));
ok(beiFuenfNullZwei !== "Ging nicht."
&& /MODERN_ROUTER_DATA/.test(beiFuenfNullZwei),
"auch ein 502 zeigt den Satz des Servers, nicht den Ersatztext");
await ctx.close();
}
/* ---- Und ein Creator sieht den Kasten gar nicht --------------------- */
{
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
+26 -1
View File
@@ -166,8 +166,33 @@ export function datenAusSeite(html) {
const text = String(html || "");
const treffer = DATENBLOCK.exec(text);
if (!treffer) {
/* ZWEI SEHR VERSCHIEDENE FAELLE, EINE FEHLENDE MARKE
(nachgetragen 07.10.2026, nachdem genau das passiert ist).
(a) TikTok hat die Seite umgebaut -- dann kommt eine volle
Seite an, nur anders gebaut. Das muss jemand ansehen.
(b) TikTok schickt nur eine HUELLE, die sich erst im Browser
aufbaut. Gemessen am 07.10.2026: 17 KB statt rund 1 MB,
Titel "Campaign", leerer Koerper, zwei Skripte
(tiktok-environment, gfdatav1). Von jedem Abrufweg aus
dasselbe -- mit ehrlicher Kennung, mit Browser-Kennung,
mit Keksen, sogar aus einem echten Chromium.
Fuer den Menschen davor ist der Unterschied alles: Bei (a)
ist etwas zu reparieren, bei (b) kann er gar nichts machen
und soll das auch hoeren. Eine Meldung, die beides
zusammenwirft, schickt ihn auf die falsche Suche. */
const huelle = text.length < 200_000
&& (text.includes("gfdatav1") || text.includes("tiktok-environment"));
throw new KampagneUnlesbar(
"__MODERN_ROUTER_DATA__ fehlt in der Seite", "aufbau");
huelle
? `TikTok hat nur eine leere Hülle geschickt `
+ `(${text.length < 1024 ? text.length + " Bytes"
: Math.round(text.length / 1024) + " KB"} statt rund 1 MB). `
+ `Die Seite baut sich erst im Browser auf -- daran kann `
+ `von hier aus niemand etwas ändern`
: "__MODERN_ROUTER_DATA__ fehlt in der Seite",
"aufbau");
}
const anfang = treffer.index + treffer[0].length;
const ende = text.indexOf("</script>", anfang);
+34
View File
@@ -542,6 +542,40 @@ function faengt(tun) {
ok(faengt(() => leseKampagne(seite.gipfel)) === null,
"Gegenprobe: die vollständige Seite wirft nicht");
/* DIE LEERE HUELLE IST ETWAS ANDERES ALS EIN UMBAU
(nachgetragen 07.10.2026, nachdem genau das passiert ist).
Gemessen an diesem Tag: TikTok schickt auf einen gewoehnlichen
Abruf nur noch 17 KB -- Titel "Campaign", leerer Koerper, zwei
Skripte. Von jedem Weg aus dasselbe: ehrliche Kennung,
Browser-Kennung, mit Keksen, sogar aus einem echten Chromium.
Die Seite baut sich erst im Browser auf.
Fuer den Menschen davor ist der Unterschied alles: Bei einem
Umbau ist etwas zu reparieren, bei einer Huelle kann er nichts
machen -- und soll das hoeren statt zu suchen. */
{
const huelle = [
"<!doctype html><html><head><meta charset=\"utf-8\">",
"<title>Campaign</title>",
'<script id="tiktok-environment" type="application/json">{"regionInfo":{}}</script>',
'<script id="gfdatav1" type="application/json">{"env":"prod"}</script>',
"</head><body><div id=\"root\"></div></body></html>",
].join("");
const fh = faengt(() => leseKampagne(huelle));
ok(fh?.art === "aufbau", `eine Hülle wirft ebenfalls (${fh?.art})`);
ok(/Hülle/.test(fh?.grund || "") && /(KB|Bytes)/.test(fh?.grund || ""),
`und nennt sie beim Namen: „${fh?.grund}“`);
ok(!/__MODERN_ROUTER_DATA__/.test(fh?.grund || ""),
" ohne mit einer Marke um sich zu werfen, die hier niemandem hilft");
/* GEGENPROBE: Eine Seite OHNE TikToks Kennzeichen bleibt beim
alten Satz -- sonst hiesse jeder Unsinn "Hülle". */
const fremd = faengt(() => leseKampagne("<html><body>nichts</body></html>"));
ok(/__MODERN_ROUTER_DATA__/.test(fremd?.grund || ""),
"Gegenprobe: irgendeine fremde Seite gilt nicht als Hülle");
}
/* Die kaputte Seite enthält bewusst ein ANDERES JSON-Skript und eine
Zeile mit der Kampagnennummer. Wer „das erste Skript“ nimmt oder
„irgendwo die Nummer“, wird hier grün und im Betrieb falsch. */
+29 -2
View File
@@ -158,6 +158,18 @@ const SEITEN = {
const ROH_GIPFEL = seiteLesen("kampagne-gipfelstuermer.html.br");
const SEITE_KAPUTT = seiteLesen("kampagne-kaputt.html");
/* DIE LEERE HUELLE, wie TikTok sie am 07.10.2026 wirklich schickte:
17 KB, Titel "Campaign", leerer Koerper, die Skripte
tiktok-environment und gfdatav1. Hier nur so gross wie noetig --
gemessen wird die Unterscheidung, nicht die Dateigroesse. */
const HUELLE = [
"<!doctype html><html><head><meta charset=\"utf-8\">",
"<title>Campaign</title>",
'<script id="tiktok-environment" type="application/json">{"regionInfo":{}}</script>',
'<script id="gfdatav1" type="application/json">{"env":"prod"}</script>',
"</head><body><div id=\"root\"></div></body></html>",
].join("");
/* Eine Kampagne, deren Banner eine TEXTDATEI ist. Für die Gegenprobe
zu `bildArtVon`: Was von draußen kommt, ist von draußen -- und der
fremde Server behauptet den Typ. */
@@ -199,6 +211,7 @@ const tikDienst = createServer((anfrage, antwort) => {
if (u.pathname === "/t/fremd") return leite("https://beispiel.de/ganz-woanders");
if (u.pathname === "/t/kreis") return leite("/t/kreis");
if (u.pathname === "/t/kaputt") return leite("/falcon/kaputt");
if (u.pathname === "/t/huelle") return leite("/falcon/huelle");
if (u.pathname === "/t/ohneende") return leite("/falcon/ohneende");
if (u.pathname === "/t/stumm") return leite("/falcon/stumm");
if (u.pathname === "/t/riesig") return leite("/falcon/riesig");
@@ -237,6 +250,12 @@ const tikDienst = createServer((anfrage, antwort) => {
antwort.writeHead(200, { "content-type": "text/html" });
return antwort.end(SEITE_KAPUTT);
}
if (u.pathname === "/falcon/huelle") {
/* Genau das, was TikTok am 07.10.2026 wirklich schickte:
eine kleine Seite mit Titel und zwei Skripten, ohne Inhalt. */
antwort.writeHead(200, { "content-type": "text/html; charset=utf-8" });
return antwort.end(HUELLE);
}
if (u.pathname === "/falcon/ohneende") {
antwort.writeHead(200, { "content-type": "text/html" });
return antwort.end(SEITE_OHNE_ENDE);
@@ -675,6 +694,14 @@ melde("=== 9. Was schiefgehen kann, wird verständlich gesagt ===");
ok(fremd.code === 400 && /beispiel\.de/.test(fremd.j.fehler || ""),
`fremdes Ziel: HTTP ${fremd.code} — „${fremd.j.fehler}“`);
const huelle = await einlesen(`Klick: ${TIK}/t/huelle`);
ok(huelle.code === 502 && /Hülle/.test(huelle.j.fehler || ""),
`leere Hülle: HTTP ${huelle.code} — „${(huelle.j.fehler || "").slice(0, 95)}…“`);
ok(/nicht an dir/.test(huelle.j.fehler || ""),
" und sagt ausdrücklich, dass es nicht am Menschen davor liegt");
ok(!/umgebaut/.test(huelle.j.fehler || ""),
" ohne ihn auf die falsche Suche zu schicken");
const kaputt = await einlesen(`Klick: ${TIK}/t/kaputt`);
ok(kaputt.code === 502 && /__MODERN_ROUTER_DATA__/.test(kaputt.j.fehler || ""),
`umgebaute Seite: HTTP ${kaputt.code} — „${(kaputt.j.fehler || "").slice(0, 90)}…“`);
@@ -687,8 +714,8 @@ melde("=== 9. Was schiefgehen kann, wird verständlich gesagt ===");
nur „es kam ein Fehler“ sagt, wäre auch grün, wenn überall
dasselbe stünde. */
const saetze = new Set([ohneLink.j.fehler, fremd.j.fehler,
kaputt.j.fehler, ohneEnde.j.fehler]);
ok(saetze.size === 4, `${saetze.size} verschiedene Sätze für vier Fälle`);
kaputt.j.fehler, ohneEnde.j.fehler, huelle.j.fehler]);
ok(saetze.size === 5, `${saetze.size} verschiedene Sätze für fünf Fälle`);
ok([...saetze].every((s) => s && !/\b50\d\b/.test(s) && s.length > 25),
"keiner davon sagt „Fehler 502“, alle sind ganze Sätze");
+8 -2
View File
@@ -404,10 +404,16 @@ kampagneRouter.post("/workspace/api/kampagne/einlesen", async (req, res) => {
beide mit 502 und beide mit dem GRUND im Klartext. Eine
Kachel ohne Zeitraum wäre eine Falschauskunft; sie behauptet
einen Termin, den sie nicht kennt. */
/* DREI SAETZE STATT ZWEI (07.10.2026). Die leere Huelle
ist kein Umbau und kein Fehler des Menschen davor -- und
"vermutlich hat TikTok umgebaut" schickt ihn suchen. */
const satz = fehler.art === "pflicht"
? `Die Seite wurde gelesen, aber ${fehler.grund}. Bitte von Hand anlegen.`
: `Die Seite konnte nicht gelesen werden: ${fehler.grund}. `
+ "Vermutlich hat TikTok die Seite umgebaut.";
: (/Hülle/.test(fehler.grund)
? `${fehler.grund}. Das liegt nicht an dir und nicht am Link. `
+ "Bitte die Kachel vorerst von Hand anlegen."
: `Die Seite konnte nicht gelesen werden: ${fehler.grund}. `
+ "Vermutlich hat TikTok die Seite umgebaut.");
return schiefgegangen(502, satz, `${fehler.art}: ${fehler.grund}`);
}
throw fehler;