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`. --> - - + + - +