From 2764aea61cf4a7aa2122cff8e14eb2b020f5a5f8 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 7 Oct 2026 23:46:41 +0200 Subject: [PATCH] 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 --- server/pruef-eventkarte.mjs | 168 +++++++++++++++++++++++++++++-- server/pruef-kampagne.mjs | 151 +++++++++++++++++++++++++++ server/workspace-bereiche.js | 38 +++++++ server/workspace-kampagne.js | 87 +++++++++++++++- server/workspace.js | 30 ++++++ workspace/anleitung.html | 28 +++--- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/bereich.css | 82 +++++++++++++++ workspace/assets/js/bereich.js | 67 +++++++++++- workspace/aufgaben.html | 38 +++---- workspace/automation.html | 32 +++--- workspace/befinden.html | 34 +++---- workspace/bereich.html | 80 ++++++++++----- workspace/bewerben.html | 38 +++---- workspace/bewerbungen.html | 36 +++---- workspace/buehne.html | 6 +- workspace/calls.html | 34 +++---- workspace/chat.html | 42 ++++---- 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/manager-ziele.html | 28 +++--- workspace/material.html | 32 +++--- workspace/notizen.html | 28 +++--- workspace/personen.html | 32 +++--- workspace/profil.html | 36 +++---- workspace/reaktion.html | 38 +++---- workspace/rechte.html | 34 +++---- workspace/report.html | 36 +++---- workspace/scouting.html | 32 +++--- workspace/start.html | 38 +++---- workspace/startcheck.html | 34 +++---- workspace/steckbrief.html | 36 +++---- workspace/support.html | 28 +++--- workspace/tafel.html | 6 +- 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 | 2 +- workspace/wissen.html | 34 +++---- 54 files changed, 1333 insertions(+), 702 deletions(-) diff --git a/server/pruef-eventkarte.mjs b/server/pruef-eventkarte.mjs index 147e3334..229f2410 100644 --- a/server/pruef-eventkarte.mjs +++ b/server/pruef-eventkarte.mjs @@ -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); diff --git a/server/pruef-kampagne.mjs b/server/pruef-kampagne.mjs index d9a12fa7..9501c8b9 100644 --- a/server/pruef-kampagne.mjs +++ b/server/pruef-kampagne.mjs @@ -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 ===================================================================== */ diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 5358afdb..ee8bbdec 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -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 diff --git a/server/workspace-kampagne.js b/server/workspace-kampagne.js index c9925428..4527e22f 100644 --- a/server/workspace-kampagne.js +++ b/server/workspace-kampagne.js @@ -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) { diff --git a/server/workspace.js b/server/workspace.js index c9aec878..57c457b3 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -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) diff --git a/workspace/anleitung.html b/workspace/anleitung.html index 4b845031..7e4c0eab 100644 --- a/workspace/anleitung.html +++ b/workspace/anleitung.html @@ -7,17 +7,17 @@ - - - - - + + + + + - - + + @@ -289,16 +289,16 @@ Und anleitung.js braucht es doppelt: Symbol, Farbe, Name und Adresse jeder Karte kommen von dort, nicht vom Server. --> - + - - - - - - + + + + + + diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index db3fa609..5267d2ba 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`. --> - - + + - +