Kampagnen-Kachel aus einem TikTok-Link -- Etappe 2 bis 4
Der Auftrag ist damit fertig: Im Agentur-Bereich gibt es ein Feld, in das die ganze Einladungsnachricht eingefuegt wird -- und daraus entsteht eine Agentur-Event-Kachel mit Titel, Zeitraum, Banner, Aufgaben zum Abhaken, Regeln, Ligen und Preisen. Gemessen 38 ms vom Einfuegen bis zur Kachel. ETAPPE 2 -- DER WEG NACH DRAUSSEN (helfer-tiktok.mjs) linkAusText holt die Adresse aus dem Fliesstext, damit niemand sie heraussuchen muss. folgeUmleitung geht Sprung fuer Sprung (redirect "manual", hoechstens fuenf) und schickt JEDE Zwischenadresse durch istTikTok -- bisher folgte holeMitFrist den Umleitungen selbst, und istTikTok hatte nur die EINGABE gesehen. Das ist die einzige Stelle im Haus, an der unser Server eine fremdbestimmte Adresse abruft. holeSeite zieht die Grenze von 3 MB BEIM LESEN, nicht hinterher: Wer arrayBuffer() abwartet und dann die Laenge ansieht, hat die 50 MB schon im Speicher. Frist 12 s statt 8 -- gemessen 750 ms allein beim Ursprungsserver, dazu 1,1 MB Uebertragung. bildAdresseErlaubt kam beim Bauen dazu, nicht aus dem Bauplan: Die Banneradresse steht in der Seite, die TikTok ausliefert, ist also FREMDBESTIMMT. Ohne Schranke wuerde unser Server abrufen, was dort steht -- auch http://127.0.0.1:4100/ oder 169.254.169.254. Elf Faelle durchgemessen. ETAPPE 3 -- DIE ROUTE (workspace-kampagne.js, 5 Spalten, 1 Index) Rechte aus istLeitung, nicht neu erfunden. Bremse 20 je Stunde, VOR dem ersten Abruf nach draussen -- eine Bremse hinter dem Abruf bremst den fremden Server nicht. Dublettenpruefung zweimal: im Code (faengt den Normalfall) und als eindeutiger Index (faengt den Wettlauf zweier gleichzeitiger Aufrufe). 'kampagne_schon_da' zaehlt bei der Bremse MIT (gefunden von der Pruefung): Zuerst zaehlten nur Erfolg und Fehlschlag -- wer denselben Link wieder und wieder einfuegt, bekommt jedes Mal "schon da" und loeste jedes Mal einen Abruf aus, ohne gezaehlt zu werden. Gemessen: 21 Versuche, 0 gebremst. Der Regeltext wird SICHTBAR gekuerzt, an einer Abschnittsgrenze. Gipfelstuermer ergibt 7474 Zeichen, das Feld fasst 6000 (TEXT_MAX), und die PUT-Route lehnt mehr ab: Ungekuerzt entstuende eine Kachel, die DogFather OEFFNEN, aber nicht SPEICHERN kann -- er aendert ein Wort und bekommt eine Fehlermeldung ueber etwas, das er nie getippt hat. Verloren geht nichts: kampagne_daten traegt das Gelesene gegliedert. ETAPPE 4 -- DIE OBERFLAECHE Ein Textbereich (die Einladung ist mehrzeilig), der Knopf gibt sich nach 15 s von selbst frei, die Meldung steht neben dem Feld und nennt den Grund im Klartext. Ob jemand darf, sagt der Server ueber darf_kampagne_einlesen in /api/ich -- keine Rollenliste im Browser. Gemessen auf 412 und 1280 px (bild-kampagne.mjs, 20 Messungen): Knopf 44 px, Kontrast 7,68:1, nichts liegt auf dem Knopf, kein Ueberlauf, keine Bewegung bei prefers-reduced-motion. Die Antwortzeile steht auf .9rem statt der hausweiten .76rem -- dort erscheint, was der Server geantwortet hat, und eine Fehlermeldung in 12 px liest niemand zweimal. Die hausweite Klasse bleibt unberuehrt. VIERZEHN SABOTAGEN, VIERZEHN TREFFER Sieben an kampagne-lesen.mjs (Etappe 1) und sieben an Route und Weg: Umleitung ungeprueft folgen, Tag nach Serverzeit, Dublettenpruefung weglassen, dem fremden Server den Bildtyp glauben, Bremse abschalten, Bremse wieder blind fuer Dubletten, jeden einlesen lassen. Jede wurde bemerkt, jede im erwarteten Abschnitt. EINE BLIEB ZUERST BLIND, und das war lehrreich: Nimmt man die Dublettenpruefung aus dem Code, faengt der INDEX es auf -- die Antwort ist dieselbe, die Pruefung blieb gruen. Kein Schaden, aber ein blinder Fleck. Jetzt unterscheidet die Pruefung am Protokolltext, WELCHE der beiden Sicherungen gegriffen hat. VIER VORBESTEHENDE BEFUNDE MIT ERLEDIGT willkommen.html hatte kein theme-color, kein Manifest, kein apple-touch-icon. Drei Zeilen sind billiger und haltbarer als eine Ausnahme in der Pruefung. workspace-anleitung.js bildete den Tag "ab wann geht mehr" aus UTC. Wer zwischen 00:00 und 02:00 beitritt, bekam einen Termin, der einen Tag zu frueh war. workspace.js hat dafuer jetzt tagLokalVon(zeitpunkt, versatz) -- und tagLokal ist nur noch ein Aufruf davon. Es gibt also WENIGER Rechenwege als vorher, nicht mehr. pruef-anleitung.mjs legte Testdaten auf den UTC-Tag und waere zwischen 00:00 und 02:00 rot gewesen, ohne dass etwas kaputt ist. mess-anleitung-crew.mjs rechnete seinen zweiten Port als PORT + 1. Das sieht aus wie eine Ableitung und ist keine: portNummer ueberspringt gesperrte Nummern, und dann gehoert PORT + 1 der naechsten DATEI. pruef-struktur ist damit zum ersten Mal vollstaendig gruen. GEPRUEFT (alle gruen, keine Zahl gesunken) pruef-kampagne 96 (neu) · pruef-kampagne-lesen 108 · pruef-video 74 · pruef-eventkarte 94 · pruef-agentur 62 · pruef-anleitung 196 · pruef-portnummern 41 (war 40 mit einem Fehler) · pruef-struktur, pruef-zeichen 8, pruef-ports 10, pruef-buehnen-fest 13, pruef-css-klassen 39, pruef-haus-trennung 107, pruef-lesbarkeit, pruef-tippziele, pruef-deutsche-texte 12, pruef-formulare, pruef-fingermass 5, pruef-workspace-seiten, pruef-tempo-workspace Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -3264,3 +3264,94 @@ body[data-ton] .kopf-zeile::before {
|
||||
@media (forced-colors: active) {
|
||||
.ev-zaehler { border: 1px solid CanvasText; }
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
EINE KAMPAGNE AUS EINER EINLADUNG (07.10.2026) — Etappe 4
|
||||
|
||||
Gebaut auf `.videoholen` darueber: gleicher Rahmen, gleicher
|
||||
Abstand, gleiche Flaeche. Ein zweiter Kastenstil fuer dieselbe Sache
|
||||
waere zwei Fassungen, die auseinanderlaufen -- und der Unterschied
|
||||
fiele nur dem auf, der beide Bereiche nacheinander ansieht.
|
||||
|
||||
WAS HIER ANDERS IST UND WARUM:
|
||||
|
||||
`textarea` statt `input`. Die Einladung ist mehrzeilig; wer sie in
|
||||
ein Einzeilenfeld einfuegt, verliert die Umbrueche und sieht nur das
|
||||
Ende. `resize: vertical` -- wer einen langen Text prueft, soll ihn
|
||||
aufziehen koennen, aber nicht in die Breite (das zerreisst das
|
||||
Raster).
|
||||
|
||||
`min-height: 44px` am Knopf. Das ist die Zahl aus
|
||||
pruef-handy-teamdogi: Unter 44 Pixel trifft ein Daumen nicht
|
||||
verlaesslich. Am 07.10.2026 lagen fuenf Flaechen in diesem Haus bei
|
||||
40 und wurden genau deshalb auf 44 gezogen.
|
||||
===================================================================== */
|
||||
.kampagneholen {
|
||||
margin: 0 0 20px; padding: 16px 18px;
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: var(--radius);
|
||||
background: var(--flaeche);
|
||||
}
|
||||
.kampagneholen__form { margin: 0; }
|
||||
.kampagneholen__titel {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1rem; font-weight: 600;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
.kampagneholen textarea {
|
||||
display: block; width: 100%; box-sizing: border-box;
|
||||
min-height: 72px; resize: vertical;
|
||||
font: inherit; font-size: .9rem; line-height: 1.5;
|
||||
}
|
||||
|
||||
/* DIE MELDUNG LINKS, DER KNOPF RECHTS -- und die Meldung darf wachsen.
|
||||
`flex-wrap: wrap` statt einer festen Schwelle: Es bricht genau dann
|
||||
um, wenn der Platz nicht reicht. Eine Zahl an dieser Stelle waere
|
||||
eine Rechnung von gestern -- genau der Fehler, der am 06.09.2026 in
|
||||
der Kopfleiste zweimal an einem Tag passiert ist. */
|
||||
.kampagneholen__zeile {
|
||||
display: flex; flex-wrap: wrap; align-items: center;
|
||||
gap: 10px; margin-top: 12px;
|
||||
}
|
||||
/* HIER STEHT KEIN HINWEIS, SONDERN DIE ANTWORT (07.10.2026).
|
||||
|
||||
Gemessen mit bild-kampagne: Die hausweite Klasse .feld-hinweis
|
||||
steht auf .76rem = 12,16 px. Fuer einen Hinweis unter einem Feld
|
||||
ist das richtig, und sie bleibt dort unangetastet -- sie gilt auf
|
||||
fuenf Seiten, und eine Schriftgroesse hausweit zu aendern ist
|
||||
keine Entscheidung, die in diese Kachel gehoert.
|
||||
|
||||
An DIESER Stelle ist es aber etwas anderes: Hier erscheint, was
|
||||
der Server geantwortet hat -- "Dieser Link fuehrt zu beispiel.de,
|
||||
nicht zu TikTok", "TikTok hat nicht geantwortet (12 s)". Das ist
|
||||
die einzige Rueckmeldung, die der Mensch bekommt, und eine
|
||||
Fehlermeldung in 12 px liest niemand zweimal. Deshalb .9rem
|
||||
(14,4 px) -- nur hier, mit Begruendung.
|
||||
|
||||
Der Kontrast ist mit 7,68:1 ohnehin reichlich (gemessen); zu
|
||||
klein war allein die Groesse. */
|
||||
.kampagneholen__zeile .feld-hinweis {
|
||||
flex: 1 1 16rem; min-width: 0; margin: 0;
|
||||
font-size: .9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.kampagneholen__zeile .knopf {
|
||||
flex: 0 0 auto;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* WAEHREND ES LAEUFT. Ein ruhiges Atmen, kein Blinken -- und bei
|
||||
`prefers-reduced-motion` gar nichts. Augenschonend heisst hier
|
||||
ausdruecklich: kein Flackern, kein greller Zustandston, nur ein
|
||||
leicht gedaempfter Kasten, der sagt "ich arbeite noch". */
|
||||
.kampagneholen__form[data-laeuft="ja"] textarea { opacity: .6; }
|
||||
.kampagneholen__form[data-laeuft="ja"] .feld-hinweis {
|
||||
animation: kampagne-atmen 2.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes kampagne-atmen {
|
||||
0%, 100% { opacity: .65; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kampagneholen__form[data-laeuft="ja"] .feld-hinweis { animation: none; }
|
||||
}
|
||||
|
||||
@@ -2387,6 +2387,107 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- EINE KAMPAGNE AUS EINER EINLADUNG (07.10.2026) ------------------
|
||||
|
||||
Etappe 4 des Bauplans. Gebaut nach dem Vorbild des Videokastens
|
||||
darueber -- derselbe Weg, dieselben Worte, dieselbe Stelle fuer die
|
||||
Meldung. Ein zweites Muster fuer dieselbe Sache waere die Sorte
|
||||
Dopplung, die beim naechsten Umbau auseinanderlaeuft. */
|
||||
|
||||
function kampagneFeldZeigen() {
|
||||
const ziel = $('kampagneholen');
|
||||
if (!ziel) return;
|
||||
/* NUR im Agentur-Bereich UND nur, wenn der Server das Recht
|
||||
bestaetigt. `darf_kampagne_einlesen` kommt aus /api/ich und
|
||||
stammt aus derselben Regel wie die Schranke dahinter -- hier
|
||||
steht keine Rollenliste. Faellt die Auskunft aus, ist `ich`
|
||||
null und der Kasten bleibt zu: Ein Feld, das der Server
|
||||
ablehnt, laesst den Menschen davor sich selbst fuer den Fehler
|
||||
halten. */
|
||||
ziel.hidden = !(bereich === 'agentur' && ich && ich.darf_kampagne_einlesen === true);
|
||||
}
|
||||
|
||||
function kampagneFeldBedienen() {
|
||||
const form = $('kampagne-form');
|
||||
if (!form || form.dataset.bereit === 'ja') return;
|
||||
form.dataset.bereit = 'ja';
|
||||
|
||||
const feld = $('kampagne-text');
|
||||
const knopf = $('kampagne-holen');
|
||||
const sagt = $('kampagne-sagt');
|
||||
const ruheText = sagt ? sagt.textContent : '';
|
||||
|
||||
/* DER KNOPF GIBT SICH VON SELBST WIEDER FREI.
|
||||
|
||||
Der Server hat zwoelf Sekunden Frist fuer TikTok, dazu das
|
||||
Banner. Bleibt die Antwort ganz aus -- Netz weg, Browser
|
||||
schlafend --, blieb der Knopf bisher fuer immer gesperrt. Ein
|
||||
Knopf, der fuer immer gesperrt bleibt, ist schlimmer als einer,
|
||||
der scheitert: Beim zweiten ist klar, dass man es nochmal
|
||||
versuchen kann.
|
||||
|
||||
15 Sekunden, nicht 12: Die Frist des Servers plus Luft fuer die
|
||||
Antwort. Wer hier 12 setzt, gibt frei, waehrend der Server noch
|
||||
arbeitet -- und ein zweiter Druck schickt denselben Link ein
|
||||
zweites Mal. */
|
||||
const FREI_NACH_MS = 15000;
|
||||
let wecker = null;
|
||||
const sperren = () => {
|
||||
knopf.disabled = true;
|
||||
form.dataset.laeuft = 'ja';
|
||||
if (sagt) sagt.textContent = 'Wird geholt …';
|
||||
clearTimeout(wecker);
|
||||
wecker = setTimeout(() => {
|
||||
knopf.disabled = false;
|
||||
delete form.dataset.laeuft;
|
||||
if (sagt) sagt.textContent = 'Das dauert länger als erwartet. '
|
||||
+ 'Du kannst es noch einmal versuchen.';
|
||||
}, FREI_NACH_MS);
|
||||
};
|
||||
const freigeben = (text) => {
|
||||
clearTimeout(wecker);
|
||||
knopf.disabled = false;
|
||||
delete form.dataset.laeuft;
|
||||
if (sagt) sagt.textContent = text || ruheText;
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const text = (feld?.value || '').trim();
|
||||
if (!text) { freigeben('Da war kein Text. Füge die ganze Einladung ein.'); return; }
|
||||
sperren();
|
||||
try {
|
||||
const a = await hole('/workspace/api/kampagne/einlesen', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
const r = await a.json().catch(() => ({}));
|
||||
if (!a.ok) {
|
||||
/* DER GRUND DES SERVERS, nicht "Ging nicht". Er weiss, ob
|
||||
kein Link da war, ob der Link wegfuehrte oder ob TikTok
|
||||
nicht geantwortet hat -- und das sind drei verschiedene
|
||||
Dinge, die drei verschiedene Handgriffe brauchen. */
|
||||
freigeben(sagWas(r.fehler, 'Ging nicht.'));
|
||||
return;
|
||||
}
|
||||
if (feld) feld.value = '';
|
||||
if (r.schon) {
|
||||
freigeben('Diese Kampagne war schon da — die Kachel steht unten.');
|
||||
} else {
|
||||
const spanne = r.von && r.bis ? ` (${r.von} bis ${r.bis})` : '';
|
||||
const luecken = (r.hinweise || []).length
|
||||
? ` ${r.hinweise.length} Angabe(n) fehlten — sie stehen im Regeltext.`
|
||||
: '';
|
||||
freigeben(`„${r.titel || 'Kampagne'}“ ist angelegt${spanne}.${luecken}`);
|
||||
}
|
||||
await laden();
|
||||
} catch (f) {
|
||||
void f;
|
||||
freigeben('Die Anfrage kam nicht durch. Bitte noch einmal versuchen.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- DER STARTKATALOG ------------------------------------------------
|
||||
Fertige Texte, die jemand aus dem Team uebernimmt. Nicht
|
||||
automatisch: Ein Text, den niemand gelesen hat, stuende sonst als
|
||||
@@ -2614,6 +2715,8 @@
|
||||
startkatalogZeigen();
|
||||
videoFeldZeigen();
|
||||
videoFeldBedienen();
|
||||
kampagneFeldZeigen();
|
||||
kampagneFeldBedienen();
|
||||
maxStimmen = (daten.eintraege || [])
|
||||
.reduce((n, e) => Math.max(n, Number(e.stimmen) || 0), 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user