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:
2026-10-07 17:14:26 +02:00
co-authored by Claude Opus 5
parent 6156eb3a05
commit 10e24b0c93
59 changed files with 3025 additions and 708 deletions
+264
View File
@@ -0,0 +1,264 @@
/* Bild und Mass des Kastens „Kampagne aus einer Einladung" (07.10.2026)
Etappe 4 des Bauplans. Die Zahlen sind gruen (pruef-kampagne, 94
Pruefungen) -- das sagt, dass der Weg stimmt, aber nicht, wie es
AUSSIEHT und ob man es mit dem Daumen treffen kann.
GEMESSEN WIRD AUF 412 UND 1280 PIXEL. Das sind nicht zwei beliebige
Breiten: An genau diesen beiden ist die Kopfleiste dieses Hauses am
06.09.2026 zweimal an einem Tag gebrochen -- 412 px ist die
haeufigste Android-Breite, 1280 px die schmalste, auf der die Seite
als „Rechner" gilt. Wer nur bei 1400 nachsieht, sieht beides nicht.
UND AUGENSCHONUNG WIRD GEMESSEN, NICHT ANGENOMMEN: Kontrast des
Hinweistextes, und dass bei `prefers-reduced-motion` keine Bewegung
laeuft.
Aufrufen mit: node server/bild-kampagne.mjs
*/
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { notbremse } from "./helfer-notbremse.mjs";
notbremse(240_000, "bild-kampagne");
const ordner = mkdtempSync(join(tmpdir(), "ws-kmpbild-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { portMussFreiSein } = await import("./helfer-port.mjs");
const PORT = await portMussFreiSein(4338, "das Bildwerkzeug Kampagne");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) {
return ec.call(this, n, w, { ...(o || {}), secure: false });
};
await import("./index.js");
await new Promise((r) => setTimeout(r, 1200));
const BASIS = `http://127.0.0.1:${PORT}`;
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const j = new Date().toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name,rolle,code_hash,code_salt,code_n,aktiv,erstellt)"
+ " VALUES (?,?,?,?,?,1,?)").run(name, rolle, hash, salt, 32768, j);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Luna", "creator", "CODE-CREA-0001");
d.close();
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch();
const fehlerAufSeite = [];
const anmelden = async (seite, rolle, code) => {
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
await seite.click(`.rolle[data-rolle="${rolle}"]`);
await seite.fill("#code", code);
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 25000 });
};
/** Der Kontrast zweier Farben nach WCAG. Gerechnet, nicht geschaetzt. */
const leuchte = ([r, g, b]) => {
const f = (c) => { const x = c / 255; return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; };
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
};
const kontrast = (a, b) => {
const [x, y] = [leuchte(a), leuchte(b)].sort((p, q) => q - p);
return (x + 0.05) / (y + 0.05);
};
const farbe = (s) => {
const m = /rgba?\(([^)]+)\)/.exec(s || "");
if (!m) return null;
const t = m[1].split(",").map((x) => parseFloat(x));
return [t[0], t[1], t[2]];
};
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
async function messen(breite, hoehe, name) {
const ctx = await browser.newContext({
viewport: { width: breite, height: hoehe },
deviceScaleFactor: 2,
hasTouch: breite < 700, isMobile: breite < 700,
});
const seite = await ctx.newPage();
seite.on("console", (m) => { if (m.type() === "error") fehlerAufSeite.push(m.text()); });
seite.on("pageerror", (f) => fehlerAufSeite.push(String(f)));
await anmelden(seite, "admin", "CODE-DOGI-0001");
await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" });
await seite.waitForTimeout(1600);
console.log(`\n=== ${name} (${breite} px) ===`);
const mass = await seite.evaluate(() => {
const k = document.getElementById("kampagneholen");
if (!k || k.hidden) return { da: false };
const r = (e) => { const b = e.getBoundingClientRect();
return { x: Math.round(b.x), y: Math.round(b.y),
b: Math.round(b.width), h: Math.round(b.height) }; };
const feld = document.getElementById("kampagne-text");
const knopf = document.getElementById("kampagne-holen");
const sagt = document.getElementById("kampagne-sagt");
const titel = k.querySelector(".kampagneholen__titel");
const st = getComputedStyle(sagt);
return {
da: true,
kasten: r(k), feld: r(feld), knopf: r(knopf), sagt: r(sagt), titel: r(titel),
knopfText: knopf.textContent.trim(),
sagtText: sagt.textContent.trim(),
textFarbe: st.color,
kastenFarbe: getComputedStyle(k).backgroundColor,
seitenFarbe: getComputedStyle(document.body).backgroundColor,
schrift: st.fontSize,
/* Ueberdeckt etwas den Knopf? Nicht gerechnet, sondern gefragt:
Was liegt an seinem Mittelpunkt wirklich obenauf? */
obenAufKnopf: (() => {
const b = knopf.getBoundingClientRect();
const e = document.elementFromPoint(b.x + b.width / 2, b.y + b.height / 2);
return e ? (e.id || e.className || e.tagName) : null;
})(),
ueberlauf: k.scrollWidth > document.documentElement.clientWidth + 1,
};
});
ok(mass.da, "der Kasten ist da");
if (!mass.da) { await ctx.close(); return; }
console.log(` Kasten x=${mass.kasten.x} b=${mass.kasten.b} h=${mass.kasten.h}`);
console.log(` Titel „${"Kampagne aus einer Einladung anlegen"}" h=${mass.titel.h}`);
console.log(` Feld x=${mass.feld.x} b=${mass.feld.b} h=${mass.feld.h}`);
console.log(` Knopf x=${mass.knopf.x} b=${mass.knopf.b} h=${mass.knopf.h} „${mass.knopfText}"`);
console.log(` Hinweis x=${mass.sagt.x} b=${mass.sagt.b} h=${mass.sagt.h}`);
/* 44 PIXEL. Die Zahl aus pruef-handy-teamdogi: Darunter trifft ein
Daumen nicht verlaesslich. Am 07.10.2026 lagen fuenf Flaechen in
diesem Haus bei 40 und wurden genau deshalb gezogen. */
ok(mass.knopf.h >= 44, `der Knopf ist ${mass.knopf.h} px hoch (mindestens 44)`);
ok(mass.feld.b > 120, `das Textfeld ist ${mass.feld.b} px breit -- kein Schlitz`);
ok(mass.feld.h >= 72, `und ${mass.feld.h} px hoch, also mehrzeilig`);
/* NICHTS LIEGT AUF DEM KNOPF. Am 06.09.2026 lag der Sicht-Umschalter
quer ueber dem Chat-Knopf: Wer auf „Meine Sicht" tippte, landete
im Chat -- auf zwoelf Seiten. */
ok(String(mass.obenAufKnopf).includes("kampagne-holen"),
`am Mittelpunkt des Knopfes liegt der Knopf (${mass.obenAufKnopf})`);
ok(!mass.ueberlauf, "der Kasten laeuft nicht aus der Seite heraus");
/* BRICHT ES UM, WENN DER PLATZ NICHT REICHT? Keine feste Schwelle,
sondern `flex-wrap` -- gemessen daran, ob Knopf und Hinweis bei
412 px untereinander stehen. */
const nebeneinander = Math.abs(mass.knopf.y - mass.sagt.y) < 10;
console.log(` Knopf und Hinweis ${nebeneinander ? "nebeneinander" : "untereinander"}`);
/* AUGENSCHONEND, GERECHNET. */
const vorn = farbe(mass.textFarbe);
const hinten = farbe(mass.kastenFarbe)?.[0] !== undefined
&& farbe(mass.kastenFarbe) || farbe(mass.seitenFarbe);
if (vorn && hinten) {
const k = kontrast(vorn, hinten);
ok(k >= 4.5, `Kontrast des Hinweises ${k.toFixed(2)}:1 (mindestens 4,5)`);
} else {
ok(false, `Farben nicht messbar: ${mass.textFarbe} auf ${mass.kastenFarbe}`);
}
ok(parseFloat(mass.schrift) >= 13,
`Schriftgroesse ${mass.schrift} (mindestens 13 px)`);
const kasten = await seite.$("#kampagneholen");
await kasten.screenshot({ path: `server/bild-kampagne-${breite}.png` });
/* UND WIE ES WAEHREND DES HOLENS AUSSIEHT. */
await seite.evaluate(() => {
document.getElementById("kampagne-form").dataset.laeuft = "ja";
document.getElementById("kampagne-sagt").textContent = "Wird geholt …";
});
await seite.waitForTimeout(300);
await kasten.screenshot({ path: `server/bild-kampagne-${breite}-laeuft.png` });
await ctx.close();
}
await messen(1280, 900, "Am Rechner, schmalste Rechnerbreite");
await messen(412, 915, "Auf dem Handy, haeufigste Android-Breite");
/* ---- Keine Bewegung, wenn jemand keine will -------------------------- */
{
const ctx = await browser.newContext({
viewport: { width: 412, height: 915 }, deviceScaleFactor: 2,
hasTouch: true, isMobile: true, reducedMotion: "reduce",
});
const seite = await ctx.newPage();
await anmelden(seite, "admin", "CODE-DOGI-0001");
await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" });
await seite.waitForTimeout(1200);
const ruht = await seite.evaluate(() => {
const f = document.getElementById("kampagne-form");
f.dataset.laeuft = "ja";
const s = getComputedStyle(document.getElementById("kampagne-sagt"));
return { animation: s.animationName, dauer: s.animationDuration };
});
console.log("\n=== Wer keine Bewegung will ===");
ok(ruht.animation === "none" || ruht.dauer === "0s",
`keine Bewegung bei prefers-reduced-motion (${ruht.animation}, ${ruht.dauer})`);
}
/* ---- Und ein Creator sieht den Kasten gar nicht --------------------- */
{
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const seite = await ctx.newPage();
await anmelden(seite, "creator", "CODE-CREA-0001");
await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" });
await seite.waitForTimeout(1400);
const sicht = await seite.evaluate(() => {
const k = document.getElementById("kampagneholen");
return { da: !!k, versteckt: k ? k.hidden : null,
sichtbar: k ? k.getBoundingClientRect().height > 0 : null };
});
console.log("\n=== Ein Creator ===");
ok(sicht.versteckt === true && sicht.sichtbar === false,
`der Kasten bleibt zu (hidden=${sicht.versteckt}, Hoehe>0=${sicht.sichtbar})`);
await ctx.close();
}
/* ---- Und auf einem anderen Brett steht er nicht --------------------- */
{
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const seite = await ctx.newPage();
await anmelden(seite, "admin", "CODE-DOGI-0001");
await seite.goto(BASIS + "/workspace/bereich.html?b=technik", { waitUntil: "networkidle" });
await seite.waitForTimeout(1400);
const sicht = await seite.evaluate(() => {
const k = document.getElementById("kampagneholen");
return k ? k.hidden : null;
});
console.log("\n=== Auf dem Brett Technik ===");
ok(sicht === true, `dort bleibt der Kasten zu (hidden=${sicht})`);
await ctx.close();
}
console.log("\n=== Browser-Meldungen ===");
ok(fehlerAufSeite.length === 0,
fehlerAufSeite.length ? fehlerAufSeite.slice(0, 4).join(" | ") : "keine Fehler");
console.log(`\n${geprueft} Messungen, ${fehler} Fehler`);
console.log("Bilder: server/bild-kampagne-1280.png, -412.png und je -laeuft.png");
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }
process.exit(fehler ? 1 : 0);
+247 -9
View File
@@ -1,10 +1,11 @@
/* =====================================================================
DER WEG NACH DRAUSSEN ZU TIKTOK — gemeinsam genutzt (07.10.2026)
Diese Datei ist ein VERSCHIEBEN, keine Neuerfindung. `istTikTok` und
`holeMitFrist` standen bis heute in `workspace-video.js` und haben
sich dort bewährt — inklusive der Reparatur vom 05.10.2026, bei der
auffiel, dass die Frist nur den Anfang der Anfrage bewachte.
Diese Datei begann als ein VERSCHIEBEN, keine Neuerfindung.
`istTikTok` und `holeMitFrist` standen bis heute in
`workspace-video.js` und haben sich dort bewährt — inklusive der
Reparatur vom 05.10.2026, bei der auffiel, dass die Frist nur den
Anfang der Anfrage bewachte.
Ab jetzt holt auch die Kampagnen-Kachel eine Seite von TikTok. Zwei
Fassungen derselben Regel wären der Fehler, den CLAUDE.md am
@@ -29,11 +30,6 @@
Kampagnenprüfung die echte Regel umgehen und prüfte damit eine
andere als die, die im Betrieb gilt.
Sonst nichts. Kein neues Verhalten, keine neue Grenze, keine neue
Entscheidung. Wer hier etwas vermisst (Umleitungen Station für
Station, Größengrenze beim Lesen), findet es in Etappe 2 — nicht
hier, und nicht halb.
---------------------------------------------------------------------
KEINE DATENBANK, KEIN EXPRESS
@@ -68,6 +64,248 @@ export function istTikTok(roh) {
} catch { return false; }
}
/* =====================================================================
DER WEG ZU EINER GANZEN SEITE (07.10.2026) — Etappe 2
Bis hierher holte dieses Haus bei TikTok nur kleine Auskuenfte: eine
JSON-Antwort zu einem Video, ein Vorschaubild. Eine Kampagnenseite
ist etwas anderes -- 1,1 MB serverseitig gerendertes HTML, und die
Adresse dahin kommt aus einem Text, den ein Mensch eingefuegt hat.
DAS IST DIE EINZIGE STELLE IM GANZEN HAUS, an der unser Server eine
FREMDBESTIMMTE Adresse abruft. Ueberall sonst steht das Ziel im
Quelltext. Deshalb stehen hier drei Grenzen, und jede hat einen
gemessenen Grund:
1. JEDE STATION EINZELN.
holeMitFrist folgt Umleitungen selbst (redirect "follow"). Das
heisst: istTikTok() hat nur die EINGABE gesehen, und wohin der
Kurzlink wirklich fuehrt, weiss niemand. Hier wird deshalb
Sprung fuer Sprung gegangen (redirect "manual", hoechstens
fuenf), und JEDE Zwischenadresse geht durch istTikTok().
Verlaesst die Kette TikTok, bricht sie ab und sagt, wohin.
2. ZWOELF SEKUNDEN, NICHT ACHT.
Gemessen am 07.10.2026: allein 750 ms beim Ursprungsserver, dazu
die Uebertragung von 1,1 MB. Acht Sekunden reichen an einem
langsamen Nachmittag nicht, und eine Frist, die im Normalfall
zuschlaegt, ist keine Grenze, sondern ein Fehler.
3. DREI MEGABYTE, GEZOGEN BEIM LESEN.
Nicht hinterher geprueft. Wer erst arrayBuffer() abwartet und
dann die Laenge ansieht, hat die 50 MB schon im Speicher, bevor
er sie ablehnt -- die Grenze schuetzt dann genau nichts. Gelesen
wird deshalb stueckweise, und beim Ueberschreiten wird die
Verbindung abgebrochen.
Dazu ein ehrlicher Erkennungsname: Wer gefragt wird, soll sehen,
wer fragt. Ein Abruf, der sich als Browser ausgibt, ist eine Luege
gegenueber einem Dienst, der uns eingeladen hat.
===================================================================== */
/* Fuer eine ganze Seite. Siehe Grund 2 oben. */
export const FRIST_SEITE_MS = 12_000;
/* Siehe Grund 3. 1,1 MB ist der gemessene Normalfall, 3 MB lassen
Luft fuer eine Kampagne mit mehr Bausteinen -- und sind weit unter
dem, was einem Server wehtut. */
export const SEITE_MAX = 3 * 1024 * 1024;
/* Hoechstens fuenf Spruenge. Ein Kurzlink braucht einen, TikToks
Weiterleitung auf die Langform einen zweiten. Wer fuenf braucht,
hat kein Ziel, sondern eine Schleife. */
export const UMLEITUNGEN_MAX = 5;
/* WER FRAGT DA? Keine Browser-Verkleidung. Steht eine Adresse dahinter,
kann TikTok nachsehen, wer wir sind, statt zu raten. */
export const KENNUNG =
"DogfatherUniverse-Workspace/1.0 (+https://workspace.dogfather-universe.com; "
+ "Kampagnen-Einleser, ein Abruf je eingefuegtem Link)";
/** Der Rechnername einer Adresse, fuer Meldungen an Menschen.
* Gibt einen Ausschnitt der Eingabe zurueck, wenn sie keine ist. */
export function hostVon(roh) {
try { return new URL(String(roh)).hostname; } catch { return String(roh).slice(0, 60); }
}
/* Adressen in einem Fliesstext. Das Muster FINDET nur -- geprueft wird
mit new URL() und istTikTok(). Ein regulaerer Ausdruck, der
entscheidet, ob eine Adresse zu TikTok gehoert, ist derselbe Fehler
wie eine Zeichenkettensuche nach "tiktok.com". */
const LINK_MUSTER = /https?:\/\/[^\s"'<>)\]}]+/gi;
/** Die erste TikTok-Adresse in einem Text -- oder null.
*
* WARUM DER GANZE TEXT HINEINDARF: Die Einladung kommt so, wie TikTok
* sie schickt, mit Fliesstext drumherum:
*
* Dein Creator Network laedt dich ein, an einer Kampagne
* teilzunehmen. Klicke hier, um weitere Informationen zu
* erhalten: https://www.tiktok.com/t/ZSbqPsFa8/
*
* Niemand soll den Link erst heraussuchen muessen. */
export function linkAusText(text) {
for (const treffer of String(text || "").matchAll(LINK_MUSTER)) {
/* Satzzeichen am Ende gehoeren zum Satz, nicht zur Adresse --
mit einem Punkt dahinter waere es eine andere Seite. */
const sauber = treffer[0].replace(/[.,;:!?"'»«)\]]+$/u, "");
if (istTikTok(sauber)) return sauber;
}
return null;
}
/* Was NICHT abgerufen wird, egal wer es vorschlaegt. Buchstabenform,
keine Namensaufloesung: Eine Pruefung, die erst DNS fragt, kann
zwischen Frage und Abruf ueberholt werden (DNS-Rebinding). Was
hier steht, faengt den realistischen Fall -- eine Adresse, die auf
unseren eigenen Rechner oder ins Hausnetz zeigt. */
const PRIVATE_NAMEN = /^(localhost|.*\.local|.*\.internal|.*\.home\.arpa)$/i;
const PRIVATE_ZAHLEN = new RegExp([
"^127\\.", // Rueckschleife
"^10\\.", // privates Netz
"^192\\.168\\.", // privates Netz
"^172\\.(1[6-9]|2\\d|3[01])\\.", // privates Netz
"^169\\.254\\.", // Link-lokal, darunter 169.254.169.254
"^0\\.", "^::1$", "^\\[?::1\\]?$", "^f[cd]", "^fe80:",
].join("|"), "i");
/** Darf unser Server dieses BILD holen?
*
* WARUM ES DIESE FRAGE GIBT: Die Banneradresse steht in der Seite,
* die TikTok ausliefert -- sie ist FREMDBESTIMMT. Ohne Schranke
* wuerde unser Server abrufen, was dort steht, auch
* http://127.0.0.1:4100/... oder http://169.254.169.254/ (die
* Metadatenadresse vieler Rechenzentren). Das ist der Lehrbuchfall
* SSRF, und er kostet nichts zu verhindern.
*
* WAS DAS NICHT KANN: Ein Name, der auf eine private Adresse
* zeigt, faellt hier nicht auf -- dafuer muesste aufgeloest werden,
* und zwischen Aufloesung und Abruf laesst sich die Antwort
* tauschen. Das steht hier, damit niemand mehr Schutz annimmt, als
* da ist. */
export function bildAdresseErlaubt(roh) {
let u;
try { u = new URL(String(roh)); } catch { return false; }
const h = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
/* NUR IN EINER PRUEFUNG: dann liegt der nachgebaute Bilddienst
auf 127.0.0.1. Derselbe Schalter wie bei istTikTok -- eine
Pruefung, die die echte Regel umgeht, prueft eine andere. */
if (process.env.TIKTOK_KAMPAGNE_BASIS && (h === "127.0.0.1" || h === "localhost")) {
return true;
}
if (u.protocol !== "https:") return false;
if (u.port && u.port !== "443") return false;
if (PRIVATE_NAMEN.test(h)) return false;
if (PRIVATE_ZAHLEN.test(h)) return false;
return true;
}
/** Folgt einer Umleitung Station fuer Station.
*
* Gibt { ok: true, url, weg } oder { ok: false, grund, ... } mit
* grund aus: "fremd" (verlaesst TikTok), "zu_viele", "unlesbar",
* "frist", "netz".
*
* Der Koerper jeder Zwischenantwort wird WEGGEWORFEN, nicht gelesen:
* Sonst bleibt die Verbindung offen und wir laden jede Zwischenseite
* umsonst herunter. */
export async function folgeUmleitung(roh, { frist = FRIST_SEITE_MS,
schritte = UMLEITUNGEN_MAX } = {}) {
let hier = String(roh || "");
if (!istTikTok(hier)) {
return { ok: false, grund: "fremd", url: hier, host: hostVon(hier), weg: [] };
}
const weg = [hier];
/* EINE Frist fuer die GANZE Kette. Fuenf Spruenge mit je zwoelf
Sekunden waeren eine Minute -- und ein Mensch, der auf einen Knopf
gedrueckt hat, wartet keine Minute. */
const abbruch = new AbortController();
const uhr = setTimeout(() => abbruch.abort(), frist);
try {
for (let i = 0; i < schritte; i++) {
let a;
try {
a = await fetch(hier, {
method: "GET", redirect: "manual", signal: abbruch.signal,
headers: { "user-agent": KENNUNG },
});
} catch (f) {
return { ok: false, grund: abbruch.signal.aborted ? "frist" : "netz",
url: hier, host: hostVon(hier), weg, fehler: f?.message };
}
try { await a.body?.cancel(); } catch { /* egal */ }
const ort = a.headers.get("location");
if (!ort) return { ok: true, url: hier, weg, status: a.status };
let naechste;
try { naechste = new URL(ort, hier).toString(); }
catch { return { ok: false, grund: "unlesbar", url: ort, host: hostVon(ort), weg }; }
if (!istTikTok(naechste)) {
return { ok: false, grund: "fremd", url: naechste, host: hostVon(naechste), weg };
}
hier = naechste;
weg.push(hier);
}
return { ok: false, grund: "zu_viele", url: hier, host: hostVon(hier), weg };
} finally { clearTimeout(uhr); }
}
/** Holt eine ganze Seite -- mit Frist UND mit einer Grenze, die beim
* LESEN gezogen wird.
*
* Gibt { ok: true, status, text, bytes } oder { ok: false, grund }
* mit grund aus: "frist", "zu_gross", "netz", "status". Ein Aufrufer,
* der jeden dieser Faelle verschieden beantworten muss, soll sie
* nicht aus einer Fehlermeldung raten. */
export async function holeSeite(adresse, { frist = FRIST_SEITE_MS,
maxBytes = SEITE_MAX } = {}) {
const abbruch = new AbortController();
const uhr = setTimeout(() => abbruch.abort(), frist);
try {
let a;
try {
a = await fetch(adresse, {
signal: abbruch.signal, redirect: "error",
headers: { "user-agent": KENNUNG, accept: "text/html" },
});
} catch (f) {
return { ok: false, grund: abbruch.signal.aborted ? "frist" : "netz",
fehler: f?.message };
}
if (!a.ok) {
try { await a.body?.cancel(); } catch { /* egal */ }
return { ok: false, grund: "status", status: a.status };
}
/* Ein Kopf, der die Groesse schon ansagt, erspart das Herunterladen
-- aber er ist eine BEHAUPTUNG des fremden Servers und kann
fehlen oder luegen. Deshalb beides: hier nachsehen UND unten
beim Lesen mitzaehlen. */
const angesagt = Number(a.headers.get("content-length"));
if (Number.isFinite(angesagt) && angesagt > maxBytes) {
try { await a.body?.cancel(); } catch { /* egal */ }
return { ok: false, grund: "zu_gross", bytes: angesagt, angesagt: true };
}
const leser = a.body?.getReader?.();
if (!leser) return { ok: false, grund: "netz", fehler: "kein Koerper" };
const stuecke = [];
let gesamt = 0;
for (;;) {
let stueck;
try { stueck = await leser.read(); }
catch (f) {
return { ok: false, grund: abbruch.signal.aborted ? "frist" : "netz",
fehler: f?.message };
}
if (stueck.done) break;
gesamt += stueck.value.byteLength;
if (gesamt > maxBytes) {
try { await leser.cancel(); } catch { /* egal */ }
return { ok: false, grund: "zu_gross", bytes: gesamt, angesagt: false };
}
stuecke.push(Buffer.from(stueck.value));
}
return { ok: true, status: a.status, bytes: gesamt,
text: Buffer.concat(stuecke).toString("utf8") };
} finally { clearTimeout(uhr); }
}
/** Holt etwas von draussen -- MIT Frist ueber den ganzen Vorgang.
*
* DIE ALTE FASSUNG HAT NUR DEN ANFANG BEWACHT (behoben 05.10.2026).
+15
View File
@@ -68,6 +68,7 @@ import { pushRouter, pushStarten } from "./workspace-push.js";
import { chatRouter, toeneNachruestenStarten } from "./workspace-chat.js";
import { anrufRouter } from "./workspace-anruf.js";
import { videoRouter, videoWacheStarten } from "./workspace-video.js";
import { kampagneRouter } from "./workspace-kampagne.js";
import { bewerbungRouter } from "./workspace-bewerbung.js";
import { leistungRouter } from "./workspace-leistung.js";
import { managerZieleRouter } from "./workspace-manager-ziele.js";
@@ -307,6 +308,20 @@ app.use(anrufRouter);
derselbe Fehler, der heute frueh beim Antwort-Weg drei Anlaeufe
gekostet hat. */
app.use(videoRouter);
/* EINE KAMPAGNE AUS EINEM TIKTOK-LINK (07.10.2026).
Steht NACH `app.use("/workspace/api", gleicheHerkunft)` weiter
oben -- und zwar nicht zufaellig: Express arbeitet die Schichten in
der Reihenfolge ab, in der sie eingehaengt werden. Ein Router, der
VOR der Herkunftswache steht, ist von ihr nicht geschuetzt. Am
07.10.2026 war genau das der Grund, warum die Wache in EINER Zeile
in index.js steht und nicht in jedem Router einzeln.
Eigener Pfad (`/workspace/api/kampagne`), nicht `/bereich/...`:
Dort horcht der Bereichs-Router auf `/:bereich/:id` und wuerde
"kampagne" fuer eine Nummer halten -- derselbe Fehler, der beim
Video drei Anlaeufe gekostet hat. */
app.use(kampagneRouter);
/* LEISTUNG und FRUEHWARNUNG (06.09.2026, Stufe 1+2 des Plans). Die
Fruehwarnung liest aus `leistung` -- ohne Zahlen hat sie nichts zu
melden, deshalb stehen sie beieinander. */
+44 -4
View File
@@ -540,9 +540,24 @@ export function leseKampagne(html) {
/* Banner: `props.imageUrl[0].url`, NICHT `props.url` und nicht
`pageInfo.topImageInfo` -- die zeigen auf das Vorlagenbild. */
const bildBaustein = erster("live_top_image");
/* HIER STEHT ABSICHTLICH KEIN "nur https" (berichtigt 07.10.2026).
Vorher stand hier `/^https:/`. Das war eine ENTSCHEIDUNG an einer
Stelle, die nur LESEN soll -- und sie stand damit zum zweiten Mal
im Haus, denn `bildAdresseErlaubt` in helfer-tiktok.mjs prueft
Protokoll, Port und private Adressen ohnehin, bevor etwas geholt
wird.
Zwei Fassungen derselben Regel laufen auseinander, und hier tat es
sofort weh: Keine Pruefung konnte einen nachgebauten Bilddienst
auf 127.0.0.1 benutzen -- der Leser verwarf die Adresse, bevor die
Schranke sie je sah. Die Pruefung haette dafuer das echte TikTok
anfragen muessen.
Der Leser nimmt deshalb jede absolute Adresse. Ob sie abgerufen
werden DARF, entscheidet die Schranke. */
const bannerUrl = (bildBaustein?.props?.imageUrl || [])
.map((b) => b?.url || b?.urlSG)
.find((u) => typeof u === "string" && /^https:\/\//i.test(u)) || null;
.find((u) => typeof u === "string" && /^https?:\/\//i.test(u)) || null;
if (!bannerUrl) hinweise.push("Das Banner konnte nicht gelesen werden.");
/* Laender */
@@ -586,6 +601,23 @@ export function leseKampagne(html) {
kurznummer: aktion.short_id ?? null,
laender,
bausteine: [...teile.keys()],
/* DAS GELESENE SELBST GEHOERT MIT HINEIN (07.10.2026, beim Bauen
der Route aufgefallen).
Der Regeltext einer echten Kampagne ist 7474 Zeichen lang; das
Feld in der Kachel fasst 6000 (TEXT_MAX in
workspace-bereiche.js, und die PUT-Route lehnt mehr ab -- eine
Kachel mit 7474 Zeichen koennte DogFather oeffnen, aber nicht
speichern). Der Text wird deshalb an einer Abschnittsgrenze
gekuerzt. Damit dabei nichts VERLOREN geht, steht hier das
Gelesene in seiner gegliederten Form.
Das ist genau der Zweck, den der Bauplan fuer diese Spalte
nennt: Wenn in drei Monaten auffaellt, dass die Ligen auch
gebraucht werden, steht die Antwort schon da -- und eine
abgelaufene Kampagne antwortet dann vielleicht nicht mehr. */
einleitung, aufgaben, geschenke, preise, klapptexte,
banner_url: bannerUrl,
woerterbuch_eintraege: Object.keys(woerterbuch).length,
platzhalter_ohne_eintrag: ohneEintrag.length,
},
@@ -656,8 +688,16 @@ export function regelnText(kuer, hinweise = []) {
/* Was nicht gelesen werden konnte, steht IN der Kachel -- nicht nur
in einer Antwort, die niemand mehr sieht. Eine Kachel, die
unvollstaendig ist, soll das selbst sagen. */
for (const h of hinweise || []) bloecke.push(`* ${h} Bitte nachtragen.`);
unvollstaendig ist, soll das selbst sagen.
return bloecke.join("\n\n").trim();
UND ZWAR GANZ OBEN (berichtigt 07.10.2026). Vorher standen die
Hinweise am Ende. Gemessen: Der Regeltext von "Gipfelstuermer" ist
7474 Zeichen lang, das Feld fasst 6000 -- es wird also gekuerzt,
und gekuerzt wird von HINTEN. Die Hinweise waren damit genau das
Erste, was wegfiel: Eine Kachel, der die Preise fehlten, sagte es
nicht mehr. Die wichtigste Zeile darf nicht die sein, die als
erste verschwindet. */
const vorweg = (hinweise || []).map((h) => `* ${h} Bitte nachtragen.`);
return [...vorweg, ...bloecke].join("\n\n").trim();
}
+13 -1
View File
@@ -170,7 +170,19 @@ for (const [rolle, wie] of ROLLEN) {
weiterreicht. Der Hostname bleibt damit `crew.` -- und nur daran
erkennt der Server das Haus. */
const { zertifikatBauen } = await import("./helfer-openssl.mjs");
const HP = PORT + 1;
/* DIE ZWEITE NUMMER WIRD ABGELEITET, NICHT GERECHNET
(berichtigt 07.10.2026, gefunden von pruef-portnummern).
Hier stand `PORT + 1`. Das sieht aus wie eine Ableitung und ist
keine: `portNummer` UEBERSPRINGT gesperrte Nummern. Liegt einmal
eine dazwischen, ist `PORT + 1` die Nummer der naechsten DATEI --
und zwei Laeufe teilen sich still einen Port. Heute liegt dort
keine gesperrte Nummer; das ist Glueck, kein Entwurf.
`eigenerPort(import.meta, …, 1)` gibt die zweite der beiden
Nummern, die dieser Datei gehoeren -- und ueberspringt dabei
dasselbe wie die erste. */
const HP = await eigenerPort(import.meta, "der HTTPS-Vorbau", 1);
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
zertifikatBauen(schl, zert, CREW);
const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) },
+14 -1
View File
@@ -39,6 +39,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import vm from "node:vm";
import { eigenerPort } from "./helfer-port.mjs";
import { heuteLokal } from "./helfer-tag.mjs";
const PORT = await eigenerPort(import.meta, "pruef-anleitung");
const ordner = mkdtempSync(join(tmpdir(), "ws-anl-"));
@@ -1120,8 +1121,20 @@ melde("\n=== „Neu für dich“ beim Aufstieg ===");
const satz = d.prepare(
`INSERT INTO eintraege (bereich, art, titel, datum, erstellt, erstellt_von)
VALUES ('highlight','clip',?,?,?,?)`);
/* DER TAG IN ORTSZEIT, der Zeitstempel daneben in UTC
(berichtigt 07.10.2026, gefunden von pruef-struktur). Hier
stand zweimal dieselbe Zeichenkette: `datum` bekam den
UTC-Tag, die Anwendung sucht aber nach dem lokalen. Zwischen
00:00 und 02:00 deutscher Zeit legte diese Pruefung ihre
Proben damit auf GESTERN und wunderte sich, dass sie heute
keine findet -- zwei Stunden am Tag rot, ohne dass etwas
kaputt ist. Genau der Fall vom 06.09.2026.
`heuteLokal` kommt aus helfer-tag.mjs und wird nicht hier
abgeschrieben: Eine Pruefung, die ihren eigenen Kalender
mitbringt, misst irgendwann einen anderen Tag als das Haus. */
const heute = new Date().toISOString();
for (let i = 1; i <= 3; i++) satz.run(`Probe ${i}`, heute.slice(0, 10), heute, gastId);
for (let i = 1; i <= 3; i++) satz.run(`Probe ${i}`, heuteLokal(), heute, gastId);
d.close();
}
const aStamm = await aufstieg("gast", CREW_HOST);
+879
View File
@@ -0,0 +1,879 @@
/* =====================================================================
EINE KAMPAGNEN-KACHEL AUS EINEM TIKTOK-LINK (07.10.2026)
Prüft Etappe 2 und 3 des Bauplans: den Weg nach draußen
(`helfer-tiktok.mjs`) und die Route (`workspace-kampagne.js`).
Das Auslesen selbst prüft `pruef-kampagne-lesen.mjs` — ohne Server,
ohne Netz, gegen gespeicherte Seiten.
---------------------------------------------------------------------
DER NACHGEBAUTE DIENST LIEGT AUF 127.0.0.1
Umgestellt über `TIKTOK_KAMPAGNE_BASIS` — genau wie
`TIKTOK_OEMBED_BASIS` beim Video. Eine Prüfung gegen das echte
TikTok wäre langsam, hinge am Netz und würde irgendwann rot, ohne
dass etwas kaputt ist. Sie wäre die Sorte Prüfung, die man abschaltet.
Ausgeliefert werden die ECHTEN Seiten aus `server/pruefdaten/`.
Verändert wird daran genau eine Sache: die Banneradresse zeigt auf
diesen Dienst statt auf TikToks CDN. Ohne das würde diese Prüfung
bei jedem Lauf ein Bild aus dem Internet holen — und wäre damit
genau das, was sie vermeiden soll.
---------------------------------------------------------------------
KEINE TESTDATEN IM LIVE-SYSTEM
Alles läuft auf einer frisch angelegten Datenbank in einem
Wegwerfordner. Es entsteht nie eine Kachel auf
workspace.dogfather-universe.com.
KEINE WANDUHR ALS ANNAHME bei den Daten, die geprüft werden: Die
Kampagnen in den gespeicherten Seiten haben feste Zeitstempel, und
genau die werden nachgerechnet (1791064800000 → 2026-10-04 in
Europe/Berlin). Das ist eine EINGABE, keine Annahme über „jetzt“ —
der Kalender kann es nicht einholen.
Aufrufen mit: node server/pruef-kampagne.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
import { brotliDecompressSync } from "node:zlib";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { heuteLokal } from "./helfer-tag.mjs";
const HIER = dirname(fileURLToPath(import.meta.url));
const DATEN = join(HIER, "pruefdaten");
const ordner = mkdtempSync(join(tmpdir(), "ws-kmp-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
import { notbremse } from "./helfer-notbremse.mjs";
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "die Kampagnen-Pruefung");
const TIKPORT = await eigenerPort(import.meta, "der nachgebaute TikTok-Dienst", 1);
/* DER SCHALTER. Er macht zweierlei: `istTikTok` lässt 127.0.0.1 durch,
und `bildAdresseErlaubt` ebenfalls. Beides nur hier — im Betrieb
steht die Variable nicht, und dann gilt die echte Regel. */
process.env.TIKTOK_KAMPAGNE_BASIS = "http://127.0.0.1:" + TIKPORT;
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) {
return ec.call(this, n, w, { ...(o || {}), secure: false });
};
await import("./index.js");
notbremse(300_000, "pruef-kampagne");
await new Promise((r) => setTimeout(r, 1000));
const BASIS = "http://127.0.0.1:" + PORT;
let fehler = 0;
let geprueft = 0;
const melde = (z) => console.log(z);
const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
function nichtNachsehbar(grund, rat) {
melde("");
melde("KONNTE NICHT NACHSEHEN");
melde(" Grund: " + grund);
melde(" Was tun: " + rat);
process.exit(2);
}
/* =====================================================================
DIE GESPEICHERTEN SEITEN
===================================================================== */
function seiteLesen(name) {
const pfad = join(DATEN, name);
if (!existsSync(pfad)) {
nichtNachsehbar(`die Prüfdatei ${name} fehlt in ${DATEN}`,
"Sie ist in Gitea versioniert: git checkout -- server/pruefdaten/");
}
try {
return name.endsWith(".br")
? brotliDecompressSync(readFileSync(pfad)).toString("utf8")
: readFileSync(pfad, "utf8");
} catch (f) {
return nichtNachsehbar(`${name} lässt sich nicht lesen (${f.message})`,
"Aus Gitea neu holen.");
}
}
/** Macht das Datenpaket einer gespeicherten Seite auf, lässt daran
* ändern, schreibt es zurück. Steht hier und nicht im Leser: Der
* Leser liest, er baut nicht. */
function umbauen(html, aendern) {
const marke = html.indexOf('id="__MODERN_ROUTER_DATA__"');
const a = html.indexOf(">", marke) + 1;
const e = html.indexOf("</script>", a);
const daten = JSON.parse(html.slice(a, e));
aendern(daten.loaderData.home_page.COSMIC_D_K.value, daten);
return html.slice(0, a) + JSON.stringify(daten) + html.slice(e);
}
const BANNER_HIER = `http://127.0.0.1:${TIKPORT}/banner.png`;
const TEXT_STATT_BILD = `http://127.0.0.1:${TIKPORT}/banner.txt`;
/** Die Banneradresse auf unseren Dienst biegen — der EINZIGE Eingriff
* an den echten Seiten. */
function bannerHierher(html, ziel = BANNER_HIER) {
return umbauen(html, (w) => {
const s = JSON.parse(w.globalData.activityInfo.ac_schema_with_interaction_rules
?? JSON.stringify(w.schema));
let getroffen = 0;
(function lauf(k, t = 0) {
if (!k || typeof k !== "object" || t > 20) return;
if (typeof k.type === "string" && k.type.includes("live_top_image")
&& Array.isArray(k.props?.imageUrl)) {
for (const b of k.props.imageUrl) { b.url = ziel; b.urlSG = ziel; getroffen++; }
}
for (const v of Object.values(k)) {
if (Array.isArray(v)) { for (const x of v) lauf(x, t + 1); }
else if (v && typeof v === "object") lauf(v, t + 1);
}
})(s);
if (!getroffen) throw new Error("kein Bildbaustein gefunden");
if (w.globalData.activityInfo.ac_schema_with_interaction_rules) {
w.globalData.activityInfo.ac_schema_with_interaction_rules = JSON.stringify(s);
} else {
w.schema = typeof w.schema === "string" ? JSON.stringify(s) : s;
}
});
}
const ID_GIPFEL = "7691493175936699143";
const ID_BEENDET = "7657456323453405959";
const ID_OHNEPREISE = "7670899782609996552";
const ID_TEXTBANNER = "7600000000000000001";
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")),
};
const ROH_GIPFEL = seiteLesen("kampagne-gipfelstuermer.html.br");
const SEITE_KAPUTT = seiteLesen("kampagne-kaputt.html");
/* Eine Kampagne, deren Banner eine TEXTDATEI ist. Für die Gegenprobe
zu `bildArtVon`: Was von draußen kommt, ist von draußen -- und der
fremde Server behauptet den Typ. */
SEITEN[ID_TEXTBANNER] = umbauen(bannerHierher(ROH_GIPFEL, TEXT_STATT_BILD), (w) => {
w.globalData.activityInfo.activity_id = ID_TEXTBANNER;
w.globalData.activityId = ID_TEXTBANNER;
w.globalData.pageInfo.title = "Kampagne mit kaputtem Banner";
});
/* Eine Kampagne ohne Enddatum: Pflichtfeld fehlt, es darf KEINE Kachel
entstehen. */
const SEITE_OHNE_ENDE = umbauen(ROH_GIPFEL, (w) => {
delete w.globalData.activityInfo.end_time;
});
/* =====================================================================
DER NACHGEBAUTE DIENST
===================================================================== */
const { createServer } = await import("node:http");
const { pngBauen } = await import("./helfer-png-bauen.mjs");
const BANNERBILD = pngBauen(750, 500, (x, y, b, h) => {
const g = Math.max(0, 1 - Math.hypot(x / b - .5, y / h - .5) * 1.6);
return [Math.round(30 + g * 120), Math.round(40 + g * 90), Math.round(20 + g * 60)];
});
let abrufe = 0;
const gesehen = [];
const tikDienst = createServer((anfrage, antwort) => {
abrufe++;
const u = new URL(anfrage.url, "http://127.0.0.1");
gesehen.push({ pfad: u.pathname, kennung: anfrage.headers["user-agent"] || "" });
const leite = (nach) => { antwort.writeHead(302, { location: nach }); antwort.end(); };
if (u.pathname === "/t/gipfel") return leite(`/falcon/home?activityId=${ID_GIPFEL}&campaign_traffic_id=1`);
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/textbanner") return leite(`/falcon/home?activityId=${ID_TEXTBANNER}`);
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");
if (u.pathname === "/t/ohneende") return leite("/falcon/ohneende");
if (u.pathname === "/t/stumm") return leite("/falcon/stumm");
if (u.pathname === "/t/riesig") return leite("/falcon/riesig");
if (u.pathname === "/banner.png") {
antwort.writeHead(200, { "content-type": "image/png",
"content-length": BANNERBILD.length });
return antwort.end(BANNERBILD);
}
if (u.pathname === "/banner.txt") {
const b = Buffer.from("Das ist kein Bild, sondern Text.");
/* Der Typ wird BEHAUPTET -- genau darum geht es: Die Byteprüfung
glaubt dem fremden Server nicht. */
antwort.writeHead(200, { "content-type": "image/png", "content-length": b.length });
return antwort.end(b);
}
if (u.pathname === "/falcon/stumm") {
/* Nie antworten. Die Verbindung bleibt offen, bis die Frist greift. */
return;
}
if (u.pathname === "/falcon/riesig") {
antwort.writeHead(200, { "content-type": "text/html" });
/* OHNE content-length (gestückelt): Die Grenze muss beim LESEN
greifen, nicht über einen Kopf, den man auch weglassen kann. */
const brocken = Buffer.alloc(256 * 1024, 0x41);
let n = 0;
const weiter = () => {
if (antwort.writableEnded || antwort.destroyed) return;
if (n++ >= 20) { antwort.end(); return; } /* 5 MB */
if (antwort.write(brocken)) setImmediate(weiter);
else antwort.once("drain", weiter);
};
return weiter();
}
if (u.pathname === "/falcon/kaputt") {
antwort.writeHead(200, { "content-type": "text/html" });
return antwort.end(SEITE_KAPUTT);
}
if (u.pathname === "/falcon/ohneende") {
antwort.writeHead(200, { "content-type": "text/html" });
return antwort.end(SEITE_OHNE_ENDE);
}
if (u.pathname === "/falcon/home") {
const id = u.searchParams.get("activityId");
if (SEITEN[id]) {
antwort.writeHead(200, { "content-type": "text/html; charset=utf-8" });
return antwort.end(SEITEN[id]);
}
}
antwort.writeHead(404, { "content-type": "text/plain" });
antwort.end("nicht da");
});
await new Promise((f, s) => {
tikDienst.once("error", s);
tikDienst.listen(TIKPORT, "127.0.0.1", f);
});
const TIK = `http://127.0.0.1:${TIKPORT}`;
/* =====================================================================
PERSONEN UND ZUGANG
===================================================================== */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,1,?)").run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001");
anlegen("Luna", "creator", "CODE-CREA-0001");
anlegen("Mara", "manager", "CODE-MANA-0001");
anlegen("Bremser", "manager", "CODE-BREM-0001");
/* EIN EINTRAG IM ANDEREN HAUS -- damit „Crew unverändert“ nicht eine
Behauptung über eine leere Menge ist. `every()` auf einem leeren Feld
meldet `true`; eine Zählung über null Zeilen ebenso. */
d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum,"
+ " dringlichkeit, erstellt, erstellt_von, haus)"
+ " VALUES (?,?,'treff','frage','offen',?, 'mittel', ?, ?, 'crew')")
/* DER TAG IN ORTSZEIT (berichtigt 07.10.2026, gefunden von
pruef-struktur). Hier stand `jetzt.slice(0, 10)` -- das ist der
UTC-Tag. Zwischen 00:00 und 02:00 deutscher Zeit legt das den
Eintrag auf gestern; harmlos fuer DIESE Pruefung, aber genau die
Gewohnheit, die anderswo zwei Stunden am Tag rot macht. */
.run("Etwas vom Rudel", "Gehört Team Dogi.", heuteLokal(), jetzt, idDogi);
d.close();
const { request } = await import("node:http");
const HOST = "workspace.dogfather-universe.com";
const CREW_HOST = "crew.dogfather-universe.com";
function roh(pfad, o = {}) {
return new Promise((f, s) => {
const daten = o.rumpf === undefined ? null : Buffer.from(JSON.stringify(o.rumpf));
const wirt = o.wirt || HOST;
const a = request({
host: "127.0.0.1", port: PORT, path: pfad,
method: o.methode || (daten ? "POST" : "GET"),
headers: {
Host: wirt, Origin: "https://" + wirt,
...(o.keks ? { Cookie: o.keks } : {}),
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
...(o.kopf || {}),
},
}, (r) => {
const t = [];
r.on("data", (x) => t.push(x));
r.on("end", () => f({ code: r.statusCode, kopf: r.headers,
text: Buffer.concat(t).toString("utf8") }));
});
a.on("error", s);
if (daten) a.write(daten);
a.end();
});
}
async function anmelden(rolle, code, wirt = HOST) {
const a = await roh("/workspace/api/anmelden", { rumpf: { rolle, code }, wirt });
if (a.code !== 200) {
nichtNachsehbar(`Anmeldung ${rolle} auf ${wirt} ergab ${a.code}`,
"Ohne Anmeldung lässt sich nichts von dem hier messen.");
}
return [].concat(a.kopf["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
}
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
const keksLuna = await anmelden("creator", "CODE-CREA-0001");
const keksMara = await anmelden("manager", "CODE-MANA-0001");
const keksBrem = await anmelden("manager", "CODE-BREM-0001");
const keksDogiCrew = await anmelden("admin", "CODE-DOGI-0001", CREW_HOST);
/** Eine Einladung einlesen -- so, wie es die Oberfläche tut. */
const einlesen = (text, keks = keksDogi, wirt = HOST) =>
roh("/workspace/api/kampagne/einlesen", { rumpf: { text }, keks, wirt })
.then((a) => ({ ...a, j: JSON.parse(a.text || "{}") }));
const dbLesen = (sql, ...w) => {
const x = new DatabaseSync(process.env.WORKSPACE_DB);
try { return x.prepare(sql).all(...w); } finally { x.close(); }
};
/* =====================================================================
1. DEN LINK AUS DEM GANZEN TEXT HOLEN
===================================================================== */
melde("=== 1. Der Link steckt mitten im Fließtext ===");
const { linkAusText, istTikTok, hostVon, bildAdresseErlaubt,
FRIST_SEITE_MS, SEITE_MAX, UMLEITUNGEN_MAX, KENNUNG, holeSeite, folgeUmleitung }
= await import("./helfer-tiktok.mjs");
{
const einladung = "Dein Creator Network lädt dich ein, an einer Kampagne "
+ "teilzunehmen.\nKlicke hier, um weitere Informationen zu erhalten: "
+ "https://www.tiktok.com/t/ZSbqPsFa8/";
ok(linkAusText(einladung) === "https://www.tiktok.com/t/ZSbqPsFa8/",
`aus 150 Zeichen Einladung: ${linkAusText(einladung)}`);
ok(linkAusText("Schau mal: https://www.tiktok.com/t/ZSbqPsFa8/.") === "https://www.tiktok.com/t/ZSbqPsFa8/",
"ein Satzpunkt dahinter gehört nicht zur Adresse");
ok(linkAusText("Hier (https://www.tiktok.com/t/ABC/) steht er") === "https://www.tiktok.com/t/ABC/",
"eine Klammer dahinter auch nicht");
ok(linkAusText("Ohne jede Adresse.") === null,
"ein Text ohne Link ergibt nichts");
ok(linkAusText("https://example.com/x https://www.tiktok.com/t/B/") === "https://www.tiktok.com/t/B/",
"eine fremde Adresse davor wird übersprungen");
/* DIE GEGENPROBE, DIE ZÄHLT. "tiktok.com.beispiel.de" enthält
"tiktok.com" -- wer mit einer Zeichenkettensuche arbeitet, ruft
einen fremden Server auf. */
ok(linkAusText("https://tiktok.com.beispiel.de/t/X/") === null,
"tiktok.com.beispiel.de wird NICHT genommen");
ok(istTikTok("https://tiktok.com.beispiel.de/") === false,
" und istTikTok lehnt es einzeln ebenso ab");
ok(istTikTok("https://www.tiktok.com/t/X/") === true,
" die echte Adresse dagegen nicht");
ok(linkAusText("http://www.tiktok.com/t/X/") === null,
"und ohne Verschlüsselung gar nicht");
ok(hostVon("https://www.tiktok.com/t/X/") === "www.tiktok.com",
"der Rechnername für die Meldung an den Menschen stimmt");
}
/* =====================================================================
2. DIE UMLEITUNG, STATION FÜR STATION
===================================================================== */
melde("");
melde("=== 2. Jede Station wird einzeln geprüft ===");
{
const gut = await folgeUmleitung(`${TIK}/t/gipfel`);
ok(gut.ok && gut.url.includes(`activityId=${ID_GIPFEL}`),
`der Kurzlink führt zur Kampagne (${gut.weg.length} Stationen)`);
const weg = await folgeUmleitung(`${TIK}/t/fremd`);
ok(weg.ok === false && weg.grund === "fremd" && hostVon(weg.url) === "beispiel.de",
`eine Umleitung von TikTok weg bricht ab und nennt das Ziel (${hostVon(weg.url)})`);
const kreis = await folgeUmleitung(`${TIK}/t/kreis`);
/* weg.length ist UMLEITUNGEN_MAX + 1: Der Weg beginnt mit der
eingegebenen Adresse, danach kommen fuenf Spruenge. Hier stand
zuerst "=== UMLEITUNGEN_MAX" -- geraten, nicht gezaehlt. */
ok(kreis.ok === false && kreis.grund === "zu_viele"
&& kreis.weg.length === UMLEITUNGEN_MAX + 1,
`eine Schleife endet nach ${UMLEITUNGEN_MAX} Spruengen (${kreis.weg.length} Stationen)`);
ok(UMLEITUNGEN_MAX === 5 && FRIST_SEITE_MS === 12_000 && SEITE_MAX === 3 * 1024 * 1024,
`die drei Grenzen stehen: ${UMLEITUNGEN_MAX} Sprünge, `
+ `${FRIST_SEITE_MS / 1000} s, ${SEITE_MAX / 1048576} MB`);
/* EIN EHRLICHER NAME. Kein Abruf gibt sich als Browser aus. */
ok(gesehen.length > 0 && gesehen.every((g) => g.kennung.startsWith("DogfatherUniverse-Workspace/")),
`alle ${gesehen.length} Abrufe nennen, wer fragt`);
ok(/workspace\.dogfather-universe\.com/.test(KENNUNG) && !/Mozilla/i.test(KENNUNG),
"und die Kennung ist keine Browser-Verkleidung");
}
/* =====================================================================
3. FRIST UND GRÖSSE
===================================================================== */
melde("");
melde("=== 3. Ein Server, der nicht antwortet, und einer, der zu viel schickt ===");
{
/* MIT KURZER FRIST GEMESSEN, nicht mit zwölf Sekunden: Geprüft wird
der MECHANISMUS. Dass im Betrieb 12 s gelten, steht oben als
eigene Zeile -- und die Route selbst wird unten noch einmal gegen
den stummen Server geschickt, dort mit der echten Frist. */
const t0 = Date.now();
const stumm = await holeSeite(`${TIK}/falcon/stumm`, { frist: 700 });
const dauer = Date.now() - t0;
ok(stumm.ok === false && stumm.grund === "frist" && dauer < 3000,
`ein stummer Server wird nach ${dauer} ms abgebrochen (${stumm.grund})`);
const t1 = Date.now();
const riesig = await holeSeite(`${TIK}/falcon/riesig`, { maxBytes: 512 * 1024 });
const dauer1 = Date.now() - t1;
ok(riesig.ok === false && riesig.grund === "zu_gross" && riesig.angesagt === false,
`5 MB ohne angesagte Größe werden beim LESEN abgebrochen (nach ${dauer1} ms)`);
/* DIE GEGENPROBE: Derselbe Abruf unter der Grenze geht durch. Ohne
sie bewiese die Zeile darüber nur, dass irgendetwas scheitert. */
const passt = await holeSeite(`${TIK}/falcon/home?activityId=${ID_GIPFEL}`);
ok(passt.ok === true && passt.bytes > 1_000_000 && passt.text.includes("Gipfelstürmer"),
`eine echte Seite (${Math.round(passt.bytes / 1024)} KB) kommt ganz an`);
}
/* =====================================================================
4. DIE KACHEL ENTSTEHT
===================================================================== */
melde("");
melde("=== 4. Aus der Einladung wird eine Agentur-Event-Kachel ===");
let kachelId = null;
{
const t0 = Date.now();
const a = await einlesen(
"Dein Creator Network lädt dich ein, an einer Kampagne teilzunehmen.\n"
+ `Klicke hier, um weitere Informationen zu erhalten: ${TIK}/t/gipfel`);
const dauer = Date.now() - t0;
ok(a.code === 201 && Number.isFinite(a.j.id),
`HTTP ${a.code}, Kachel #${a.j.id}, in ${dauer} ms`);
ok(dauer < 15_000, `und das in unter 15 Sekunden (${(dauer / 1000).toFixed(1)} s)`);
kachelId = a.j.id;
ok(a.j.titel === "Gipfelstürmer" && a.j.von === "2026-10-04" && a.j.bis === "2026-10-07",
`die Antwort nennt „${a.j.titel}“, ${a.j.von} bis ${a.j.bis}`);
ok(Array.isArray(a.j.hinweise) && a.j.hinweise.length === 0,
`nichts fehlte (${(a.j.hinweise || []).length} Hinweise)`);
const z = dbLesen("SELECT * FROM eintraege WHERE id = ?", kachelId)[0];
ok(!!z, "der Eintrag steht in der Datenbank");
ok(z.bereich === "agentur" && z.art === "kampagne",
`Bereich ${z.bereich}, Art ${z.art}`);
/* DER TAG IN DER ZONE DER KAMPAGNE, nicht in UTC und nicht nach der
Serverzeit. `toISOString()` ergäbe hier den 03.10. */
ok(z.datum === "2026-10-04",
`Beginn ${z.datum} -- nicht 2026-10-03, wie UTC es ergäbe`);
ok(z.event_ende === "2026-10-07", `Ende ${z.event_ende}`);
ok(z.kampagne_zone === "Europe/Berlin", `Zone ${z.kampagne_zone}`);
ok(z.kampagne_id === ID_GIPFEL, `Nummer ${z.kampagne_id}`);
ok(z.kampagne_quelle === `${TIK}/t/gipfel`,
"der eingefügte Link steht unverändert dabei");
ok(!!z.kampagne_geholt_am && Math.abs(Date.parse(z.kampagne_geholt_am) - Date.now()) < 120_000,
"mit Zeitstempel");
ok(z.nur_leitung === 0, "eine Kampagne ist für alle, nicht nur für die Leitung");
ok(z.titel === "Gipfelstürmer" && (z.text || "").includes("Alpen-Vibes"),
"Titel und Einleitung stehen drin");
/* KEIN FELD LÄNGER ALS DAS FORMULAR ERLAUBT. Sonst entstünde eine
Kachel, die DogFather öffnen, aber nicht speichern kann. */
for (const feld of ["text", "event_aufgaben", "event_regeln"]) {
ok((z[feld] || "").length <= 6000,
` ${feld}: ${(z[feld] || "").length} Zeichen (höchstens 6000)`);
}
ok((z.event_regeln || "").includes("Der Regeltext war länger"),
"und wo gekürzt wurde, sagt die Kachel es selbst");
/* DER AUSZUG BEWAHRT, WAS NICHT INS FELD PASST. */
const auszug = JSON.parse(z.kampagne_daten || "{}");
ok(auszug.preise?.length === 6 && auszug.geschenke?.length === 5
&& auszug.klapptexte?.length === 2,
`die Rohdaten haben alles: ${auszug.preise?.length} Preisgruppen, `
+ `${auszug.geschenke?.length} Geschenke, ${auszug.klapptexte?.length} Klapptexte`);
}
/* =====================================================================
5. FÜNF HAKEN ZUM ABHAKEN
===================================================================== */
melde("");
melde("=== 5. Die Aufgaben werden zu Haken ===");
{
const b = JSON.parse((await roh("/workspace/api/bereich/agentur", { keks: keksDogi })).text);
const k = (b.eintraege || []).find((e) => e.id === kachelId);
ok(!!k, "die Kachel steht auf dem Agentur-Brett");
ok(Array.isArray(k.event_punkte) && k.event_punkte.length === 5,
`${(k.event_punkte || []).length} Haken`);
ok((k.event_punkte || [])[0]?.text === "1 Diamant = 1 Punkt",
`der erste heißt „${(k.event_punkte || [])[0]?.text}“`);
ok((k.event_punkte || []).every((p) => !/^[-–—•·*▪◦‣>+]/.test(p.text)),
"keiner trägt ein Aufzählungszeichen, das niemand getippt hat");
/* EIN HAKEN BLEIBT STEHEN. Abnahmepunkt 2 des Bauplans. */
const s = k.event_punkte[0].schluessel;
const setzen = await roh(`/workspace/api/eintrag/${kachelId}/punkt/${s}`,
{ methode: "PUT", rumpf: { an: true }, keks: keksLuna });
ok(setzen.code === 200, `ein Creator setzt einen Haken (HTTP ${setzen.code})`);
const b2 = JSON.parse((await roh("/workspace/api/bereich/agentur", { keks: keksLuna })).text);
const k2 = (b2.eintraege || []).find((e) => e.id === kachelId);
/* DAS FELD HEISST `meiner`, nicht `an` (berichtigt 07.10.2026).
Abgeschrieben aus workspace-bereiche.js statt geraten -- hier
stand zuerst `an`, und die Pruefung meldete einen Fehler an einer
Stelle, an der keiner war. */
ok((k2?.event_punkte || []).find((p) => p.schluessel === s)?.meiner === true,
"und er steht beim nächsten Laden noch da");
}
/* =====================================================================
6. DER WICHTIGSTE NACHWEIS: DAS SCHEMA, NICHT DAS WÖRTERBUCH
===================================================================== */
melde("");
melde("=== 6. „1 schenkende Person“ steht NICHT in der Kachel ===");
{
const z = dbLesen("SELECT * FROM eintraege WHERE id = ?", kachelId)[0];
const alles = [z.titel, z.text, z.event_aufgaben, z.event_regeln, z.kampagne_daten]
.join("\n");
/* GEGENPROBE ZUERST: Der Satz steht sehr wohl in der Seite, die wir
gelesen haben. Ohne diese Zeile wäre die nächste auch dann grün,
wenn gar nichts gelesen worden wäre. */
ok(ROH_GIPFEL.includes("schenkende Person"),
"Gegenprobe: in der gelesenen Seite kommt er vor");
ok(alles.length > 3000 && !alles.includes("schenkende"),
`in ${alles.length} Zeichen Kachelinhalt kommt er nicht vor`);
}
/* =====================================================================
7. DAS BANNER LIEGT BEI UNS
===================================================================== */
melde("");
melde("=== 7. Das Banner wird geholt, nicht verlinkt ===");
{
const dat = dbLesen("SELECT * FROM dateien WHERE eintrag_id = ?", kachelId);
ok(dat.length === 1, `${dat.length} Datei am Eintrag`);
ok(dat[0]?.typ === "image/png" && dat[0]?.groesse > 1000,
`als ${dat[0]?.typ}, ${dat[0]?.groesse} Bytes`);
ok(existsSync(join(ordner, "dateien", dat[0]?.name_datei || "x")),
"und sie liegt wirklich auf der Platte");
ok(!(dat[0]?.name_datei || "").startsWith("http"),
"gespeichert ist ein Dateiname, keine fremde Adresse");
/* SICHTBAR AUCH FÜR EINEN CREATOR. Am 03.10.2026 war genau das
schon einmal ein 404. */
/* DER LESWEG IST /api/dateien/:id/bild (berichtigt 07.10.2026).
Hier stand /api/bereich/agentur/:id/bild -- das ist der Weg zum
HOCHLADEN (POST), und als GET gibt es ihn nicht. Abgeschrieben aus
workspace-bereiche.js statt geraten. */
const bild = await roh(`/workspace/api/dateien/${dat[0]?.id}/bild`, { keks: keksLuna });
ok(bild.code === 200, `ein Creator bekommt das Banner (HTTP ${bild.code})`);
/* GEGENPROBE: ohne Anmeldung nicht. Sonst bewiese die Zeile darueber
nur, dass irgendeine Datei ausgeliefert wird. */
const ohne = await roh(`/workspace/api/dateien/${dat[0]?.id}/bild`);
ok(ohne.code === 401 || ohne.code === 403,
`Gegenprobe: ohne Anmeldung nicht (HTTP ${ohne.code})`);
/* GEGENPROBE: Eine Textdatei mit `content-type: image/png` wird
abgelehnt -- die Byteprüfung glaubt dem fremden Server nicht. */
const t = await einlesen(`Einladung: ${TIK}/t/textbanner`);
ok(t.code === 201, `eine Kampagne mit Textdatei als Banner entsteht trotzdem (HTTP ${t.code})`);
const datT = dbLesen("SELECT * FROM dateien WHERE eintrag_id = ?", t.j.id);
ok(datT.length === 0, `aber ohne Datei (${datT.length})`);
ok((t.j.hinweise || []).some((h) => h.includes("Banner")),
"und die Kachel sagt, dass das Banner fehlt");
/* DIE SCHRANKE GEGEN FREMDE ZIELE. */
ok(bildAdresseErlaubt("https://p16.tiktokcdn.com/x.png") === true
&& bildAdresseErlaubt("https://169.254.169.254/latest/") === false
&& bildAdresseErlaubt("http://p16.tiktokcdn.com/x.png") === false,
"eine Banneradresse ins eigene Netz würde abgelehnt");
}
/* =====================================================================
8. DIESELBE KAMPAGNE NUR EINMAL
===================================================================== */
melde("");
melde("=== 8. Zwei Einladungen zur selben Kampagne ergeben EINE Kachel ===");
{
const vorher = dbLesen("SELECT COUNT(*) AS n FROM eintraege WHERE kampagne_id = ?", ID_GIPFEL)[0].n;
const zweit = await einlesen(`Noch eine Einladung: ${TIK}/t/gipfel-zweiter-versand`);
ok(zweit.code === 200 && zweit.j.schon === true && zweit.j.id === kachelId,
`ein ZWEITER Kurzlink auf dieselbe Kampagne: HTTP ${zweit.code}, `
+ `dieselbe Kachel #${zweit.j.id}`);
const nachher = dbLesen("SELECT COUNT(*) AS n FROM eintraege WHERE kampagne_id = ?", ID_GIPFEL)[0].n;
ok(vorher === 1 && nachher === 1, `und es blieb bei ${nachher} Eintrag`);
/* WELCHE DER ZWEI SICHERUNGEN HAT GEGRIFFEN? (nachgetragen 07.10.2026)
Es gibt zwei: die Pruefung im Code (faengt den Normalfall) und den
eindeutigen Index (faengt den Wettlauf). Beide antworten gleich --
und genau daran ist eine Gegenprobe gescheitert: Nimmt man die
Pruefung im Code heraus, faengt der Index es auf, die Antwort ist
dieselbe, und die Pruefung blieb gruen.
Das war kein Schaden, aber ein blinder Fleck. Unterschieden wird
deshalb am Protokolltext: Der Index vermerkt "Wettlauf, Index".
Steht das im Normalfall da, fehlt die Pruefung im Code. */
const wie = dbLesen("SELECT detail FROM protokoll WHERE aktion = 'kampagne_schon_da'"
+ " ORDER BY id DESC LIMIT 1")[0];
ok(/-> Eintrag [0-9]+$/.test((wie?.detail || "").trim()),
`gefangen hat es die Pruefung im Code: „${wie?.detail}“`);
ok(!/Wettlauf/.test(wie?.detail || ""),
" nicht erst der Index -- sonst waere die Pruefung im Code wirkungslos");
/* GEGENPROBE: Eine ANDERE Kampagne entsteht sehr wohl. */
const andere = await einlesen(`Einladung: ${TIK}/t/beendet`);
ok(andere.code === 201 && andere.j.id !== kachelId,
`Gegenprobe: eine andere Kampagne wird angelegt (#${andere.j.id}, `
+ `„${andere.j.titel}“)`);
/* Und sie kommt aus der ZWEITEN Schemaquelle -- eine beendete
Kampagne liefert das Interaktionsschema nicht mehr. */
const az = dbLesen("SELECT kampagne_daten FROM eintraege WHERE id = ?", andere.j.id)[0];
ok(JSON.parse(az.kampagne_daten).gelesen_aus === "value.schema",
" gelesen aus value.schema, weil sie beendet ist");
/* DER EINDEUTIGE INDEX fängt den Wettlauf, den der Code nicht sieht. */
const idx = dbLesen("SELECT sql FROM sqlite_master WHERE name = 'idx_eintraege_kampagne'");
ok(idx.length === 1 && /UNIQUE/i.test(idx[0].sql) && /kampagne_id IS NOT NULL/i.test(idx[0].sql),
"und ein eindeutiger Index steht dahinter, nicht nur die Prüfung im Code");
}
/* =====================================================================
9. DREI VERSCHIEDENE, VERSTÄNDLICHE MELDUNGEN
===================================================================== */
melde("");
melde("=== 9. Was schiefgehen kann, wird verständlich gesagt ===");
{
const vorher = dbLesen("SELECT COUNT(*) AS n FROM eintraege")[0].n;
const ohneLink = await einlesen("Hier steht gar keine Adresse drin.");
ok(ohneLink.code === 400 && /keine TikTok-Adresse/i.test(ohneLink.j.fehler || ""),
`kein Link: HTTP ${ohneLink.code} — „${ohneLink.j.fehler}“`);
const fremd = await einlesen(`Klick: ${TIK}/t/fremd`);
ok(fremd.code === 400 && /beispiel\.de/.test(fremd.j.fehler || ""),
`fremdes Ziel: HTTP ${fremd.code} — „${fremd.j.fehler}“`);
const kaputt = await einlesen(`Klick: ${TIK}/t/kaputt`);
ok(kaputt.code === 502 && /__MODERN_ROUTER_DATA__/.test(kaputt.j.fehler || ""),
`umgebaute Seite: HTTP ${kaputt.code} — „${(kaputt.j.fehler || "").slice(0, 90)}…“`);
const ohneEnde = await einlesen(`Klick: ${TIK}/t/ohneende`);
ok(ohneEnde.code === 502 && /Ende/.test(ohneEnde.j.fehler || ""),
`ohne Enddatum: HTTP ${ohneEnde.code} — „${(ohneEnde.j.fehler || "").slice(0, 90)}…“`);
/* DIE DREI MELDUNGEN SIND WIRKLICH VERSCHIEDEN. Eine Prüfung, die
nur „es kam ein Fehler“ sagt, wäre auch grün, wenn überall
dasselbe stünde. */
const saetze = new Set([ohneLink.j.fehler, fremd.j.fehler,
kaputt.j.fehler, ohneEnde.j.fehler]);
ok(saetze.size === 4, `${saetze.size} verschiedene Sätze für vier Fälle`);
ok([...saetze].every((s) => s && !/\b50\d\b/.test(s) && s.length > 25),
"keiner davon sagt „Fehler 502“, alle sind ganze Sätze");
const nachher = dbLesen("SELECT COUNT(*) AS n FROM eintraege")[0].n;
ok(vorher === nachher,
`und keiner der vier hat eine halbe Kachel hinterlassen (${vorher} = ${nachher})`);
/* OHNE PREISE: 201 MIT HINWEIS, nicht 502. Unvollständig ist etwas
anderes als falsch. */
const op = await einlesen(`Klick: ${TIK}/t/ohnepreise`);
ok(op.code === 201 && (op.j.hinweise || []).some((h) => h.includes("Preise")),
`ohne Preise: HTTP ${op.code} mit Hinweis „${(op.j.hinweise || [])[0]}“`);
const opZ = dbLesen("SELECT event_regeln FROM eintraege WHERE id = ?", op.j.id)[0];
ok(/Bitte nachtragen/.test(opZ.event_regeln || ""),
"und die Kachel selbst sagt, dass etwas nachzutragen ist");
}
/* =====================================================================
10. EIN STUMMER SERVER HÄNGT DIE ROUTE NICHT AUF
===================================================================== */
melde("");
melde("=== 10. Die echte Frist am echten Weg ===");
{
const t0 = Date.now();
const a = await einlesen(`Klick: ${TIK}/t/stumm`);
const dauer = Date.now() - t0;
ok(a.code === 502 && /12 s/.test(a.j.fehler || ""),
`HTTP ${a.code} nach ${(dauer / 1000).toFixed(1)} s — „${a.j.fehler}“`);
ok(dauer >= 11_000 && dauer < 20_000,
`die Frist greift bei 12 s, nicht früher und nicht nie (${(dauer / 1000).toFixed(1)} s)`);
}
/* =====================================================================
11. WER DARF DAS
===================================================================== */
melde("");
melde("=== 11. Kampagnen anlegen dürfen DogFather und die Manager ===");
{
const creator = await einlesen(`Klick: ${TIK}/t/gipfel`, keksLuna);
ok(creator.code === 403, `ein Creator darf nicht (HTTP ${creator.code})`);
ok(/DogFather und die Manager/.test(creator.j.fehler || ""),
`und erfährt warum: „${creator.j.fehler}“`);
const ohne = await roh("/workspace/api/kampagne/einlesen", { rumpf: { text: "x" } });
ok(ohne.code === 401, `ohne Anmeldung gar nicht (HTTP ${ohne.code})`);
/* GEGENPROBE: Eine Managerin darf -- sonst bewiese die Zeile oben
nur, dass die Route überhaupt ablehnt. */
const mara = await einlesen(`Klick: ${TIK}/t/gipfel`, keksMara);
ok(mara.code === 200 && mara.j.schon === true,
`eine Managerin darf (HTTP ${mara.code}, Kampagne war schon da)`);
/* UND DIE OBERFLÄCHE FRAGT DIESELBE REGEL. Keine Rollenliste im
Browser -- am 10.09.2026 widersprachen sich zwei solche Listen
drei Zeilen auseinander. */
const ichDogi = JSON.parse((await roh("/workspace/api/ich", { keks: keksDogi })).text);
const ichLuna = JSON.parse((await roh("/workspace/api/ich", { keks: keksLuna })).text);
const ichMara = JSON.parse((await roh("/workspace/api/ich", { keks: keksMara })).text);
ok(ichDogi.darf_kampagne_einlesen === true && ichMara.darf_kampagne_einlesen === true
&& ichLuna.darf_kampagne_einlesen === false,
"/api/ich sagt der Oberfläche dasselbe wie die Schranke der Route");
/* FREMDE HERKUNFT. Die Wache steht in index.js, VOR diesem Router. */
const fremd = await new Promise((f, s) => {
const daten = Buffer.from(JSON.stringify({ text: "x" }));
const a = request({
host: "127.0.0.1", port: PORT, path: "/workspace/api/kampagne/einlesen",
method: "POST",
headers: { Host: HOST, Origin: "https://boeswillig.example", Cookie: keksDogi,
"Content-Type": "application/json", "Content-Length": daten.length },
}, (r) => { r.resume(); r.on("end", () => f({ code: r.statusCode })); });
a.on("error", s); a.write(daten); a.end();
});
ok(fremd.code === 403, `eine Anfrage von fremder Seite wird abgewiesen (HTTP ${fremd.code})`);
}
/* =====================================================================
12. DAS ANDERE HAUS
===================================================================== */
melde("");
melde("=== 12. Team Dogi bleibt unberührt ===");
{
const crewVorher = dbLesen("SELECT COUNT(*) AS n FROM eintraege WHERE haus = 'crew'")[0].n;
ok(crewVorher === 1,
`es gibt ${crewVorher} Crew-Eintrag -- gezählt wird über etwas, nicht über nichts`);
const alle = dbLesen("SELECT haus, COUNT(*) AS n FROM eintraege"
+ " WHERE kampagne_id IS NOT NULL GROUP BY haus");
ok(alle.length === 1 && alle[0].haus === "agentur",
`alle ${alle[0]?.n} Kampagnen liegen im Haus „${alle[0]?.haus}“`);
/* VON DER CREW-ADRESSE AUS: nicht sichtbar. */
const crewBrett = await roh("/workspace/api/bereich/agentur",
{ keks: keksDogiCrew, wirt: CREW_HOST });
const sichtbar = JSON.parse(crewBrett.text || "{}").eintraege || [];
ok(!sichtbar.some((e) => e.id === kachelId),
`die Kampagne erscheint auf der Crew-Adresse NICHT (${sichtbar.length} Einträge dort)`);
/* GEGENPROBE: Auf der Agentur-Adresse sehr wohl. */
const wsBrett = JSON.parse((await roh("/workspace/api/bereich/agentur", { keks: keksDogi })).text);
ok((wsBrett.eintraege || []).some((e) => e.id === kachelId),
"Gegenprobe: auf der Agentur-Adresse schon");
const crewNachher = dbLesen("SELECT COUNT(*) AS n FROM eintraege WHERE haus = 'crew'")[0].n;
ok(crewVorher === crewNachher,
`und die Zahl der Crew-Einträge ist unverändert (${crewVorher} = ${crewNachher})`);
}
/* =====================================================================
13. DAS PROTOKOLL
===================================================================== */
melde("");
melde("=== 13. Jeder Versuch steht im Protokoll, nicht nur der Erfolg ===");
{
const p = dbLesen("SELECT aktion, COUNT(*) AS n FROM protokoll"
+ " WHERE aktion LIKE 'kampagne%' GROUP BY aktion ORDER BY aktion");
const nach = Object.fromEntries(p.map((z) => [z.aktion, z.n]));
ok((nach.kampagne_eingelesen || 0) >= 3,
`${nach.kampagne_eingelesen || 0} erfolgreiche Einlesevorgänge`);
ok((nach.kampagne_gescheitert || 0) >= 5,
`${nach.kampagne_gescheitert || 0} gescheiterte -- genau die, die sonst spurlos wären`);
ok((nach.kampagne_schon_da || 0) >= 2,
`${nach.kampagne_schon_da || 0}× „war schon da“`);
const einer = dbLesen("SELECT detail FROM protokoll WHERE aktion = 'kampagne_gescheitert'"
+ " ORDER BY id DESC LIMIT 1")[0];
ok((einer?.detail || "").length > 5,
`und der Grund steht dabei: „${(einer?.detail || "").slice(0, 70)}“`);
}
/* =====================================================================
14. DIE BREMSE
===================================================================== */
melde("");
melde("=== 14. Zwanzig in einer Stunde, dann ist Schluss ===");
{
/* EINE EIGENE PERSON dafür: Die Bremse zählt je Person, und die
Versuche der Abschnitte darüber gehören DogFather. */
const { KAMPAGNEN_JE_STUNDE } = await import("./workspace-kampagne.js");
ok(KAMPAGNEN_JE_STUNDE === 20, `die Grenze steht bei ${KAMPAGNEN_JE_STUNDE}`);
let letzterGute = null;
for (let i = 1; i <= KAMPAGNEN_JE_STUNDE; i++) {
letzterGute = await einlesen(`Klick: ${TIK}/t/gipfel`, keksBrem);
if (letzterGute.code === 429) break;
}
ok(letzterGute && letzterGute.code !== 429,
`der ${KAMPAGNEN_JE_STUNDE}. Versuch geht noch durch (HTTP ${letzterGute?.code})`);
const zuviel = await einlesen(`Klick: ${TIK}/t/gipfel`, keksBrem);
ok(zuviel.code === 429, `der ${KAMPAGNEN_JE_STUNDE + 1}. wird gebremst (HTTP ${zuviel.code})`);
ok(/Minute/.test(zuviel.j.fehler || ""),
`und sagt, wann es weitergeht: „${zuviel.j.fehler}“`);
/* GEGENPROBE: Eine andere Person ist NICHT gebremst. Die Bremse ist
persönlich, keine Sperre für alle. */
const andere = await einlesen(`Klick: ${TIK}/t/gipfel`, keksMara);
ok(andere.code !== 429, `eine andere Person darf weiter (HTTP ${andere.code})`);
/* UND DIE BREMSE VERLÄNGERT SICH NICHT SELBST: Ihre eigene Meldung
zählt nicht mit, sonst käme man nie wieder heraus. */
const bremsZaehler = dbLesen("SELECT COUNT(*) AS n FROM protokoll"
+ " WHERE aktion = 'kampagne_bremse'")[0].n;
ok(bremsZaehler >= 1, `${bremsZaehler} Bremsvorgänge stehen im Protokoll`);
const gezaehlt = dbLesen("SELECT COUNT(*) AS n FROM protokoll"
+ " WHERE aktion IN ('kampagne_eingelesen','kampagne_gescheitert','kampagne_schon_da')"
+ " AND person_id = (SELECT id FROM personen WHERE name = 'Bremser')")[0].n;
ok(gezaehlt === KAMPAGNEN_JE_STUNDE,
`gezählt werden genau ${gezaehlt} von ${KAMPAGNEN_JE_STUNDE} Versuchen`
+ " -- die Bremsmeldungen selbst nicht");
}
/* =====================================================================
SCHLUSS
===================================================================== */
melde("");
melde(`${abrufe} Abrufe beim nachgebauten Dienst, keiner im echten Netz.`);
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
tikDienst.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }
process.exit(fehler ? 1 : 0);
+7 -2
View File
@@ -33,7 +33,7 @@
===================================================================== */
import express from "express";
import { db, sitzungLesen, AUSSEN_ROLLEN, bereicheFuer } from "./workspace.js";
import { db, sitzungLesen, AUSSEN_ROLLEN, bereicheFuer, tagLokalVon } from "./workspace.js";
import { darfSeite } from "./rechte.js";
import {
STUFEN, PFLEGE_ROLLEN, fassungenFuer,
@@ -191,7 +191,12 @@ function stufenkarte(person) {
if (!erstellt) return null;
const t = Date.parse(erstellt);
if (!Number.isFinite(t)) return null;
return new Date(t + tage * 86400_000).toISOString().slice(0, 10);
/* ORTSZEIT, NICHT UTC (berichtigt 07.10.2026, gefunden von
pruef-struktur). Hier stand `.toISOString().slice(0, 10)`.
Wer zwischen 00:00 und 02:00 deutscher Zeit beitritt, liegt
in UTC noch im Vortag -- die Seite nannte dann einen Termin,
der einen Tag zu frueh war. */
return tagLokalVon(t, tage);
};
return {
jetzt,
+509
View File
@@ -0,0 +1,509 @@
/* =====================================================================
EINE KAMPAGNEN-KACHEL AUS EINEM TIKTOK-LINK (07.10.2026) — Etappe 3
Haus: workspace.dogfather-universe.com (Agentur / Spicy Media).
Team Dogi bleibt unberührt — `hausFuerNeuenEintrag` entscheidet das,
nicht diese Datei.
Der Auftrag in einem Satz: Im Agentur-Bereich gibt es ein Feld, in
das die GANZE Einladungsnachricht eingefügt wird — und daraus
entsteht eine fertige Agentur-Event-Kachel mit Titel, Zeitraum,
Banner, Aufgaben zum Abhaken, Regeln, Ligen und Preisen. Ohne einen
einzigen Handgriff danach.
Eingabe ist genau das, was TikTok schickt, mitsamt dem Fließtext:
Dein Creator Network lädt dich ein, an einer Kampagne
teilzunehmen. Klicke hier, um weitere Informationen zu
erhalten: https://www.tiktok.com/t/ZSbqPsFa8/
---------------------------------------------------------------------
HIER WIRD WENIG ERFUNDEN
Die Kachel gibt es schon: Bereich `agentur`, Art `kampagne`, Tabelle
`eintraege` mit `event_ende`, `event_aufgaben`, `event_regeln`,
Haken über `event_punkte`, Banner über `dateien.eintrag_id`. Diese
Datei füllt vorhandene Felder — sie baut keine neuen.
`workspace-bereiche.js` wird NICHT angefasst. Fiele beim Bauen auf,
dass doch etwas nötig ist, wäre das ein Zeichen, dass die Abbildung
nicht passt — dann lieber die Abbildung ändern als die
Bereichslogik.
Gelesen wird in `kampagne-lesen.mjs` (kein Netz, keine Datenbank),
geholt wird mit `helfer-tiktok.mjs` (kein Schreiben). Hier steht nur
die Reihenfolge, die Rechtefrage und das Schreiben.
---------------------------------------------------------------------
DIE REIHENFOLGE — und warum sie so ist
1. Link aus dem Text holen ohne Netz, ohne Datenbank
2. Bremse VOR dem ersten Abruf nach draußen
3. Umleitung Station für Station jede Zwischenadresse geprüft
4. Kampagnennummer erst aus der Adresse (billig)
5. Dublettenprüfung spart die ganze Seite
6. Seite holen 12 s, 3 MB beim Lesen
7. Lesen drei Ausgänge
8. Banner herunterladen eigene Frist, Byteprüfung
9. Schreiben in EINER Transaktion
10. Protokoll
Die Bremse steht an Stelle 2 und nicht an Stelle 9: Eine Bremse, die
erst nach dem Abruf greift, bremst den fremden Server nicht.
Die Dublettenprüfung steht zweimal da — einmal hier (Stelle 5, fängt
den Normalfall) und einmal als eindeutiger Index in der Datenbank
(fängt den Wettlauf zweier gleichzeitiger Aufrufe). Beides, nicht
eins davon.
---------------------------------------------------------------------
JEDER FEHLSCHLAG WIRD PROTOKOLLIERT, nicht nur der Erfolg
Sonst lässt sich auf „warum kam meine Kampagne nicht an?" nicht
einmal sagen, OB der Versuch den Server erreicht hat. Genau diese
Reihenfolge hat am 06.09.2026 im Shop zwei Stunden gekostet: Die
Frage „warum kommt die Mail nicht an?" setzt voraus, dass „wurde sie
überhaupt abgeschickt?" schon beantwortet ist.
Aufrufen: POST /workspace/api/kampagne/einlesen { text }
===================================================================== */
import express from "express";
import { randomBytes } from "node:crypto";
import { join } from "node:path";
import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
import {
db, protokolliere, echteIp, sitzungLesen, istLeitung,
DATEN_ORDNER, hausFuerNeuenEintrag } from "./workspace.js";
import { bildArtVon } from "./workspace-dateien.js";
import {
linkAusText, folgeUmleitung, holeSeite, holeMitFrist, bildAdresseErlaubt,
hostVon, FRIST_SEITE_MS } from "./helfer-tiktok.mjs";
import { leseKampagne, regelnText, aufgabenText, KampagneUnlesbar }
from "./kampagne-lesen.mjs";
export const kampagneRouter = express.Router();
const ORDNER = join(DATEN_ORDNER, "dateien");
try { mkdirSync(ORDNER, { recursive: true }); } catch { /* gibt es schon */ }
/* WIE OFT IN EINER STUNDE? Wir lesen eine Seite, zu der wir eingeladen
wurden, einmal je Link, ausgelöst durch einen Menschen. Zwanzig ist
reichlich für einen Menschen und wenig für ein Skript. Die Zahl
steht hier und nicht in der Route, damit sie an genau einer Stelle
steht -- die Prüfung liest sie mit. Vorbild: BEITRAEGE_JE_STUNDE
in workspace-bereiche.js. */
export const KAMPAGNEN_JE_STUNDE = 20;
/* Das Banner. Dieselbe Grenze wie beim Video-Cover: Was von draußen
kommt, ist von draußen. */
const BANNER_MAX = 4 * 1024 * 1024;
/* Ab wann drängt es? Der Bauplan: „hoch", wenn das Ende weniger als
sieben Tage entfernt ist. Gerechnet, nicht geraten -- und hier, nicht
im Leser: Eine Lesefunktion, die die Wanduhr befragt, liefert zu
verschiedenen Zeiten verschiedene Ergebnisse auf dieselbe Eingabe. */
const DRAENGT_AB_TAGEN = 7;
/* SO VIEL FASST EIN TEXTFELD DER KACHEL. Nicht hier erfunden:
TEXT_MAX in workspace-bereiche.js, und die PUT-Route dort lehnt
mehr mit "Die Regeln sind zu lang." ab.
WARUM DAS UEBERHAUPT EINE FRAGE IST: Der Regeltext von
"Gipfelstuermer" ist gemessen 7474 Zeichen lang -- Geschenke, sechs
Preisgruppen, zwei Klapptexte mit Ligen und Teilnahmebedingungen.
Wer das ungekuerzt hineinschreibt, baut eine Kachel, die DogFather
OEFFNEN, aber nicht SPEICHERN kann: Er aendert ein Wort, drueckt
Speichern und bekommt eine Fehlermeldung ueber etwas, das er nie
getippt hat. Ein Formular, das der Server ablehnt, laesst den
Menschen davor sich selbst fuer den Fehler halten. */
const FELD_MAX = 6000;
/** Kuerzt an einer ABSCHNITTSGRENZE und sagt, dass gekuerzt wurde.
*
* Mitten im Wort abzuschneiden waere das Schlimmere von zweien: Es
* sieht nicht nach einer Grenze aus, sondern nach einem Fehler beim
* Einlesen -- und danach sucht jemand im Code statt im Textfeld.
*
* Verloren geht nichts: Das Gelesene steht gegliedert in
* kampagne_daten. */
function kuerzen(text, max = FELD_MAX) {
const s = String(text || "");
if (s.length <= max) return s;
const fussnote = "\n\n* Der Regeltext war länger als das Feld fasst und endet "
+ "hier. Vollständig steht er in den Rohdaten der Kampagne.";
const platz = max - fussnote.length;
const schnitt = s.lastIndexOf("\n\n", platz);
/* Gibt es keine Abschnittsgrenze, dann die letzte Zeilengrenze --
und erst wenn auch die fehlt, hart. */
const bis = schnitt > platz * 0.4 ? schnitt
: (s.lastIndexOf("\n", platz) > platz * 0.4 ? s.lastIndexOf("\n", platz) : platz);
return s.slice(0, bis).trimEnd() + fussnote;
}
const jetztIso = () => new Date().toISOString();
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
next();
}
kampagneRouter.use("/workspace/api/kampagne", angemeldet);
/** Kurz und für Menschen. Steht im Protokoll, nicht auf dem Bildschirm. */
const kurz = (s, n = 180) => String(s ?? "").slice(0, n);
/** Die Kampagnennummer aus einer Adresse -- oder null.
* `new URL()`, kein regulärer Ausdruck: Ein Muster, das `activityId=`
* 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");
return n && /^\d{6,25}$/.test(n) ? n : null;
} catch { return null; }
}
/** Wie viele Einlesevorgänge hatte diese Person in der letzten Stunde?
*
* GEZÄHLT WIRD IM PROTOKOLL, nicht in `eintraege`. Ein Versuch, der
* scheitert, erzeugt keinen Eintrag -- wer Einträge zählt, bremst
* genau den nicht, der in einer Schleife hängt und bei jedem Versuch
* eine fremde Seite abruft. Gezählt werden deshalb VERSUCHE, und das
* Protokoll ist die Stelle, an der beide Ausgänge stehen.
*
* UND "kampagne_schon_da" GEHOERT DAZU (nachgetragen 07.10.2026,
* gefunden von der Pruefung). Zuerst standen hier nur "eingelesen"
* und "gescheitert". Damit war die Bremse genau fuer den haeufigsten
* Missbrauchsfall blind: Wer denselben Link wieder und wieder
* einfuegt, bekommt jedes Mal "schon da" -- und loeste jedes Mal
* einen Abruf nach draussen aus, ohne gezaehlt zu werden. Gemessen:
* 21 Versuche, 0 gebremst.
*
* "kampagne_bremse" steht BEWUSST nicht in der Liste: Sonst zaehlte
* die Bremse ihre eigenen Meldungen mit und verlaengerte sich
* selbst -- man kaeme nie wieder heraus. */
function versucheLetzteStunde(personId) {
const seit = new Date(Date.now() - 3600_000).toISOString();
const z = db().prepare(
`SELECT COUNT(*) AS anzahl, MIN(zeitpunkt) AS aeltester
FROM protokoll
WHERE person_id = ? AND zeitpunkt >= ?
AND aktion IN ('kampagne_eingelesen','kampagne_gescheitert',
'kampagne_schon_da')`)
.get(personId, seit);
return { anzahl: z?.anzahl || 0, aeltester: z?.aeltester || null };
}
/** Das Banner holen und ablegen. Gibt `{ name, groesse, art }` zurück
* -- oder `null`, wenn es kein Bild ist.
*
* WARUM HERUNTERLADEN UND NICHT VERLINKEN: Beim Video ist es bewiesen
* (`x-expires`, gemessen 48 Stunden). Bei den Kampagnenbildern ist
* nicht einmal nachweisbar, OB sie eine Frist tragen -- die Adressen
* sehen anders aus. Das ist kein Grund, es zu riskieren: Ein fremdes
* CDN kann jederzeit umziehen, und wer die Adresse gespeichert hat,
* merkt es nicht beim Bauen und nicht beim Testen, sondern Wochen
* später an leeren Kacheln.
*
* Und es ist nicht nur Haltbarkeit: Als Datei mit `eintrag_id` greift
* automatisch `darfEintragSehen()`, und das Bild ist für Creator
* sichtbar. Am 03.10.2026 war genau das schon einmal ein 404. */
async function bannerAblegen(adresse) {
if (!adresse) return null;
/* Die Adresse kommt aus einer FREMDEN Seite. Siehe bildAdresseErlaubt. */
if (!bildAdresseErlaubt(adresse)) return null;
let a;
try { a = await holeMitFrist(adresse, {}, { frist: FRIST_SEITE_MS }); }
catch { return null; }
if (!a.ok) return null;
const roh = a.roh;
if (!roh.length || roh.length > BANNER_MAX) return null;
/* DIESELBE BYTEPRÜFUNG WIE BEI HOCHGELADENEN DATEIEN. `dateien.typ`
wäre hier sogar noch wertloser als sonst: Den Typ behauptet dann
ein fremder Server. */
const art = bildArtVon(roh.subarray(0, 16));
if (!art) return null;
const endung = { "image/png": ".png", "image/jpeg": ".jpg",
"image/gif": ".gif", "image/webp": ".webp" }[art] || ".bin";
const name = `${Date.now().toString(36)}-${randomBytes(8).toString("hex")}${endung}`;
writeFileSync(join(ORDNER, name), roh, { flag: "wx" });
return { name, groesse: roh.length, art };
}
/* =====================================================================
DIE ROUTE
===================================================================== */
kampagneRouter.post("/workspace/api/kampagne/einlesen", async (req, res) => {
const person = req.person;
const ip = echteIp(req);
let abgelegt = null;
/** Ein Fehlschlag, immer gleich: protokollieren UND antworten.
* Zwei getrennte Stellen dafür wären zwei Stellen, an denen man das
* Protokollieren vergessen kann. */
const schiefgegangen = (status, nachricht, grund) => {
protokolliere("kampagne_gescheitert", {
personId: person.id, rolle: person.rolle, ip,
detail: kurz(grund),
});
return res.status(status).json({ fehler: nachricht });
};
try {
/* ---- Recht --------------------------------------------------
Eine Kampagne ist eine Ansage an das ganze Team. Wer sie anlegen
darf, ist dieselbe Frage wie „wer darf ein Agentur-Event
anlegen" -- also wird die VORHANDENE Regel gefragt, nicht eine
neue erfunden. Eine zweite Rechteregel neben einer ersten ist
die Sorte Dopplung, die beim nächsten Umbau auseinanderläuft. */
if (!istLeitung(person)) {
return schiefgegangen(403,
"Kampagnen anlegen dürfen DogFather und die Manager.",
`keine Leitung (${person.rolle})`);
}
const text = String(req.body?.text ?? "");
if (!text.trim()) {
return schiefgegangen(400,
"Da war kein Text. Füge die ganze Einladung ein.",
"leerer Text");
}
/* ---- 1. Den Link herauslösen -------------------------------- */
const link = linkAusText(text);
if (!link) {
return schiefgegangen(400,
"In diesem Text steht keine TikTok-Adresse. "
+ "Füge die Einladung noch einmal ein — der ganze Text reicht.",
`kein Link in ${text.length} Zeichen`);
}
/* ---- 2. Bremse, VOR dem ersten Abruf nach draußen ------------ */
{
const { anzahl, aeltester } = versucheLetzteStunde(person.id);
if (anzahl >= KAMPAGNEN_JE_STUNDE) {
const frei = new Date(Date.parse(aeltester) + 3600_000);
const min = Math.max(1, Math.ceil((frei.getTime() - Date.now()) / 60000));
/* Hier NICHT über `schiefgegangen`: Sonst zählte die Bremse
ihre eigene Meldung mit und verlängerte sich selbst. */
protokolliere("kampagne_bremse", {
personId: person.id, rolle: person.rolle, ip,
detail: `${anzahl} Versuche in einer Stunde`,
});
return res.status(429).json({
fehler: `Du hast in der letzten Stunde ${anzahl} Kampagnen eingelesen. `
+ `In ${min} ${min === 1 ? "Minute" : "Minuten"} geht es weiter.`,
});
}
}
/* ---- 3. Umleitung Station für Station ------------------------ */
const weg = await folgeUmleitung(link);
if (!weg.ok) {
if (weg.grund === "fremd") {
return schiefgegangen(400,
`Dieser Link führt zu ${hostVon(weg.url)}, nicht zu TikTok.`,
`Umleitung verlässt TikTok: ${weg.url}`);
}
if (weg.grund === "zu_viele") {
return schiefgegangen(400,
"Dieser Link führt im Kreis. Bitte die Einladung noch einmal öffnen.",
`mehr als 5 Umleitungen ab ${link}`);
}
if (weg.grund === "frist") {
return schiefgegangen(502,
"TikTok hat nicht geantwortet (12 s). Bitte später noch einmal.",
`Frist bei der Umleitung ab ${link}`);
}
return schiefgegangen(502,
"TikTok war nicht erreichbar. Bitte später noch einmal.",
`Umleitung ${weg.grund}: ${weg.fehler || weg.url}`);
}
const adresse = weg.url;
/* ---- 4./5. Kampagnennummer und Dublettenprüfung --------------
Erst aus der ADRESSE, weil das nichts kostet: Steht sie da und
kennen wir die Kampagne schon, sparen wir die ganze Seite.
Fehlt sie dort, wird sie unten aus der Seite gelesen -- und dann
noch einmal geprüft. */
const vorab = nummerAusAdresse(adresse);
if (vorab) {
const da = db().prepare(
"SELECT id FROM eintraege WHERE kampagne_id = ?").get(vorab);
if (da) {
protokolliere("kampagne_schon_da", {
personId: person.id, rolle: person.rolle, ip,
detail: kurz(`${vorab} -> Eintrag ${da.id}`),
});
return res.status(200).json({ id: da.id, schon: true });
}
}
/* ---- 6. Die Seite holen -------------------------------------- */
const seite = await holeSeite(adresse);
if (!seite.ok) {
if (seite.grund === "frist") {
return schiefgegangen(502,
"TikTok hat nicht geantwortet (12 s). Bitte später noch einmal.",
`Frist beim Holen von ${adresse}`);
}
if (seite.grund === "zu_gross") {
return schiefgegangen(502,
"Die Seite von TikTok war unerwartet groß und wurde abgebrochen.",
`über 3 MB (${seite.bytes}, angesagt: ${seite.angesagt})`);
}
if (seite.grund === "status") {
return schiefgegangen(502,
`TikTok hat die Seite nicht herausgegeben (${seite.status}).`,
`HTTP ${seite.status} bei ${adresse}`);
}
return schiefgegangen(502,
"TikTok war nicht erreichbar. Bitte später noch einmal.",
`Abruf ${seite.grund}: ${seite.fehler || adresse}`);
}
/* ---- 7. Lesen ------------------------------------------------ */
let gelesen;
try {
gelesen = leseKampagne(seite.text);
} catch (fehler) {
if (fehler instanceof KampagneUnlesbar) {
/* Die beiden Sorten werden verschieden beantwortet -- aber
beide mit 502 und beide mit dem GRUND im Klartext. Eine
Kachel ohne Zeitraum wäre eine Falschauskunft; sie behauptet
einen Termin, den sie nicht kennt. */
const satz = fehler.art === "pflicht"
? `Die Seite wurde gelesen, aber ${fehler.grund}. Bitte von Hand anlegen.`
: `Die Seite konnte nicht gelesen werden: ${fehler.grund}. `
+ "Vermutlich hat TikTok die Seite umgebaut.";
return schiefgegangen(502, satz, `${fehler.art}: ${fehler.grund}`);
}
throw fehler;
}
/* Die Nummer aus der Seite ist die verbindliche. Falls die Adresse
keine trug, wird jetzt zum ersten Mal auf Dubletten geprüft. */
const nummer = gelesen.pflicht.id;
if (!vorab) {
const da = db().prepare(
"SELECT id FROM eintraege WHERE kampagne_id = ?").get(nummer);
if (da) {
protokolliere("kampagne_schon_da", {
personId: person.id, rolle: person.rolle, ip,
detail: kurz(`${nummer} -> Eintrag ${da.id} (aus der Seite)`),
});
return res.status(200).json({ id: da.id, schon: true });
}
}
/* ---- 8. Banner ------------------------------------------------ */
abgelegt = await bannerAblegen(gelesen.kuer.bannerUrl);
const hinweise = [...gelesen.hinweise];
if (gelesen.kuer.bannerUrl && !abgelegt) {
hinweise.push("Das Banner konnte nicht geholt werden.");
}
/* ---- 9. Schreiben, in EINER Transaktion ----------------------
Eintrag und Banner gehören zusammen. Bricht das zweite INSERT
ab, darf das erste nicht stehen bleiben: Eine Kachel mit einem
Bild, das es nicht gibt, sieht aus wie ein Fehler im Anzeigen --
und man sucht ihn dort. */
const jetzt = jetztIso();
const tageBisEnde = Math.round(
(Date.parse(`${gelesen.pflicht.ende}T12:00:00Z`) - Date.parse(jetzt)) / 86400_000);
const dringlichkeit = tageBisEnde < DRAENGT_AB_TAGEN ? "hoch" : "mittel";
const d = db();
let eintragId = null;
let meine = false;
try {
try { d.exec("BEGIN IMMEDIATE"); meine = true; } catch { meine = false; }
const { lastInsertRowid } = d.prepare(`
INSERT INTO eintraege
(bereich, art, titel, text, datum, event_ende,
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, ?,?,?, ?,?,?,?,?)`)
.run(
gelesen.pflicht.titel.slice(0, 160),
kuerzen(gelesen.kuer.einleitung || ""),
/* Der TAG der Kampagne in IHRER Zone -- nicht heuteLokal().
Eine Kampagne, die morgen beginnt, steht sonst heute in der
Liste. */
gelesen.pflicht.start,
gelesen.pflicht.ende,
kuerzen(aufgabenText(gelesen.kuer)),
kuerzen(regelnText(gelesen.kuer, hinweise)),
dringlichkeit,
jetzt, person.id,
/* NICHT von Hand auf "agentur" gesetzt: Das Haus entscheidet
die Adresse, über die jemand hereinkommt. Wer es hier
festschreibt, baut die Trennung ein zweites Mal -- und
zwei Fassungen derselben Regel laufen auseinander. */
hausFuerNeuenEintrag(person, "agentur"),
nummer, link, jetzt, gelesen.pflicht.zone,
JSON.stringify(gelesen.auszug));
eintragId = Number(lastInsertRowid);
if (abgelegt) {
d.prepare(`
INSERT INTO dateien (name_original, name_datei, groesse, typ, status, notiz,
hochgeladen_von, erstellt, eintrag_id, haus)
VALUES (?,?,?,?, 'entwurf', NULL, ?,?,?,?)`)
.run(`banner-${nummer}${abgelegt.name.slice(abgelegt.name.lastIndexOf("."))}`,
abgelegt.name, abgelegt.groesse, abgelegt.art,
person.id, jetzt, eintragId, person.haus || null);
abgelegt = null; /* ab hier gehört die Datei dem Eintrag */
}
if (meine) d.exec("COMMIT");
} catch (fehler) {
if (meine) { try { d.exec("ROLLBACK"); } catch { /* schon zurück */ } }
/* Der eindeutige Index hat zugeschlagen: zwei gleichzeitige
Aufrufe für dieselbe Kampagne. Das ist kein Fehler, das ist
genau sein Zweck -- und die Antwort ist dieselbe wie bei der
Prüfung oben. */
if (/UNIQUE|constraint/i.test(String(fehler?.message))) {
const da = db().prepare(
"SELECT id FROM eintraege WHERE kampagne_id = ?").get(nummer);
if (da) {
protokolliere("kampagne_schon_da", {
personId: person.id, rolle: person.rolle, ip,
detail: kurz(`${nummer} -> Eintrag ${da.id} (Wettlauf, Index)`),
});
return res.status(200).json({ id: da.id, schon: true });
}
}
throw fehler;
}
/* ---- 10. Protokoll ------------------------------------------- */
protokolliere("kampagne_eingelesen", {
personId: person.id, rolle: person.rolle, ip,
detail: kurz(`${nummer} "${gelesen.pflicht.titel}" `
+ `${gelesen.pflicht.start}..${gelesen.pflicht.ende} -> Eintrag ${eintragId}`
+ (hinweise.length ? ` (${hinweise.length} Hinweise)` : "")),
});
return res.status(201).json({
id: eintragId,
titel: gelesen.pflicht.titel,
von: gelesen.pflicht.start,
bis: gelesen.pflicht.ende,
hinweise,
});
} catch (fehler) {
console.error("[kampagne] einlesen:", fehler?.message);
/* Ein abgelegtes Banner ohne Eintrag wäre eine Datei, die niemand
je wieder findet -- und die Ablage wächst still. */
if (abgelegt) {
try { unlinkSync(join(ORDNER, abgelegt.name)); } catch { /* weg ist weg */ }
}
return schiefgegangen(500,
"Beim Anlegen ist etwas schiefgegangen. Bitte noch einmal versuchen.",
`unerwartet: ${fehler?.message}`);
}
});
+101 -3
View File
@@ -4548,6 +4548,39 @@ function umstellungen(d) {
sichtbar (siehe hausWo). Eine woechentliche Serie haette die
Trennung damit Woche fuer Woche neu unterlaufen. */
["termin_serien", "haus", "TEXT"],
/* ================================================================
EINE KAMPAGNE AUS EINEM TIKTOK-LINK (07.10.2026), Etappe 3
Fuenf Spalten an einer Tabelle, die es schon gibt -- KEINE neue
Tabelle. Eine 'kampagnen'-Tabelle neben 'eintraege' waere ein
zweiter Ort fuer dasselbe, und beim naechsten Umbau liefe einer
von beiden aus. Die Kachel IST ein Eintrag; was dazukommt, ist
nur ihre Herkunft.
ADD COLUMN und kein Tabellenneubau: Es aendert sich kein CHECK.
Der Neubau vom 06.09. war noetig, weil dort der erlaubte
Wertebereich von 'bereich' wuchs -- und er ist der Weg, bei dem
man Inhalte verlieren kann.
WARUM 'kampagne_daten' die Rohwerte mitnimmt: Wenn in drei
Monaten auffaellt, dass auch die Ligen gebraucht werden, steht
die Antwort schon da. Ohne diese Spalte muesste jede Kampagne
neu geholt werden -- und eine abgelaufene antwortet dann
vielleicht nicht mehr. Gemessen am 07.10.2026: Bei einer
beendeten Kampagne liefert TikTok das Interaktionsschema gar
nicht mehr. Gespeichert wird ein AUSZUG, nicht die 1,1 MB.
WARUM 'kampagne_zone' eine eigene Spalte ist und nicht gerechnet
wird: Der Kalendertag einer Kampagne haengt an IHRER Zone, nicht
an der des Servers. Wer sie nicht mitspeichert, kann spaeter
nicht mehr richtig rechnen -- und eine Zahl, die um eins
danebenliegt, sieht aus wie eine Zahl. */
["eintraege", "kampagne_id", "TEXT"], // activityId -- Dublettenschluessel
["eintraege", "kampagne_quelle", "TEXT"], // der eingefuegte Link, unveraendert
["eintraege", "kampagne_geholt_am", "TEXT"], // ISO-Zeitstempel
["eintraege", "kampagne_zone", "TEXT"], // tz_location_name, z.B. Europe/Berlin
["eintraege", "kampagne_daten", "TEXT"], // JSON-Auszug der Rohwerte
]) {
try {
const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name);
@@ -4565,6 +4598,33 @@ function umstellungen(d) {
Tabelle ohne Spalte und meldet einen Fehler, den niemand braucht. */
haeuserNachtragen(d);
/* ================================================================
EINE KAMPAGNE NUR EINMAL (07.10.2026)
WARUM EIN EINDEUTIGER INDEX UND NICHT NUR DIE PRUEFUNG IM CODE:
Zwei gleichzeitige Aufrufe laufen BEIDE durch das SELECT, bevor
einer schreibt. Die Pruefung im Code faengt den Normalfall, der
Index faengt den Wettlauf. Beides, nicht eins davon.
Und WARUM auf kampagne_id und nicht auf der Adresse: Es gibt
MEHRERE Kurzlinks auf dieselbe Kampagne -- jeder Versand erzeugt
eine eigene campaign_traffic_id --, und dieselbe Kampagne laesst
sich auch ueber die lange Adresse oeffnen. Wer auf quelle_url
prueft (so macht es das Video, dort richtig), bekommt fuer zwei
Einladungen zur selben Kampagne zwei Kacheln.
WHERE kampagne_id IS NOT NULL: Alle anderen Eintraege -- Aufgaben,
Termine, Videos, Aushaenge -- haben dort NULL. Ohne diese
Bedingung waere nach dem ersten davon kein zweiter mehr moeglich.
Steht hier unten und nicht oben im Bauplan, weil die Spalte erst
durch die Schleife darueber entsteht. */
try {
d.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_eintraege_kampagne"
+ " ON eintraege (kampagne_id) WHERE kampagne_id IS NOT NULL");
} catch (fehler) {
console.error("[workspace] Index idx_eintraege_kampagne:", fehler?.message);
}
/* Der Nachfüller fragt bei jedem Lauf "welche Ausprägungen dieser
Serie gibt es schon?". Ohne diesen Verbund-Index liest SQLite dafür
die ganze Termintabelle. Er steht hier unten und nicht oben im
@@ -7608,6 +7668,19 @@ workspaceRouter.get("/workspace/api/ich", (req, res) => {
`ich.rolle === 'admin'`, und dieselbe Zeile schaltete auch das
Loeschen frei; die beiden gehoeren nicht zusammen. */
darf_rollen_wechseln: darfRollenWechseln(person),
/* OB DER KASTEN "KAMPAGNE AUS EINER EINLADUNG" ERSCHEINT
(07.10.2026). Aus derselben Regel wie die Schranke dahinter
(istLeitung in workspace-kampagne.js) -- die Oberflaeche
vergleicht keine Rollennamen selbst.
WARUM NICHT ["admin","spicy","manager"] IM BROWSER: Das waere
die vierte abgeschriebene Liste in diesem Haus. Am 10.09.2026
standen zwei solche Listen drei Zeilen auseinander und
widersprachen einander -- Spicy Media sah einen Knopf nicht,
obwohl der Weg dahinter offen war. Und die umgekehrte Richtung
ist schlimmer: Ein Feld, das erscheint und dann 403 bekommt,
laesst den Menschen davor sich selbst fuer den Fehler halten. */
darf_kampagne_einlesen: istLeitung(person),
/* ZWEI LISTEN STATT EINER RECHNUNG IM BROWSER (20.09.2026).
Die Rollenwahl nahm bisher die Knoepfe des Anlege-Formulars --
@@ -7896,10 +7969,35 @@ export function heuteLokal() {
* schweigen -- sonst wandert der Unsinn in die Datenbank und in
* die Anzeige, und gefunden wird er Wochen später. */
export function tagLokal(versatz = 0) {
if (!Number.isFinite(Number(versatz))) {
throw new TypeError(`tagLokal: "${versatz}" ist keine Zahl von Tagen.`);
return tagLokalVon(Date.now(), versatz);
}
const d = new Date(Date.now() + versatz * 86400_000);
/** Derselbe Kalendertag, aber von einem BELIEBIGEN Zeitpunkt aus.
*
* WOZU (07.10.2026, gefunden von pruef-struktur): In
* workspace-anleitung.js stand
*
* new Date(t + tage * 86400_000).toISOString().slice(0, 10)
*
* -- gerechnet wurde also ab dem Beitrittstag, der Tag aber in UTC
* gebildet. Wer zwischen 00:00 und 02:00 deutscher Zeit beitritt,
* liegt in UTC noch im Vortag: Die Seite nannte dann einen Termin,
* der einen Tag zu frueh war. Ein Datum, das um eins danebenliegt,
* sieht immer noch wie ein Datum aus -- es faellt niemandem auf.
*
* WARUM HIER UND NICHT DORT: Es waere die DRITTE Abschrift
* derselben drei Zeilen gewesen. `tagLokal` ist jetzt selbst nur
* noch ein Aufruf dieser Funktion mit `Date.now()` -- es gibt
* also WENIGER Rechenwege als vorher, nicht mehr. */
export function tagLokalVon(zeitpunkt, versatz = 0) {
if (!Number.isFinite(Number(versatz))) {
throw new TypeError(`tagLokalVon: "${versatz}" ist keine Zahl von Tagen.`);
}
const ms = Number(zeitpunkt);
if (!Number.isFinite(ms)) {
throw new TypeError(`tagLokalVon: "${zeitpunkt}" ist kein Zeitpunkt.`);
}
const d = new Date(ms + versatz * 86400_000);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
}
+14 -14
View File
@@ -7,17 +7,17 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/anleitung.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/anleitung.css?v=202610071712" />
<!-- module.css an VORLETZTER, haus.css an LETZTER Stelle. Die
Reihenfolge ist keine Geschmacksfrage: haus.css setzt nur
Variablen, und bei Variablen entscheidet allein, wer zuletzt
kommt. pruef-css-klassen.mjs misst genau dieses Paar nach. -->
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="halle">
@@ -289,16 +289,16 @@
Und anleitung.js braucht es doppelt: Symbol, Farbe, Name und
Adresse jeder Karte kommen von dort, nicht vom Server. -->
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<!-- wahl.js gehört zu kopf.js: Der Sicht-Umschalter baut daraus seine
Liste. pruef-css-klassen setzt das durch, und zwar auch die
Reihenfolge -- kopf.js fragt beim Aufbau nach dem fertigen
Bedienelement. -->
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/anleitung.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/anleitung.js?v=202610071712" defer></script>
</body>
</html>
+5 -5
View File
@@ -16,8 +16,8 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
<!-- Ohne diese drei nimmt ein iPhone einen Bildschirmausschnitt als
Symbol, Android zeichnet die Leiste weiss, und die Seite oeffnet
ausserhalb der App -- auf einer Seite, die genau erklaeren soll,
@@ -33,7 +33,7 @@
schreibt jede Seite app.webmanifest -- auch die hier. -->
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<style>
/* Bewusst in der Seite und nicht in einer eigenen Datei: Diese Seite
soll auch dann noch etwas anzeigen, wenn genau das schiefgeht,
@@ -135,7 +135,7 @@
<script src="assets/js/anruf-probe.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/anruf-probe.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
</body>
</html>
+4 -4
View File
@@ -23,25 +23,25 @@
},
"icons": [
{
"src": "/assets/img/app-symbole/workspace-192.png?v=202610071405",
"src": "/assets/img/app-symbole/workspace-192.png?v=202610071712",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-512.png?v=202610071405",
"src": "/assets/img/app-symbole/workspace-512.png?v=202610071712",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202610071405",
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202610071712",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202610071405",
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202610071712",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+91
View File
@@ -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; }
}
+103
View File
@@ -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);
+19 -19
View File
@@ -12,19 +12,19 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="garage">
@@ -430,22 +430,22 @@
<!-- TEXTFORM ZUERST (21.09.2026): Die Seite fragt beim Zeichnen nach
`window.Textform`. Kaeme sie spaeter, waere der erste Eintrag ohne
Gestaltung. Kein `defer`, damit sie wirklich vorher dasteht. -->
<script src="assets/js/textform.js?v=202610071405"></script>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/hochladen.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/textform.js?v=202610071712"></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/hochladen.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/zuteilung.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/zuteilung.js?v=202610071712" defer></script>
<!-- Der Vorlagenkatalog stand bis zum 23.09.2026 mitten in
aufgaben.js. Er bedient jetzt zwei Seiten und hat deshalb eine
eigene Datei; hier zeigt er den Creator-Katalog.
Vor aufgaben.js, weil die Seite ihn beim Start einrichtet. -->
<script src="assets/js/vorlagenbrett.js?v=202610071405" defer></script>
<script src="assets/js/aufgaben.js?v=202610071405" defer></script>
<script src="assets/js/vorlagenbrett.js?v=202610071712" defer></script>
<script src="assets/js/aufgaben.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/automation.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/automation.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="garage">
@@ -168,15 +168,15 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/automation.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/automation.js?v=202610071712" defer></script>
</body>
</html>
+17 -17
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -197,16 +197,16 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/befinden.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<script src="assets/js/befinden.js?v=202610071712" defer></script>
</body>
</html>
+51 -24
View File
@@ -12,29 +12,29 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202610071712" />
<!-- Wegen des Blocks „Willst du dazugehoeren?" auf dem Brett
„Mitmachen". Eine eigene Kopie der Regel hier waere die zweite
Stelle, an der dieselbe Sache anders aussieht. -->
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202610071712" />
<!-- Die Bildschau: ein Banner wird darin mittig gezeigt statt als nackte
Datei in einem neuen Tab. Sie ist seit dem 07.10.2026 eine eigene
Datei und keine Kopie aus chat.css -- dieselbe Schau im Chat und
hier, ein Stil. -->
<link rel="stylesheet" href="assets/css/bildschau.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bildschau.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -346,6 +346,33 @@
<p class="feld-hinweis" id="video-hinweis">Titel und Coverbild kommen von selbst. Es zählen nur deine drei Kanäle.</p>
</form>
</section>
<!-- EINE KAMPAGNE AUS EINER EINLADUNG (07.10.2026), Etappe 4 des
Bauplans. NUR im Agentur-Bereich und nur fuer die Leitung --
ob jemand darf, sagt der Server (`darf_kampagne_einlesen` in
/workspace/api/ich), nicht eine Rollenliste im Browser.
EIN TEXTBEREICH, KEIN EINZEILENFELD. Die Einladung ist
mehrzeilig; wer sie in ein <input> einfuegt, verliert die
Zeilenumbrueche und sieht nur das Ende. Und niemand soll den
Link erst heraussuchen muessen -- das Feld nimmt den ganzen
Text und holt sich die Adresse selbst heraus.
DIE MELDUNG STEHT NEBEN DEM FELD (`#kampagne-sagt`), nicht in
einem Dialog, der weggeklickt wird. Sie nennt den Grund im
Klartext -- "Dieser Link fuehrt zu beispiel.de, nicht zu
TikTok" --, nicht "Fehler 502". -->
<section id="kampagneholen" class="kampagneholen" hidden>
<form id="kampagne-form" class="kampagneholen__form">
<h2 class="kampagneholen__titel">Kampagne aus einer Einladung anlegen</h2>
<label class="feld-schild" for="kampagne-text">Einladung einfügen</label>
<textarea id="kampagne-text" rows="3" spellcheck="false"
placeholder="Einladung hier einfügen – der ganze Text reicht."></textarea>
<div class="kampagneholen__zeile">
<p class="feld-hinweis" id="kampagne-sagt" role="status" aria-live="polite">Titel, Zeitraum, Banner, Aufgaben und Preise kommen von selbst.</p>
<button type="submit" class="knopf knopf--klein" id="kampagne-holen">Kampagne holen</button>
</div>
</form>
</section>
<section id="startkatalog" class="startkatalog" hidden></section>
<div id="liste" aria-busy="true"><p class="leise">Einträge werden geladen …</p></div>
@@ -354,20 +381,20 @@
<!-- TEXTFORM ZUERST (21.09.2026): Die Seite fragt beim Zeichnen nach
`window.Textform`. Kaeme sie spaeter, waere der erste Eintrag ohne
Gestaltung. Kein `defer`, damit sie wirklich vorher dasteht. -->
<script src="assets/js/textform.js?v=202610071405"></script>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/textform.js?v=202610071712"></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/checkliste.js?v=202610071405" defer></script>
<script src="assets/js/ampel.js?v=202610071405" defer></script>
<script src="assets/js/schulung.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/bildschau.js?v=202610071405"></script>
<script src="assets/js/bereich.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/checkliste.js?v=202610071712" defer></script>
<script src="assets/js/ampel.js?v=202610071712" defer></script>
<script src="assets/js/schulung.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<script src="assets/js/bildschau.js?v=202610071712"></script>
<script src="assets/js/bereich.js?v=202610071712" defer></script>
</body>
</html>
+19 -19
View File
@@ -7,17 +7,17 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/treff.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/treff.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein - siehe treff-regeln.html. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -108,15 +108,15 @@
</main>
<!-- Der Fuß gehört zu jeder Seite - siehe pruef-css-klassen. -->
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/auskunft.js?v=202610071405" defer></script>
<script src="assets/js/bewerben.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/auskunft.js?v=202610071712" defer></script>
<script src="assets/js/bewerben.js?v=202610071712" defer></script>
</body>
</html>
+18 -18
View File
@@ -7,16 +7,16 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein - siehe treff-regeln.html. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -80,16 +80,16 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/auskunft.js?v=202610071405" defer></script>
<script src="assets/js/bewerbungen.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/auskunft.js?v=202610071712" defer></script>
<script src="assets/js/bewerbungen.js?v=202610071712" defer></script>
</body>
</html>
+3 -3
View File
@@ -59,11 +59,11 @@
<!-- DIE WERBEEINBLENDUNG. Dieselbe Datei wie in der Reaction-Seite:
Zwei Fassungen altern unterschiedlich, und dann sieht der Stream
anders aus als das, worueber geredet wird. -->
<link rel="stylesheet" href="assets/css/werbung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/werbung.css?v=202610071712" />
</head>
<body class="obs-buehne">
<div id="platz"></div>
<script src="assets/js/werbung.js?v=202610071405" defer></script>
<script src="assets/js/buehne.js?v=202610071405" defer></script>
<script src="assets/js/werbung.js?v=202610071712" defer></script>
<script src="assets/js/buehne.js?v=202610071712" defer></script>
</body>
</html>
+17 -17
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/calls.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/calls.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="lounge">
@@ -89,15 +89,15 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/ki.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/calls.js?v=202610071405" defer></script>
<script src="assets/js/ki.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/calls.js?v=202610071712" defer></script>
</body>
</html>
+21 -21
View File
@@ -12,31 +12,31 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/chat.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/chat.css?v=202610071712" />
<!-- Die Bildschau stand bis zum 07.10.2026 in chat.css. Sie wird jetzt
auch vom Event-Banner benutzt (bereich.html) und hat deshalb eine
eigene Datei. Sie steht GENAU HIER -- direkt hinter chat.css, also
an derselben Stelle im Ladeweg wie der Block vorher. -->
<link rel="stylesheet" href="assets/css/bildschau.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bildschau.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="lounge">
@@ -744,24 +744,24 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202610071405"></script>
<script src="assets/js/bereiche.js?v=202610071712"></script>
<!-- TEXTFORM VOR ALLEM ANDEREN (21.09.2026): chat.js und bereich.js
fragen beim Zeichnen nach `window.Textform`. Kaeme sie spaeter,
waere die erste gezeichnete Nachricht ohne Gestaltung -- und das
faellt nur dem auf, der genau hinsieht. KEIN `defer`, damit sie
wirklich vor den anderen dasteht. -->
<script src="assets/js/textform.js?v=202610071405"></script>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/hochladen.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405"></script>
<script src="assets/js/glocke.js?v=202610071405"></script>
<script src="assets/js/textform.js?v=202610071712"></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/hochladen.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712"></script>
<script src="assets/js/glocke.js?v=202610071712"></script>
<!-- ANRUF VOR CHAT (18.09.2026): anruf.js stellt `window.anrufStarten`
bereit, und chat.js baut die Knoepfe nur, wenn es das gibt.
Andersherum waere die Reihenfolge eine stille Abhaengigkeit --
die Knoepfe fehlten, und niemand wuesste warum. -->
<script src="assets/js/anruf.js?v=202610071405"></script>
<script src="assets/js/anruf.js?v=202610071712"></script>
<!-- DIE BILDSCHAU (25.09.2026). Eigene Datei, weil sie nichts vom
Chat weiss: Sie bekommt einen Weg und einen Namen und zeigt ein
Bild. Jede andere Seite mit Fotos kann sie ebenso benutzen, ohne
@@ -772,8 +772,8 @@
sie beim ersten Foto noch nicht da -- und der erste Klick ginge
wie frueher in den Tab. Ein Fehler, der nur beim ERSTEN Mal
auftritt, ist der, den man nie nachstellen kann. -->
<script src="assets/js/bildschau.js?v=202610071405"></script>
<script src="assets/js/chat.js?v=202610071405"></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/bildschau.js?v=202610071712"></script>
<script src="assets/js/chat.js?v=202610071712"></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
</body>
</html>
+17 -17
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/content.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/content.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="portal">
@@ -179,15 +179,15 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/checkliste.js?v=202610071405" defer></script>
<script src="assets/js/content.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/checkliste.js?v=202610071712" defer></script>
<script src="assets/js/content.js?v=202610071712" defer></script>
</body>
</html>
+6 -6
View File
@@ -36,15 +36,15 @@
<link rel="preload" as="image" href="assets/img/buehne/crew-gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<!-- Diese Wand steht NUR auf der Adresse von Team Dogi (ueberall sonst
404, siehe crewWeiche). Sie darf das Haus deshalb direkt nennen
statt ueber den Platzhalter haus.css zu gehen -- und sie MUSS es:
Eine Zugangswand, die sich erst nach dem Umbiegen faerbt, waere
die eine Seite, auf der man das Umfaerben sieht. -->
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202610071712" />
</head>
<body class="gate gate--crew">
@@ -459,7 +459,7 @@
<script src="assets/js/gate.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/gate.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
</body>
</html>
+4 -4
View File
@@ -23,25 +23,25 @@
},
"icons": [
{
"src": "/assets/img/app-symbole/crew-192.png?v=202610071405",
"src": "/assets/img/app-symbole/crew-192.png?v=202610071712",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-512.png?v=202610071405",
"src": "/assets/img/app-symbole/crew-512.png?v=202610071712",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202610071405",
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202610071712",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202610071405",
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202610071712",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+17 -17
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="arena">
@@ -108,15 +108,15 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/hochladen.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/hochladen.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/dateien.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/dateien.js?v=202610071712" defer></script>
</body>
</html>
+18 -18
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -306,20 +306,20 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<!-- VOR entwicklung.js: Die Seite richtet den Katalog beim Start ein,
also muss es ihn zu dem Zeitpunkt geben. Mit "defer" laufen beide
in Dokumentreihenfolge. -->
<script src="assets/js/vorlagenbrett.js?v=202610071405" defer></script>
<script src="assets/js/entwicklung.js?v=202610071405" defer></script>
<script src="assets/js/vorlagenbrett.js?v=202610071712" defer></script>
<script src="assets/js/entwicklung.js?v=202610071712" defer></script>
</body>
</html>
+15 -15
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/hilfe.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/hilfe.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -146,14 +146,14 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/hilfe.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/hilfe.js?v=202610071712" defer></script>
</body>
</html>
+5 -5
View File
@@ -30,9 +30,9 @@
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
</head>
<body class="gate">
@@ -427,7 +427,7 @@
<script src="assets/js/gate.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/gate.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
</body>
</html>
+17 -17
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="skyline">
@@ -383,15 +383,15 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/kalender.js?v=202610071405" defer></script>
<script src="assets/js/kalender-abo.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/kalender.js?v=202610071712" defer></script>
<script src="assets/js/kalender-abo.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="arena">
@@ -444,13 +444,13 @@
</form>
</dialog>
<script src="assets/js/meldung.js?v=202610071405"></script>
<script src="assets/js/nachfrage.js?v=202610071405"></script>
<script src="assets/js/wahl.js?v=202610071405"></script>
<script src="assets/js/bereiche.js?v=202610071405"></script>
<script src="assets/js/kopf.js?v=202610071405"></script>
<script src="assets/js/glocke.js?v=202610071405"></script>
<script src="assets/js/leistung.js?v=202610071405"></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712"></script>
<script src="assets/js/nachfrage.js?v=202610071712"></script>
<script src="assets/js/wahl.js?v=202610071712"></script>
<script src="assets/js/bereiche.js?v=202610071712"></script>
<script src="assets/js/kopf.js?v=202610071712"></script>
<script src="assets/js/glocke.js?v=202610071712"></script>
<script src="assets/js/leistung.js?v=202610071712"></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
</body>
</html>
+14 -14
View File
@@ -7,17 +7,17 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/manager-ziele.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/manager-ziele.css?v=202610071712" />
<!-- module.css an VORLETZTER, haus.css an LETZTER Stelle. Die
Reihenfolge ist keine Geschmacksfrage: haus.css setzt nur
Variablen, und bei Variablen entscheidet allein, wer zuletzt
kommt. pruef-css-klassen.mjs misst genau dieses Paar nach. -->
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="arena">
@@ -230,16 +230,16 @@
kopf.js liest daraus die Leitungsrollen. Ohne sie stürzt jede
Seite ab, und zwar nur in der Browserkonsole -- die Kopfleiste
kommt trotzdem. -->
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<!-- wahl.js gehört zu kopf.js: Der Sicht-Umschalter baut daraus seine
Liste. pruef-css-klassen setzt das durch, und zwar auch die
Reihenfolge -- kopf.js fragt beim Aufbau nach dem fertigen
Bedienelement. -->
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/manager-ziele.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/manager-ziele.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -12,13 +12,13 @@
vergessen wird. -->
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/material.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/material.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-ton="42" data-buehne="studio">
@@ -138,14 +138,14 @@
<p class="m-laedt" id="laedt">Einen Moment …</p>
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/hochladen.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/material.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/hochladen.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/material.js?v=202610071712" defer></script>
</body>
</html>
+14 -14
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/notizen.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/notizen.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-ton="46">
@@ -181,12 +181,12 @@
<!-- bereiche.js VOR kopf.js: Dort steht `window.Bereiche`, und kopf.js
liest daraus die Leitungsrollen. -->
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/notizen.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/notizen.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/personen.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/personen.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="halle">
@@ -453,14 +453,14 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/personen.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/personen.js?v=202610071712" defer></script>
</body>
</html>
+18 -18
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/profil.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/profil.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="halle">
@@ -214,16 +214,16 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/hochladen.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/hochladen.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/steckbrief.js?v=202610071405" defer></script>
<script src="assets/js/profil.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/steckbrief.js?v=202610071712" defer></script>
<script src="assets/js/profil.js?v=202610071712" defer></script>
</body>
</html>
+19 -19
View File
@@ -7,15 +7,15 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/reaktion.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/spendenkarte.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/werbung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/reaktion.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/spendenkarte.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/werbung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start reaktion-seite" data-stand="laedt">
@@ -905,15 +905,15 @@
<!-- bereiche.js VOR kopf.js: Dort steht `window.Bereiche`, und kopf.js
liest daraus die Leitungsrollen. -->
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/spendenkarte.js?v=202610071405" defer></script>
<script src="assets/js/werbung.js?v=202610071405" defer></script>
<script src="assets/js/reaktion.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/spendenkarte.js?v=202610071712" defer></script>
<script src="assets/js/werbung.js?v=202610071712" defer></script>
<script src="assets/js/reaktion.js?v=202610071712" defer></script>
</body>
</html>
+17 -17
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/treff.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/treff.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -134,14 +134,14 @@
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
die seit dem 07.09. jede Seite danach absucht. -->
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/rechte.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<script src="assets/js/rechte.js?v=202610071712" defer></script>
</body>
</html>
+18 -18
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/report.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/report.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="showbuehne">
@@ -106,17 +106,17 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/ki.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/report.js?v=202610071405" defer></script>
<script src="assets/js/ki.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<script src="assets/js/report.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="wald">
@@ -194,14 +194,14 @@
</div>
</div>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/scouting.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/scouting.js?v=202610071712" defer></script>
</body>
</html>
+19 -19
View File
@@ -12,26 +12,26 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
<link rel="stylesheet" href="assets/css/heim.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/heim.css?v=202610071712" />
<!-- Die Einweisung bringt ihr Aussehen mit. Sie steht in
anleitung.css, weil sie dorthin gehoert -- und nicht in
start.css, wo sie zwischen 48 Kachelfarben verschwinden
wuerde. -->
<link rel="stylesheet" href="assets/css/anleitung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/anleitung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -768,22 +768,22 @@
</p>
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/live.js?v=202610071405" defer></script>
<script src="assets/js/start.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/live.js?v=202610071712" defer></script>
<script src="assets/js/start.js?v=202610071712" defer></script>
<!-- Die Begruessung und der Rundgang beim ersten Anmelden.
EIGENE DATEI und nicht ein Stueck start.js: Sie laeuft auf der
meistbenutzten Seite des Hauses, und wenn hier etwas schiefgeht,
darf davon nichts anderes betroffen sein. -->
<script src="assets/js/rundgang.js?v=202610071405" defer></script>
<script src="assets/js/aufstieg.js?v=202610071405" defer></script>
<script src="assets/js/rundgang.js?v=202610071712" defer></script>
<script src="assets/js/aufstieg.js?v=202610071712" defer></script>
</body>
</html>
+17 -17
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="wald">
@@ -85,16 +85,16 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/ki.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/startcheck.js?v=202610071405" defer></script>
<script src="assets/js/ki.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/startcheck.js?v=202610071712" defer></script>
</body>
</html>
+18 -18
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/profil.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/profil.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="halle">
@@ -150,16 +150,16 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/hochladen.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/hochladen.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/auskunft.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/steckbrief.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/auskunft.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/steckbrief.js?v=202610071712" defer></script>
</body>
</html>
+14 -14
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/support.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/support.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="studio">
@@ -154,16 +154,16 @@
(reading 'LEITUNG')" ab -- gemessen am 24.09.2026, als diese
Zeile beim Anlegen der Seite fehlte. Die Kopfleiste kam
trotzdem, deshalb sah man es nur in der Browserkonsole. -->
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<!-- wahl.js gehoert zu kopf.js: Der Sicht-Umschalter in der
Kopfleiste baut daraus seine Liste. pruef-css-klassen setzt das
seit dem 07.09.2026 durch -- damals fehlte es auf chat.html und
uebersicht.html, und heute hier. -->
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/support.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/support.js?v=202610071712" defer></script>
</body>
</html>
+3 -3
View File
@@ -23,7 +23,7 @@
„Eigenes CSS" leer lassen (der Grund ist schon
durchsichtig).
====================================================================== -->
<link rel="stylesheet" href="assets/css/spendenkarte.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/spendenkarte.css?v=202610071712" />
<style>
/* DURCHSICHTIG BIS AUF DIE KARTEN. OBS legt die Quelle über das
Bild; jede Fläche, die hier steht, deckt dort etwas zu. */
@@ -80,7 +80,7 @@
<body>
<div class="obs-tafel spendentafel" id="tafel" aria-live="polite"></div>
<script src="assets/js/spendenkarte.js?v=202610071405" defer></script>
<script src="assets/js/tafel.js?v=202610071405" defer></script>
<script src="assets/js/spendenkarte.js?v=202610071712" defer></script>
<script src="assets/js/tafel.js?v=202610071712" defer></script>
</body>
</html>
+18 -18
View File
@@ -7,18 +7,18 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071712" />
<!-- Wegen des Kastens „Anfragen“ ueber der Liste. Eine eigene Kopie
der Regel hier waere die zweite Stelle, an der dieselbe Sache
anders aussieht. -->
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -111,15 +111,15 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/talente.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<script src="assets/js/talente.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -7,20 +7,20 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/team.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/team.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="halle">
@@ -110,15 +110,15 @@
</main>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/team.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<script src="assets/js/team.js?v=202610071712" defer></script>
</body>
</html>
+17 -17
View File
@@ -7,20 +7,20 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/team.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/team.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<!-- `data-buehne` steht hier fest und nicht im Skript: Die Kachel zu
@@ -84,15 +84,15 @@
Kaputt ist dabei nichts -- er funktioniert. Genau deshalb faellt
es niemandem auf, und deshalb prueft pruef-css-klassen die
ABHAENGIGKEIT statt der Optik. Hier hat sie angeschlagen. -->
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/teamlage.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<script src="assets/js/teamlage.js?v=202610071712" defer></script>
</body>
</html>
+15 -15
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -112,15 +112,15 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/teilen.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/teilen.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -7,14 +7,14 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/treff.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/treff.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -126,14 +126,14 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/treff-moderation.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/treff-moderation.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -7,18 +7,18 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/treff.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/treff.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein. Auf der Adresse des
Rudels biegt der Server genau diesen Dateinamen auf crew-haus.css
um, und die Seite kommt schon beim ERSTEN Abruf in Lila und
Babyblau an -- ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -248,14 +248,14 @@
Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen
Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen,
die seit dem 07.09. jede Seite danach absucht. -->
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/auskunft.js?v=202610071405" defer></script>
<script src="assets/js/treff-regeln.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/auskunft.js?v=202610071712" defer></script>
<script src="assets/js/treff-regeln.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -12,22 +12,22 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="showbuehne">
@@ -149,16 +149,16 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/uebersicht.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/uebersicht.js?v=202610071712" defer></script>
</body>
</html>
+15 -15
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/unsere-seiten.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/unsere-seiten.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -335,14 +335,14 @@
</main>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/live.js?v=202610071405" defer></script>
<script src="assets/js/unsere-seiten.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/live.js?v=202610071712" defer></script>
<script src="assets/js/unsere-seiten.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -7,13 +7,13 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/unterstuetzen.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/unterstuetzen.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -106,17 +106,17 @@
<!-- bereiche.js VOR kopf.js: Dort steht `window.Bereiche`, und kopf.js
liest daraus die Leitungsrollen. Ohne sie stürzt jede Seite mit
„Cannot read properties of undefined (reading 'LEITUNG')" ab. -->
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/holen.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/holen.js?v=202610071712" defer></script>
<!-- Die Satz-Tafel. Ohne sie stand auf dieser Seite das Maschinenwort
des Servers (z.B. "nicht_gefunden") statt eines Satzes -- gemeldet
von pruef-meldungen, nicht beim Ansehen. -->
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/unterstuetzen.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/unterstuetzen.js?v=202610071712" defer></script>
</body>
</html>
+16 -16
View File
@@ -7,15 +7,15 @@
<meta name="robots" content="noindex, nofollow" />
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/werdegang.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/werdegang.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start">
@@ -144,14 +144,14 @@
</main>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/werdegang.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/werdegang.js?v=202610071712" defer></script>
</body>
</html>
+23
View File
@@ -35,6 +35,29 @@
<meta http-equiv="refresh" content="0; url=anleitung.html" />
<link rel="canonical" href="anleitung.html" />
<!-- DIE DREI, DIE JEDE SEITE DIESES HAUSES BRAUCHT (07.10.2026).
pruef-struktur hat sie hier vermisst, und das zu Recht -- auch
wenn diese Seite nur einen Wimpernschlag zu sehen ist:
theme-color sonst zeichnet Android die Fensterleiste WEISS,
und wer aus einem alten Lesezeichen kommt,
bekommt einen hellen Balken ueber einer
durchweg dunklen Seite.
manifest sonst oeffnet die Seite auf dem Handy AUSSERHALB
der App -- die Weiterleitung landet dann im
Browser statt im Workspace.
apple-touch-icon sonst nimmt das iPhone einen Bildschirmabzug,
wenn jemand diese alte Adresse auf den
Startbildschirm legt.
Drei Zeilen sind hier billiger und haltbarer als eine Ausnahme
in der Pruefung: Eine Ausnahmeliste muesste gepflegt werden und
veraltet beim naechsten Umbau. -->
<link rel="manifest" href="/workspace/app.webmanifest" />
<meta name="theme-color" content="#06090f" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<style>
/* Bewusst ohne eigenes Stilblatt: Die Seite ist in aller Regel nur
einen Wimpernschlag zu sehen. Ein Stilblatt nachzuladen hiesse,
+17 -17
View File
@@ -12,20 +12,20 @@
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
eine Flaeche sind statt zweier. -->
<meta name="theme-color" content="#06090f" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071405" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071405" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071405" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202610071712" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202610071712" />
<link rel="stylesheet" href="assets/css/gate.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/start.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202610071712" />
<link rel="stylesheet" href="assets/css/module.css?v=202610071712" />
<!-- haus.css MUSS die letzte Stilvorlage sein (10.09.2026).
Auf der Agenturadresse ist sie leer -- das Haus hier IST der
Grundzustand. Auf der Adresse von Team Dogi biegt der Server
genau diesen Dateinamen auf crew-haus.css um, und die Seite
kommt schon beim ERSTEN Abruf in Lila und Babyblau an --
ohne Umfaerben im Browser, ohne Flackern. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202610071405" />
<link rel="stylesheet" href="assets/css/haus.css?v=202610071712" />
</head>
<body class="start" data-buehne="arena">
@@ -190,15 +190,15 @@
</main>
<script src="assets/js/meldung.js?v=202610071405" defer></script>
<script src="assets/js/hochladen.js?v=202610071405" defer></script>
<script src="assets/js/nachfrage.js?v=202610071405" defer></script>
<script src="assets/js/meldung.js?v=202610071712" defer></script>
<script src="assets/js/hochladen.js?v=202610071712" defer></script>
<script src="assets/js/nachfrage.js?v=202610071712" defer></script>
<script src="assets/js/wahl.js?v=202610071405" defer></script>
<script src="assets/js/bereiche.js?v=202610071405" defer></script>
<script src="assets/js/installieren.js?v=202610071405" defer></script>
<script src="assets/js/kopf.js?v=202610071405" defer></script>
<script src="assets/js/glocke.js?v=202610071405" defer></script>
<script src="assets/js/wissen.js?v=202610071405" defer></script>
<script src="assets/js/wahl.js?v=202610071712" defer></script>
<script src="assets/js/bereiche.js?v=202610071712" defer></script>
<script src="assets/js/installieren.js?v=202610071712" defer></script>
<script src="assets/js/kopf.js?v=202610071712" defer></script>
<script src="assets/js/glocke.js?v=202610071712" defer></script>
<script src="assets/js/wissen.js?v=202610071712" defer></script>
</body>
</html>