Kampagnen & Events: links TikTok, rechts die Agentur -- und ein Weg nach Discord
Vier Wuensche auf einmal, alle aus demselben Bildschirm.
LINKS TIKTOK, RECHTS DIE AGENTUR
Filipe: „die sollen perfekt getrennt sein."
Woran es haengt: `kampagne_id` ist genau dann gefuellt, wenn die
Kachel aus einer TikTok-Einladung eingelesen wurde. Keine Vermutung
aus dem Titel, kein zweites Feld, das jemand pflegen muss -- es
entsteht beim Einlesen und sonst nie.
Beide Spalten stehen IMMER, auch wenn eine leer ist: Eine Trennung,
die verschwindet, sobald eine Seite nichts hat, ist keine. Nur Events
werden getrennt; Schulungen und Anliegen stehen darunter ueber die
ganze Breite. Auf dem Handy untereinander, keine gequetschte Spalte.
„AGENTUR" HEISST JETZT „KAMPAGNEN & EVENTS"
Geaendert wurde der NAME, nicht der Schluessel. An `agentur` haengen
79 Zeilen in der Datenbank, der CHECK in eintraege.bereich, die
Adresse bereich.html?b=agentur und jedes Lesezeichen.
DABEI HABE ICH EINMAL ZU VIEL UMBENANNT: Das Feld „Gilt fuer" nennt
das HAUS, nicht das Brett -- der Server bildet es aus person.haus
(„Das Rudel", „Team Dogi", „Agentur"). „Gilt fuer: Kampagnen &
Events" waere Unsinn. pruef-agentur hat es gemeldet, und die Zeile
steht wieder richtig.
TEILEN NACH DISCORD -- mit einer Seite, die so wenig zeigt wie moeglich
Discord baut seine Vorschau selbst: Es ruft die Adresse ab, liest die
Open-Graph-Angaben und zeigt Bild, Titel, Text. Es meldet sich
NIRGENDS an. Eine Seite hinter der Wand ergibt im Chat einen nackten
Link -- oder die Vorschau der Anmeldeseite.
Es braucht also eine offene Seite. Die Frage ist nicht OB, sondern
WIE WENIG. Fuenf Regeln tragen den Schutz:
1. Nichts ist offen, bis jemand es oeffnet (Vorgabe ist zu).
2. Der Schluessel wird gewuerfelt (16 Byte), nicht gezaehlt.
3. Hinaus geht nur, was auf ein Plakat gehoert: Titel, Zeitraum,
Einleitung, Banner. KEINE Aufgaben, KEINE Regeln, keine Namen,
keine Rollen, keine Nummern.
4. Zurueckziehen wirkt sofort -- gemessen: danach 404.
5. noindex im HTML UND als X-Robots-Tag im Kopf der Antwort.
Ein Eintrag aus dem Haus Team Dogi laesst sich nicht teilen (403),
und beim zweiten Druck kommt DERSELBE Link -- ein neuer wuerde den
stillschweigend ins Leere laufen lassen, den jemand schon gepostet
hat.
Vier Gegenproben gefahren, alle vier schlagen an: jeder darf teilen,
Schluessel gezaehlt statt gewuerfelt, Aufgaben gehen mit hinaus,
Zurueckziehen wirkt nicht.
DIE KACHELN UND DER EINLESE-KASTEN
Herkunftsschild an jeder Eventkachel (TikTok blau, Agentur gelb) mit
farbigem Punkt, ein ruhiger Schein im eigenen Ton, der Zustandspunkt
pulst solange das Event laeuft. Der Einlese-Kasten hat einen
TikTok-blauen Streifen, eine auslaufende Linie hinter der
Ueberschrift und ein Feld, das beim Tippen waermer wird.
VIER TOTE REGELN, GEFUNDEN STATT GESCHRIEBEN
Beim Bauen zielten vier Regeln ins Leere, und keine haette man im
Bild gesehen:
.ev-stand__punkt gibt es nicht; der Punkt haengt an
.ev-noch::before
.marke-art[data-quelle] (0,2,0) verliert gegen
.eintrag-karte[...] .marke-art (0,3,0)
-- gemessen 10px statt 22px Polster
--q-ton je Herkunft dieselbe Falle eine Ebene tiefer:
gemessen kam der Ersatzton heraus
#liste > [data-event="…"] Direktkind -- seit der Trennung haengen
die Karten eine Ebene tiefer, und alle
drei Zustaende kamen wieder in
Agenturgelb heraus
Die letzte nahm ein Feature von gestern zurueck. pruef-eventkarte hat
sie gefunden; die Selektoren sind jetzt Nachkommen- statt
Direktkind-Selektoren -- die ueberleben den naechsten Umbau.
UND MEINE EIGENE MESSUNG LOG EINMAL: „sein Farbpunkt wird wirklich
gezeichnet" prueft auf /rgb/ -- "rgba(0, 0, 0, 0)" passt darauf. Ein
gruener Haken, der nichts angesehen hat. Sie verlangt jetzt eine
Farbe, und zwar die richtige.
GEPRUEFT (18 Dateien, alle gruen)
pruef-kampagne 103 -> 123 (Teilen) · pruef-eventkarte 94 (misst jetzt
die Trennung mit) · pruef-agentur 62 · pruef-kampagne-lesen 128 ·
bild-kampagne 20 -> 53 Messungen · struktur, css-klassen,
haus-trennung 107, alle-wege, schranke, zeichen 8, deutsche-texte 12,
lesbarkeit, tippziele, video 74, workspace-seiten, buehnen-fest 13,
formulare, fingermass 5
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -863,6 +863,123 @@ melde("=== 12. Team Dogi bleibt unberührt ===");
|
||||
`und die Zahl der Crew-Einträge ist unverändert (${crewVorher} = ${crewNachher})`);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
12b. TEILEN — eine offene Seite, und nur so viel wie nötig
|
||||
===================================================================== */
|
||||
melde("");
|
||||
melde("=== 12b. Ein Event nach draußen teilen ===");
|
||||
|
||||
{
|
||||
/* Der offene Weg laeuft ohne Anmeldung und ohne Keks -- so, wie
|
||||
Discord ihn abruft. `roh()` schickt zwar einen Host-Kopf mit,
|
||||
aber keinen Keks, wenn keiner uebergeben wird. */
|
||||
const offen = (pfad) => roh(pfad, { wirt: HOST });
|
||||
|
||||
/* ---- Vorher gibt es nichts ---------------------------------- */
|
||||
const vorher = await offen("/e/gibtesnichtgibtesnicht");
|
||||
ok(vorher.code === 404, `ein erfundener Schlüssel gibt 404 (${vorher.code})`);
|
||||
ok(!/war mal|zurückgezogen|zurueckgezogen/i.test(vorher.text),
|
||||
" und verrät nicht, ob es ihn einmal gab");
|
||||
|
||||
/* ---- Nur die Leitung darf teilen ----------------------------- */
|
||||
const alsLuna = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
|
||||
{ methode: "POST", keks: keksLuna, rumpf: {} });
|
||||
ok(alsLuna.code === 403, `ein Creator darf nicht teilen (${alsLuna.code})`);
|
||||
|
||||
/* ---- DogFather teilt ------------------------------------------ */
|
||||
const t1 = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
|
||||
{ methode: "POST", keks: keksDogi, rumpf: {} });
|
||||
const j1 = JSON.parse(t1.text || "{}");
|
||||
ok(t1.code === 200 && /\/e\/[A-Za-z0-9_-]{20,}$/.test(j1.weg || ""),
|
||||
`der Link entsteht: ${String(j1.weg).replace(/^https?:\/\/[^/]+/, "…")}`);
|
||||
|
||||
/* DERSELBE SCHLUESSEL BEIM ZWEITEN DRUCK. Ein neuer wuerde den
|
||||
Link still ins Leere laufen lassen, den jemand schon gepostet
|
||||
hat. */
|
||||
const t2 = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
|
||||
{ methode: "POST", keks: keksDogi, rumpf: {} });
|
||||
const j2 = JSON.parse(t2.text || "{}");
|
||||
ok(j2.weg === j1.weg && j2.schon === true,
|
||||
"der zweite Druck gibt denselben Link, keinen neuen");
|
||||
|
||||
const schluessel = String(j1.weg).split("/e/")[1];
|
||||
|
||||
/* ---- Die offene Seite ----------------------------------------- */
|
||||
const seiteOffen = await offen(`/e/${schluessel}`);
|
||||
ok(seiteOffen.code === 200, `die Seite ist ohne Anmeldung da (${seiteOffen.code})`);
|
||||
ok(/<meta property="og:title" content="Gipfelstürmer"/.test(seiteOffen.text),
|
||||
"og:title trägt den Titel — daraus baut Discord die Überschrift");
|
||||
ok(/<meta property="og:description" content="[^"]*2026/.test(seiteOffen.text),
|
||||
"og:description trägt den Zeitraum");
|
||||
ok(/<meta property="og:image" content="[^"]+\/bild"/.test(seiteOffen.text),
|
||||
"og:image zeigt auf das Banner");
|
||||
ok(/twitter:card" content="summary_large_image"/.test(seiteOffen.text),
|
||||
" und zwar als großes Bild, nicht als Briefmarke");
|
||||
ok(/noindex/.test(seiteOffen.text)
|
||||
&& /noindex/.test(String(seiteOffen.kopf["x-robots-tag"] || "")),
|
||||
"Suchmaschinen bleiben draußen — im HTML UND im Kopf der Antwort");
|
||||
|
||||
/* ---- UND NUR DAS GEHT HINAUS ---------------------------------- */
|
||||
const verboten = [
|
||||
["eine Aufgabe", "1 Diamant = 1 Punkt"],
|
||||
["der Regeltext", "KAMPAGNENGESCHENKE"],
|
||||
["ein Name", "Dogfather"],
|
||||
["eine Rolle", "admin"],
|
||||
["die Kampagnennummer", ID_GIPFEL],
|
||||
["ein Feldname aus der Datenbank", "erstellt_von"],
|
||||
["die Hausangabe", "\"haus\""],
|
||||
["ein Keks", "workspace_sitzung"],
|
||||
];
|
||||
/* NUR DER KOERPER, NICHT DAS STILBLATT (berichtigt 07.10.2026).
|
||||
Hier stand zuerst schlicht "agentur" als verbotenes Wort -- und
|
||||
die Pruefung wurde rot, weil im eingebetteten Stil die Regel
|
||||
[data-quelle="agentur"] steht. Das ist eine Auswahlregel, keine
|
||||
Auskunft ueber ein Haus. Eine Pruefung, die ein Wort im
|
||||
Stilblatt fuer ein Leck haelt, meldet jeden Tag einen Schaden,
|
||||
den es nicht gibt -- und wird nach dem zweiten Mal weggeklickt. */
|
||||
const ohneStil = seiteOffen.text.split('<style')[0] + (seiteOffen.text.split('</style>')[1] || '');
|
||||
const durchgerutscht = verboten.filter(([, wort]) => ohneStil.includes(wort));
|
||||
ok(durchgerutscht.length === 0,
|
||||
`nichts Internes auf der offenen Seite`
|
||||
+ (durchgerutscht.length ? `: ${durchgerutscht.map((x) => x[0]).join(", ")}` : ""));
|
||||
|
||||
/* GEGENPROBE: Das, was hinausgehen SOLL, steht auch wirklich da --
|
||||
sonst wäre die Zeile darüber auch bei einer leeren Seite grün. */
|
||||
ok(seiteOffen.text.includes("Gipfelstürmer")
|
||||
&& /04\.10\.2026/.test(seiteOffen.text),
|
||||
"Gegenprobe: Titel und Zeitraum stehen sehr wohl darauf");
|
||||
|
||||
/* ---- Das Banner ------------------------------------------------ */
|
||||
const bild = await offen(`/e/${schluessel}/bild`);
|
||||
ok(bild.code === 200 && /^image\//.test(String(bild.kopf["content-type"] || "")),
|
||||
`das Banner kommt ohne Anmeldung (${bild.code}, ${bild.kopf["content-type"]})`);
|
||||
|
||||
/* ---- Zurückziehen wirkt sofort -------------------------------- */
|
||||
const weg = await roh(`/workspace/api/eintrag/${kachelId}/teilen`,
|
||||
{ methode: "DELETE", keks: keksDogi });
|
||||
ok(weg.code === 200, `zurückziehen geht (${weg.code})`);
|
||||
const danach = await offen(`/e/${schluessel}`);
|
||||
ok(danach.code === 404, `und die Seite ist sofort weg (${danach.code})`);
|
||||
const bildDanach = await offen(`/e/${schluessel}/bild`);
|
||||
ok(bildDanach.code === 404, ` auch das Banner (${bildDanach.code})`);
|
||||
|
||||
/* ---- Was NICHT geteilt werden darf ---------------------------- */
|
||||
const fremd = dbLesen("SELECT id FROM eintraege WHERE haus = 'crew' LIMIT 1")[0];
|
||||
const crewTeilen = await roh(`/workspace/api/eintrag/${fremd.id}/teilen`,
|
||||
{ methode: "POST", keks: keksDogi, rumpf: {} });
|
||||
ok(crewTeilen.code === 403,
|
||||
`ein Eintrag aus dem Haus Team Dogi lässt sich nicht teilen (${crewTeilen.code})`);
|
||||
const crewZ = dbLesen("SELECT teilen_token FROM eintraege WHERE id = ?", fremd.id)[0];
|
||||
ok(crewZ.teilen_token === null,
|
||||
" und bekommt auch keinen Schlüssel");
|
||||
|
||||
/* DER EINDEUTIGE INDEX steht dahinter -- zwei Events mit demselben
|
||||
Schlüssel wären eine Seite, die zwei Dinge zeigt. */
|
||||
const idx = dbLesen("SELECT sql FROM sqlite_master WHERE name = 'idx_eintraege_teilen'");
|
||||
ok(idx.length === 1 && /UNIQUE/i.test(idx[0].sql),
|
||||
"ein eindeutiger Index sichert die Schlüssel ab");
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
13. DAS PROTOKOLL
|
||||
===================================================================== */
|
||||
|
||||
Reference in New Issue
Block a user