Kampagnen: auswaehlen, wo die Kachel hingehoert -- TikTok oder Agentur

Filipe, mit einem Bild des Einlesekastens: "das sind die von der
agentur selbst. die krieg ich so, perfektioniere das und auch das ich
auswaehlen kann wo es hingehoert, zu tiktok oder der agentur." Dazu
sein echter Link: https://vm.tiktok.com/ZSbACwAnK/

ZWEI FRAGEN, DIE BISHER EINE WAREN. "Woher kam der Link" und "wem
gehoert das Event" wurden beide aus `kampagne_id` beantwortet: Wer
ueber eine Einladung eingelesen wurde, stand links. Die Agentur
schickt ihre eigenen Events aber ebenfalls als TikTok-Link -- damit
war die Zuordnung zwangslaeufig falsch.

Neue Spalte `kampagne_seite` mit DREI Zustaenden, und NULL ist einer
davon: niemand hat gewaehlt -> wie bisher nach der Herkunft. Alle
bestehenden Kacheln behalten dadurch genau ihren Platz, ohne dass
etwas umgestellt werden musste. Gewaehlt wird im Einlesekasten
(Automatisch / TikTok / Agentur, "Automatisch" vorbelegt) und
nachtraeglich an der Kachel ("zur Agentur" / "zu TikTok"). Nur die
Leitung; bei einem Creator wird das Feld stillschweigend entfernt.

GEMESSEN AN SEINEM ECHTEN LINK, mit Gegenprobe:
  vm.tiktok.com/ZSbACwAnK/
    -> www.tiktok.com/tcn/activity?activity_id=7691301069758087175
    -> 233 KB reine Huelle, kein __MODERN_ROUTER_DATA__
  dieselbe Nummer auf dem Weg der oeffentlichen Kampagnenseite
    -> 42 KB, ebenfalls ohne Datenpaket
  eine echte Kampagne auf demselben Weg
    -> 967 KB MIT Datenpaket
Die Kampagne existiert also nicht als oeffentliche Seite: Das Creator
Network baut seine Seiten erst im Browser auf, angemeldet. Von aussen
kann das niemand lesen -- auch kein anderes Programm.

Daraus zwei Dinge:
  - `nummerAusAdresse` liest jetzt BEIDE Schreibweisen (`activityId`
    auf der Kampagnenseite, `activity_id` im Creator Network).
  - `istCreatorNetwork` erkennt solche Links VOR dem Abruf. Statt 502
    "TikTok hat nicht geantwortet" (eine Ursache, die es nicht gibt)
    kommt 422 mit dem, was wirklich los ist -- und mit dem Weg
    daneben: das Event von Hand anlegen, Link in die Beschreibung.

BERICHTIGT: Der PATCH-Weg stand als '/workspace/api/bereich/agentur/'
+ e.id im Browser und war fuer pruef-struktur nicht mehr zuzuordnen
-- jetzt `${bereich}` wie bei den Nachbarn, was nebenbei den zweiten
Ort mit "agentur" im Text wegnimmt. Und "Automatisch" hatte keinen
eigenen Ton und fiel auf `--akzent` zurueck: auf dem Agenturbrett
genau das Gelb der rechten Spalte, die vorbelegte Moeglichkeit sah
also aus wie "Agentur".

GEPRUEFT: 169 (Route, +22), 170 (Kachel, +12), 67 (Agentur), 67
Messungen (Bild), 107 (Haus-Trennung, unveraendert), dazu struktur,
css-klassen, zeichen, deutsche-texte, formulare, tippziele. Drei
Gegenproben: Auswahl verwerfen, Creator-Network durchlassen, Kachel
wieder nur nach Herkunft -- alle drei werden bemerkt, alle Dateien
byte-gleich wiederhergestellt.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-07 23:46:41 +02:00
co-authored by Claude Opus 5
parent cadab24a5f
commit 2764aea61c
54 changed files with 1333 additions and 702 deletions
+162 -6
View File
@@ -104,13 +104,13 @@ const tag = (v) => tagLokal(v);
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) {
function evt(titel, von, bis, auf, reg, kampagneId = null, daten = null, seite = null) {
d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum, event_ende,"
+ " event_aufgaben, event_regeln, dringlichkeit, erstellt, erstellt_von, kampagne_id,"
+ " kampagne_daten)"
+ " VALUES (?,?,'agentur','kampagne','offen',?,?,?,?,'mittel',?,?,?,?)")
+ " kampagne_daten, kampagne_seite)"
+ " VALUES (?,?,'agentur','kampagne','offen',?,?,?,?,'mittel',?,?,?,?,?)")
.run(titel, "Beschreibung zum Test.", von, bis, auf, reg, jetzt, idDogi, kampagneId,
daten === null ? null : JSON.stringify(daten));
daten === null ? null : JSON.stringify(daten), seite);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
/* Die drei Zustaende, die eventStand kennt -- jeder einzeln, nicht als
@@ -167,6 +167,22 @@ const struktText = [
"TEILNAHME",
"Länder: DE, CH, AT, LI, LU",
].join("\n");
/* ==== VON TIKTOK, ABER NICHT VON TIKTOK (07.10.2026) ========
Filipe: „das sind die von der agentur selbst. die krieg ich so,
... ich will auswaehlen koennen wo es hingehoert, zu tiktok oder
der agentur."
DAS IST DER FALL, UM DEN ES GEHT: Die Agentur schickt ihre
eigenen Events ebenfalls als TikTok-Link. Diese Kachel hat
also eine `kampagne_id` -- und gehoert trotzdem nach RECHTS.
Ohne sie liesse sich „die Wahl gewinnt gegen die Herkunft" gar
nicht messen: Bei einer Kachel ohne `kampagne_id` stuende
rechts ja ohnehin das Richtige. */
const agenturEvent = evt("Agentur-Aktion über TikTok", tag(-1), tag(14),
"Eigene Prämie einlösen", "· Nur für das eigene Team",
"7600000000000000009", null, "agentur");
const strukt = evt("Glow Up Struktur", tag(-2), tag(20),
"Tägliche Mission\nWöchentliche Mission", struktText,
"7690927903777262344", {
@@ -613,8 +629,11 @@ async function alsRolle(rolle, code, breite = 1280) {
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. */
/* FUENF SEIT DEM 07.10.2026 (zweite Runde): dazu kam
„Agentur-Aktion über TikTok" -- eine Kachel MIT kampagne_id,
die trotzdem rechts gehoert. */
const karten = await seite.$$('.eintrag-karte[data-eventkarte="ja"]');
ok(karten.length === 4, `4 Eventkarten (${karten.length}) -- die Schulung ist keine`);
ok(karten.length === 5, `5 Eventkarten (${karten.length}) -- die Schulung ist keine`);
/* GEGENPROBE: Die Schulung bleibt eine gewoehnliche Karte. Ohne sie
waere "nur Events sind umgebaut" unbelegt. */
@@ -1305,7 +1324,7 @@ melde("\n=== 4b. Die Gliederung auf dem Bildschirm ===");
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})`);
ok(summe === 5, `alle 5 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.
@@ -1370,6 +1389,143 @@ melde("\n=== 4b. Die Gliederung auf dem Bildschirm ===");
await ctx.close();
}
/* =======================================================================
4c. WOHIN GEHÖRT DIE KACHEL? (07.10.2026)
Die Herkunft beantwortet „woher kam der Link“, die Frage auf dem
Brett ist aber „wem gehört das Event“. Gemessen wird deshalb
genau der Fall, in dem beide auseinandergehen.
======================================================================= */
melde("\n=== 4c. Die Seitenwahl ===");
{
const { ctx, seite } = await alsRolle("admin", "CODE-DOGI-0001", 1400);
const lage = await seite.evaluate(() => {
const seiten = [...document.querySelectorAll(".kv-seite")].map((s) => ({
seite: s.dataset.seite,
titel: [...s.querySelectorAll(".eintrag-titel")].map((x) => x.textContent.trim()),
}));
return { seiten };
});
melde(" -- Lage: " + JSON.stringify(lage));
const links = lage.seiten.find((s) => s.seite === "tiktok")?.titel || [];
const rechts = lage.seiten.find((s) => s.seite === "agentur")?.titel || [];
/* DIE EINE ZEILE, UM DIE ES GEHT. */
ok(rechts.includes("Agentur-Aktion über TikTok"),
`eine Kachel MIT kampagne_id steht rechts, weil sie dorthin gewählt wurde `
+ `(rechts: ${JSON.stringify(rechts)})`);
ok(!links.includes("Agentur-Aktion über TikTok"),
" und nicht zusätzlich links");
/* GEGENPROBE: Eine Kachel OHNE Wahl steht weiterhin nach ihrer
Herkunft. Ohne sie wäre „die Wahl gewinnt“ auch dann grün,
wenn gar nichts mehr links landet. */
ok(links.includes("Oktober Event") && links.includes("Glow Up Struktur"),
`ohne Wahl entscheidet weiterhin die Herkunft (links: ${JSON.stringify(links)})`);
/* ---- Die Auswahl im Kasten -------------------------------- */
const wahl = await seite.evaluate(() => {
const f = document.querySelector(".kampagneholen__wahl");
if (!f) return null;
const k = [...f.querySelectorAll('input[name="kampagne-seite"]')];
return {
werte: k.map((x) => x.value),
gewaehlt: k.find((x) => x.checked)?.value ?? null,
woerter: [...f.querySelectorAll("span")].map((x) => x.textContent.trim()),
sichtbar: f.getBoundingClientRect().height > 0,
};
});
melde(" -- Auswahl: " + JSON.stringify(wahl));
ok(wahl && wahl.werte.length === 3, `drei Möglichkeiten (${wahl?.werte.length})`);
ok(wahl?.werte.includes("tiktok") && wahl?.werte.includes("agentur"),
`TikTok und Agentur stehen zur Wahl: ${JSON.stringify(wahl?.werte)}`);
/* „Automatisch“ IST VORBELEGT. Eine Auswahl, die man jedes Mal
treffen muss, ist ein Hindernis -- und sie wäre eine
Verhaltensänderung für alles, was bisher funktioniert hat. */
ok(wahl?.gewaehlt === "", `„Automatisch“ ist vorbelegt (${JSON.stringify(wahl?.gewaehlt)})`);
ok(wahl?.sichtbar === true, "und der Kasten ist wirklich zu sehen");
/* ---- Der Knopf an der Kachel ------------------------------ */
const knopf = await seite.evaluate(() => {
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.find((x) => x.textContent.includes("Oktober Event"));
const b = [...(k?.querySelectorAll(".schritt--seite") || [])];
return { anzahl: b.length, wort: b[0]?.textContent.trim() || null };
});
/* „Oktober Event“ steht links -- der Knopf muss also nach rechts
führen und das auch sagen. Ein Knopf, der nur „wechseln“ heißt,
ließe offen, wohin. */
ok(knopf.anzahl === 1 && knopf.wort === "zur Agentur",
`der Knopf nennt sein Ziel: ${JSON.stringify(knopf.wort)}`);
await seite.evaluate(() => {
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.find((x) => x.textContent.includes("Oktober Event"));
k.querySelector(".schritt--seite").click();
});
await seite.waitForTimeout(1200);
const danach = await seite.evaluate(() => {
const s = [...document.querySelectorAll(".kv-seite")].map((x) => ({
seite: x.dataset.seite,
titel: [...x.querySelectorAll(".eintrag-titel")].map((y) => y.textContent.trim()),
}));
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
.find((x) => x.textContent.includes("Oktober Event"));
return { s, wort: k?.querySelector(".schritt--seite")?.textContent.trim() || null };
});
melde(" -- nach dem Druck: " + JSON.stringify(danach));
const rechtsDanach = danach.s.find((x) => x.seite === "agentur")?.titel || [];
const linksDanach = danach.s.find((x) => x.seite === "tiktok")?.titel || [];
ok(rechtsDanach.includes("Oktober Event") && !linksDanach.includes("Oktober Event"),
`ein Druck verschiebt die Kachel wirklich `
+ `(links ${linksDanach.length}, rechts ${rechtsDanach.length})`);
/* UND DER KNOPF ZEIGT JETZT ZURÜCK. Ein Weg ohne Rückweg wäre
beim ersten Versehen ein Dauerzustand. */
ok(danach.wort === "zu TikTok",
`und führt jetzt zurück: ${JSON.stringify(danach.wort)}`);
await ctx.close();
}
/* ---- Und ein Creator sieht von alledem nichts ---------------- */
{
const { ctx, seite } = await alsRolle("creator", "CODE-CREA-0001", 1400);
/* GEMESSEN WIRD, WAS MAN SIEHT -- nicht, was im Baum steht
(berichtigt 07.10.2026). Hier stand `querySelectorAll(...).length`,
und das zählte 1: Der ganze Kasten `#kampagneholen` ist für einen
Creator `hidden`, aber `hidden` entfernt nichts aus dem Baum.
Die Prüfung meldete einen Schaden, den es nicht gab.
Es ist derselbe Fehlgriff wie zwei Stunden vorher bei den
zugeklappten Gruppen -- und deshalb wird hier GENAU dieselbe
Frage gestellt: Hat das Element ein Rechteck? */
const nix = await seite.evaluate(() => {
const sichtbar = (w) => !!w && w.getBoundingClientRect().height > 0;
const kasten = document.querySelector(".kampagneholen__wahl");
return {
kastenDa: !!kasten,
kastenSichtbar: sichtbar(kasten),
abschnittZu: document.getElementById("kampagneholen")?.hidden === true,
knoepfe: [...document.querySelectorAll(".schritt--seite")].filter(sichtbar).length,
karten: document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]').length,
};
});
melde(" -- Creator: " + JSON.stringify(nix));
/* DIE KARTENZAHL GEHÖRT IN DIE BEDINGUNG: Ohne sie wäre „ein
Creator sieht die Knöpfe nicht“ auch auf einer leeren Seite
grün (CLAUDE.md, 01.09.2026). */
ok(nix.karten > 0 && !nix.kastenSichtbar && nix.knoepfe === 0,
`ein Creator sieht ${nix.karten} Kacheln, aber keine Seitenwahl `
+ `(Kasten sichtbar: ${nix.kastenSichtbar}, ${nix.knoepfe} Knöpfe)`);
/* UND ZWAR, WEIL DER GANZE ABSCHNITT ZU IST -- nicht, weil die
Auswahl zufällig keine Höhe hat. Eine Begründung, die man nicht
mitprüft, kann beim nächsten Umbau wegfallen, ohne dass es
auffällt. */
ok(nix.abschnittZu === true,
`und der ganze Einlese-Kasten ist zu (hidden=${nix.abschnittZu})`);
await ctx.close();
}
melde("\n=== 5. Abhaken im Browser, mit Neuladen ===");
{
const { ctx, seite } = await alsRolle("creator", "CODE-CREA-0001", 1280);
+151
View File
@@ -149,11 +149,22 @@ const ID_GIPFEL = "7691493175936699143";
const ID_BEENDET = "7657456323453405959";
const ID_OHNEPREISE = "7670899782609996552";
const ID_TEXTBANNER = "7600000000000000001";
/* Die vierte gespeicherte Seite -- bis zum 07.10.2026 benutzte sie
nur pruef-kampagne-lesen.mjs. Hier wird sie gebraucht, weil die
Seitenwahl eine Kampagne braucht, die es noch NICHT gibt: Bei
einer schon eingelesenen kaeme „schon da", und dann entstuende
gar keine neue Kachel, an der sich etwas messen liesse. */
const ID_GLOWUP = "7690927903777262344";
/* Die Nummer aus Filipes echtem Agenturlink -- sie steht hier,
damit die Pruefung dieselbe Form misst, die wirklich ankommt
(snake_case `activity_id`, nicht `activityId`). */
const ID_TCN = "7691301069758087175";
const SEITEN = {
[ID_GIPFEL]: bannerHierher(seiteLesen("kampagne-gipfelstuermer.html.br")),
[ID_BEENDET]: bannerHierher(seiteLesen("kampagne-beendet.html.br")),
[ID_OHNEPREISE]: bannerHierher(seiteLesen("kampagne-ohne-preise.html.br")),
[ID_GLOWUP]: bannerHierher(seiteLesen("kampagne-aufgabengruppe.html.br")),
};
const ROH_GIPFEL = seiteLesen("kampagne-gipfelstuermer.html.br");
const SEITE_KAPUTT = seiteLesen("kampagne-kaputt.html");
@@ -207,7 +218,21 @@ const tikDienst = createServer((anfrage, antwort) => {
if (u.pathname === "/t/gipfel-zweiter-versand") return leite(`/falcon/home?activityId=${ID_GIPFEL}&campaign_traffic_id=2`);
if (u.pathname === "/t/beendet") return leite(`/falcon/home?activityId=${ID_BEENDET}`);
if (u.pathname === "/t/ohnepreise") return leite(`/falcon/home?activityId=${ID_OHNEPREISE}`);
if (u.pathname === "/t/glowup") return leite(`/falcon/home?activityId=${ID_GLOWUP}`);
if (u.pathname === "/t/textbanner") return leite(`/falcon/home?activityId=${ID_TEXTBANNER}`);
/* DAS CREATOR NETWORK (07.10.2026). Nachgemessen am echten
Agenturlink von Filipe: `vm.tiktok.com/ZSbACwAnK/` leitet auf
`www.tiktok.com/tcn/activity?activity_id=...` -- eine Seite,
die TikTok erst im Browser aufbaut, angemeldet. Von aussen
steht dort nichts.
Der Weg wird hier nachgebaut, damit die Pruefung nicht das
echte TikTok braucht: Sie misst dann fremde Verfuegbarkeit
statt unseren Code. Die ANTWORT auf diesen Weg ist
absichtlich eine leere Huelle -- genau wie draussen. */
if (u.pathname === "/t/agentur") {
return leite(`/tcn/activity?activity_id=${ID_TCN}&activity_type=ranklist`);
}
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");
@@ -819,6 +844,132 @@ melde("=== 9. Was schiefgehen kann, wird verständlich gesagt ===");
"und die Kachel selbst sagt, dass etwas nachzutragen ist");
}
/* =====================================================================
9b. DER AGENTURLINK — UND WOHIN DIE KACHEL GEHÖRT (07.10.2026)
Filipe: „das sind die von der agentur selbst. die krieg ich so,
perfektioniere das und auch das ich auswählen kann wo es
hingehört, zu tiktok oder der agentur.“
===================================================================== */
melde("");
melde("=== 9b. Agenturlink und Seitenwahl ===");
{
/* AUS DEM ECHTEN MODUL, nicht nachgebaut: Geprüft wird die
Funktion, die im Betrieb läuft. Eine Abschrift hier wäre eine
zweite Fassung derselben Regel -- und die läuft auseinander. */
const { nummerAusAdresse, istCreatorNetwork } = await import("./workspace-kampagne.js");
/* ---- Die Nummer in BEIDEN Schreibweisen ------------------- */
ok(nummerAusAdresse(`${TIK}/falcon/home?activityId=${ID_GIPFEL}`) === ID_GIPFEL,
"activityId wird gelesen (die Kampagnenseite)");
ok(nummerAusAdresse(`${TIK}/tcn/activity?activity_id=${ID_TCN}`) === ID_TCN,
"activity_id ebenso (das Creator Network)");
/* GEGENPROBE: Etwas, das keine Nummer ist, gibt auch keine --
sonst wäre „beide Schreibweisen“ auch bei einer Funktion
grün, die immer irgendetwas zurückgibt. */
ok(nummerAusAdresse(`${TIK}/tcn/activity?activity_id=hallo`) === null,
" und Buchstaben sind keine Nummer");
ok(nummerAusAdresse(`${TIK}/tcn/activity`) === null,
" und ohne Angabe kommt nichts");
/* ---- Die Erkennung selbst, mit beiden Richtungen ---------- */
ok(istCreatorNetwork(`${TIK}/tcn/activity?activity_id=${ID_TCN}`),
"eine /tcn/-Adresse wird als Creator Network erkannt");
/* GEGENPROBE: Die echte Kampagnenseite NICHT -- sonst wäre jede
Einladung ein Agenturlink und nichts ließe sich mehr einlesen. */
ok(!istCreatorNetwork(`${TIK}/falcon/campaign/fusion_cosmic_runtime/home?activityId=${ID_GIPFEL}`),
" die echte Kampagnenseite dagegen nicht");
ok(!istCreatorNetwork(`${TIK}/tcnx/activity`),
" und ein Pfad, der nur so anfängt, auch nicht");
/* ---- Der Agenturlink wird ERKANNT, nicht blind geholt ----- */
const vorher = abrufe;
const tcn = await einlesen(`Hier die Einladung: ${TIK}/t/agentur`);
ok(tcn.code === 422,
`ein Creator-Network-Link gibt 422, nicht 502 (HTTP ${tcn.code})`);
ok(/Creator Network/.test(tcn.j.fehler || ""),
`und sagt, was für eine Seite das ist: „${tcn.j.fehler}“`);
/* DIE MELDUNG SAGT, WAS ZU TUN IST. Eine Ursache ohne Ausweg
lässt den Menschen davor stehen. */
ok(/von Hand/.test(tcn.j.fehler || ""),
" und was man stattdessen tun kann");
/* UND DIE SEITE WIRD GAR NICHT ERST GEHOLT. 233 KB für eine
Hülle, von der wir schon wissen, dass nichts drinsteht --
und danach eine falsche Ursache melden. Gezählt werden die
Abrufe beim nachgebauten Dienst: die Weiterleitung ja, die
Seite selbst nicht. */
const geholt = abrufe - vorher;
ok(geholt <= 2,
`nur der Weg wurde verfolgt, die Hülle nicht geholt (${geholt} Abrufe)`);
/* ---- DIE SEITENWAHL BEIM EINLESEN ------------------------- */
const mitSeite = await roh("/workspace/api/kampagne/einlesen", {
methode: "POST", keks: keksDogi, wirt: HOST,
rumpf: { text: `Klick: ${TIK}/t/glowup`, seite: "agentur" },
});
const neuId = JSON.parse(mitSeite.text || "{}").id;
ok(mitSeite.code === 201 && Number.isInteger(neuId),
`eine Kampagne mit Seitenwahl entsteht (HTTP ${mitSeite.code}, #${neuId})`);
const gz = dbLesen("SELECT kampagne_id, kampagne_seite FROM eintraege WHERE id = ?", neuId)[0];
ok(gz?.kampagne_seite === "agentur",
`und steht auf der gewählten Seite: ${JSON.stringify(gz?.kampagne_seite)}`);
/* GEGENPROBE: Sie kommt trotzdem von TikTok -- die Herkunft wird
nicht verfälscht, nur die Zuordnung gewählt. Das ist der
ganze Unterschied zwischen beiden Angaben. */
ok(gz?.kampagne_id === ID_GLOWUP,
`die Herkunft bleibt unberührt (${gz?.kampagne_id})`);
/* OHNE WAHL BLEIBT ES NULL -- „niemand hat gewählt“ ist etwas
anderes als „links“. Die zuerst eingelesene Kachel hatte keine
Wahl dabei. */
const alt = dbLesen("SELECT kampagne_seite FROM eintraege WHERE id = ?", kachelId)[0];
ok(alt?.kampagne_seite === null,
`ohne Wahl bleibt die Seite offen (${JSON.stringify(alt?.kampagne_seite)})`);
/* Ein erfundener Wert wird NICHT stillschweigend übernommen. */
const quatsch = await roh(`/workspace/api/bereich/agentur/${kachelId}`, {
methode: "PATCH", keks: keksDogi, wirt: HOST,
rumpf: { kampagne_seite: "mond" },
});
ok(quatsch.code === 400,
`ein erfundener Wert wird abgelehnt (HTTP ${quatsch.code})`);
const unberuehrt = dbLesen("SELECT kampagne_seite FROM eintraege WHERE id = ?", kachelId)[0];
ok(unberuehrt?.kampagne_seite === null,
" und die Kachel bleibt, wie sie war");
/* ---- NACHTRÄGLICH VERSCHIEBEN ----------------------------- */
const schieben = await roh(`/workspace/api/bereich/agentur/${kachelId}`, {
methode: "PATCH", keks: keksDogi, wirt: HOST,
rumpf: { kampagne_seite: "agentur" },
});
ok(schieben.code === 200, `die Seite lässt sich ändern (HTTP ${schieben.code})`);
const nachher = dbLesen("SELECT kampagne_seite FROM eintraege WHERE id = ?", kachelId)[0];
ok(nachher?.kampagne_seite === "agentur",
`und steht jetzt rechts (${nachher?.kampagne_seite})`);
/* ZURÜCKNEHMEN GEHT AUCH. Eine Wahl ohne Weg zurück ist beim
ersten Versehen ein Dauerzustand. */
const zurueck = await roh(`/workspace/api/bereich/agentur/${kachelId}`, {
methode: "PATCH", keks: keksDogi, wirt: HOST,
rumpf: { kampagne_seite: "" },
});
const offen = dbLesen("SELECT kampagne_seite FROM eintraege WHERE id = ?", kachelId)[0];
ok(zurueck.code === 200 && offen?.kampagne_seite === null,
`und wieder auf automatisch (${JSON.stringify(offen?.kampagne_seite)})`);
/* ---- EIN CREATOR DARF DAS NICHT --------------------------- */
/* Wo eine Kachel steht, ist eine Entscheidung über das Brett,
nicht über den eigenen Eintrag. */
const alsCreator = await roh(`/workspace/api/bereich/agentur/${kachelId}`, {
methode: "PATCH", keks: keksLuna, wirt: HOST,
rumpf: { kampagne_seite: "tiktok" },
});
const danach = dbLesen("SELECT kampagne_seite FROM eintraege WHERE id = ?", kachelId)[0];
ok(danach?.kampagne_seite === null,
`ein Creator verschiebt nichts (HTTP ${alsCreator.code}, `
+ `Seite ${JSON.stringify(danach?.kampagne_seite)})`);
}
/* =====================================================================
10. EIN STUMMER SERVER HÄNGT DIE ROUTE NICHT AUF
===================================================================== */
+38
View File
@@ -1522,6 +1522,13 @@ const SPALTEN = `
KEINE BACKTICKS IN DIESEM BLOCK: SPALTEN ist selbst ein Template.
Ein Backtick im Kommentar beendet es mitten im SQL. */
e.kampagne_id, e.kampagne_quelle,
/* AUF WELCHER SEITE DIE KACHEL STEHT (07.10.2026). NULL heisst
„niemand hat gewaehlt" -- dann entscheidet wie bisher
kampagne_id. Die Begruendung steht bei der Spalte in
workspace.js.
KEINE BACKTICKS IN DIESEM BLOCK, siehe oben. */
e.kampagne_seite,
/* DAS GELESENE SELBST (07.10.2026, zweite Runde). Daraus baut
der Server die Geschenkliste mit Coinpreisen, den Steckbrief
und die Beschreibung je Missionsgruppe. Die Spalte selbst
@@ -2265,6 +2272,29 @@ function pruefe(bereich, körper, { neu }) {
|| körper.nur_leitung === "1") ? 1 : 0;
}
/* ---- AUF WELCHE SEITE GEHÖRT DIESE KACHEL? (07.10.2026) ------
Filipe: „ich will auswählen können wo es hingehört, zu tiktok
oder der agentur." Beim Einlesen wird es gewählt; hier lässt
es sich NACHTRÄGLICH ändern -- eine Wahl, die man nur einmal
treffen darf, ist beim ersten Versehen ein Dauerzustand.
NUR IM AGENTUR-BEREICH: Auf jedem anderen Brett gibt es keine
zwei Seiten, und das Feld wäre dort eine Angabe über etwas,
das es nicht gibt -- genauso wie die Content-Felder unten.
DREI ERLAUBTE WERTE, und einer davon ist das Zurücknehmen:
"tiktok", "agentur" und leer (= wieder automatisch). Was
anders aussieht, ist ein Fehler und wird gemeldet, nicht
stillschweigend verworfen: Ein Feld, das Eingaben schluckt,
lässt den Menschen davor glauben, es sei gespeichert. */
if (bereich === "agentur" && körper.kampagne_seite !== undefined) {
const s = String(körper.kampagne_seite ?? "").trim().toLowerCase();
if (s === "") aus.kampagne_seite = null;
else if (s === "tiktok" || s === "agentur") aus.kampagne_seite = s;
else fehler.push("Die Seite muss „tiktok“ oder „agentur“ sein.");
}
/* Die vier Content-Felder gibt es nur in der Content-Planung. In jedem
anderen Bereich werden sie stillschweigend verworfen -- wie Bewertung
und Dringlichkeit auch. Sonst stuende irgendwann ein Aufhaenger an
@@ -3041,6 +3071,14 @@ bereicheRouter.patch("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (re
const { aus, fehler } = pruefe(bereich, req.body || {}, { neu: false });
if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") });
if (!istLeitung(req.person)) delete aus.creator_id;
/* UND DIE SEITE EBENSO (07.10.2026). Wo eine Kachel steht, ist
eine Entscheidung ueber das Brett, nicht ueber den eigenen
Eintrag -- dieselbe Ueberlegung wie bei `creator_id` eine Zeile
darueber. Stillschweigend entfernt und nicht abgelehnt: Wer das
Feld gar nicht sieht, kann es nicht absichtlich geschickt
haben, und eine Fehlermeldung ueber einen Knopf, den es fuer
ihn nicht gibt, verwirrt mehr als sie hilft. */
if (!istLeitung(req.person)) delete aus.kampagne_seite;
/* Zeitraum beim AENDERN -- genauso wichtig wie beim Anlegen und
leichter zu uebersehen: pruefe() sieht nur die Felder, die im
+83 -4
View File
@@ -179,11 +179,43 @@ const kurz = (s, n = 180) => String(s ?? "").slice(0, n);
* irgendwo im Text sucht, findet es auch in einem Pfadstück. */
export function nummerAusAdresse(roh) {
try {
const n = new URL(String(roh)).searchParams.get("activityId");
const p = new URL(String(roh)).searchParams;
/* ZWEI SCHREIBWEISEN (07.10.2026). Die Kampagnenseite nennt
es `activityId`, das Creator Network `activity_id` --
gemessen an `tiktok.com/tcn/activity?activity_id=769130...`.
Wer nur die erste kennt, hat bei einem Agenturlink gar keine
Nummer und kann nicht einmal sagen, um welche Kampagne es
geht. Die erste gewinnt, wenn beide dastehen: Sie steht auf
der Seite, die wir wirklich lesen koennen. */
const n = p.get("activityId") || p.get("activity_id");
return n && /^\d{6,25}$/.test(n) ? n : null;
} catch { return null; }
}
/** Ist das eine Seite des Creator Network statt einer Kampagne?
*
* GEMESSEN AM 07.10.2026, mit Gegenprobe: Der Agenturlink
* `vm.tiktok.com/ZSbACwAnK/` fuehrt auf
* `www.tiktok.com/tcn/activity?activity_id=7691301069758087175`
* und liefert 233 KB reine Huelle -- die Daten holt die Seite
* erst im Browser, angemeldet. Dieselbe Nummer auf dem Weg der
* oeffentlichen Kampagnenseite
* (`/falcon/campaign/fusion_cosmic_runtime/home?activityId=...`)
* gibt 42 KB ohne Datenpaket, waehrend eine echte Kampagne dort
* 967 KB MIT Datenpaket liefert. Die Kampagne existiert also
* nicht als oeffentliche Seite.
*
* DAS IST KEIN FEHLER, SONDERN EINE ANDERE SORTE LINK -- und
* dafuer gehoert eine andere Antwort hin als „TikTok hat nicht
* geantwortet". Wer eine Ursache nennt, die es nicht gibt,
* schickt jemanden in die falsche Richtung suchen. */
export function istCreatorNetwork(roh) {
try {
const u = new URL(String(roh));
return u.pathname === "/tcn" || u.pathname.startsWith("/tcn/");
} catch { return false; }
}
/** Wie viele Einlesevorgänge hatte diese Person in der letzten Stunde?
*
* GEZÄHLT WIRD IM PROTOKOLL, nicht in `eintraege`. Ein Versuch, der
@@ -321,6 +353,25 @@ kampagneRouter.post("/workspace/api/kampagne/einlesen", async (req, res) => {
"leerer Text");
}
/* ---- 0. Auf welche Seite gehört die Kachel? ------------------
Filipe: „ich will auswaehlen koennen wo es hingehoert, zu
tiktok oder der agentur." Die Auswahl kommt aus dem
Formular; fehlt sie oder steht etwas Fremdes darin, bleibt
es NULL -- und dann entscheidet wie bisher die Herkunft.
NICHT STILLSCHWEIGEND AUF 'tiktok' AUSWEICHEN: Das wäre
eine Entscheidung, die wie eine Auswahl aussieht. NULL
heißt „niemand hat gewählt", und das ist etwas anderes als
„links". */
const rohSeite = String(req.body?.seite ?? "").trim().toLowerCase();
/* `wunschSeite`, nicht `seite`: Weiter unten heisst die GEHOLTE
Seite so (`const seite = await holeSeite(...)`). Zwei Dinge mit
demselben Namen im selben Bereich sind genau die Sorte Falle,
die man beim naechsten Lesen nicht sieht -- hier hat sie der
Uebersetzer gefangen, beim naechsten Mal vielleicht nicht. */
const wunschSeite = rohSeite === "tiktok" || rohSeite === "agentur" ? rohSeite : null;
/* ---- 1. Den Link herauslösen -------------------------------- */
const link = linkAusText(text);
if (!link) {
@@ -392,6 +443,30 @@ kampagneRouter.post("/workspace/api/kampagne/einlesen", async (req, res) => {
}
/* ---- 6. Die Seite holen -------------------------------------- */
/* ---- IST DAS UEBERHAUPT EINE KAMPAGNENSEITE? ----------------
Vor dem Abruf, nicht danach: Wer eine Seite holt, von der er
schon weiss, dass sie nichts enthaelt, holt 233 KB und sagt
danach etwas Falsches („TikTok hat nicht geantwortet").
Die ausfuehrliche Messung steht bei `istCreatorNetwork`. Kurz:
Das Creator Network rendert nichts auf dem Server, und
dieselbe Kampagnennummer gibt es als oeffentliche Seite
nicht. Niemand kann das von aussen lesen -- auch kein
anderes Programm.
DIE MELDUNG SAGT, WAS ZU TUN IST. „Geht nicht" waere
richtig und nutzlos; was hilft, ist der Weg daneben: das
Event von Hand anlegen. */
if (istCreatorNetwork(adresse)) {
return schiefgegangen(422,
"Dieser Link führt ins Creator Network von TikTok. Diese Seiten "
+ "baut TikTok erst im Browser auf, angemeldet — von außen ist dort "
+ "nichts zu lesen. Leg das Event unten von Hand an und füge den "
+ "Link in die Beschreibung ein.",
`Creator Network: ${kurz(adresse)}`);
}
const seite = await holeSeite(adresse);
if (!seite.ok) {
if (seite.grund === "frist") {
@@ -482,8 +557,8 @@ kampagneRouter.post("/workspace/api/kampagne/einlesen", async (req, res) => {
event_aufgaben, event_regeln, dringlichkeit, status, nur_leitung,
erstellt, erstellt_von, haus,
kampagne_id, kampagne_quelle, kampagne_geholt_am, kampagne_zone,
kampagne_daten)
VALUES ('agentur','kampagne', ?,?,?,?, ?,?,?, 'offen', 0, ?,?,?, ?,?,?,?,?)`)
kampagne_daten, kampagne_seite)
VALUES ('agentur','kampagne', ?,?,?,?, ?,?,?, 'offen', 0, ?,?,?, ?,?,?,?,?,?)`)
.run(
gelesen.pflicht.titel.slice(0, 160),
kuerzen(gelesen.kuer.einleitung || ""),
@@ -502,7 +577,11 @@ kampagneRouter.post("/workspace/api/kampagne/einlesen", async (req, res) => {
Regel -- und die laufen auseinander. */
haus,
nummer, link, jetzt, gelesen.pflicht.zone,
JSON.stringify(gelesen.auszug));
JSON.stringify(gelesen.auszug),
/* NULL, wenn niemand gewaehlt hat -- dann entscheidet
wie bisher die Herkunft. Siehe die Begruendung bei
der Spalte in workspace.js. */
wunschSeite);
eintragId = Number(lastInsertRowid);
if (abgelegt) {
+30
View File
@@ -4582,6 +4582,36 @@ function umstellungen(d) {
["eintraege", "kampagne_zone", "TEXT"], // tz_location_name, z.B. Europe/Berlin
["eintraege", "kampagne_daten", "TEXT"], // JSON-Auszug der Rohwerte
/* ================================================================
AUF WELCHER SEITE STEHT DIESE KACHEL? (07.10.2026)
Filipe: „perfektioniere das und auch das ich auswaehlen kann
wo es hingehoert, zu tiktok oder der agentur."
Bis heute entschied das `kampagne_id`: Wer ueber eine
Einladung eingelesen wurde, stand links, alles andere
rechts. Das war eine Vermutung ueber die HERKUNFT, und sie
stimmte nicht immer: Die Agentur schickt ihre eigenen
Events ebenfalls als TikTok-Link -- gemessen am 07.10.2026
fuehrt `vm.tiktok.com/ZSbACwAnK/` auf
`tiktok.com/tcn/activity?...`, also ins Creator Network.
Herkunft und Zugehoerigkeit sind zwei verschiedene Fragen,
und nur die zweite entscheidet, wo die Kachel hingehoert.
DREI ZUSTAENDE, UND NULL IST EINER DAVON:
NULL niemand hat entschieden -> wie bisher nach
`kampagne_id`. Alle bestehenden Kacheln
behalten damit genau ihren Platz.
'tiktok' ausdruecklich links
'agentur' ausdruecklich rechts
KEIN CHECK AUF DIE WERTE in der Tabelle: Ein CHECK haette
einen Tabellenneubau verlangt (siehe oben), und genau dabei
sind in diesem Haus schon einmal drei Spalten verloren
gegangen. Die Werte prueft die Route -- an einer Stelle,
an der man den Fehler auch MELDEN kann. */
["eintraege", "kampagne_seite", "TEXT"], // NULL | tiktok | agentur
/* ================================================================
EIN EVENT NACH DRAUSSEN TEILEN (07.10.2026)