Die Reaction: zusammen schauen, live, mit Kamera und Chat

Filipes Kurznotiz vom 28.09.2026, von links nach rechts:

    Kachel sichtbar -> geschlossen -> Vorbereitung -> Wartebereich/
    Chat -> Countdown -> LIVE -> Reaction + Gaeste + Chat + PayPal
    -> Ende

DREI STAENDE, NICHT SIEBEN

„Wartebereich" und „Countdown" sind keine eigenen Zustaende, sondern
das, was „Vorbereitung" auf dem Bildschirm TUT. Drei Staende, die
sich gegenseitig ausschliessen, sind pruefbar; sieben, von denen sich
vier ueberlappen, sind es nicht.

DAS VIDEO LAEUFT NICHT UEBER DIESEN SERVER

Naheliegend waere: Der Host spielt ab, alle sehen seinen Bildschirm.
Das waere aus zwei Gruenden falsch. Rechtlich ist ein
weitergesendetes YouTube-Video eine oeffentliche Wiedergabe -- genau
die Sache, fuer die Kanaele gesperrt werden. Und technisch kostet es
Bandbreite und Qualitaet.

Jeder Zuschauer laedt das Video deshalb SELBST. Uebertragen wird nur
der Spielstand: Kennung, laeuft/pausiert, Sekunde. Das sind ein paar
Byte, jeder sieht es in voller Qualitaet, und alle sind auf derselben
Sekunde. Nachgefuehrt wird erst ab anderthalb Sekunden Abweichung --
ein Player, dem man jede Sekunde eine neue Position gibt, ruckelt
sichtbar.

DIE KAMERAS LAUFEN DIREKT VON MENSCH ZU MENSCH

Ueber denselben Weg wie die Anrufe im Haus (seit 18.09.), nur mit
mehr Empfaengern. Das hat eine Grenze, und sie ist gerechnet, nicht
geraten: Bei 360p und rund 350 kbit/s sind zwoelf Zuschauer etwa
4 Mbit/s Upload beim Host. Darueber schaltet die Sendung von selbst
auf Ton um -- wer keine Kamera mehr bekommt, hoert alles, sieht das
Video und kann schreiben. Ehrlicher als eine Verbindung, die stockt,
und sichtbar im Regiepult.

Heute sind es elf Menschen im ganzen Haus (gemessen: 1 admin, 1 hand,
1 linke, 4 modi, 4 gast). Die Grenze ist weit weg -- sie steht
trotzdem drin, weil sie sonst erst auffaellt, wenn es zu spaet ist.

DIE SEITE IST ANDERS GEBAUT ALS JEDE ANDERE IM HAUS

Ueberall sonst: Kacheln, Karten, Listen -- man liest, entscheidet,
geht wieder. Hier sitzt man. Eine Stunde, mit anderen, auf EINE
Sache schauend. Deshalb kein Raster, sondern ein SAAL: grosse Flaeche
fuer das Video, Kamerabilder als schwebende Fenster darueber, der
Chat als Schiene daneben. Die Seite scrollt nicht -- ein Video, das
beim Tippen im Chat nach oben rutscht, ist der schnellste Weg, dass
jemand aufhoert zu schreiben.

Fuer den Host ein REGIEPULT: vier senkrechte Regler nebeneinander wie
an einem Mischpult, darueber die Sendung, daneben Gaeste und
Anordnung, unten drei grosse Knoepfe. Es SCHIEBT den Saal, es deckt
ihn nicht zu.

Die Kachel traegt ihren Zustand als Farbe: grau geschlossen,
bernstein in Vorbereitung, rot auf Sendung. Keine Ton-Nummer -- der
Farbraum ist bei 46 voll, und sie braucht auch keine.

PAYPAL: EINE QUELLE

Der Knopf nimmt den Weg, der auf der Unterstuetzen-Seite hinterlegt
ist -- derselbe Eintrag, dieselbe Pflege. Ist dort nichts eingetragen
oder steht er auf unsichtbar, erscheint hier kein Knopf. Eine
geratene Adresse ist an dieser Stelle die gefaehrlichste aller
Abkuerzungen.

=======================================================================
ACHT FEHLER, DIE OHNE MESSUNG LIVE GEGANGEN WAEREN
=======================================================================

1. `data-live` WAR SCHON VERGEBEN. Die Draussen-Kachel bekommt es,
   sobald Filipe auf Twitch sendet. Meine Regel haette ihr waehrend
   jedes Streams die Farbe genommen -- genau dann, wenn sie wichtig
   ist. Heisst jetzt `data-sendung`, und pruef-reaktion haelt beides
   auseinander.

2. DIE INHALTSRICHTLINIE HAETTE YOUTUBE LAUTLOS GESPERRT. Die Datei
   warnt an genau dieser Stelle selbst davor: Am 27.08.2026 hat
   `frame-src 'none'` den Musik-Knopf stillgelegt -- der Knopf
   reagierte, das Feld ging auf, und wo die Player sein sollten,
   blieb es leer. Hier waere das Ergebnis eine schwarze Leinwand vor
   Publikum gewesen. youtube-nocookie.com fuer den Rahmen (setzt keine
   Werbekennungen), www.youtube.com fuer die Einbett-API,
   i.ytimg.com fuer die Vorschaubilder.

3. KAMERA UND MIKROFON WAREN GESPERRT. Dieselbe Falle, vor der
   index.js selbst warnt -- und die am 18.09. schon einmal zugeschlagen
   hat. Die Ausnahme ist jetzt eine benannte MENGE statt eines zweiten
   Sonderfalls, und pruef-kamera-richtlinie.mjs haelt sie GEGEN DEN
   QUELLTEXT: Welche Seite laedt ein Skript, das getUserMedia
   aufruft? Genau die muss drinstehen -- und keine andere. Eine
   Liste, die abgeleitet wird, kann nicht veralten.

4. ZWEI ANRUFE AN DIESELBE PERSON. Zwischen `await kameraHolen()` und
   dem Anlegen der Verbindung laeuft alles andere weiter; jeder Takt
   sagte wieder „den kenne ich noch nicht". Der Empfaenger antwortete
   auf beide Angebote, und die zweite Antwort traf eine Verbindung,
   die laengst stand.

5. DAS ANGEBOT GING HINAUS, BEVOR DER EMPFAENGER ZUHOEREN KONNTE.
   Gemessen:

       [spur] an [3] reaktion_signal | offen: [2,1]
       ...
       [spur] Strom auf fuer 3 Lenny

   Die Anmeldung ist ein gewoehnlicher Abruf und sofort durch, der
   Ereignisstrom eine stehende Verbindung. Der Host erfaehrt vom
   Neuankoemmling also zuverlaessig, BEVOR der zuhoeren kann.
   Die Richtung ist jetzt umgedreht: Wer bereit ist, BITTET um den
   Anruf -- er ist der Einzige, der das sicher weiss. Dazu ein
   eigener, schneller Takt (2,5 s) und eine Ruecknahme, wenn ein
   Angebot bei niemandem ankommt.

6. EIN VIDEO MIT TON STARTET NICHT VON ALLEIN. `videoWidth` war 640,
   das Bild kam also an -- und das Fenster blieb schwarz. Kein
   Fehler, keine Meldung, es passiert einfach nichts. Die Kameras
   starten jetzt stumm (stumm darf losgehen), ein Knopf schaltet den
   Ton frei, und die erste Beruehrung der Seite tut es ohnehin.

7. DIE LADE AM HANDY GING NICHT AUF. Gemessen: ein 390x775 grosser
   Saal mit 219 px Video und 556 px Leere darunter. Statt den Knopf
   zu reparieren, ist die Lade weg -- unter Kopfleiste und Video
   bleiben auf einem Telefon rund 550 px, das ist mehr Chat, als eine
   Lade je zeigen wuerde. Ein Zustand weniger ist besser als ein
   Zustand, der funktioniert.

8. `sendBeacon` KANN NUR POST. Beim Schliessen des Fensters wird ein
   gewoehnlicher Abruf abgebrochen; mein DELETE waere nie angekommen,
   und jeder haette zwei Minuten lang als anwesend gegolten.

Dazu drei Funde der Hauspruefungen, alle von mir verursacht:
17 Schriftgroessen unter der Lesbarkeitsgrenze von 11,5 px, elf
Maschinenworte ohne deutschen Satz, und ein Aufbewahrungseintrag ohne
Rechtsgrundlage.

=======================================================================

GEMESSEN

pruef-reaktion            74 Punkte, 0 Fehler (11 Abschnitte)
pruef-kamera-richtlinie   10 Punkte, 0 Fehler (neu, abgeleitet)
mess-reaktion             beide Kameras kommen an, 640 px, laufen --
                          beim Zuschauer UND beim Host. Diese Messung
                          hat einen Rueckgabewert: Alles andere kann
                          gruen sein, und trotzdem sitzt jeder vor
                          einem schwarzen Rechteck.
pruef-handy               180 (vorher 177), pruef-notizen 79,
pruef-aufbewahrung        45, pruef-meldungen 8, pruef-css-klassen 33,
pruef-struktur            35, pruef-crew-adresse 161,
pruef-haus-trennung       100, pruef-start-ansicht 160 -- alle 0 Fehler.
This commit is contained in:
2026-09-28 01:52:22 +02:00
parent 6261d548d0
commit 5d89d108f9
61 changed files with 5040 additions and 650 deletions
+1
View File
@@ -386,6 +386,7 @@ export const GEHOERT_ZU_ADRESSE = [
(nurTeamDogi in workspace-notizen.js) -- die Seite ist nur HTML,
die Daten sind die Sache. */
{ datei: "/workspace/notizen.html", zuhause: istCrewAdresse },
{ datei: "/workspace/reaktion.html", zuhause: istCrewAdresse },
];
/** Wird hier eine Wand verlangt, die zu einer ANDEREN Tuer gehoert? */
+19 -2
View File
@@ -39,6 +39,7 @@ import { befindenRouter } from "./workspace-befinden.js";
import { hilfeRouter } from "./workspace-hilfe.js";
import { supportRouter } from "./workspace-support.js";
import { notizenRouter } from "./workspace-notizen.js";
import { reaktionRouter } from "./workspace-reaktion.js";
import { unterstuetzungRouter } from "./workspace-unterstuetzung.js";
import { reportRouter } from "./workspace-reports.js";
import { scoutRouter } from "./workspace-scouts.js";
@@ -103,9 +104,24 @@ app.use((req, res, next) => {
`(self)` und nicht `*`: Nur diese Seite selbst, keine
eingebetteten Fremdinhalte. */
const chatSeite = /^\/workspace\/chat\.html/.test(req.path);
/* DIE ZWEITE STELLE KAM AM 28.09.2026 DAZU: die Reaction. Sie
zeigt die Kamera des Hosts und bis zu zwei Gaeste, ueber
denselben Weg wie der Anruf im Chat.
Und sie kam als MENGE dazu, nicht als zweiter Sonderfall: Beim
Chat stand die Bedingung als Einzelfall im Code, und ein zweiter
Einzelfall daneben ist die Bauweise, bei der der dritte
vergessen wird. Vergessen heisst hier: Die Sendung startet, alle
sind da, und die Kamera geht nicht an -- mit einer Meldung, die
auf die falsche Faehrte fuehrt.
pruef-kamera-richtlinie.mjs haelt die Menge gegen den Quelltext:
Jede Seite, deren Skripte `getUserMedia` aufrufen, muss hier
stehen -- und jede andere darf es nicht. */
const MIT_KAMERA = [/^\/workspace\/chat\.html/, /^\/workspace\/reaktion\.html/];
const darfKamera = MIT_KAMERA.some((m) => m.test(req.path));
res.setHeader("Permissions-Policy",
chatSeite
darfKamera
? "geolocation=(), microphone=(self), camera=(self), payment=()"
: "geolocation=(), microphone=(), camera=(), payment=()");
@@ -317,6 +333,7 @@ app.use(supportRouter);
sieht nur seinen eigenen -- auch DogFather. Begruendung im Kopf von
notiz-tabellen.js. */
app.use(notizenRouter);
app.use(reaktionRouter);
/* Unterstützen (24.09.2026): die Wege, auf denen die Community etwas
beitragen kann – lesen alle, ändern nur DogFather. */
app.use(unterstuetzungRouter);
+22 -3
View File
@@ -76,6 +76,25 @@ import { join, normalize } from "node:path";
/* Spotify: die zwei Player im Musik-Feld (main.js, jede Seite). */
const SPOTIFY = "https://open.spotify.com";
/* ==== YOUTUBE (28.09.2026, fuer die Reaction) ======================
Die Reaction zeigt ein YouTube-Video, und jeder Zuschauer laedt es
SELBST -- weitergesendet wird nur der Spielstand (siehe
reaktion-tabellen.js). Dafuer braucht das Dokument drei Dinge:
RAHMEN der eingebettete Player. `youtube-nocookie.com` setzt
keine Werbekennungen, solange niemand abspielt; das ist
der Grund, warum diese Adresse zuerst steht.
SKRIPT die Einbett-API (`/iframe_api`) liegt auf www.youtube.com
und laedt von dort ihren eigentlichen Teil nach. Ohne
sie liesse sich der Player nicht fernsteuern -- und genau
das ist der Kern: Alle sehen dieselbe Sekunde.
BILDER die Vorschaubilder (i.ytimg.com), die im Wartebereich
stehen, wenn der Host kein eigenes Bild hinterlegt hat. */
const YOUTUBE_RAHMEN = "https://www.youtube-nocookie.com https://www.youtube.com";
const YOUTUBE_SKRIPT = "https://www.youtube.com";
const YOUTUBE_BILDER = "https://i.ytimg.com https://i9.ytimg.com";
/* PayPal: das SDK selbst liegt auf www.paypal.com, Bilder/Schriften der
Knöpfe auf www.paypalobjects.com. Der Bezahlvorgang läuft anschließend
in einem Rahmen von www.paypal.com.
@@ -241,7 +260,7 @@ export function inhaltsrichtlinie(wurzel) {
wenn ohnehin etwas grundlegend nicht stimmt. */
if (!summen) return next();
const skript = ["'self'", ...summen, PAYPAL_SKRIPT, GOOGLE_SKRIPT].join(" ");
const skript = ["'self'", ...summen, PAYPAL_SKRIPT, GOOGLE_SKRIPT, YOUTUBE_SKRIPT].join(" ");
res.setHeader("Content-Security-Policy", [
"default-src 'self'",
@@ -259,7 +278,7 @@ export function inhaltsrichtlinie(wurzel) {
ihn: Der Deploy hätte Erfolg gemeldet, der Server wäre
gestartet, und der Schaden wäre erst jemandem aufgefallen, der
die Seite ansieht. */
`img-src 'self' data: blob: https://postfach.dogfather-universe.com ${PAYPAL_BILDER}`,
`img-src 'self' data: blob: https://postfach.dogfather-universe.com ${PAYPAL_BILDER} ${YOUTUBE_BILDER}`,
"font-src 'self'",
/* Die Seite spricht mit dem eigenen Server und mit dem internen
Bereich unter postfach. -- mehr nicht. Ein eingeschleustes
@@ -302,7 +321,7 @@ export function inhaltsrichtlinie(wurzel) {
pruef-musik.mjs und pruef-kasse.mjs prüfen das im echten Browser
mit -- inklusive der Kasse, die vorher genau hier gescheitert
wäre. */
`frame-src ${SPOTIFY} ${PAYPAL_RAHMEN} ${GOOGLE_RAHMEN}`,
`frame-src ${SPOTIFY} ${PAYPAL_RAHMEN} ${GOOGLE_RAHMEN} ${YOUTUBE_RAHMEN}`,
"worker-src 'self'",
/* Hebt gemischte Inhalte auf HTTPS, statt sie zu blockieren.
+377
View File
@@ -0,0 +1,377 @@
/* =====================================================================
WIE DIE REACTION AUSSIEHT (28.09.2026)
Die Pruefung nebenan misst, was die Schnittstelle antwortet. Das
sagt nichts darueber, ob ein Mensch damit einen Abend verbringen
moechte. Dieses Werkzeug oeffnet die Seite in einem echten Browser,
schaltet sie durch alle drei Zustaende und legt Bilder ab.
Gemessen wird dabei auch, was man sonst uebersieht:
- laeuft etwas in die Konsole?
- ragt etwas seitlich hinaus?
- ist der Saal so hoch wie das Fenster, oder muss man scrollen?
NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes
Zertifikat -- und am Ende ist alles weg.
Aufrufen mit: node server/mess-reaktion.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { request } from "node:http";
import { execFileSync } from "node:child_process";
import { createServer as httpsServer } from "node:https";
const HIER = dirname(fileURLToPath(import.meta.url));
const ordner = mkdtempSync(join(tmpdir(), "mess-reakt-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const PORT = 5483;
const HTTPS_PORT = PORT + 1;
process.env.PORT = `${PORT}`;
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
for (const art of ["uncaughtException", "unhandledRejection"]) {
process.on(art, (f) => {
console.error(`\nABBRUCH (${art}): ${f?.stack || f}`);
process.exit(8);
});
}
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, 1000));
const CREW = "crew.dogfather-universe.com";
/* ---------- Leute ------------------------------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const kenn = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", kenn, new Date().toISOString());
const ALT = new Date(Date.now() - 200 * 86400000).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,"
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", kenn).update(code).digest("hex"), ALT);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const CODES = { admin: "CODE-M00-000001", gast: "CODE-M01-000001", gast2: "CODE-M02-000001" };
const IDS = {
admin: anlegen("DogFather", "admin", CODES.admin),
gast: anlegen("Nachtfalter", "gast", CODES.gast),
gast2: anlegen("Lenny", "gast", CODES.gast2),
};
d.close();
function roh(pfad, { method = "GET", keks = "", rumpf = null } = {}) {
return new Promise((fertig, schief) => {
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
const a = request({
host: "127.0.0.1", port: PORT, path: pfad, method,
headers: {
Host: CREW, Origin: `https://${CREW}`,
...(keks ? { Cookie: keks } : {}),
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
},
}, (antwort) => {
const teile = [];
antwort.on("data", (s) => teile.push(s));
antwort.on("end", () => {
const text = Buffer.concat(teile).toString("utf8");
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers });
});
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
}
async function anmelden(rolle) {
const a = await roh("/workspace/api/anmelden", {
method: "POST",
rumpf: { rolle: rolle === "gast2" ? "gast" : rolle, code: CODES[rolle], alter_ok: true },
});
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
}
const k = {
admin: await anmelden("admin"),
gast: await anmelden("gast"),
gast2: await anmelden("gast2"),
};
/* ---------- Die Buehne fuellen ------------------------------------------ */
await roh("/workspace/api/reaktion", {
method: "PUT", keks: k.admin,
rumpf: {
titel: "Wir schauen den neuen Trailer",
video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
beginnt_am: new Date(Date.now() + 7 * 60_000).toISOString(),
},
});
const REDEN = [
["gast", "Bin schon daaa 🐺"],
["gast2", "Ich auch! Hab extra Popcorn geholt"],
["gast", "Wie lange noch?"],
["admin", "Zwei Minuten, ich richte noch die Kamera"],
["gast2", "Kein Stress, wir sitzen bequem"],
["gast", "Letztes Mal war so gut, ich hab geheult"],
];
/* ---------- Der Vorbau -------------------------------------------------- */
const schluesselDatei = join(ordner, "p.key");
const zertDatei = join(ordner, "p.crt");
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
"-keyout", schluesselDatei, "-out", zertDatei, "-days", "2",
"-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" });
const vorbau = httpsServer({
key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei),
}, (von, nach) => {
const weiter = request({
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` },
}, (antwort) => {
nach.writeHead(antwort.statusCode || 502, antwort.headers);
antwort.pipe(nach);
});
weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
von.pipe(weiter);
});
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
const BASIS = `https://${CREW}:${HTTPS_PORT}`;
const pw = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch({
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors",
/* Kamera und Mikrofon ohne Rueckfrage -- und ein bewegtes
Testbild statt eines schwarzen Rechtecks. Ohne das sieht jede
Aufnahme aus, als waere die Kamera kaputt. */
"--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"],
});
/* JEDE GEOEFFNETE SEITE HAELT ZWEI STEHENDE VERBINDUNGEN (der
Ereignisstrom der Kopfleiste und der dieser Seite). Ein Browser
laesst je Adresse nur wenige davon zu -- wer seine Seiten nicht
schliesst, nimmt den spaeteren den Platz weg.
GEMESSEN: Die Seiten aus den ersten beiden Abschnitten blieben
offen, und danach kam Lennys Strom gar nicht mehr zustande. Die
Signale gingen ins Leere, und die Kamerafenster blieben schwarz --
ein Messfehler, der wie ein Programmfehler aussah. */
async function alsWer(rolle, breite, hoehe) {
const kontext = await browser.newContext({
viewport: { width: breite, height: hoehe },
ignoreHTTPSErrors: true,
permissions: ["camera", "microphone"],
});
await kontext.addCookies((k[rolle] || "").split("; ").filter(Boolean).map((z) => {
const [n, ...r] = z.split("=");
return { name: n, value: r.join("="), domain: CREW, path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
const anfragen = [];
seite.on("request", (r) => { if (/\/api\//.test(r.url())) anfragen.push(r.url().replace(/^https?:\/\/[^/]+/, "")); });
seite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
seite.on("response", (a) => {
if (a.status() >= 400) konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`);
});
return { kontext, seite, konsole, anfragen };
}
async function bild(seite, name) {
await seite.screenshot({ path: join(HIER, `mess-reaktion-${name}.png`) });
console.log(` Bild: server/mess-reaktion-${name}.png`);
}
async function vermessen(seite, wo) {
const m = await seite.evaluate(() => ({
ueber: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
hoch: Math.max(0, document.documentElement.scrollHeight - document.documentElement.clientHeight),
stand: document.getElementById("saal")?.dataset.stand || "?",
schild: document.getElementById("schild-wort")?.textContent || "",
}));
console.log(` ${wo}: Stand „${m.stand}", Schild „${m.schild}", `
+ `seitlich ${m.ueber} px, senkrecht ${m.hoch} px`);
return m;
}
console.log("\n=== ZU ===");
{
const { kontext, seite, konsole } = await alsWer("gast", 1440, 900);
await seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
await vermessen(seite, "Rechner");
await bild(seite, "1-zu-rechner");
if (konsole.length) console.log(" KONSOLE:", konsole.slice(0, 3).join(" | "));
await kontext.close();
}
console.log("\n=== VORBEREITUNG ===");
await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" } });
for (const [wer, text] of REDEN) {
await roh("/workspace/api/reaktion/chat", { method: "POST", keks: k[wer], rumpf: { text } });
}
{
const { kontext, seite, konsole } = await alsWer("gast", 1440, 900);
await seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(1400);
await vermessen(seite, "Rechner");
await bild(seite, "2-warte-rechner");
if (konsole.length) console.log(" KONSOLE:", konsole.slice(0, 3).join(" | "));
await kontext.close();
}
console.log("\n=== LIVE ===");
await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "live" } });
await roh("/workspace/api/reaktion/gast", { method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 } });
/* ==== BEIDE SEITEN GLEICHZEITIG ====================================
Der Host sendet, die Zuschauerin empfaengt -- das geht nur, wenn
beide zur selben Zeit offen sind. */
const host = await alsWer("admin", 1440, 900);
await host.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
await host.seite.waitForTimeout(2500);
const gast2 = await alsWer("gast2", 1280, 800);
await gast2.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
await gast2.seite.waitForTimeout(2500);
const zuschauer = await alsWer("gast", 1440, 900);
await zuschauer.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
/* Verbindungen brauchen ein paar Sekunden: Adressen holen, Angebot,
Antwort, Kandidaten. Acht Sekunden sind grosszuegig -- es geht
darum, ob es UEBERHAUPT ankommt, nicht wie schnell. */
await zuschauer.seite.waitForTimeout(8000);
async function bildBreiten(seite) {
return seite.evaluate(() => {
const raus = {};
for (const k2 of document.querySelectorAll("#kameras .kamera")) {
const v = k2.querySelector("video");
raus[k2.querySelector(".kamera__name")?.textContent || k2.dataset.wer] = {
breite: v ? v.videoWidth : -1,
strom: !!(v && v.srcObject),
laeuft: !!(v && !v.paused),
stumm: !!(v && v.muted),
};
}
return raus;
});
}
console.log("\n --- Fliesst ein Bild? ---");
for (const [wer, w] of [["Host", host.seite], ["Gast", gast2.seite], ["Zuschauerin", zuschauer.seite]]) {
const z = await w.evaluate(() => ({
url: location.pathname,
saal: !!document.getElementById("saal"),
stand: document.getElementById("saal")?.dataset.stand,
kameras: document.querySelectorAll("#kameras .kamera").length,
})).catch((f) => ({ url: "FEHLER " + f.message }));
console.log(` ${wer.padEnd(12)} ${z.url} saal=${z.saal} stand=${z.stand} kameras=${z.kameras}`);
const q = { Host: host, Gast: gast2, Zuschauerin: zuschauer }[wer];
console.log(" Strom-Anfragen: "
+ q.anfragen.filter((x) => x.includes("strom")).length + " | Signale: " + q.anfragen.filter((x) => x.includes("signal")).length);
}
const beiZuschauer = await bildBreiten(zuschauer.seite);
for (const [name, m] of Object.entries(beiZuschauer)) {
const gut = m.breite > 0;
console.log(` Zuschauerin sieht ${name.padEnd(12)} `
+ `${gut ? "BILD " + m.breite + " px" : (m.strom ? "Strom da, aber 0 px" : "NICHTS")}`
+ ` ${m.laeuft ? "laeuft" : "STEHT"} ${m.stumm ? "stumm" : "mit Ton"}`);
}
/* AUCH DER HOST MUSS SEINEN GAST SEHEN. Wenn nur die Zuschauer
etwas sehen, redet der Host gegen eine schwarze Kachel -- und
merkt nicht, wenn beim Gast die Kamera aus ist. */
const beimHost = await bildBreiten(host.seite);
for (const [name, m] of Object.entries(beimHost)) {
console.log(` Host sieht ${name.padEnd(12)} `
+ `${m.breite > 0 ? "BILD " + m.breite + " px" : "NICHTS"}`
+ ` ${m.laeuft ? "laeuft" : "STEHT"}`);
}
const zaehlt = Object.values(beiZuschauer).filter((m) => m.breite > 0).length;
console.log(` -> ${zaehlt} von ${Object.keys(beiZuschauer).length} Kameras kommen an.`);
/* DIESE MESSUNG HAT EINEN RUECKGABEWERT -- und zwar wegen dieser
einen Zahl. Alles andere kann gruen sein: Die Schnittstelle
antwortet, die Seite sieht gut aus, die Verbindung steht. Wenn
hier 0 steht, sitzt trotzdem jeder vor einem schwarzen Rechteck.
Gemessen wurde genau das zweimal: einmal, weil das Angebot
hinausging, bevor der Empfaenger zuhoeren konnte -- einmal, weil
ein Video mit Ton nicht von allein startet. Beide Male sahen die
Bildschirmfotos plausibel aus. */
let ausgang = 0;
const beimHostAn = Object.values(beimHost).filter((m) => m.breite > 0).length;
if (beimHostAn < Object.keys(beimHost).length) {
console.log(" ACHTUNG: Der Host sieht nicht alle Kameras.");
ausgang = 1;
}
if (!zaehlt) {
console.log(" ACHTUNG: Es kommt kein einziges Bild an.");
ausgang = 1;
}
const stehen = [...Object.values(beiZuschauer), ...Object.values(beimHost)]
.filter((m) => m.breite > 0 && !m.laeuft).length;
if (stehen) {
console.log(` ACHTUNG: ${stehen} Bild(er) kommen an, stehen aber still `
+ "-- ein Video mit Ton startet nicht von allein.");
ausgang = 1;
}
await host.seite.click("#pult-griff").catch(() => {});
await host.seite.waitForTimeout(700);
await vermessen(host.seite, "Host am Rechner");
await bild(host.seite, "3-live-host");
if (host.konsole.length) console.log(" KONSOLE:", host.konsole.slice(0, 4).join(" | "));
await vermessen(zuschauer.seite, "Zuschauerin am Rechner");
await bild(zuschauer.seite, "4-live-zuschauer");
if (zuschauer.konsole.length) console.log(" KONSOLE:", zuschauer.konsole.slice(0, 4).join(" | "));
await host.kontext.close();
await gast2.kontext.close();
await zuschauer.kontext.close();
{
const handy = await alsWer("gast2", 390, 844);
await handy.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
await handy.seite.waitForTimeout(4000);
await vermessen(handy.seite, "Handy");
await bild(handy.seite, "5-live-handy");
await handy.seite.waitForTimeout(500);
await bild(handy.seite, "6-live-handy-chat");
const hm = await handy.seite.evaluate(() => {
const g = (w) => { const e = document.querySelector(w); if (!e) return "(fehlt)";
const r = e.getBoundingClientRect(); const c = getComputedStyle(e);
return `${Math.round(r.width)}x${Math.round(r.height)} display=${c.display}`; };
return { saal: g("#saal"), kino: g("#teil-live"), leinwand: g(".leinwand"),
schiene: g(".schiene"), schieneWert: document.getElementById("teil-live")?.dataset.schiene };
});
console.log(" Handy-Masse:", JSON.stringify(hm));
if (handy.konsole.length) console.log(" KONSOLE:", handy.konsole.slice(0, 4).join(" | "));
}
await browser.close();
await new Promise((r) => vorbau.close(r));
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log("\nFertig.");
process.exit(ausgang);
+138
View File
@@ -0,0 +1,138 @@
/* =====================================================================
WER DIE KAMERA DARF — und wer sie NICHT darf (28.09.2026)
In index.js steht eine Sperre: Mikrofon und Kamera sind auf JEDER
Seite verboten, ausser auf wenigen benannten. Das ist richtig und
soll so bleiben -- ohne sie koennte ein eingeschleustes Skript
ueberall das Mikrofon anschalten.
Die Sperre hat aber eine unangenehme Eigenschaft: Wenn sie zu weit
geht, merkt es niemand beim Bauen. Die Seite laedt, die Knoepfe
sind da, und erst wenn jemand auf „Kamera an" drueckt, kommt eine
Meldung, die auf die falsche Faehrte fuehrt -- man sucht dann in
den Browsereinstellungen. Genau so ist es am 18.09.2026 beim
Anruf passiert und am 28.09.2026 bei der Reaction.
---------------------------------------------------------------------
DESHALB WIRD HIER NICHT EINE LISTE GEPRUEFT, SONDERN ABGELEITET
Gesucht wird im Quelltext: Welche Seite laedt ein Skript, das
`getUserMedia` aufruft? Genau diese Seiten brauchen die Erlaubnis.
Eine Liste, die gegen den Quelltext gehalten wird, kann nicht
veralten -- wer morgen eine dritte Seite mit Kamera baut, bekommt
hier einen roten Punkt statt in drei Wochen einen Anruf.
UND DIE GEGENRICHTUNG WIRD MITGEPRUEFT. Eine Seite, die die
Erlaubnis hat und sie nicht braucht, ist eine offene Tuer ohne
Grund. Auch das faellt hier auf.
Aufrufen mit: node server/pruef-kamera-richtlinie.mjs
===================================================================== */
import { readFileSync, readdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const HIER = dirname(fileURLToPath(import.meta.url));
const WURZEL = join(HIER, "..");
const SEITEN_ORDNER = join(WURZEL, "workspace");
const JS_ORDNER = join(SEITEN_ORDNER, "assets", "js");
let fehler = 0, geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const melde = (t) => console.log(t);
/* ---------- 1. Welche Skripte fassen die Kamera an? --------------------- */
melde("=== 1. Welche Skripte die Kamera anfassen ===");
const jsDateien = readdirSync(JS_ORDNER).filter((n) => n.endsWith(".js"));
const mitKamera = new Set();
for (const name of jsDateien) {
const text = readFileSync(join(JS_ORDNER, name), "utf8");
/* Der Aufruf selbst, nicht das Wort in einem Kommentar: Es muss
eine Klammer folgen. Sonst zaehlte jede Erklaerung mit, in der
`getUserMedia` erwaehnt wird -- und davon gibt es hier viele. */
if (/getUserMedia\s*\(/.test(text.replace(/\/\*[\s\S]*?\*\//g, ""))) {
mitKamera.add(name);
}
}
ok(mitKamera.size > 0,
`${mitKamera.size} Skript(e) rufen getUserMedia auf: ${[...mitKamera].join(", ")}`);
/* ---------- 2. Welche Seiten laden diese Skripte? ----------------------- */
melde("");
melde("=== 2. Welche Seiten sie laden ===");
const seiten = readdirSync(SEITEN_ORDNER).filter((n) => n.endsWith(".html"));
const brauchen = new Set();
for (const seite of seiten) {
const html = readFileSync(join(SEITEN_ORDNER, seite), "utf8");
for (const js of mitKamera) {
if (html.includes(`assets/js/${js}`)) { brauchen.add(seite); break; }
}
}
ok(brauchen.size > 0,
`${brauchen.size} Seite(n) brauchen die Erlaubnis: ${[...brauchen].sort().join(", ")}`);
/* ---------- 3. Und was steht in index.js? ------------------------------- */
melde("");
melde("=== 3. Der Abgleich ===");
const indexText = readFileSync(join(HIER, "index.js"), "utf8");
const zeile = indexText.match(/const MIT_KAMERA = \[([^\]]*)\]/);
ok(!!zeile, "die Menge MIT_KAMERA steht in index.js");
const erlaubt = new Set();
if (zeile) {
for (const m of zeile[1].matchAll(/workspace\\\/([a-z0-9-]+)\\\.html/g)) {
erlaubt.add(m[1] + ".html");
}
}
ok(erlaubt.size > 0, `sie nennt ${erlaubt.size} Seite(n): ${[...erlaubt].sort().join(", ")}`);
const fehlend = [...brauchen].filter((s) => !erlaubt.has(s));
ok(fehlend.length === 0,
fehlend.length
? `FEHLT: ${fehlend.join(", ")} laedt ein Kamera-Skript, steht aber nicht in MIT_KAMERA `
+ "-- dort geht die Kamera nicht an, ohne dass jemand einen Fehler sieht"
: "jede Seite, die die Kamera braucht, steht drin");
const zuviel = [...erlaubt].filter((s) => !brauchen.has(s));
ok(zuviel.length === 0,
zuviel.length
? `ZU VIEL: ${zuviel.join(", ")} hat die Erlaubnis, braucht sie aber nicht `
+ "-- eine offene Tuer ohne Grund"
: "und keine Seite hat sie, ohne sie zu brauchen");
/* ---------- 4. Die Gegenprobe ------------------------------------------
Eine Pruefung, die nur bestaetigen kann, bestaetigt nichts. Hier
wird erfunden, was passiert waere, haette jemand die Reaction
vergessen. */
melde("");
melde("=== 4. Gegenprobe ===");
{
const erfunden = new Set([...erlaubt].filter((s) => s !== "reaktion.html"));
const haetteGefehlt = [...brauchen].filter((s) => !erfunden.has(s));
ok(haetteGefehlt.includes("reaktion.html"),
"ohne reaktion.html in der Menge wuerde die Pruefung rot "
+ `(${haetteGefehlt.join(", ") || "sie wuerde NICHT anschlagen"})`);
}
/* ---------- 5. Und die Sperre steht ueberhaupt ---------------------------
Wenn jemand die Zeile ganz entfernt, waeren Kamera und Mikrofon
ueberall offen -- und keine der Pruefungen oben wuerde das sehen,
denn beide Mengen waeren dann leer und stimmten wunderbar ueberein. */
melde("");
melde("=== 5. Die Sperre selbst ===");
{
ok(/microphone=\(\), camera=\(\)/.test(indexText),
"die Vorgabe verbietet Mikrofon und Kamera");
ok(/microphone=\(self\), camera=\(self\)/.test(indexText),
"und oeffnet sie nur fuer die eigene Seite, nicht fuer fremde Rahmen");
ok(/Permissions-Policy/.test(indexText),
"der Kopf heisst Permissions-Policy");
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
process.exit(fehler === 0 ? 0 : 1);
+578
View File
@@ -0,0 +1,578 @@
/* =====================================================================
DIE REACTION (28.09.2026)
Geprueft wird der ganze Weg aus Filipes Kurznotiz:
Kachel sichtbar -> geschlossen -> Vorbereitung ->
Wartebereich/Chat -> Countdown -> LIVE ->
Reaction + Gaeste + Chat + PayPal -> Ende
---------------------------------------------------------------------
WARUM DIESE PRUEFUNG UEBER DIE HAUSADRESSE SPRICHT
Die Community meldet sich ueber den STILLEN ZUGANG an -- ein Modi
oder ein Zuschauer tippt auf irgendeine Kachel und gibt seinen Code
ein, der Code allein entscheidet. Dieser Weg ist auf einer
Pruefadresse (127.0.0.1) absichtlich abgeschaltet: Er wuerde dort
eine messbare Zeitspur hinterlassen, und genau die soll er nicht.
Folge: Ohne Host-Kopfzeile koennte sich hier kein einziger
Zuschauer anmelden -- und eine Pruefung, die ihren Hauptdarsteller
nicht hereinbekommt, prueft nichts. `fetch` kann die Kopfzeile
nicht setzen, deshalb node:http.
Aufrufen mit: node server/pruef-reaktion.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { request } from "node:http";
const HIER = dirname(fileURLToPath(import.meta.url));
const ordner = mkdtempSync(join(tmpdir(), "ws-reakt-"));
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 Reaction-Pruefung");
process.env.PORT = `${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(260_000, "pruef-reaktion");
await new Promise((r) => setTimeout(r, 900));
let fehler = 0, geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const melde = (t) => console.log(t);
process.on("exit", () => { try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } });
const CREW = "crew.dogfather-universe.com";
const AGENTUR = "workspace.dogfather-universe.com";
/* ---------- Zugaenge anlegen -------------------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const nun = new Date().toISOString();
const kennSchluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", kennSchluessel, nun);
/* 200 Tage alt: Der Treff hat Stufen, und wer neu ist, darf weniger.
Fuer die Reaction spielt das keine Rolle -- aber ein Zugang von
heute waere ein Sonderfall, den niemand im Blick hat. */
const ALT = new Date(Date.now() - 200 * 86400000).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,"
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", kennSchluessel).update(code).digest("hex"), ALT);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const ROLLEN = [["admin", "Filipe"], ["hand", "VanVan"], ["linke", "Funny"],
["modi", "Miss"], ["gast", "Nachtfalter"], ["gast2", "Lenny"], ["creator", "Luna"]];
const CODES = {}; const IDS = {};
for (const [i, [rolle, name]] of ROLLEN.entries()) {
CODES[rolle] = `CODE-R${String(i).padStart(2, "0")}-000001`;
IDS[rolle] = anlegen(name, rolle === "gast2" ? "gast" : rolle, CODES[rolle]);
}
d.close();
/* ---------- Sprechen mit Hausadresse ------------------------------------ */
function roh(host, pfad, { method = "GET", keks = "", rumpf = null, origin } = {}) {
return new Promise((fertig, schief) => {
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
const a = request({
host: "127.0.0.1", port: PORT, path: pfad, method,
headers: {
Host: host,
Origin: origin === undefined ? `https://${host}` : origin,
...(keks ? { Cookie: keks } : {}),
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
},
}, (antwort) => {
const teile = [];
antwort.on("data", (s) => teile.push(s));
antwort.on("end", () => {
const text = Buffer.concat(teile).toString("utf8");
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers });
});
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
}
async function anmelden(rolle, host) {
const a = await roh(host, "/workspace/api/anmelden", {
method: "POST", rumpf: { rolle: rolle === "gast2" ? "gast" : rolle,
code: CODES[rolle], alter_ok: true },
});
const keks = [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
return a.code === 200 && keks ? keks : null;
}
const k = {};
for (const [rolle] of ROLLEN) k[rolle] = await anmelden(rolle, CREW);
/* =======================================================================
1. ALLE KOMMEN HEREIN — auch die Community
======================================================================= */
melde("=== 1. Wer die Reaction sieht ===");
{
const fehlend = ROLLEN.map(([r]) => r).filter((r) => r !== "creator" && !k[r]);
ok(fehlend.length === 0,
`alle Rollen des Hauses sind angemeldet (${fehlend.length ? "fehlt: " + fehlend.join(", ") : "5 von 5"})`);
for (const rolle of ["admin", "hand", "linke", "modi", "gast"]) {
const a = await roh(CREW, "/workspace/api/reaktion", { keks: k[rolle] });
ok(a.code === 200 && a.j?.stand === "zu",
`${rolle}: sieht die Lage und sie ist „zu" (${a.code}/${a.j?.stand})`);
}
const s = await roh(CREW, "/workspace/reaktion.html", { keks: k.gast });
ok(s.code === 200, `die Seite steht der Community offen (${s.code})`);
}
/* =======================================================================
2. DAS ANDERE HAUS SIEHT NICHTS
======================================================================= */
melde("");
melde("=== 2. Die Agentur kennt sie nicht ===");
{
const kA = await anmelden("admin", AGENTUR);
ok(!!kA, "DogFather meldet sich auch auf der Agenturadresse an");
const a = await roh(AGENTUR, "/workspace/api/reaktion", { keks: kA });
ok(a.code === 404, `und bekommt dort 404 (${a.code})`);
const s = await roh(AGENTUR, "/workspace/reaktion.html", { keks: kA });
ok(s.code === 404, `auch die Seite gibt es dort nicht (${s.code})`);
/* Das andere Ende derselben Regel: Auf einer Pruefadresse hat
niemand ein Haus, und die Schranke darf das nicht mit dem
fremden Haus verwechseln. Genau dieser Fehler ist am 27.09.
beim Notizblock passiert. */
const kP = await anmelden("admin", "127.0.0.1");
const p = await roh("127.0.0.1", "/workspace/api/reaktion", { keks: kP });
ok(p.code === 200, `auf der Pruefadresse ist sie offen (${p.code}) -- ohne Haus wird nicht gesperrt`);
}
/* =======================================================================
3. NUR DER HOST STELLT EIN
======================================================================= */
melde("");
melde("=== 3. Wer die Sendung fuehrt ===");
{
for (const rolle of ["hand", "linke", "modi", "gast"]) {
const a = await roh(CREW, "/workspace/api/reaktion", {
method: "PUT", keks: k[rolle], rumpf: { titel: "Nicht du" },
});
ok(a.code === 403, `${rolle}: darf nicht einstellen (${a.code})`);
}
const a = await roh(CREW, "/workspace/api/reaktion", {
method: "PUT", keks: k.admin,
rumpf: { titel: "Wir schauen den Trailer", video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" },
});
ok(a.code === 200 && a.j?.video === "dQw4w9WgXcQ",
`DogFather schon -- und aus der Adresse wird die Kennung (${a.j?.video})`);
}
/* ---- Die Kennung kommt aus jeder Form, die man wirklich einfuegt ---- */
{
const { videoKennung } = await import("./workspace-reaktion.js");
const faelle = [
["https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42s", "dQw4w9WgXcQ"],
["https://youtu.be/dQw4w9WgXcQ", "dQw4w9WgXcQ"],
["https://www.youtube.com/embed/dQw4w9WgXcQ", "dQw4w9WgXcQ"],
["https://www.youtube.com/shorts/dQw4w9WgXcQ", "dQw4w9WgXcQ"],
["dQw4w9WgXcQ", "dQw4w9WgXcQ"],
];
const sitzt = faelle.filter(([roh2, soll]) => videoKennung(roh2) === soll).length;
ok(sitzt === faelle.length, `alle ${faelle.length} Formen der Adresse ergeben die Kennung`);
/* Gegenprobe: Was keine Kennung ist, darf auch keine werden -- sonst
stuende vor Publikum ein schwarzes Rechteck. */
const mist = ["", "https://vimeo.com/12345", "kein-video", "https://www.youtube.com/watch?v=zu_kurz"];
ok(mist.every((m) => videoKennung(m) === ""),
"und was keine ist, wird auch keine (Gegenprobe)");
const b = await roh(CREW, "/workspace/api/reaktion", {
method: "PUT", keks: k.admin, rumpf: { video: "https://vimeo.com/12345" },
});
ok(b.code === 400 && b.j?.fehler === "video_unklar",
`eine fremde Adresse wird abgelehnt (${b.code}/${b.j?.fehler})`);
}
/* =======================================================================
4. DER ABLAUF
======================================================================= */
melde("");
melde("=== 4. zu -> vorbereitung -> live -> zu ===");
{
const a = await roh(CREW, "/workspace/api/reaktion/stand", {
method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" },
});
ok(a.code === 200 && a.j?.stand === "vorbereitung", `Vorbereitung geht (${a.j?.stand})`);
const b = await roh(CREW, "/workspace/api/reaktion/stand", {
method: "POST", keks: k.admin, rumpf: { stand: "erfunden" },
});
ok(b.code === 400, `ein erfundener Stand wird abgelehnt (${b.code})`);
/* Im Wartebereich darf schon geschrieben werden -- das ist der
halbe Sinn der Vorbereitung. */
const c = await roh(CREW, "/workspace/api/reaktion/chat", {
method: "POST", keks: k.gast, rumpf: { text: "Bin schon da!" },
});
ok(c.code === 201, `im Wartebereich ist der Chat offen (${c.code})`);
const e = await roh(CREW, "/workspace/api/reaktion/stand", {
method: "POST", keks: k.admin, rumpf: { stand: "live" },
});
ok(e.code === 200 && e.j?.stand === "live" && e.j?.gestartet_am,
`auf Sendung -- mit Startzeit (${e.j?.stand})`);
}
/* ---- Ohne Video kein Live ---- */
{
await roh(CREW, "/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "zu" } });
await roh(CREW, "/workspace/api/reaktion", { method: "PUT", keks: k.admin, rumpf: { titel: "Ohne", video: "" } });
const a = await roh(CREW, "/workspace/api/reaktion/stand", {
method: "POST", keks: k.admin, rumpf: { stand: "live" },
});
ok(a.code === 400 && a.j?.fehler === "kein_video",
`ohne Video laesst sich nicht senden (${a.code}/${a.j?.fehler})`);
/* und danach wieder aufbauen */
await roh(CREW, "/workspace/api/reaktion", {
method: "PUT", keks: k.admin,
rumpf: { titel: "Wir schauen den Trailer", video: "dQw4w9WgXcQ" },
});
await roh(CREW, "/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "live" } });
}
/* =======================================================================
5. WER DABEI IST
======================================================================= */
melde("");
melde("=== 5. Dabei sein ===");
{
for (const rolle of ["gast", "gast2", "modi"]) {
await roh(CREW, "/workspace/api/reaktion/dabei", { method: "POST", keks: k[rolle] });
}
const a = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
ok(a.j?.dabei === 3, `drei sind dabei (${a.j?.dabei})`);
ok(Array.isArray(a.j?.zuschauer) && a.j.zuschauer.length === 3,
"DogFather sieht, WER dabei ist");
const z = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
ok(z.j?.zuschauer === null,
"ein Zuschauer bekommt die Namensliste NICHT");
ok(z.j?.dabei === 3, `die Zahl sieht er aber (${z.j?.dabei})`);
/* Jeder Dabeisitzende hat einen Kameraplatz bekommen -- es sind
erst drei, und zwoelf waeren erlaubt. */
ok(a.j.zuschauer.every((x) => x.sicht === true),
"alle drei haben einen Kameraplatz (bei 12 Plaetzen)");
/* Abmelden: der Weg, den `sendBeacon` beim Schliessen benutzt --
POST, nicht DELETE. Beides muss gehen. */
const w1 = await roh(CREW, "/workspace/api/reaktion/dabei-weg", { method: "POST", keks: k.modi });
ok(w1.code === 200, `abmelden per POST geht (fuer sendBeacon) (${w1.code})`);
const n1 = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
ok(n1.j?.dabei === 2, `danach sind es zwei (${n1.j?.dabei})`);
await roh(CREW, "/workspace/api/reaktion/dabei", { method: "POST", keks: k.modi });
}
/* =======================================================================
6. GAESTE
======================================================================= */
melde("");
melde("=== 6. Ein bis zwei Gaeste ===");
{
const { GAESTE_MAX } = await import("./reaktion-tabellen.js");
ok(GAESTE_MAX === 2, `zwei Plaetze, wie in der Notiz (${GAESTE_MAX})`);
const a = await roh(CREW, "/workspace/api/reaktion/gast", {
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast },
});
ok(a.code === 201 && a.j?.gaeste?.length === 1, `Gast 1 dazu (${a.code})`);
ok(a.j.gaeste[0].platz === 1, `er bekommt Platz 1 (${a.j?.gaeste?.[0]?.platz})`);
const b = await roh(CREW, "/workspace/api/reaktion/gast", {
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 },
});
ok(b.code === 201 && b.j?.gaeste?.length === 2, `Gast 2 dazu (${b.code})`);
const c = await roh(CREW, "/workspace/api/reaktion/gast", {
method: "POST", keks: k.admin, rumpf: { person_id: IDS.modi },
});
ok(c.code === 409 && c.j?.fehler === "gaeste_voll",
`ein dritter passt nicht (${c.code}/${c.j?.fehler})`);
/* EIN GAST BEKOMMT DIE NAMENSLISTE -- und das ist kein Zufall:
Ohne sie wuesste er nicht, wen er anrufen soll, und sein Bild
kaeme bei keinem Zuschauer an. */
const g = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
ok(Array.isArray(g.j?.zuschauer),
"wer auf der Buehne steht, bekommt die Liste (sonst sieht ihn niemand)");
ok(g.j?.ich?.gast === true, "und er weiss, dass er Gast ist");
/* Stummschalten gilt fuer alle -- das steht am Server, nicht im
Browser des Hosts. */
const s = await roh(CREW, `/workspace/api/reaktion/gast/${IDS.gast}/stumm`, {
method: "PUT", keks: k.admin, rumpf: { stumm: true },
});
ok(s.code === 200 && s.j?.gaeste?.find((x) => x.id === IDS.gast)?.stumm === true,
"stummschalten wirkt");
const s2 = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast2 });
ok(s2.j?.gaeste?.find((x) => x.id === IDS.gast)?.stumm === true,
"und alle anderen sehen es auch (nicht nur der Host)");
/* Ein Gast kann sich nicht selbst holen oder entfernen. */
const f = await roh(CREW, "/workspace/api/reaktion/gast", {
method: "POST", keks: k.gast, rumpf: { person_id: IDS.modi },
});
ok(f.code === 403, `ein Gast holt niemanden dazu (${f.code})`);
const w = await roh(CREW, `/workspace/api/reaktion/gast/${IDS.gast2}`, {
method: "DELETE", keks: k.admin,
});
ok(w.code === 200 && w.j?.gaeste?.length === 1, `entfernen geht (${w.code})`);
/* Der frei gewordene Platz wird WIEDERVERWENDET. Mit „Anzahl + 1"
bliebe Platz 1 fuer immer leer, sobald einmal jemand gegangen
ist -- und das faellt erst im dritten Stream auf. */
const neu = await roh(CREW, "/workspace/api/reaktion/gast", {
method: "POST", keks: k.admin, rumpf: { person_id: IDS.modi },
});
const plaetze = (neu.j?.gaeste || []).map((x) => x.platz).sort();
ok(plaetze.join(",") === "1,2",
`der frei gewordene Platz wird wiederverwendet (${plaetze.join(",")})`);
}
/* =======================================================================
7. DER LIVE-CHAT UND SEINE MODERATION
======================================================================= */
melde("");
melde("=== 7. Chat und Moderation ===");
let fremderBeitrag = 0;
{
const a = await roh(CREW, "/workspace/api/reaktion/chat", {
method: "POST", keks: k.gast2, rumpf: { text: "Das ist gut!" },
});
ok(a.code === 201 && a.j?.id > 0, `schreiben geht (${a.code})`);
fremderBeitrag = a.j.id;
const leer = await roh(CREW, "/workspace/api/reaktion/chat", {
method: "POST", keks: k.gast2, rumpf: { text: " " },
});
ok(leer.code === 400, `ein leerer Beitrag wird abgelehnt (${leer.code})`);
const liste = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.gast });
ok(liste.code === 200 && liste.j.beitraege.some((b) => b.id === fremderBeitrag),
"und steht in der Liste");
/* Ein Zuschauer nimmt einen FREMDEN Beitrag nicht weg. */
const nein = await roh(CREW, `/workspace/api/reaktion/chat/${fremderBeitrag}`, {
method: "DELETE", keks: k.gast,
});
ok(nein.code === 403, `ein Zuschauer nimmt fremde Beitraege nicht weg (${nein.code})`);
/* Sein eigener geht. */
const eigen = await roh(CREW, "/workspace/api/reaktion/chat", {
method: "POST", keks: k.gast, rumpf: { text: "ups" },
});
const weg = await roh(CREW, `/workspace/api/reaktion/chat/${eigen.j.id}`, {
method: "DELETE", keks: k.gast,
});
ok(weg.code === 200, `den eigenen schon (${weg.code})`);
/* Ein Modi moderiert. */
const mod = await roh(CREW, `/workspace/api/reaktion/chat/${fremderBeitrag}`, {
method: "DELETE", keks: k.modi,
});
ok(mod.code === 200, `ein Modi moderiert (${mod.code})`);
const nach = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.gast });
ok(!nach.j.beitraege.some((b) => b.id === fremderBeitrag),
"und der Beitrag ist aus der Liste");
/* WEGGENOMMEN UND NICHT GELOESCHT -- wer moderiert, soll spaeter
sagen koennen, was er weggenommen hat. */
const { DatabaseSync: DB2 } = await import("node:sqlite");
const d2 = new DB2(process.env.WORKSPACE_DB, { readOnly: true });
const zeile = d2.prepare("SELECT weg_am, weg_von, text FROM reaktion_chat WHERE id = ?")
.get(fremderBeitrag);
d2.close();
ok(!!zeile && !!zeile.weg_am && zeile.weg_von === IDS.modi && zeile.text === "Das ist gut!",
"er steht noch da -- mit Zeitpunkt und Namen dessen, der ihn genommen hat");
}
/* ---- Die Schreibbremse ---- */
{
const { SCHREIB_BREMSE } = await import("./reaktion-tabellen.js");
let gebremst = 0;
for (let i = 0; i < SCHREIB_BREMSE + 6; i++) {
const a = await roh(CREW, "/workspace/api/reaktion/chat", {
method: "POST", keks: k.linke, rumpf: { text: "spam " + i },
});
if (a.code === 429) gebremst++;
}
ok(gebremst > 0, `die Schreibbremse greift (${gebremst} von ${SCHREIB_BREMSE + 6} abgewiesen)`);
}
/* =======================================================================
8. DIE VERBINDUNGSDATEN GEHEN NUR AN LEUTE, DIE DABEI SIND
======================================================================= */
melde("");
melde("=== 8. Kameras: der Hinweg ===");
{
const a = await roh(CREW, "/workspace/api/reaktion/signal", {
method: "POST", keks: k.admin, rumpf: { an: IDS.gast, daten: { typ: "angebot" } },
});
ok(a.code === 200, `an jemanden, der dabei ist, geht es (${a.code})`);
/* Die Creatorin ist nicht dabei -- und im anderen Haus. An sie darf
nichts gehen, sonst waere die Reaction ein Weg, jemanden
anzuklingeln. */
const b = await roh(CREW, "/workspace/api/reaktion/signal", {
method: "POST", keks: k.admin, rumpf: { an: IDS.creator, daten: { typ: "angebot" } },
});
ok(b.code === 409 && b.j?.fehler === "nicht_dabei",
`an jemanden, der nicht dabei ist, nicht (${b.code}/${b.j?.fehler})`);
const c = await roh(CREW, "/workspace/api/reaktion/signal", {
method: "POST", keks: k.admin, rumpf: { an: IDS.gast },
});
ok(c.code === 400, `ohne Inhalt wird abgelehnt (${c.code})`);
}
/* =======================================================================
9. DAS ENDE RAEUMT AUF
======================================================================= */
melde("");
melde("=== 9. Beenden ===");
{
const vorher = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.admin });
ok(vorher.j.beitraege.length > 0, `vor dem Ende steht etwas im Chat (${vorher.j.beitraege.length})`);
const a = await roh(CREW, "/workspace/api/reaktion/stand", {
method: "POST", keks: k.admin, rumpf: { stand: "zu" },
});
ok(a.code === 200 && a.j?.stand === "zu", `beenden geht (${a.j?.stand})`);
ok(a.j.gaeste.length === 0, "die Gaeste sind entlassen");
ok(a.j.dabei === 0, "und niemand sitzt mehr im Saal");
const nach = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.admin });
ok(nach.j.beitraege.length === 0,
"der Live-Chat dieser Sendung ist abgeraeumt -- die naechste faengt nicht mit den Rufen der letzten an");
/* Was gelaufen ist, steht im Verlauf. */
const { DatabaseSync: DB3 } = await import("node:sqlite");
const d3 = new DB3(process.env.WORKSPACE_DB, { readOnly: true });
const v = d3.prepare("SELECT * FROM reaktion_verlauf ORDER BY id DESC LIMIT 1").get();
d3.close();
ok(!!v && v.titel === "Wir schauen den Trailer" && v.beitraege > 0,
`die Sendung steht im Verlauf (${v?.titel}, ${v?.beitraege} Beitraege)`);
/* Bei „zu" ist der Chat zu -- sonst redeten die Leute in einen
leeren Saal. */
const c = await roh(CREW, "/workspace/api/reaktion/chat", {
method: "POST", keks: k.gast, rumpf: { text: "Hallo?" },
});
ok(c.code === 409 && c.j?.fehler === "geschlossen",
`und schreiben geht nicht mehr (${c.code}/${c.j?.fehler})`);
}
/* =======================================================================
10. DIE KACHEL, DIE RECHTE UND DIE INHALTSRICHTLINIE
======================================================================= */
melde("");
melde("=== 10. Was um die Seite herum stimmen muss ===");
{
const { SEITEN } = await import("./rechte.js");
const erlaubt = SEITEN["/workspace/reaktion.html"] || [];
ok(erlaubt.includes("gast"),
`die Community steht in der Rechtetafel (${erlaubt.join(", ")})`);
/* Die Kachel. Gesucht wird sie am ZIEL und nicht am Namen -- Namen
haben sich in diesem Haus schon zweimal geaendert. */
const w = await import("./workspace.js");
const alle = w.bereicheFuer({ id: IDS.gast, rolle: "gast", name: "Nachtfalter" }, CREW);
const kachel = (alle || []).find((b) => b.ziel === "reaktion.html");
ok(!!kachel, "die Community hat die Reaction-Kachel");
ok(kachel?.live === true,
"sie traegt das Merkmal `live` statt einer Ton-Nummer (der Farbraum ist bei 46 voll)");
ok(!kachel?.ton, "und ausdruecklich KEINE Ton-Nummer");
/* DAS MERKMAL HEISST NICHT `data-live`. Das ist seit dem 21.09.
vergeben -- die Draussen-Kachel bekommt es, sobald Filipe auf
Twitch sendet. Haetten beide denselben Namen, waere der
Draussen-Kachel waehrend jedes Streams die Farbe abhanden
gekommen. */
const startJs = readFileSync(join(HIER, "..", "workspace", "assets", "js", "start.js"), "utf8");
ok(/dataset\.sendung = 'ja'/.test(startJs),
"die Wand setzt `data-sendung` (und nicht `data-live`, das ist vergeben)");
const startCss = readFileSync(join(HIER, "..", "workspace", "assets", "css", "start.css"), "utf8");
ok(!/\.kachel\[data-live="ja"\]\s*\{\s*--ton/.test(startCss),
"und kein Stil greift der Draussen-Kachel ihre Farbe ab");
/* DIE INHALTSRICHTLINIE. Ohne YouTube darin startet die Sendung,
alle sind da, und die Leinwand bleibt schwarz -- ohne
Fehlermeldung, nur ein Eintrag in der Browser-Konsole. Genau so
ist am 27.08.2026 der Musik-Knopf ausgefallen. */
const seite = await roh(CREW, "/workspace/reaktion.html", { keks: k.gast });
const regel = String(seite.kopf["content-security-policy"] || "");
ok(/frame-src[^;]*youtube-nocookie\.com/.test(regel),
"der YouTube-Rahmen ist erlaubt");
ok(/script-src[^;]*www\.youtube\.com/.test(regel),
"und die Einbett-API darf geladen werden");
ok(/img-src[^;]*ytimg\.com/.test(regel),
"und die Vorschaubilder auch");
}
/* =======================================================================
11. KEIN WEG HEISST ZWEIMAL GLEICH
======================================================================= */
melde("");
melde("=== 11. Keine doppelten Wege ===");
{
/* Beim Notizblock hat genau das eine Stunde gekostet:
`DELETE /workspace/api/notizen/:id` gab es schon woanders, und
express nimmt schweigend die erste passende Route. */
const { default: app } = await import("./index.js");
const wege = [];
const sammeln = (stapel, vorn = "") => {
for (const s of stapel) {
if (s.route) {
for (const m of Object.keys(s.route.methods)) {
wege.push(`${m.toUpperCase()} ${vorn}${s.route.path}`);
}
} else if (s.handle?.stack) {
sammeln(s.handle.stack, vorn);
}
}
};
sammeln(app._router.stack);
ok(wege.length > 300, `${wege.length} Wege im Baum gefunden`);
const doppelt = wege.filter((x, i) => wege.indexOf(x) !== i);
ok(doppelt.length === 0,
doppelt.length ? `DOPPELT: ${[...new Set(doppelt)].join(", ")}` : "kein Weg kommt zweimal vor");
const reaktionswege = wege.filter((x) => x.includes("/api/reaktion")).length;
ok(reaktionswege >= 11, `die Reaction bringt ${reaktionswege} eigene Wege mit`);
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
process.exit(fehler === 0 ? 0 : 1);
+169
View File
@@ -0,0 +1,169 @@
/* =====================================================================
DIE REACTION — die Tabellen (28.09.2026)
Filipe hat eine Kurznotiz geschickt: eine Reaction-Kachel, die
fuer alle sichtbar ist, normalerweise geschlossen; bei
Vorbereitung ein Vorschaubild und ein Countdown; ein paar Minuten
vorher geht der Wartebereich auf, damit die Leute schon
dasitzen und schreiben koennen; beim Start laufen YouTube-Video
und Host-Kamera gleichzeitig; ein bis zwei Gaeste mit Kamera und
Mikrofon; getrennte Lautstaerken; ein Live-Chat mit Moderation;
und ein Weg, den Host zu unterstuetzen.
---------------------------------------------------------------------
EINE SENDUNG, NICHT VIELE
Es gibt genau EINE Reaction-Zeile. Sie wird nicht angelegt und
geloescht, sie wechselt ihren Stand: zu -> vorbereitung -> live ->
zu. Der Grund ist nicht Bequemlichkeit, sondern Wahrheit: Eine
Kachel, die „offen" oder „geschlossen" anzeigt, muss auf EINE
Zeile zeigen koennen. Bei einer Liste von Sendungen waere die
erste Frage jedes Mal „welche denn?", und die Antwort darauf haette
jede der sieben Stellen einzeln gegeben, die den Stand anzeigen.
Was vorbei ist, wandert nach `reaktion_verlauf`. Damit bleibt die
Geschichte erhalten, ohne dass die Gegenwart mehrdeutig wird.
---------------------------------------------------------------------
WARUM DAS VIDEO NICHT UEBER DEN SERVER LAEUFT
Naheliegend waere: der Host spielt das YouTube-Video ab, und alle
sehen seinen Bildschirm. Das waere aus zwei Gruenden falsch.
Erstens rechtlich: Ein YouTube-Video an Dritte weiterzusenden ist
eine oeffentliche Wiedergabe und damit genau die Sache, fuer die
Kanaele gesperrt werden. Jeder Zuschauer laedt das Video deshalb
SELBST von YouTube -- so wie bei jeder Watch-Party.
Zweitens technisch: Ein weitergesendetes Video kostet Bandbreite
und Qualitaet. Uebertragen wird stattdessen nur, WO das Video
gerade steht (Kennung, laeuft/pausiert, Sekunde). Das sind ein paar
Byte, und jeder sieht es in voller Qualitaet.
---------------------------------------------------------------------
DIE KAMERAS LAUFEN DIREKT VON MENSCH ZU MENSCH
Der Anrufteil des Hauses (workspace-anruf.js, assets/js/anruf.js)
macht das seit dem 18.09. und hat STUN/TURN eingerichtet. Die
Reaction benutzt denselben Weg, nur mit mehr Empfaengern: Der Host
baut zu jedem Zuschauer eine eigene Verbindung auf.
DAS HAT EINE GRENZE, UND SIE IST GEMESSEN, NICHT GERATEN. Jede
Verbindung kostet den Host Upload. Bei 360p und rund 350 kbit/s
sind zwoelf Zuschauer etwa 4 Mbit/s -- das traegt eine normale
Leitung. Darueber schaltet die Sendung von selbst auf Ton um: Wer
keine Kamera mehr bekommt, hoert alles, sieht das Video und kann
schreiben. Das ist ehrlicher als eine Verbindung, die stockt, und
es ist sichtbar: Die Zahl steht im Regiepult.
Heute sind es elf Menschen im ganzen Haus (gemessen am 28.09.2026:
1 admin, 1 hand, 1 linke, 4 modi, 4 gast). Die Grenze ist also
weit weg -- sie steht trotzdem hier, weil sie sonst erst auffaellt,
wenn es zu spaet ist.
===================================================================== */
/** Wie lange ein beendeter Verlaufseintrag aufgehoben wird. */
export const VERLAUF_TAGE = 180;
/** Hoechstens so viele Zuschauer bekommen Kamerabild. Darueber: nur Ton. */
export const SICHT_PLAETZE = Number(process.env.REAKTION_SICHT_PLAETZE || 12);
/** Hoechstens so viele Gaeste gleichzeitig (Filipes Notiz: 1-2). */
export const GAESTE_MAX = 2;
/** Ein Chatbeitrag in der Reaction. */
export const TEXT_MAX = 400;
/** So viele Beitraege je Minute und Person. */
export const SCHREIB_BREMSE = 20;
/** Die Staende, die es gibt -- und nur diese. */
export const STAENDE = ["zu", "vorbereitung", "live"];
/** Die Anordnungen des Bildes. */
export const LAYOUTS = ["kino", "gleich", "kamera"];
export function reaktionTabellen(d) {
d.exec(`
/* Genau EINE Zeile. Die Sperre steht in der Tabelle und nicht im
Programm: Ein Programm kann man umgehen, eine Bedingung nicht. */
CREATE TABLE IF NOT EXISTS reaktion (
id INTEGER PRIMARY KEY CHECK (id = 1),
stand TEXT NOT NULL DEFAULT 'zu'
CHECK (stand IN ('zu','vorbereitung','live')),
titel TEXT NOT NULL DEFAULT '',
video TEXT NOT NULL DEFAULT '',
vorschau TEXT NOT NULL DEFAULT '',
beginnt_am TEXT,
host_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
layout TEXT NOT NULL DEFAULT 'kino',
laeuft INTEGER NOT NULL DEFAULT 0,
sekunde REAL NOT NULL DEFAULT 0,
stand_seit TEXT NOT NULL DEFAULT '',
gestartet_am TEXT,
geaendert TEXT NOT NULL DEFAULT ''
);
/* Wer gerade mit Kamera dabei ist. Der Host steht NICHT hier --
er ist an der Sendung selbst vermerkt. Ein Gast, der geht,
wird geloescht; wer stumm ist, bleibt stehen. */
CREATE TABLE IF NOT EXISTS reaktion_gaeste (
person_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
seit TEXT NOT NULL,
stumm INTEGER NOT NULL DEFAULT 0,
platz INTEGER NOT NULL DEFAULT 1
);
/* Wer gerade zusieht. Wird beim Verlassen geloescht und von der
Pflege entfernt, wenn sich jemand zwei Minuten nicht meldet.
Daraus kommt die Zuschauerzahl -- und die Entscheidung, wer
noch ein Kamerabild bekommt. */
CREATE TABLE IF NOT EXISTS reaktion_dabei (
person_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
seit TEXT NOT NULL,
gesehen TEXT NOT NULL,
sicht INTEGER NOT NULL DEFAULT 0
);
/* Der Live-Chat. Eigene Tabelle und nicht der Hauschat: Was hier
gesagt wird, gehoert zu dieser einen Sendung und ist danach
vorbei. Im Hauschat wuerde es die Gespraeche der Leute
ueberschwemmen, und die Moderation waere eine andere. */
CREATE TABLE IF NOT EXISTS reaktion_chat (
id INTEGER PRIMARY KEY AUTOINCREMENT,
person_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
name TEXT NOT NULL DEFAULT '',
rolle TEXT NOT NULL DEFAULT '',
text TEXT NOT NULL,
erstellt TEXT NOT NULL,
weg_am TEXT,
weg_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
);
/* Was gelaufen ist. Eine Sendung je Zeile, erst beim Beenden. */
CREATE TABLE IF NOT EXISTS reaktion_verlauf (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titel TEXT NOT NULL DEFAULT '',
video TEXT NOT NULL DEFAULT '',
host_id INTEGER REFERENCES personen(id) ON DELETE SET NULL,
begonnen TEXT,
beendet TEXT NOT NULL,
zuschauer INTEGER NOT NULL DEFAULT 0,
beitraege INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_reaktion_chat_zeit
ON reaktion_chat (weg_am, id);
CREATE INDEX IF NOT EXISTS idx_reaktion_dabei_gesehen
ON reaktion_dabei (gesehen);
`);
/* DIE EINE ZEILE ANLEGEN, WENN ES SIE NOCH NICHT GIBT.
`INSERT OR IGNORE` und nicht „erst fragen, dann schreiben": Zwei
Anfragen gleichzeitig beim ersten Start wuerden sonst beide
„gibt es nicht" lesen und beide schreiben wollen. */
d.prepare(`INSERT OR IGNORE INTO reaktion
(id, stand, stand_seit, geaendert) VALUES (1, 'zu', ?, ?)`)
.run(new Date().toISOString(), new Date().toISOString());
}
+5
View File
@@ -524,6 +524,11 @@ export const SEITEN = {
Seite gehoert zum Haus von Team Dogi und ist auf der
Agenturadresse ein 404 (GEHOERT_ZU_ADRESSE in crew-adresse.js). */
"/workspace/notizen.html": ["admin", "hand", "linke", "modi"],
/* Die Reaction ist fuer ALLE im Haus -- Filipes Notiz sagt
woertlich „fuer alle sichtbar". Die Community steht deshalb
ausdruecklich mit drin; wer nicht Host ist, sieht dort die
Zuschauerseite. */
"/workspace/reaktion.html": ["admin", "hand", "linke", "modi", "gast"],
"/workspace/rechte.html": ["admin", "hand"],
};
+19
View File
@@ -46,6 +46,8 @@ import { AUFBEWAHRUNG_TAGE as HILFE_TAGE, FALL_STAND } from "./hilfe-tabellen.js
import { AUFBEWAHRUNG_TAGE as SUPPORT_TAGE } from "./support-tabellen.js";
import { supportAufraeumen } from "./workspace-support.js";
import { notizenAufraeumen } from "./workspace-notizen.js";
import { reaktionAufraeumen } from "./workspace-reaktion.js";
import { VERLAUF_TAGE } from "./reaktion-tabellen.js";
import { PAPIERKORB_TAGE as NOTIZ_TAGE } from "./notiz-tabellen.js";
import { ZAEHLUNG_TAGE } from "./unterstuetzung-tabellen.js";
import { unterstuetzungAufraeumen } from "./workspace-unterstuetzung.js";
@@ -323,6 +325,23 @@ export const FRISTEN = [
vorTagen(NOTIZ_TAGE)),
raeumen: () => notizenAufraeumen(),
},
{
schluessel: "reaktion_verlauf",
was: "Verlauf der Reaction-Sendungen",
art: "raeumen",
frist: `${VERLAUF_TAGE} Tage`,
zweck: "Nachsehen können, was wir gemeinsam geschaut haben – "
+ "und ob überhaupt jemand dabei war.",
grundlage: "Art. 5 Abs. 1 lit. e DSGVO – Speicherbegrenzung",
wirkung: "Die Zeile verschwindet: Titel, Video, Datum, wie viele "
+ "zugesehen und wie viel geschrieben wurde. Der Live-Chat einer "
+ "Sendung ist ohnehin schon beim Beenden weg – was hier "
+ "läuft, ist nur die Erinnerung, dass es sie gab.",
offen: (d) => zaehle(d,
"SELECT COUNT(*) AS n FROM reaktion_verlauf WHERE beendet < ?",
vorTagen(VERLAUF_TAGE)),
raeumen: () => reaktionAufraeumen(),
},
{
schluessel: "unterstuetzung_striche",
was: "Tagesstriche der Unterstützen-Seite",
+35
View File
@@ -3797,3 +3797,38 @@ export { chatEreignis };
export function siehtZu(personId) {
return (zuschauer.get(Number(personId)) || []).size > 0;
}
/* =====================================================================
DER KANAL FUER ANDERE MODULE (28.09.2026)
Der Ereignisstrom liegt beim Chat, weil er dort entstanden ist --
aber er ist laengst der einzige offene Draht zwischen Server und
Browser. `chatEreignis()` schickt an die Teilnehmer eines CHATRAUMS;
wer keinen Raum hat, kommt damit nicht weiter.
Die Reaction hat keinen Chatraum. Sie hat einen Kreis von Leuten,
der sich jede Minute aendert -- wer gerade zusieht. Genau dafuer
ist das hier: Empfaenger werden uebergeben, nicht nachgeschlagen.
EIN ZWEITER STROM WAERE DER FALSCHE WEG. Jeder Browser haelt dann
zwei offene Verbindungen, jede mit eigenem Puls, eigenem
Wiederverbinden und eigener Luecke danach. Und Anker: Manche
Browser lassen nur sechs gleichzeitige Verbindungen je Adresse zu --
zwei davon fuer Ereignisse wegzugeben, ist teuer.
===================================================================== */
export function stromAn(personIds, art, daten) {
let erreicht = 0;
for (const roh of personIds) {
const id = Number(roh);
for (const antwort of zuschauer.get(id) || []) {
try {
antwort.write(`event: ${art}
data: ${JSON.stringify(daten)}
`);
erreicht++;
} catch { /* die Verbindung raeumt sich selbst ab */ }
}
}
return erreicht;
}
+665
View File
@@ -0,0 +1,665 @@
/* =====================================================================
DIE REACTION — die Wege (28.09.2026)
Der Ablauf aus Filipes Notiz, von links nach rechts:
Kachel sichtbar -> geschlossen -> Vorbereitung ->
Wartebereich/Chat -> Countdown -> LIVE ->
Reaction + Gaeste + Chat + PayPal -> Ende
Das sind drei Staende und nicht sieben. „Wartebereich" und
„Countdown" sind keine eigenen Zustaende, sondern das, was
„vorbereitung" auf dem Bildschirm TUT: Der Chat ist offen, der
Countdown laeuft, das Vorschaubild steht. Drei Staende, die sich
gegenseitig ausschliessen, sind pruefbar; sieben, von denen sich
vier ueberlappen, sind es nicht.
---------------------------------------------------------------------
WER WAS DARF
Host = DogFather. Er stellt ein, startet, beendet, holt und
entfernt Gaeste, schaltet stumm, bestimmt die Anordnung.
Moderation= DogFather, rechte und linke Hand, Modis. Sie koennen
Beitraege aus dem Live-Chat nehmen.
Gast = wen der Host holt. Jede Rolle des Hauses.
Zuschauer = jeder im Team-Dogi-Haus, auch die Community.
Die Reaction gibt es NUR im Team-Dogi-Haus. Auf der Agenturadresse
antwortet jeder Weg mit 404 -- nicht mit 403, denn ein „verboten"
verraet, dass es hier etwas gibt.
---------------------------------------------------------------------
DAS VIDEO LAEUFT NICHT UEBER DIESEN SERVER
Uebertragen wird nur der Spielstand: Kennung, laeuft/pausiert,
Sekunde. Jeder Browser laedt das Video selbst von YouTube. Die
Begruendung steht in reaktion-tabellen.js.
Aufrufen laesst sich alles ohne Sonderfall: Wer zusieht, meldet
sich alle 30 Sekunden mit `/dabei`. Wer sich zwei Minuten nicht
meldet, faellt heraus -- ein sauberes „Tschuess" gibt es im Netz
nicht, ein Handy geht einfach aus.
===================================================================== */
import express from "express";
import { db, sitzungLesen, protokolliere, echteIp } from "./workspace.js";
import { stromAn } from "./workspace-chat.js";
import { einstellung } from "./workspace.js";
import { mitZugangsdaten, wegeAusfaechern } from "./workspace-turn.js";
import {
SICHT_PLAETZE, GAESTE_MAX, TEXT_MAX, SCHREIB_BREMSE, STAENDE, LAYOUTS,
VERLAUF_TAGE,
} from "./reaktion-tabellen.js";
export const reaktionRouter = express.Router();
/** Wer die Sendung fuehrt. */
const HOST_ROLLEN = new Set(["admin"]);
/** Wer Beitraege aus dem Chat nehmen darf. */
const MODERIERT = new Set(["admin", "hand", "linke", "modi"]);
export const REAKTION_HOST_ROLLEN = HOST_ROLLEN;
const jetzt = () => new Date().toISOString();
/* ---------- Schranken --------------------------------------------------- */
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
next();
}
/** Nicht das andere Haus.
*
* Gefragt wird „ist es die Agentur?" und nicht „ist es crew?" --
* auf einer Pruefadresse hat niemand ein Haus, und eine Schranke,
* die das mit dem fremden Haus verwechselt, macht die Seite dort
* still tot. Genau dieser Fehler ist am 27.09. beim Notizblock
* passiert und von pruef-handy gefunden worden. */
function nurTeamDogi(req, res, next) {
if (req.person?.haus === "agentur") {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
next();
}
function nurHost(req, res, next) {
if (!HOST_ROLLEN.has(req.person?.rolle)) {
return res.status(403).json({ fehler: "nur_host" });
}
next();
}
/* Schutz gegen Anfragen von fremden Seiten -- wortgleich zu den
uebrigen Modulen. */
function gleicheHerkunft(req, res, next) {
const herkunft = req.get("origin");
if (!herkunft) return next();
let erlaubt;
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
next();
}
/* ---------- Die Schreibbremse -------------------------------------------
Ein Live-Chat ist der eine Ort im Haus, an dem jemand in zehn
Sekunden dreissig Zeilen schreiben kann -- aus Begeisterung oder
aus Aerger. Die Bremse zaehlt je Person und Minute. */
const bremse = new Map();
function zuSchnell(personId) {
const minute = Math.floor(Date.now() / 60_000);
const s = bremse.get(personId);
if (!s || s.minute !== minute) { bremse.set(personId, { minute, n: 1 }); return false; }
s.n += 1;
return s.n > SCHREIB_BREMSE;
}
/* ---------- Hilfen ------------------------------------------------------ */
const sendung = () => db().prepare("SELECT * FROM reaktion WHERE id = 1").get();
function gaesteListe() {
return db().prepare(`
SELECT g.person_id AS id, g.stumm, g.platz, p.name, p.rolle
FROM reaktion_gaeste g JOIN personen p ON p.id = g.person_id
ORDER BY g.platz`).all();
}
/** Wer gerade dabei ist -- und ob noch ein Kameraplatz frei war.
*
* ZWEI MINUTEN OHNE LEBENSZEICHEN HEISST WEG. Ein Browser, der
* zugeklappt wird, schickt kein „ich gehe". Ohne diese Grenze
* waechst die Zuschauerzahl den ganzen Abend und stimmt nie. */
function dabeiListe() {
const grenze = new Date(Date.now() - 120_000).toISOString();
db().prepare("DELETE FROM reaktion_dabei WHERE gesehen < ?").run(grenze);
return db().prepare(`
SELECT d.person_id AS id, d.sicht, d.seit, p.name, p.rolle
FROM reaktion_dabei d JOIN personen p ON p.id = d.person_id
ORDER BY d.seit`).all();
}
/** Alle, die von einer Aenderung erfahren sollen. */
function empfaenger() {
const ids = new Set(dabeiListe().map((d) => d.id));
for (const g of gaesteListe()) ids.add(g.id);
const s = sendung();
if (s?.host_id) ids.add(s.host_id);
return [...ids];
}
/** Was jeder sehen darf -- egal ob Zuschauer, Gast oder Host. */
function lage(person) {
const s = sendung();
const gaeste = gaesteListe();
const dabei = dabeiListe();
const istHost = HOST_ROLLEN.has(person.rolle);
const binGast = gaeste.some((g) => g.id === person.id);
return {
stand: s.stand,
titel: s.titel,
video: s.video,
vorschau: s.vorschau,
beginnt_am: s.beginnt_am,
layout: s.layout,
laeuft: !!s.laeuft,
sekunde: s.sekunde,
gestartet_am: s.gestartet_am,
host: s.host_id
? db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(s.host_id)
: null,
gaeste: gaeste.map((g) => ({ id: g.id, name: g.name, rolle: g.rolle, stumm: !!g.stumm, platz: g.platz })),
dabei: dabei.length,
/* ==== WER DIE LISTE BEKOMMT -- UND WARUM GENAU DIESE ==========
Namen der Zusehenden bekommt, wer auf der BUEHNE steht (Host,
Gaeste) oder moderiert. Ein Zuschauer bekommt sie nicht: Er
soll nicht ablesen koennen, wer sonst noch zusieht -- im Live
sieht man einander am Chat, und das reicht.
DIE GAESTE STEHEN HIER NICHT AUS HOEFLICHKEIT. Wer sendet,
muss wissen, wen er anrufen soll: Zuschauer rufen grundsaetzlich
nie an (sonst rufen zwei gleichzeitig an, und beide
scheitern -- „glare"). Ohne die Liste kaeme das Bild eines
Gastes deshalb bei keinem Zuschauer an. Zwei fuer sich richtige
Regeln, die zusammen ein Loch ergeben. */
zuschauer: (istHost || binGast || MODERIERT.has(person.rolle))
? dabei.map((d) => ({ id: d.id, name: d.name, rolle: d.rolle, sicht: !!d.sicht }))
: null,
ich: {
id: person.id,
name: person.name,
rolle: person.rolle,
host: istHost,
gast: binGast,
moderiert: MODERIERT.has(person.rolle),
sicht: !!(dabei.find((d) => d.id === person.id)?.sicht),
},
plaetze: SICHT_PLAETZE,
gaeste_max: GAESTE_MAX,
text_max: TEXT_MAX,
};
}
function melden(art, daten) {
stromAn(empfaenger(), art, daten);
}
/* ======================================================================
DIE WEGE
====================================================================== */
reaktionRouter.use("/workspace/api/reaktion", angemeldet, nurTeamDogi, gleicheHerkunft);
/** Wie steht es? — der eine Aufruf, den jede Seite beim Laden macht. */
reaktionRouter.get("/workspace/api/reaktion", (req, res) => {
try {
res.json(lage(req.person));
} catch (fehler) {
console.error("[reaktion] Lage:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Der Host stellt ein: Titel, Video, Vorschaubild, Beginn. */
reaktionRouter.put("/workspace/api/reaktion", nurHost, express.json({ limit: "32kb" }),
(req, res) => {
try {
const titel = String(req.body?.titel ?? "").trim().slice(0, 120);
const video = videoKennung(String(req.body?.video ?? ""));
const vorschau = String(req.body?.vorschau ?? "").trim().slice(0, 400);
const beginn = req.body?.beginnt_am ? String(req.body.beginnt_am).slice(0, 40) : null;
if (req.body?.video && !video) {
return res.status(400).json({ fehler: "video_unklar" });
}
/* EIN BEGINN IN DER VERGANGENHEIT IST KEIN BEGINN. Der Countdown
zaehlte sonst rueckwaerts ins Minus, und niemand koennte
sagen, ob das Absicht war. */
if (beginn && Number.isFinite(Date.parse(beginn)) && Date.parse(beginn) < Date.now() - 60_000) {
return res.status(400).json({ fehler: "beginn_vorbei" });
}
db().prepare(`UPDATE reaktion SET titel = ?, video = ?, vorschau = ?,
beginnt_am = ?, host_id = ?, geaendert = ? WHERE id = 1`)
.run(titel, video, vorschau, beginn, req.person.id, jetzt());
const l = lage(req.person);
melden("reaktion", { was: "eingestellt", lage: oeffentlich(l) });
res.json(l);
} catch (fehler) {
console.error("[reaktion] Einstellen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Der Stand wechselt: zu / vorbereitung / live. */
reaktionRouter.post("/workspace/api/reaktion/stand", nurHost, express.json({ limit: "4kb" }),
(req, res) => {
try {
const neu = String(req.body?.stand ?? "");
if (!STAENDE.includes(neu)) return res.status(400).json({ fehler: "stand_unbekannt" });
const s = sendung();
/* OHNE VIDEO KEIN LIVE. Der Knopf waere sonst da, das Ergebnis
ein schwarzes Rechteck -- und die Leute sitzen davor. */
if (neu === "live" && !s.video) {
return res.status(400).json({ fehler: "kein_video" });
}
const felder = { stand: neu, stand_seit: jetzt(), geaendert: jetzt() };
if (neu === "live" && s.stand !== "live") {
felder.gestartet_am = jetzt();
felder.laeuft = 1;
felder.sekunde = 0;
}
if (neu === "zu" && s.stand === "live") {
/* WAS VORBEI IST, WANDERT IN DEN VERLAUF -- und der Live-Chat
dieser Sendung ist damit Geschichte. Wuerde er stehen
bleiben, faenge die naechste Sendung mit den Rufen der
letzten an. */
const zahl = db().prepare("SELECT COUNT(*) AS n FROM reaktion_chat WHERE weg_am IS NULL").get();
db().prepare(`INSERT INTO reaktion_verlauf
(titel, video, host_id, begonnen, beendet, zuschauer, beitraege)
VALUES (?,?,?,?,?,?,?)`)
.run(s.titel, s.video, s.host_id, s.gestartet_am, jetzt(),
dabeiListe().length, zahl?.n || 0);
db().exec("DELETE FROM reaktion_chat");
db().exec("DELETE FROM reaktion_gaeste");
felder.laeuft = 0;
felder.sekunde = 0;
felder.gestartet_am = null;
}
if (neu === "zu") db().exec("DELETE FROM reaktion_dabei");
const spalten = Object.keys(felder).map((k) => `${k} = ?`).join(", ");
db().prepare(`UPDATE reaktion SET ${spalten} WHERE id = 1`)
.run(...Object.values(felder));
protokolliere(req.person.id, "reaktion_stand", `${s.stand} -> ${neu}`, echteIp(req));
const l = lage(req.person);
melden("reaktion", { was: "stand", lage: oeffentlich(l) });
res.json(l);
} catch (fehler) {
console.error("[reaktion] Stand:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Ich bin da / ich bin noch da. Alle 30 Sekunden. */
reaktionRouter.post("/workspace/api/reaktion/dabei", express.json({ limit: "2kb" }),
(req, res) => {
try {
const s = sendung();
if (s.stand === "zu") return res.status(409).json({ fehler: "geschlossen" });
const schon = db().prepare("SELECT * FROM reaktion_dabei WHERE person_id = ?")
.get(req.person.id);
if (schon) {
db().prepare("UPDATE reaktion_dabei SET gesehen = ? WHERE person_id = ?")
.run(jetzt(), req.person.id);
} else {
/* DER KAMERAPLATZ WIRD BEIM EINTRETEN VERGEBEN, nicht beim
Zeichnen. Sonst haetten zwei gleichzeitig eintretende
Zuschauer denselben letzten Platz gelesen und beide
gerechnet, sie haetten ihn. */
const mitSicht = db().prepare(
"SELECT COUNT(*) AS n FROM reaktion_dabei WHERE sicht = 1").get()?.n || 0;
db().prepare("INSERT INTO reaktion_dabei (person_id, seit, gesehen, sicht) VALUES (?,?,?,?)")
.run(req.person.id, jetzt(), jetzt(), mitSicht < SICHT_PLAETZE ? 1 : 0);
melden("reaktion", { was: "dabei", lage: oeffentlich(lage(req.person)) });
}
res.json(lage(req.person));
} catch (fehler) {
console.error("[reaktion] Dabei:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
function abmelden(req, res) {
try {
db().prepare("DELETE FROM reaktion_dabei WHERE person_id = ?").run(req.person.id);
melden("reaktion", { was: "weg", lage: oeffentlich(lage(req.person)) });
res.json({ ok: true });
} catch (fehler) {
console.error("[reaktion] Weg:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
}
reaktionRouter.delete("/workspace/api/reaktion/dabei", abmelden);
/* DERSELBE WEG NOCH EINMAL ALS POST.
Beim Schliessen des Fensters wird ein gewoehnlicher Abruf
abgebrochen -- dafuer gibt es `navigator.sendBeacon`. Der schickt
aber IMMER POST; ein DELETE waere nie angekommen, und jeder haette
danach zwei Minuten lang als anwesend gegolten. Die Zuschauerzahl
waere den ganzen Abend gestiegen und nie gefallen.
Kein zweiter Code, nur ein zweiter Eingang: Beide zeigen auf
dieselbe Funktion. */
reaktionRouter.post("/workspace/api/reaktion/dabei-weg", abmelden);
/* ---------- Die Verbindungsadressen -------------------------------------
STUN sagt einem Browser, wie er von aussen aussieht; TURN leitet
weiter, wenn eine direkte Verbindung nicht zustande kommt.
WARUM DIE REACTION EINE EIGENE TUER HAT: Vor den Anruf-Wegen steht
`darfTelefonieren`, und die Community ist dort ausdruecklich nicht
dabei -- ein Zuschauer soll kein Teammitglied anklingeln koennen.
Das ist richtig und bleibt. Hier sitzt derselbe Mensch aber im
Saal, und ohne Adressen bliebe sein Bild schwarz.
Dieselbe Einstellung, dieselben Helfer, andere Tuer. Und die Tuer
ist nur offen, solange etwas laeuft: Bei „zu" gibt es nichts zu
holen -- sonst waere das ein dauerhaft offener Weg zu den
TURN-Zugangsdaten fuer jeden im Haus. */
reaktionRouter.get("/workspace/api/reaktion/adressen", (req, res) => {
try {
if (sendung().stand === "zu") return res.json({ adressen: [] });
let liste = [];
try { liste = JSON.parse(einstellung("anruf_adressen") || "[]"); } catch { liste = []; }
/* Als Kennung die Personennummer und nicht der Name -- sie landet
im Protokoll des TURN-Servers, und das ist kein Ort fuer
Klarnamen. Wortgleich zum Anrufteil. */
const { adressen } = mitZugangsdaten(wegeAusfaechern(liste), `r${req.person.id}`);
res.json({ adressen });
} catch (fehler) {
console.error("[reaktion] Adressen:", fehler?.message);
/* KEIN 503. Ohne eigene Adressen nimmt der Browser seine Vorgabe
und die Sendung laeuft weiter -- eine leere Liste ist hier die
ehrlichere Antwort als ein Fehler, der nach Ausfall aussieht. */
res.json({ adressen: [] });
}
});
/* ---------- Gaeste ------------------------------------------------------ */
reaktionRouter.post("/workspace/api/reaktion/gast", nurHost, express.json({ limit: "4kb" }),
(req, res) => {
try {
const id = Number(req.body?.person_id);
if (!Number.isInteger(id) || id < 1) return res.status(400).json({ fehler: "person_unklar" });
const p = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ? AND aktiv = 1").get(id);
if (!p) return res.status(404).json({ fehler: "nicht_gefunden" });
if (id === req.person.id) return res.status(400).json({ fehler: "host_ist_kein_gast" });
const da = db().prepare("SELECT COUNT(*) AS n FROM reaktion_gaeste").get()?.n || 0;
if (da >= GAESTE_MAX) return res.status(409).json({ fehler: "gaeste_voll" });
/* Der Platz ist die kleinste freie Zahl -- nicht „Anzahl + 1".
Wer Gast 1 entfernt und dann jemanden holt, bekaeme sonst
wieder Platz 2, und Platz 1 bliebe fuer immer leer. */
const belegt = new Set(gaesteListe().map((g) => g.platz));
let platz = 1;
while (belegt.has(platz)) platz++;
db().prepare("INSERT OR REPLACE INTO reaktion_gaeste (person_id, seit, stumm, platz) VALUES (?,?,0,?)")
.run(id, jetzt(), platz);
protokolliere(req.person.id, "reaktion_gast_dazu", `${p.name} (Platz ${platz})`, echteIp(req));
const l = lage(req.person);
melden("reaktion", { was: "gaeste", lage: oeffentlich(l) });
stromAn([id], "reaktion", { was: "du_bist_gast", lage: oeffentlich(l) });
res.status(201).json(l);
} catch (fehler) {
console.error("[reaktion] Gast:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
reaktionRouter.delete("/workspace/api/reaktion/gast/:id", nurHost, (req, res) => {
try {
const id = Number(req.params.id);
const weg = db().prepare("DELETE FROM reaktion_gaeste WHERE person_id = ?").run(id);
if (!weg.changes) return res.status(404).json({ fehler: "nicht_gefunden" });
const l = lage(req.person);
melden("reaktion", { was: "gaeste", lage: oeffentlich(l) });
stromAn([id], "reaktion", { was: "du_bist_kein_gast", lage: oeffentlich(l) });
res.json(l);
} catch (fehler) {
console.error("[reaktion] Gast weg:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
reaktionRouter.put("/workspace/api/reaktion/gast/:id/stumm", nurHost,
express.json({ limit: "2kb" }), (req, res) => {
try {
const id = Number(req.params.id);
const stumm = req.body?.stumm ? 1 : 0;
const a = db().prepare("UPDATE reaktion_gaeste SET stumm = ? WHERE person_id = ?")
.run(stumm, id);
if (!a.changes) return res.status(404).json({ fehler: "nicht_gefunden" });
const l = lage(req.person);
melden("reaktion", { was: "gaeste", lage: oeffentlich(l) });
res.json(l);
} catch (fehler) {
console.error("[reaktion] Stumm:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Das Video --------------------------------------------------
Der Host sagt, wo das Video steht. Alle anderen folgen.
GESCHICKT WIRD DER SPIELSTAND, NICHT DAS BILD -- die Begruendung
steht in reaktion-tabellen.js. Hier steht nur die Folge davon:
Diese Route ist die einzige, die waehrend der Sendung oft laeuft,
und sie schreibt deshalb so wenig wie moeglich. */
reaktionRouter.post("/workspace/api/reaktion/video", nurHost, express.json({ limit: "2kb" }),
(req, res) => {
try {
const laeuft = req.body?.laeuft ? 1 : 0;
const sekunde = Math.max(0, Number(req.body?.sekunde) || 0);
const video = req.body?.video === undefined ? null : videoKennung(String(req.body.video));
if (req.body?.video !== undefined && !video) {
return res.status(400).json({ fehler: "video_unklar" });
}
if (video) {
db().prepare("UPDATE reaktion SET video = ?, laeuft = ?, sekunde = ?, geaendert = ? WHERE id = 1")
.run(video, laeuft, sekunde, jetzt());
} else {
db().prepare("UPDATE reaktion SET laeuft = ?, sekunde = ? WHERE id = 1")
.run(laeuft, sekunde);
}
const s = sendung();
melden("reaktion", {
was: "video",
video: s.video, laeuft: !!s.laeuft, sekunde: s.sekunde,
/* Die Uhrzeit des Servers geht mit: Der Empfaenger rechnet
die Laufzeit seit dem Absenden dazu. Ohne das waere jeder
genau so weit hinten, wie sein Netz langsam ist. */
gesendet: Date.now(),
});
res.json({ ok: true });
} catch (fehler) {
console.error("[reaktion] Video:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
reaktionRouter.post("/workspace/api/reaktion/layout", nurHost, express.json({ limit: "2kb" }),
(req, res) => {
try {
const l = String(req.body?.layout ?? "");
if (!LAYOUTS.includes(l)) return res.status(400).json({ fehler: "layout_unbekannt" });
db().prepare("UPDATE reaktion SET layout = ? WHERE id = 1").run(l);
melden("reaktion", { was: "layout", layout: l });
res.json({ ok: true, layout: l });
} catch (fehler) {
console.error("[reaktion] Layout:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Der Live-Chat ----------------------------------------------- */
reaktionRouter.get("/workspace/api/reaktion/chat", (req, res) => {
try {
const reihen = db().prepare(`
SELECT id, person_id, name, rolle, text, erstellt
FROM reaktion_chat WHERE weg_am IS NULL ORDER BY id DESC LIMIT 120`).all();
res.json({ beitraege: reihen.reverse() });
} catch (fehler) {
console.error("[reaktion] Chat:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
reaktionRouter.post("/workspace/api/reaktion/chat", express.json({ limit: "8kb" }),
(req, res) => {
try {
const s = sendung();
if (s.stand === "zu") return res.status(409).json({ fehler: "geschlossen" });
const text = String(req.body?.text ?? "").trim().slice(0, TEXT_MAX);
if (!text) return res.status(400).json({ fehler: "leer" });
if (zuSchnell(req.person.id)) return res.status(429).json({ fehler: "zu_schnell" });
const { lastInsertRowid } = db().prepare(`INSERT INTO reaktion_chat
(person_id, name, rolle, text, erstellt) VALUES (?,?,?,?,?)`)
.run(req.person.id, req.person.name, req.person.rolle, text, jetzt());
const beitrag = db().prepare(
"SELECT id, person_id, name, rolle, text, erstellt FROM reaktion_chat WHERE id = ?")
.get(Number(lastInsertRowid));
melden("reaktion_chat", { was: "neu", beitrag });
res.status(201).json(beitrag);
} catch (fehler) {
console.error("[reaktion] Schreiben:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Moderation: einen Beitrag wegnehmen.
*
* WEGGENOMMEN UND NICHT GELOESCHT. Wer moderiert, soll spaeter
* sagen koennen, was er weggenommen hat und warum -- eine geloeschte
* Zeile kann das nicht. */
reaktionRouter.delete("/workspace/api/reaktion/chat/:id", (req, res) => {
try {
const id = Number(req.params.id);
const b = db().prepare("SELECT * FROM reaktion_chat WHERE id = ?").get(id);
if (!b || b.weg_am) return res.status(404).json({ fehler: "nicht_gefunden" });
const eigen = b.person_id === req.person.id;
if (!eigen && !MODERIERT.has(req.person.rolle)) {
return res.status(403).json({ fehler: "nicht_erlaubt" });
}
db().prepare("UPDATE reaktion_chat SET weg_am = ?, weg_von = ? WHERE id = ?")
.run(jetzt(), req.person.id, id);
if (!eigen) {
protokolliere(req.person.id, "reaktion_chat_weg", `Beitrag ${id} von ${b.name}`, echteIp(req));
}
melden("reaktion_chat", { was: "weg", id });
res.json({ ok: true });
} catch (fehler) {
console.error("[reaktion] Wegnehmen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Die Kameras ------------------------------------------------
Nur der Hinweg. Was in `signal` steht, liest dieser Server nicht --
es geht unveraendert an den Empfaenger. Genau so macht es der
Anrufteil seit dem 18.09.
GESCHICKT WIRD NUR AN LEUTE, DIE GERADE DABEI SIND. Ohne das
koennte jemand aus dem Haus Verbindungsdaten an jeden Beliebigen
schicken, und die Reaction waere ein Weg, jemanden anzuklingeln. */
reaktionRouter.post("/workspace/api/reaktion/signal", express.json({ limit: "256kb" }),
(req, res) => {
try {
const an = Number(req.body?.an);
const daten = req.body?.daten;
if (!Number.isInteger(an) || daten === undefined) {
return res.status(400).json({ fehler: "unvollstaendig" });
}
const kreis = new Set(empfaenger());
if (!kreis.has(an) || !kreis.has(req.person.id)) {
return res.status(409).json({ fehler: "nicht_dabei" });
}
const wie_viele = stromAn([an], "reaktion_signal", {
von: req.person.id, name: req.person.name, daten,
});
res.json({ ok: true, zugestellt: wie_viele });
} catch (fehler) {
console.error("[reaktion] Signal:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ======================================================================
HILFEN
====================================================================== */
/** Aus allem, was jemand einfuegen koennte, die YouTube-Kennung.
*
* Angenommen werden die vier Formen, die man wirklich in der
* Zwischenablage hat: die lange Adresse, die kurze, der
* Einbett-Pfad und die nackte Kennung. Alles andere wird
* abgelehnt -- eine falsche Kennung ist ein schwarzes Rechteck vor
* Publikum, und das faellt erst auf, wenn alle zusehen. */
export function videoKennung(roh) {
const t = String(roh || "").trim();
if (!t) return "";
if (/^[A-Za-z0-9_-]{11}$/.test(t)) return t;
const muster = [
/[?&]v=([A-Za-z0-9_-]{11})/,
/youtu\.be\/([A-Za-z0-9_-]{11})/,
/youtube\.com\/embed\/([A-Za-z0-9_-]{11})/,
/youtube\.com\/shorts\/([A-Za-z0-9_-]{11})/,
/youtube\.com\/live\/([A-Za-z0-9_-]{11})/,
];
for (const m of muster) {
const t2 = t.match(m);
if (t2) return t2[1];
}
return "";
}
/** Die Lage ohne die Namensliste -- fuer den Rundruf.
*
* `lage()` gibt einem Host die Namen aller Zusehenden. Genau diese
* Antwort geht per Strom an ALLE. Ohne dieses Sieb bekaeme jeder
* Zuschauer die Liste, die nur die Leitung sehen darf -- und zwar
* ohne dass es jemandem auffaellt, weil die Oberflaeche sie ja
* nicht anzeigt. */
function oeffentlich(l) {
const { zuschauer, ich, ...rest } = l;
return rest;
}
/** Aufraeumen: alter Verlauf und liegengebliebene Zusehende. */
export function reaktionAufraeumen() {
try {
const grenze = new Date(Date.now() - VERLAUF_TAGE * 86400_000).toISOString();
const a = db().prepare("DELETE FROM reaktion_verlauf WHERE beendet < ?").run(grenze);
const b = db().prepare("DELETE FROM reaktion_dabei WHERE gesehen < ?")
.run(new Date(Date.now() - 600_000).toISOString());
if (a.changes || b.changes) {
console.log(`[reaktion] Aufgeraeumt: ${a.changes} Verlauf, ${b.changes} Zusehende.`);
}
} catch (fehler) {
console.error("[reaktion] Aufraeumen:", fehler?.message);
}
}
+21
View File
@@ -35,6 +35,7 @@ import { treffTabellen, treffSperreLesen, MINDESTALTER } from "./treff-tabellen.
import { hilfeTabellen } from "./hilfe-tabellen.js";
import { supportTabellen } from "./support-tabellen.js";
import { notizTabellen } from "./notiz-tabellen.js";
import { reaktionTabellen } from "./reaktion-tabellen.js";
import { unterstuetzungTabellen } from "./unterstuetzung-tabellen.js";
import { sitzungPasstZurAdresse, istOhneModiAdresse, istCrewAdresse, istPruefAdresse, AUSSEN_ROLLEN,
TEAM_DOGI_ROLLEN, CREW_ADRESSE, WORKSPACE_ADRESSE, AGENTUR_ROLLEN, BEIDE_HAEUSER_ROLLEN,
@@ -1510,6 +1511,25 @@ const TREFF_BEREICHE = [
`?raum=treff` öffnet den Raum der Community direkt. Für die
Community selbst ändert sich am Namen nichts Wesentliches -- sie
hat nur diese eine. */
/* ==== DIE REACTION STEHT GANZ VORNE ============================
(28.09.2026, nach Filipes Kurznotiz.)
Sie ist die einzige Kachel im Haus, die einen ZUSTAND hat --
zu, gleich, jetzt. Was gerade laeuft, gehoert an die erste
Stelle; was immer da ist, kann warten. Steht sie geschlossen
da, sagt sie das und nimmt trotzdem den Platz: Eine Kachel,
die je nach Lage woanders steht, ist keine Kachel, die man
wiederfindet.
KEINE TON-NUMMER. Der Farbraum ist bei 46 Toenen voll (siehe
tools/kachelton-regeln.mjs), und eine 47. Farbe wuerde den
Mindestabstand reissen. Sie braucht auch keine: Ihre Farbe IST
ihr Zustand -- grau geschlossen, bernstein in Vorbereitung,
rot auf Sendung. `live: true` sagt der Wand, dass diese Kachel
ihre Flaeche selbst mitbringt, genau wie `papier` beim
Notizblock und `silber` bei den Aufgaben. */
{ ...TREFF_GRUPPE, name: "Reaction", unter: "Zusammen schauen \u2013 live mit Kamera und Chat",
zeichen: "reaktion", live: true, ziel: "reaktion.html", szene: "arena" },
{ ...TREFF_GRUPPE, name: "Rudel-Chat", unter: "Miteinander reden – nachts schläft er",
zeichen: "chat", ton: 41, ziel: "chat.html?raum=treff", szene: "lounge" },
/* HIGHLIGHTS AN DIE SPITZE (22.09.2026).
@@ -6037,6 +6057,7 @@ export function db() {
/* Der Notizblock (26.09.2026). Haengt an personen (CASCADE) und
kommt deshalb hierher, nicht in den Bauplan darueber. */
notizTabellen(d);
reaktionTabellen(d);
/* Die Wege zum Unterstützen (24.09.2026). Sie hängen an personen
(geaendert_von) und kommen deshalb hierher, nicht in den Bauplan
darüber. */
+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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<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=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/anruf-probe.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
</body>
</html>
+4 -4
View File
@@ -23,25 +23,25 @@
},
"icons": [
{
"src": "/assets/img/app-symbole/workspace-192.png?v=202609271833",
"src": "/assets/img/app-symbole/workspace-192.png?v=202609280151",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-512.png?v=202609271833",
"src": "/assets/img/app-symbole/workspace-512.png?v=202609280151",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609271833",
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609280151",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609271833",
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609280151",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+795
View File
@@ -0,0 +1,795 @@
/* =====================================================================
DIE REACTION (28.09.2026)
Diese Seite ist absichtlich anders gebaut als jede andere im Haus.
Überall sonst gilt: Kacheln, Karten, Listen -- man liest etwas,
entscheidet und geht wieder. Hier sitzt man. Eine Stunde lang, mit
anderen zusammen, und schaut auf EINE Sache.
Daraus folgen drei Entscheidungen, die sonst nirgends gelten:
1. DIE SEITE SCROLLT NICHT. Sie füllt genau den Bildschirm. Ein
Video, das beim Tippen im Chat nach oben rutscht, ist der
schnellste Weg, dass jemand aufhört zu schreiben.
2. DIE FLÄCHE IST DUNKLER ALS ÜBERALL SONST. Der Rest des Hauses
steht auf #07060f mit Licht in den Ecken; hier ist es fast
schwarz. Nicht als Stil, sondern weil daneben ein Video läuft:
Alles, was selbst leuchtet, zieht den Blick vom Bild weg.
3. DIE FARBE IST ROT -- und zwar nur hier und nur beim Senden.
Das Haus ist augenschonend gebaut, und Rot ist das Gegenteil
davon. Es ist aber kein Dauerzustand: ein bis zwei Stunden im
Monat, und in dieser Zeit SOLL es rufen. Gedeckt (#d8404a statt
reinem Rot), und was pulst, pulst in vier Sekunden -- das ist
Atmen und kein Blinken.
---------------------------------------------------------------------
DREI HÄUTE, EINE SEITE
`data-stand` am `.saal` schaltet um: zu / vorbereitung / live.
Alle drei stehen im Dokument. Beim Wechsel von „gleich" auf „live"
wird deshalb nichts neu geladen -- weder der Chat, in dem jemand
mitten im Satz ist, noch die Verbindung zur Kamera.
===================================================================== */
.reaktion-seite {
--saal: #050408;
--saal-hoch: #0b0a12;
--rand: rgba(150, 160, 190, .14);
--rand-hell: rgba(170, 180, 210, .26);
--schrift: #e9e6f2;
--schrift-leise: #a9a3bd;
--an: #d8404a;
--an-hell: #ff8b92;
--gleich: #e0a13a;
--zu: #8b93a4;
background: var(--saal);
/* Kein Scrollen: Der Saal ist so hoch wie das Fenster. `dvh` und
nicht `vh` -- auf dem Handy wandert die Adressleiste, und `vh`
rechnet mit dem Zustand von vorhin. */
min-height: 100dvh;
overflow: hidden;
}
.saal {
position: relative;
display: grid;
/* Die Kopfleiste ist 69 px hoch (gemessen, nicht geschätzt -- siehe
pruef-handy, das sie über alle Breiten abmisst). */
height: calc(100dvh - 69px);
padding: 0;
max-width: none;
}
/* ---------------------------------------------------------------------
DAS SCHILD
Oben mittig, in allen drei Zuständen. Es beantwortet die einzige
Frage, die jeder beim Öffnen hat: läuft schon was?
--------------------------------------------------------------------- */
.buehne-schild {
position: absolute;
top: 14px; left: 50%;
transform: translateX(-50%);
z-index: 40;
display: flex; align-items: center; gap: 9px;
padding: 7px 15px 8px;
border-radius: 999px;
background: rgba(8, 7, 14, .82);
border: 1px solid var(--rand);
backdrop-filter: blur(14px) saturate(1.2);
-webkit-backdrop-filter: blur(14px) saturate(1.2);
box-shadow: 0 16px 38px -18px #000;
font-size: .72rem; font-weight: 800;
letter-spacing: .14em; text-transform: uppercase;
color: var(--schrift-leise);
}
.buehne-schild__punkt {
width: 8px; height: 8px; border-radius: 50%;
background: var(--zu);
flex: none;
}
.buehne-schild__zahl {
padding-left: 9px;
border-left: 1px solid var(--rand);
color: var(--schrift);
letter-spacing: .06em;
}
.saal[data-stand="vorbereitung"] .buehne-schild {
border-color: color-mix(in srgb, var(--gleich) 40%, transparent);
color: color-mix(in srgb, var(--gleich) 30%, #ffffff);
}
.saal[data-stand="vorbereitung"] .buehne-schild__punkt { background: var(--gleich); }
.saal[data-stand="live"] .buehne-schild {
border-color: color-mix(in srgb, var(--an-hell) 50%, transparent);
color: #ffe9ea;
box-shadow: 0 0 30px -10px var(--an), 0 16px 38px -18px #000;
}
.saal[data-stand="live"] .buehne-schild__punkt {
background: var(--an);
box-shadow: 0 0 12px 1px var(--an);
animation: reakt-atem 4s ease-in-out infinite;
}
@keyframes reakt-atem {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: .5; transform: scale(.82); }
}
/* ---------------------------------------------------------------------
HÄUTE EIN- UND AUSBLENDEN
--------------------------------------------------------------------- */
#teil-zu, #teil-warte, #teil-live { display: none; }
.saal[data-stand="zu"] #teil-zu { display: grid; }
.saal[data-stand="vorbereitung"] #teil-warte { display: grid; }
.saal[data-stand="live"] #teil-live { display: grid; }
/* =====================================================================
ZU — der Vorhang
===================================================================== */
.vorhang {
position: relative;
place-items: center;
padding: 0 28px;
text-align: center;
}
.vorhang__licht {
position: absolute; inset: 0;
background:
radial-gradient(90% 55% at 50% 0%, rgba(120, 130, 165, .12), transparent 62%),
radial-gradient(120% 70% at 50% 104%, rgba(70, 80, 115, .10), transparent 60%);
pointer-events: none;
}
.vorhang__innen { position: relative; max-width: 30rem; }
.vorhang__marke {
display: flex; align-items: center; justify-content: center; gap: 10px;
font-size: .74rem; font-weight: 700;
letter-spacing: .38em; text-transform: uppercase;
color: var(--schrift-leise);
}
.vorhang__marke i {
display: block; width: 26px; height: 1px;
background: linear-gradient(90deg, transparent, var(--schrift-leise));
}
.vorhang__marke i + i { background: linear-gradient(90deg, var(--schrift-leise), transparent); }
.vorhang__titel {
margin: 16px 0 0;
font-size: clamp(1.9rem, 7vw, 2.7rem);
font-weight: 800; line-height: 1.08;
letter-spacing: -.024em;
color: var(--schrift);
text-wrap: balance;
}
.vorhang__satz {
margin: 14px 0 0;
font-size: .96rem; line-height: 1.6;
color: var(--schrift-leise);
text-wrap: pretty;
}
.vorhang__naechste {
margin: 22px 0 0;
display: inline-block;
padding: 9px 16px;
border-radius: 12px;
background: color-mix(in srgb, var(--gleich) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--gleich) 30%, transparent);
color: color-mix(in srgb, var(--gleich) 24%, #ffffff);
font-size: .86rem; font-weight: 700;
}
/* =====================================================================
VORBEREITUNG — der Wartebereich
===================================================================== */
.warte { place-items: center; padding: 0 20px; }
.warte__buehne {
display: grid; gap: 26px;
width: min(54rem, 100%);
grid-template-columns: 1fr;
text-align: center;
}
.warte__bild {
aspect-ratio: 16 / 9;
border-radius: 20px;
background: linear-gradient(160deg, #131120, #08070e);
border: 1px solid var(--rand);
background-size: cover; background-position: center;
box-shadow: 0 40px 80px -46px #000;
}
.warte__kicker {
font-size: .74rem; font-weight: 800;
letter-spacing: .34em; text-transform: uppercase;
color: color-mix(in srgb, var(--gleich) 26%, #ffffff);
}
.warte__titel {
margin: 12px 0 0;
font-size: clamp(1.6rem, 6vw, 2.3rem);
font-weight: 800; line-height: 1.1;
letter-spacing: -.022em; color: var(--schrift);
text-wrap: balance;
}
.warte__satz { margin: 14px 0 0; color: var(--schrift-leise); font-size: .92rem; }
.countdown {
display: inline-flex; align-items: baseline; gap: 10px;
margin: 18px 0 0; padding: 10px 20px 12px;
border-radius: 16px;
background: color-mix(in srgb, var(--gleich) 10%, rgba(8, 7, 14, .7));
border: 1px solid color-mix(in srgb, var(--gleich) 32%, transparent);
}
.countdown__zahl {
font-size: 2rem; font-weight: 800;
letter-spacing: -.03em;
color: color-mix(in srgb, var(--gleich) 22%, #ffffff);
/* Tabellenziffern: Ohne sie springt die ganze Zeile bei jedem
Sekundenwechsel, weil die 1 schmaler ist als die 8. */
font-variant-numeric: tabular-nums;
}
.countdown__wort { font-size: .78rem; color: var(--schrift-leise); letter-spacing: .04em; }
/* =====================================================================
LIVE — der Kinosaal
===================================================================== */
.kino {
grid-template-columns: 1fr minmax(19rem, 23rem);
gap: 0;
height: 100%;
min-height: 0; /* sonst wächst das Raster über das Fenster */
}
.leinwand {
position: relative;
min-width: 0; min-height: 0;
display: grid; place-items: center;
background: #000;
overflow: hidden;
}
.leinwand__video { position: relative; width: 100%; height: 100%; }
.leinwand__video iframe {
position: absolute; inset: 0;
width: 100%; height: 100%;
border: 0;
}
.leinwand__warte {
position: absolute; inset: 0;
display: grid; place-items: center;
color: var(--schrift-leise); font-size: .9rem;
}
/* ---- Die Kamerafenster ----------------------------------------------
Sie liegen ÜBER dem Video. Genau so sieht eine Reaction aus: das
Bild groß, das Gesicht klein davor.
`pointer-events: none` am Behälter und `auto` an den Fenstern: Der
Bereich dazwischen gehört dem Video (Pause, Lautstärke), die
Fenster selbst nehmen Klicks an. Ohne das läge eine unsichtbare
Scheibe über der ganzen Leinwand.
--------------------------------------------------------------------- */
.kameras {
position: absolute;
right: 16px; bottom: 16px;
display: flex; flex-direction: column; gap: 12px;
pointer-events: none;
z-index: 20;
}
.kamera {
position: relative;
width: clamp(120px, 17vw, 208px);
aspect-ratio: 4 / 3;
border-radius: 16px;
overflow: hidden;
background: #0b0a12;
border: 1px solid var(--rand-hell);
box-shadow: 0 24px 50px -22px #000;
pointer-events: auto;
}
.kamera video {
width: 100%; height: 100%;
object-fit: cover;
display: block;
background: #0b0a12;
}
.kamera__name {
position: absolute; left: 8px; bottom: 8px;
padding: 3px 9px 4px;
border-radius: 999px;
font-size: .72rem; font-weight: 700;
background: rgba(6, 5, 11, .76);
border: 1px solid var(--rand);
color: var(--schrift);
backdrop-filter: blur(6px);
max-width: calc(100% - 16px);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kamera[data-host="ja"] { border-color: color-mix(in srgb, var(--an-hell) 46%, transparent); }
.kamera[data-stumm="ja"]::after {
content: "stumm";
position: absolute; right: 8px; top: 8px;
padding: 3px 8px 4px; border-radius: 999px;
font-size: .72rem; font-weight: 800; letter-spacing: .08em;
background: rgba(216, 64, 74, .82); color: #fff;
}
.kamera--leer {
display: grid; place-items: center;
font-size: .72rem; color: var(--schrift-leise);
border-style: dashed;
}
/* Anordnung „gleich": Video und Kameras nebeneinander statt übereinander. */
.kino[data-layout="gleich"] .kameras {
position: static;
flex-direction: row;
justify-content: center;
padding: 10px;
background: #07060d;
}
.kino[data-layout="gleich"] .leinwand {
grid-template-rows: 1fr auto;
align-content: stretch;
}
/* Anordnung „kamera": die Kameras groß, das Video klein in der Ecke. */
.kino[data-layout="kamera"] .kamera { width: clamp(160px, 30vw, 380px); }
.kino[data-layout="kamera"] .kameras {
right: auto; left: 50%; top: 50%; bottom: auto;
transform: translate(-50%, -50%);
flex-direction: row;
}
.kino[data-layout="kamera"] .leinwand__video {
position: absolute; right: 14px; bottom: 14px;
width: clamp(140px, 22vw, 260px); height: auto; aspect-ratio: 16/9;
border-radius: 14px; overflow: hidden;
border: 1px solid var(--rand-hell);
z-index: 25;
}
/* ---- Die Chatschiene ------------------------------------------------- */
.schiene {
display: grid;
grid-template-rows: auto 1fr auto auto;
min-height: 0;
background: #07060d;
border-left: 1px solid var(--rand);
}
.schiene__kopf {
display: flex; align-items: center; gap: 10px;
padding: 14px 16px 12px;
border-bottom: 1px solid var(--rand);
}
.schiene__titel {
margin: 0; font-size: .74rem; font-weight: 800;
letter-spacing: .16em; text-transform: uppercase;
color: var(--schrift-leise);
}
.schiene__zahl {
margin-left: auto;
font-size: .72rem; font-weight: 700;
padding: 2px 9px 3px; border-radius: 999px;
background: rgba(255, 255, 255, .06);
border: 1px solid var(--rand);
color: var(--schrift-leise);
font-variant-numeric: tabular-nums;
}
.schiene__zu { display: none; }
.schiene__liste {
margin: 0; padding: 12px 14px;
list-style: none;
overflow-y: auto;
min-height: 0;
display: flex; flex-direction: column; gap: 9px;
scrollbar-width: thin;
}
.beitrag {
display: grid; gap: 2px;
padding: 8px 11px 9px;
border-radius: 12px;
background: rgba(255, 255, 255, .035);
border: 1px solid transparent;
font-size: .86rem; line-height: 1.42;
color: var(--schrift);
overflow-wrap: anywhere;
}
.beitrag__wer {
font-size: .72rem; font-weight: 800;
letter-spacing: .02em;
color: var(--schrift-leise);
display: flex; align-items: center; gap: 7px;
}
.beitrag__weg {
margin-left: auto;
appearance: none; border: 0; background: none;
color: var(--schrift-leise);
cursor: pointer; padding: 2px 4px; border-radius: 6px;
font-size: .72rem;
}
.beitrag__weg:hover { color: var(--an-hell); background: rgba(216, 64, 74, .12); }
.beitrag[data-host="ja"] {
background: color-mix(in srgb, var(--an) 12%, transparent);
border-color: color-mix(in srgb, var(--an) 30%, transparent);
}
.beitrag[data-host="ja"] .beitrag__wer { color: var(--an-hell); }
.beitrag[data-team="ja"] .beitrag__wer { color: #9ab7ff; }
.schiene__form {
display: flex; gap: 8px;
padding: 12px 14px;
border-top: 1px solid var(--rand);
}
.schiene__feld {
flex: 1 1 auto; min-width: 0;
appearance: none;
padding: 11px 13px;
border-radius: 12px;
border: 1px solid var(--rand);
background: rgba(255, 255, 255, .04);
color: var(--schrift);
font: inherit; font-size: .9rem;
}
.schiene__feld:focus-visible {
outline: none;
border-color: var(--rand-hell);
box-shadow: 0 0 0 3px rgba(154, 183, 255, .16);
}
.schiene__senden {
flex: none;
width: 44px; height: 44px;
display: grid; place-items: center;
border-radius: 12px;
border: 1px solid color-mix(in srgb, var(--an) 42%, transparent);
background: color-mix(in srgb, var(--an) 24%, transparent);
color: #ffd9db; cursor: pointer;
}
.schiene__senden svg { width: 19px; height: 19px; fill: currentColor; }
.danke {
display: flex; align-items: center; justify-content: center; gap: 9px;
margin: 0 14px 14px;
padding: 12px 14px;
min-height: 44px;
border-radius: 12px;
text-decoration: none;
font-size: .86rem; font-weight: 700;
color: #0a2a1c;
background: linear-gradient(180deg, #7de2a8, #47c97f);
box-shadow: 0 14px 30px -16px #47c97f;
}
.danke svg { width: 18px; height: 18px; fill: currentColor; flex: none; }
/* =====================================================================
DAS REGIEPULT
===================================================================== */
/* ==== DAS PULT SCHIEBT, ES DECKT NICHT ZU =========================
(28.09.2026, gefunden im ersten Bild.)
Erst lag es als `position: fixed` ueber der Seite. Aufgeklappt
verdeckte es die untere Haelfte: Das Video war abgeschnitten, der
Chat auch -- ausgerechnet fuer den, der sendet und beides im Blick
haben muss.
Jetzt ist es die zweite Zeile des Saals. Der Saal ist ein Raster
mit `1fr auto`; klappt das Pult auf, wird der Kinosaal darueber
von selbst kleiner. Nichts wird verdeckt, und niemand muss eine
Hoehe ausrechnen, die spaeter nicht mehr stimmt. */
.saal { grid-template-rows: 1fr auto; }
.pult {
z-index: 60;
background: rgba(7, 6, 13, .94);
backdrop-filter: blur(18px) saturate(1.2);
-webkit-backdrop-filter: blur(18px) saturate(1.2);
border-top: 1px solid var(--rand-hell);
box-shadow: 0 -30px 70px -40px #000;
}
.pult__griff {
width: 100%;
appearance: none; border: 0; background: none;
display: flex; align-items: center; justify-content: center; gap: 10px;
padding: 9px 0 8px;
min-height: 44px;
cursor: pointer;
color: var(--schrift-leise);
font-size: .74rem; font-weight: 800;
letter-spacing: .2em; text-transform: uppercase;
}
.pult__griff-strich {
width: 34px; height: 3px; border-radius: 3px;
background: var(--rand-hell);
}
.pult__innen {
display: none;
gap: 18px;
padding: 4px 18px 18px;
/* Hoechstens die halbe Hoehe -- darueber bleibt vom Saal nichts
mehr uebrig, und wer sendet, schaut auf das Bild und nicht auf
die Knoepfe. Was nicht hineinpasst, rollt. */
max-height: min(46dvh, 26rem);
overflow-y: auto;
}
.pult[data-auf="ja"] .pult__innen { display: grid; }
.pult__block {
display: grid; gap: 10px;
padding: 14px 15px 15px;
border-radius: 14px;
background: rgba(255, 255, 255, .03);
border: 1px solid var(--rand);
}
.pult__ueber {
margin: 0;
font-size: .74rem; font-weight: 800;
letter-spacing: .2em; text-transform: uppercase;
color: var(--schrift-leise);
display: flex; align-items: baseline; gap: 10px;
}
.pult__klein { font-size: .72rem; letter-spacing: .04em; text-transform: none; opacity: .8; }
.pult__zeile { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
.pult__feld { display: grid; gap: 5px; flex: 1 1 14rem; min-width: 0; }
.pult__feld--kurz { flex: 0 1 12rem; }
.pult__feld > span {
font-size: .72rem; font-weight: 700;
color: var(--schrift-leise); letter-spacing: .04em;
}
.pult__feld input, .pult__wahl {
appearance: none;
padding: 10px 12px; min-height: 44px;
border-radius: 11px;
border: 1px solid var(--rand);
background: rgba(255, 255, 255, .04);
color: var(--schrift); font: inherit; font-size: .88rem;
min-width: 0;
}
.pult__wahl { flex: 1 1 12rem; }
/* ---- Die Regler ------------------------------------------------------
Vier Tonquellen nebeneinander: Host, Gast 1, Gast 2, YouTube. Sie
stehen SENKRECHT wie an einem Mischpult und nicht waagerecht wie
ein Formularregler -- vier waagerechte Regler untereinander liest
man als Liste, vier senkrechte nebeneinander sieht man auf einen
Blick.
--------------------------------------------------------------------- */
.regler-reihe { display: flex; gap: 12px; flex-wrap: wrap; }
.regler {
display: grid; justify-items: center; gap: 8px;
padding: 12px 10px 10px;
border-radius: 12px;
background: rgba(0, 0, 0, .3);
border: 1px solid var(--rand);
min-width: 5.4rem;
}
.regler__wort {
font-size: .74rem; font-weight: 800;
letter-spacing: .08em; text-transform: uppercase;
color: var(--schrift-leise);
max-width: 5rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.regler input[type="range"] {
appearance: none; -webkit-appearance: none;
writing-mode: vertical-lr;
direction: rtl;
width: 26px; height: 104px;
background: none;
}
.regler input[type="range"]::-webkit-slider-runnable-track {
width: 6px; border-radius: 6px;
background: linear-gradient(180deg, var(--an), #3a3550);
}
.regler input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px; height: 14px; border-radius: 4px;
background: #e9e6f2;
border: 1px solid #0b0a12;
box-shadow: 0 2px 8px -2px #000;
margin-left: -8px;
}
.regler input[type="range"]::-moz-range-track {
width: 6px; border-radius: 6px;
background: linear-gradient(180deg, var(--an), #3a3550);
}
.regler input[type="range"]::-moz-range-thumb {
width: 22px; height: 14px; border-radius: 4px;
background: #e9e6f2; border: 1px solid #0b0a12;
}
.regler__zahl {
font-size: .72rem; font-variant-numeric: tabular-nums;
color: var(--schrift-leise);
}
.regler__stumm {
appearance: none; cursor: pointer;
min-height: 30px; padding: 4px 10px;
border-radius: 8px;
border: 1px solid var(--rand);
background: rgba(255, 255, 255, .04);
color: var(--schrift-leise);
font-size: .72rem; font-weight: 700;
}
.regler[data-stumm="ja"] .regler__stumm {
background: color-mix(in srgb, var(--an) 30%, transparent);
border-color: color-mix(in srgb, var(--an) 50%, transparent);
color: #ffd9db;
}
/* ---- Gäste ----------------------------------------------------------- */
.gast-reihe { display: flex; flex-wrap: wrap; gap: 9px; }
.gast-karte {
display: flex; align-items: center; gap: 9px;
padding: 8px 10px 8px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, .04);
border: 1px solid var(--rand);
font-size: .82rem; color: var(--schrift);
}
.gast-karte__platz {
font-size: .72rem; font-weight: 800;
color: var(--schrift-leise); letter-spacing: .08em;
}
.gast-karte button {
appearance: none; cursor: pointer;
min-width: 34px; min-height: 34px;
border-radius: 999px;
border: 1px solid var(--rand);
background: rgba(255, 255, 255, .05);
color: var(--schrift-leise);
font-size: .72rem; font-weight: 700;
}
.gast-leer { font-size: .8rem; color: var(--schrift-leise); }
/* ---- Anordnung ------------------------------------------------------- */
.layout-reihe { display: flex; gap: 9px; flex-wrap: wrap; }
.layout-knopf {
appearance: none; cursor: pointer;
min-height: 44px; padding: 9px 15px;
border-radius: 11px;
border: 1px solid var(--rand);
background: rgba(255, 255, 255, .04);
color: var(--schrift-leise);
font-size: .82rem; font-weight: 700;
}
.layout-knopf[aria-pressed="true"] {
background: rgba(154, 183, 255, .14);
border-color: rgba(154, 183, 255, .38);
color: #cfe0ff;
}
/* ---- Die drei großen Knöpfe ------------------------------------------ */
.pult__block--tun {
display: flex; flex-wrap: wrap; gap: 10px;
background: none; border: 0; padding: 0;
}
.tun {
appearance: none; cursor: pointer;
flex: 1 1 10rem;
min-height: 52px;
border-radius: 14px;
border: 1px solid var(--rand-hell);
background: rgba(255, 255, 255, .05);
color: var(--schrift);
font-size: .94rem; font-weight: 800;
letter-spacing: .01em;
}
.tun--vor {
border-color: color-mix(in srgb, var(--gleich) 44%, transparent);
background: color-mix(in srgb, var(--gleich) 14%, transparent);
color: color-mix(in srgb, var(--gleich) 24%, #ffffff);
}
.tun--live {
border-color: color-mix(in srgb, var(--an) 56%, transparent);
background: linear-gradient(180deg, color-mix(in srgb, var(--an) 74%, #000), color-mix(in srgb, var(--an) 46%, #000));
color: #fff3f3;
box-shadow: 0 16px 34px -20px var(--an);
}
.tun--aus { color: var(--schrift-leise); }
.tun[disabled] { opacity: .4; cursor: default; }
/* =====================================================================
DAS HANDY
Aus zwei Spalten wird ein Stapel, und der Chat wird zur Lade. Das
Video bleibt oben und behält sein Seitenverhältnis -- es ist der
Grund, warum jemand hier ist.
===================================================================== */
@media (max-width: 860px) {
.kino {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.leinwand {
aspect-ratio: 16 / 9;
height: auto;
width: 100%;
}
.schiene {
border-left: 0;
border-top: 1px solid var(--rand);
min-height: 0;
}
.kameras {
right: 10px; bottom: 10px;
flex-direction: row;
}
.kamera { width: clamp(88px, 26vw, 140px); }
/* ==== KEINE LADE, SONDERN EIN STAPEL =========================
(28.09.2026, nach der ersten Messung auf dem Handy.)
Hier stand eine Lade: Der Chat lag zugeklappt über allem und
fuhr auf Knopfdruck von unten ein. Gemessen war das Ergebnis
ein 390x775 großer Saal mit einem 219 Pixel hohen Video und
556 Pixeln Leere darunter -- weil der Knopf, der die Lade
öffnet, gar nicht erschien.
Statt den Knopf zu reparieren, ist die Lade weg. Sie war die
falsche Lösung für ein Problem, das es nicht gibt: Auf einem
844 Pixel hohen Telefon bleiben unter Kopfleiste und Video
rund 550 Pixel übrig. Das ist mehr Chat, als eine Lade je
zeigen würde -- und es gibt keinen Zustand, in dem etwas
klemmen kann.
Ein Zustand weniger ist besser als ein Zustand, der
funktioniert. */
.schiene { display: grid; }
.schiene__zu { display: none !important; }
/* Das Schild wird kleiner und rueckt hoch: Auf 390 Pixeln teilt es
sich den Platz mit der Titelzeile von YouTube, und zwei Zeilen
Grossbuchstaben sind dort ein Klotz. */
.buehne-schild {
top: 8px;
padding: 5px 11px 6px;
font-size: .64rem;
gap: 7px;
white-space: nowrap;
}
.buehne-schild__zahl { padding-left: 7px; }
/* Der Ton-Knopf in die MITTE des Bildes. Unten liegen bei 219
Pixeln Hoehe schon die Kamerafenster -- der Knopf lag halb
darueber. In der Mitte steht er dort, wo ohnehin hingesehen
wird, und verdeckt nur das, was man gleich sowieso hoert. */
.ton-an {
bottom: auto;
top: 50%;
transform: translate(-50%, -50%);
}
.warte__buehne { gap: 18px; }
.pult__innen { max-height: 62dvh; }
}
/* Wer weniger Bewegung eingestellt hat, bekommt keinen Puls. Die
Aussage bleibt -- der Punkt steht dann einfach da. */
@media (prefers-reduced-motion: reduce) {
.buehne-schild__punkt { animation: none !important; }
}
/* ---- Der Ton-Knopf ---------------------------------------------------
Er liegt über der Leinwand, mittig unten -- dort, wo der Blick
ohnehin ist, und weit genug von den Kamerafenstern entfernt. Er
verschwindet in dem Moment, in dem der Ton läuft; ein Knopf, der
bleibt, nachdem er getan hat, was er soll, wird zur Deko. */
.ton-an {
position: absolute;
left: 50%; bottom: 20px;
transform: translateX(-50%);
z-index: 30;
display: flex; align-items: center; gap: 9px;
appearance: none; cursor: pointer;
min-height: 44px; padding: 10px 18px;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--an-hell) 50%, transparent);
background: rgba(10, 8, 16, .88);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #ffe9ea;
font-size: .88rem; font-weight: 700;
box-shadow: 0 18px 40px -18px #000, 0 0 26px -10px var(--an);
}
.ton-an svg {
width: 19px; height: 19px;
fill: none; stroke: currentColor; stroke-width: 1.8;
stroke-linecap: round; stroke-linejoin: round;
}
+78
View File
@@ -9214,3 +9214,81 @@ body[data-ton] .kopfleiste__rechts > *:hover {
@media (prefers-reduced-motion: reduce) {
.zmarke, .ztitel { transition: none; }
}
/* =====================================================================
DIE REACTION-KACHEL TRAEGT IHREN ZUSTAND (28.09.2026)
SIE HEISST `data-sendung` UND NICHT `data-live`. Das Merkmal
`data-live` ist seit dem 21.09. vergeben: Es sitzt auf der
Draussen-Kachel, sobald Filipe auf Twitch sendet, und start.js
setzt und loescht es dort selbst. Haette die Reaction denselben
Namen genommen, waere der Draussen-Kachel waehrend jedes Streams
die Farbe abhanden gekommen -- und zwar genau dann, wenn sie
wichtig ist. Gefunden beim Lesen, nicht im Betrieb.
Sie ist die einzige Kachel im Haus, die sich von selbst aendert --
und die einzige ohne Ton-Nummer. Der Farbraum ist bei 46 Toenen
voll; eine 47. Farbe haette den Mindestabstand gerissen. Sie
braucht auch keine: Ihre Farbe IST ihr Zustand.
zu ruhiges Grau, unaufdringlich
vorbereitung Bernstein, ein Ring pulst langsam
live Rot, ein Punkt pulst
WARUM ROT HIER ERLAUBT IST, obwohl das Haus augenschonend gebaut
ist: Es ist kein Dauerzustand. Eine Sendung dauert ein bis zwei
Stunden im Monat, und in dieser Zeit SOLL die Kachel rufen -- sie
ist der Grund, warum jemand die Seite geoeffnet hat. Die
Leuchtkraft ist trotzdem gedeckt (#d8404a statt reinem Rot), und
der Puls laeuft in vier Sekunden statt im Sekundentakt: Er ist
Atmen und kein Blinken.
Wer weniger Bewegung eingestellt hat, bekommt den Puls nicht --
dann steht der Punkt einfach da. Die Aussage bleibt, die Unruhe
geht.
===================================================================== */
.kachel[data-sendung="ja"] {
--ton: #8b93a4;
}
.kachel[data-sendung="ja"][data-stand="vorbereitung"] { --ton: #e0a13a; }
.kachel[data-sendung="ja"][data-stand="live"] { --ton: #d8404a; }
/* Das Schild oben rechts. Es steht IN der Kachel und nicht daneben:
Eine Marke, die neben der Kachel schwebt, verrutscht auf jedem
zweiten Geraet. */
.kachel[data-sendung="ja"] .kachel__link::after {
content: "geschlossen";
position: absolute;
top: 12px; right: 12px;
padding: 3px 9px 4px;
border-radius: 999px;
font-size: .62rem;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
color: color-mix(in srgb, var(--ton) 26%, #ffffff);
background: color-mix(in srgb, var(--ton) 16%, rgba(6, 8, 14, .74));
border: 1px solid color-mix(in srgb, var(--ton) 38%, transparent);
backdrop-filter: blur(6px);
}
.kachel[data-sendung="ja"][data-stand="vorbereitung"] .kachel__link::after { content: "gleich"; }
.kachel[data-sendung="ja"][data-stand="live"] .kachel__link::after {
content: "● live";
color: #ffe9ea;
background: color-mix(in srgb, #d8404a 62%, rgba(6, 8, 14, .6));
border-color: color-mix(in srgb, #ff7a80 60%, transparent);
box-shadow: 0 0 18px -4px #d8404a;
animation: reaktion-puls 4s ease-in-out infinite;
}
.kachel[data-sendung="ja"][data-stand="vorbereitung"] .kachel__link::after {
animation: reaktion-puls 6s ease-in-out infinite;
}
@keyframes reaktion-puls {
0%, 100% { opacity: 1; }
50% { opacity: .58; }
}
@media (prefers-reduced-motion: reduce) {
.kachel[data-sendung="ja"] .kachel__link::after { animation: none; }
}
+17
View File
@@ -361,6 +361,23 @@
Handschrift, die mitten im Wort aufhoert. Das ist der
Unterschied zwischen „Dokument" und „Notiz", und er kostet
vier Bildpunkte. */
/* DIE REACTION (28.09.2026).
Ein Bildschirm mit einem Abspielzeichen darin, und darueber
zwei Wellen, die hinausgehen. Nicht die Kamera und nicht der
Fernseher allein: Beides zusammen ist die Aussage -- etwas
wird gezeigt UND es geht gerade hinaus.
Das Abspielzeichen ist FLAECHE, der Rest Linie. Auf 24 Pixeln
ist es der einzige Teil, der noch als Form erkennbar bleibt,
und es ist auch der Teil, der sagt, worum es geht. */
reaktion: {
fuellung: ['M10.3 10.1 14.4 12.5 10.3 14.9z'],
linien: ['M4 7.6h16v9.8H4z',
'M9 20.4h6',
'M12 17.4v3',
'M18.4 4.6a4.6 4.6 0 0 1 2.4 2.6', 'M5.6 4.6a4.6 4.6 0 0 0-2.4 2.6'],
},
notizen: {
fuellung: ['M5 3.6h14v2.9H5z'],
linien: ['M5 3.6h14v16.8H5z', 'M8.2 10.4h7.6', 'M8.2 13.8h7.6', 'M8.2 17.2h4.4'],
+33
View File
@@ -179,6 +179,39 @@
'Aus diesem Katalog bekommt diese Person keine Aufgaben. '
+ 'Wähl jemand anderen – oder leg ihr die Aufgabe von Hand an.',
/* ---- Die Reaction (28.09.2026) --------------------------------
Jede Kennung, die der Server schicken kann, bekommt hier einen
Satz. Ein Maschinenwort auf dem Bildschirm ist kein Fehler des
Servers -- es ist einer der Oberflaeche, und pruef-meldungen
zaehlt sie mit. */
nur_host:
'Das darf nur DogFather – er führt die Sendung.',
video_unklar:
'Aus dieser Adresse lässt sich kein YouTube-Video lesen. '
+ 'Füg die Adresse aus der Adresszeile ein oder die Kennung.',
beginn_vorbei:
'Der Beginn liegt in der Vergangenheit. Wähl einen Zeitpunkt, '
+ 'der noch kommt – sonst zählt der Countdown ins Minus.',
stand_unbekannt:
'Diesen Zustand gibt es nicht. Möglich sind: zu, Vorbereitung, auf Sendung.',
kein_video:
'Ohne Video keine Sendung. Trag erst ein YouTube-Video ein.',
geschlossen:
'Der Saal ist zu. Sobald DogFather aufmacht, geht es hier weiter.',
person_unklar:
'Diese Person kenne ich nicht. Wähl jemanden aus der Liste.',
host_ist_kein_gast:
'Du führst die Sendung – du bist schon im Bild.',
gaeste_voll:
'Es sind schon zwei Gäste dabei. Nimm erst jemanden heraus.',
layout_unbekannt:
'Diese Anordnung gibt es nicht.',
zu_schnell:
'Das war viel auf einmal. Warte eine Minute – danach geht es weiter.',
nicht_dabei:
'Die Person sitzt gerade nicht im Saal. Ohne sie lässt sich '
+ 'keine Verbindung aufbauen.',
eintrag_nicht_gefunden: 'Den Eintrag gibt es nicht mehr. Lade die Seite neu.',
aufgabe_nicht_gefunden: 'Die Aufgabe gibt es nicht mehr. Lade die Seite neu.',
keine_fassung: 'Dazu gibt es noch keine gespeicherte Fassung.',
File diff suppressed because it is too large Load Diff
+30
View File
@@ -449,6 +449,13 @@
wie Silber und Regenbogen: ein Merkmal am Element, damit
die Farbwerkzeuge weiter mit Zahlen rechnen koennen. */
if (b.papier) li.dataset.papier = 'ja';
/* LIVE -- die Flaeche der Reaction. Sie traegt keine
Ton-Nummer, sondern ihren Zustand; den holt start.js beim
Laden von /api/reaktion/stand nach und setzt ihn als
data-stand. Bis dahin steht sie geschlossen da, und das
ist die richtige Vorgabe: Wer nichts weiss, behauptet
nicht, es laufe etwas. */
if (b.live) { li.dataset.sendung = 'ja'; li.dataset.stand = 'zu'; }
if (b.gross) li.dataset.gross = 'ja';
/* Der Einlauf wird gestaffelt -- aber über eine Eigenschaft, die
bei "weniger Bewegung" nichts tut (siehe start.css). */
@@ -572,6 +579,29 @@
`live.js` liefert drei Antworten. Nur "live" setzt die Marke --
bei "weiss nicht" behauptet hier niemand etwas, weder in die
eine noch in die andere Richtung. */
/* ==== DIE REACTION-KACHEL HOLT IHREN ZUSTAND ===================
(28.09.2026.)
Sie steht als „geschlossen" in der Wand, bis der Server etwas
anderes sagt. Das ist die richtige Vorgabe: Wer nichts weiss,
behauptet nicht, es laufe gerade etwas.
DIE ABFRAGE HAENGT AN DER KACHEL UND NICHT AN DER SEITE. Gibt
es sie nicht -- im Agenturhaus zum Beispiel --, wird auch
nicht gefragt. Sonst stuende dort bei jedem Aufruf ein 404 in
der Konsole, und pruef-handy wuerde die Seite zu Recht
beanstanden; genau das ist am 27.09. beim Notizblock
passiert. */
const sendungsKachel = ziel.querySelector('.kachel[data-sendung="ja"]');
if (sendungsKachel) {
fetch('/workspace/api/reaktion', { credentials: 'same-origin' })
.then((a) => (a.ok ? a.json() : null))
.then((l) => {
if (l && typeof l.stand === 'string') sendungsKachel.dataset.stand = l.stand;
})
.catch(() => { /* Die Wand steht auch ohne diese Auskunft. */ });
}
if (liveKacheln.length && window.LiveStatus) {
window.LiveStatus.beobachten((lage) => {
for (const li of liveKacheln) {
+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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -391,22 +391,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=202609271833"></script>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/hochladen.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/textform.js?v=202609280151"></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/hochladen.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/zuteilung.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/zuteilung.js?v=202609280151" 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=202609271833" defer></script>
<script src="assets/js/aufgaben.js?v=202609271833" defer></script>
<script src="assets/js/vorlagenbrett.js?v=202609280151" defer></script>
<script src="assets/js/aufgaben.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -168,15 +168,15 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/automation.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/automation.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -197,16 +197,16 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/befinden.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" defer></script>
<script src="assets/js/befinden.js?v=202609280151" defer></script>
</body>
</html>
+22 -22
View File
@@ -12,24 +12,24 @@
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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -304,19 +304,19 @@
<!-- 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=202609271833"></script>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/textform.js?v=202609280151"></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/checkliste.js?v=202609271833" defer></script>
<script src="assets/js/ampel.js?v=202609271833" defer></script>
<script src="assets/js/schulung.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/bereich.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/checkliste.js?v=202609280151" defer></script>
<script src="assets/js/ampel.js?v=202609280151" defer></script>
<script src="assets/js/schulung.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" defer></script>
<script src="assets/js/bereich.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- haus.css MUSS die letzte Stilvorlage sein - siehe treff-regeln.html. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</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=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/auskunft.js?v=202609271833" defer></script>
<script src="assets/js/bewerben.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/auskunft.js?v=202609280151" defer></script>
<script src="assets/js/bewerben.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- haus.css MUSS die letzte Stilvorlage sein - siehe treff-regeln.html. -->
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -80,16 +80,16 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/auskunft.js?v=202609271833" defer></script>
<script src="assets/js/bewerbungen.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/auskunft.js?v=202609280151" defer></script>
<script src="assets/js/bewerbungen.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -89,15 +89,15 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/ki.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/calls.js?v=202609271833" defer></script>
<script src="assets/js/ki.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/calls.js?v=202609280151" defer></script>
</body>
</html>
+20 -20
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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -739,24 +739,24 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609271833"></script>
<script src="assets/js/bereiche.js?v=202609280151"></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=202609271833"></script>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/hochladen.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833"></script>
<script src="assets/js/glocke.js?v=202609271833"></script>
<script src="assets/js/textform.js?v=202609280151"></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/hochladen.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151"></script>
<script src="assets/js/glocke.js?v=202609280151"></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=202609271833"></script>
<script src="assets/js/anruf.js?v=202609280151"></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
@@ -767,8 +767,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=202609271833"></script>
<script src="assets/js/chat.js?v=202609271833"></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/bildschau.js?v=202609280151"></script>
<script src="assets/js/chat.js?v=202609280151"></script>
<script src="assets/js/installieren.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/content.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/content.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -179,15 +179,15 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/checkliste.js?v=202609271833" defer></script>
<script src="assets/js/content.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/checkliste.js?v=202609280151" defer></script>
<script src="assets/js/content.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/crew-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/crew-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/crew-haus.css?v=202609280151" />
</head>
<body class="gate gate--crew">
@@ -459,7 +459,7 @@
<script src="assets/js/gate.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/gate.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
</body>
</html>
+4 -4
View File
@@ -23,25 +23,25 @@
},
"icons": [
{
"src": "/assets/img/app-symbole/crew-192.png?v=202609271833",
"src": "/assets/img/app-symbole/crew-192.png?v=202609280151",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-512.png?v=202609271833",
"src": "/assets/img/app-symbole/crew-512.png?v=202609280151",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609271833",
"src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609280151",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609271833",
"src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609280151",
"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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -108,15 +108,15 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/hochladen.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/hochladen.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/dateien.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/dateien.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -297,20 +297,20 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" 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=202609271833" defer></script>
<script src="assets/js/entwicklung.js?v=202609271833" defer></script>
<script src="assets/js/vorlagenbrett.js?v=202609280151" defer></script>
<script src="assets/js/entwicklung.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/hilfe.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/hilfe.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -146,14 +146,14 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/hilfe.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/hilfe.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
</head>
<body class="gate">
@@ -427,7 +427,7 @@
<script src="assets/js/gate.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/gate.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -383,15 +383,15 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/kalender.js?v=202609271833" defer></script>
<script src="assets/js/kalender-abo.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/kalender.js?v=202609280151" defer></script>
<script src="assets/js/kalender-abo.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -444,13 +444,13 @@
</form>
</dialog>
<script src="assets/js/meldung.js?v=202609271833"></script>
<script src="assets/js/nachfrage.js?v=202609271833"></script>
<script src="assets/js/wahl.js?v=202609271833"></script>
<script src="assets/js/bereiche.js?v=202609271833"></script>
<script src="assets/js/kopf.js?v=202609271833"></script>
<script src="assets/js/glocke.js?v=202609271833"></script>
<script src="assets/js/leistung.js?v=202609271833"></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151"></script>
<script src="assets/js/nachfrage.js?v=202609280151"></script>
<script src="assets/js/wahl.js?v=202609280151"></script>
<script src="assets/js/bereiche.js?v=202609280151"></script>
<script src="assets/js/kopf.js?v=202609280151"></script>
<script src="assets/js/glocke.js?v=202609280151"></script>
<script src="assets/js/leistung.js?v=202609280151"></script>
<script src="assets/js/installieren.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/material.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/material.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</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=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/hochladen.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/material.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/hochladen.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/material.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/notizen.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/notizen.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</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=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/notizen.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/notizen.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -453,14 +453,14 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/personen.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/personen.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -214,16 +214,16 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/hochladen.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/hochladen.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/steckbrief.js?v=202609271833" defer></script>
<script src="assets/js/profil.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/steckbrief.js?v=202609280151" defer></script>
<script src="assets/js/profil.js?v=202609280151" defer></script>
</body>
</html>
+270
View File
@@ -0,0 +1,270 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Reaction · Team Dogi</title>
<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=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/reaktion.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start reaktion-seite" data-stand="laedt">
<div class="schleier" aria-hidden="true"></div>
<header class="kopfleiste">
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
<path d="M14.5 5.5 8 12l6.5 6.5" />
</svg>
<span class="zurueck-knopf__text">Zurück</span>
</button>
<p class="marke"><span class="marke__text"><a class="zurueck" href="start.html">Team&nbsp;Dogi</a> · Reaction</span></p>
<div class="kopfleiste__rechts">
<span class="wer" id="wer">…</span>
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
</div>
</header>
<!-- ======================================================================
DIE REACTION (28.09.2026)
Filipes Kurznotiz: Kachel sichtbar -> geschlossen -> Vorbereitung
-> Wartebereich/Chat -> Countdown -> LIVE -> Reaction + Gäste +
Chat + PayPal -> Ende.
WARUM DIESE SEITE ANDERS GEBAUT IST ALS JEDE ANDERE IM HAUS
Alle anderen Seiten sind Listen und Karten: Man liest, man
entscheidet, man geht wieder. Hier sitzt man. Eine Stunde lang,
mit anderen zusammen, und schaut auf EINE Sache.
Deshalb gibt es hier keine Kacheln und kein Raster, sondern
einen SAAL: eine große Fläche für das Video, Kamerabilder als
schwebende Fenster darüber, der Chat als Schiene daneben. Am
Handy wird daraus ein Stapel, und die Schiene fährt von unten
ein.
DREI HÄUTE ÜBEREINANDER, nicht drei Seiten nebeneinander:
.saal[data-stand="zu"] der Vorhang ist zu
.saal[data-stand="vorbereitung"] Vorschau, Countdown, Chat
.saal[data-stand="live"] Video, Kameras, Chat
Alle drei stehen im Dokument und werden über `data-stand`
umgeschaltet. Der Grund ist nicht Bequemlichkeit: Beim Wechsel
von „gleich" auf „live" soll nichts neu geladen werden -- weder
der Chat, in dem man mitten im Satz ist, noch die Verbindung zur
Kamera. Ein Seitenwechsel an dieser Stelle wäre ein Abriss.
====================================================================== -->
<main class="saal" id="saal" data-stand="zu">
<!-- ============ DAS SCHILD ÜBER ALLEM ============================
Es steht in allen drei Zuständen und sagt in drei Worten, was
los ist. Unten am Handy ist der Platz knapp; das Schild bleibt
trotzdem, weil die Antwort auf „läuft schon was?" die einzige
ist, die jeder beim Öffnen sucht. -->
<div class="buehne-schild" id="schild">
<span class="buehne-schild__punkt" aria-hidden="true"></span>
<span class="buehne-schild__wort" id="schild-wort">…</span>
<span class="buehne-schild__zahl" id="schild-zahl" hidden></span>
</div>
<p class="fehler" id="fehler" role="alert" aria-live="polite" hidden></p>
<!-- ============ ZU ============================================== -->
<section class="vorhang" id="teil-zu" aria-label="Die Reaction ist geschlossen">
<div class="vorhang__licht" aria-hidden="true"></div>
<div class="vorhang__innen">
<span class="vorhang__marke"><i></i>Team Dogi<i></i></span>
<h1 class="vorhang__titel">Der Saal ist zu.</h1>
<p class="vorhang__satz" id="zu-satz">
Hier schauen wir zusammen. Wenn etwas läuft, steht es hier —
und du siehst es schon an der Kachel.
</p>
<p class="vorhang__naechste" id="zu-naechste" hidden></p>
</div>
</section>
<!-- ============ VORBEREITUNG ==================================== -->
<section class="warte" id="teil-warte" aria-label="Wartebereich">
<div class="warte__buehne">
<div class="warte__bild" id="warte-bild" aria-hidden="true"></div>
<div class="warte__text">
<span class="warte__kicker">Gleich geht es los</span>
<h1 class="warte__titel" id="warte-titel">Reaction startet bald</h1>
<!-- Der Countdown ist eine ZAHL und kein Balken. Ein Balken
sagt „irgendwann"; eine Zahl sagt „in vier Minuten", und
danach entscheidet jemand, ob er sich noch einen Kaffee
holt. -->
<div class="countdown" id="countdown" hidden>
<span class="countdown__zahl" id="countdown-zahl">–</span>
<span class="countdown__wort" id="countdown-wort">bis zum Start</span>
</div>
<p class="warte__satz">
Der Chat ist schon offen. Schreib schon mal, wer da ist.
</p>
</div>
</div>
</section>
<!-- ============ LIVE ============================================ -->
<section class="kino" id="teil-live" aria-label="Die Reaction läuft">
<!-- Das Video. Der Rahmen bleibt stehen, auch wenn noch nichts
darin ist -- ein Kasten, der erst entsteht, wenn das Video
kommt, lässt die halbe Seite springen. -->
<div class="leinwand" id="leinwand">
<div class="leinwand__video" id="video-platz">
<div class="leinwand__warte" id="video-warte">Das Video kommt gleich …</div>
</div>
<!-- Die Kamerafenster liegen ÜBER dem Video, nicht daneben.
Genau so sieht eine Reaction aus: das Bild groß, das
Gesicht klein davor. Wer lieber beides gleich groß hat,
stellt oben die Anordnung um. -->
<div class="kameras" id="kameras"></div>
<!-- DER TON MUSS FREIGEGEBEN WERDEN. Ein Videoelement mit Ton
darf nicht von allein losgehen -- das verbietet jeder
Browser. Die Kameras starten deshalb stumm, und dieser
Knopf (oder die erste Berührung der Seite) schaltet sie
laut. Ohne ihn säße jemand vor einem stummen Bild und
suchte den Fehler bei sich. -->
<button type="button" class="ton-an" id="ton-an" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 9.4h3.6L12 5.2v13.6L7.6 14.6H4z" />
<path d="M16.2 9.2a4 4 0 0 1 0 5.6" />
<path d="M18.6 6.6a7.4 7.4 0 0 1 0 10.8" />
</svg>
<span>Ton einschalten</span>
</button>
</div>
<!-- Der Chat. Auf dem Rechner eine Schiene rechts, am Handy eine
Lade, die von unten einfährt. -->
<aside class="schiene" id="schiene" aria-label="Live-Chat">
<header class="schiene__kopf">
<h2 class="schiene__titel">Live-Chat</h2>
<span class="schiene__zahl" id="chat-zahl">0</span>
</header>
<ol class="schiene__liste" id="chat-liste" aria-live="polite"></ol>
<form class="schiene__form" id="chat-form">
<input type="text" id="chat-feld" class="schiene__feld" maxlength="400"
placeholder="Sag was …" autocomplete="off" />
<button type="submit" class="schiene__senden" aria-label="Senden">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12 20 4l-4 8 4 8z" /></svg>
</button>
</form>
<!-- Der Weg, den Host zu unterstützen. Er steht IM Chat und
nicht in einer eigenen Ecke: Wer gerade etwas gut findet,
tippt es in den Chat -- und sieht den Knopf in derselben
Bewegung. -->
<a class="danke" id="danke" hidden rel="noopener noreferrer" target="_blank">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 20.4S3.6 15.2 3.6 9.6A4.2 4.2 0 0 1 12 7.4a4.2 4.2 0 0 1 8.4 2.2c0 5.6-8.4 10.8-8.4 10.8z" />
</svg>
<span>Dogi was ausgeben</span>
</a>
</aside>
</section>
<!-- ============ DAS REGIEPULT ===================================
Nur für DogFather. Es liegt UNTEN und nicht oben: Beim Senden
schaut man auf das Bild, nicht auf die Knöpfe -- und was man
nicht ansieht, greift man mit dem Daumen, und der ist unten.
Es ist bewusst als PULT gebaut und nicht als Formular: Regler
für vier Tonquellen nebeneinander, darüber eine Reihe Schalter,
daneben der große Knopf, der die Sendung startet und beendet.
Wer einmal an einem Mischpult gestanden hat, findet sich hier
ohne Erklärung zurecht. -->
<section class="pult" id="pult" hidden aria-label="Regie">
<button type="button" class="pult__griff" id="pult-griff" aria-expanded="false">
<span class="pult__griff-strich" aria-hidden="true"></span>
<span class="pult__griff-wort">Regie</span>
</button>
<div class="pult__innen" id="pult-innen">
<!-- Reihe 1: was gesendet wird -->
<div class="pult__block">
<h3 class="pult__ueber">Die Sendung</h3>
<div class="pult__zeile">
<label class="pult__feld">
<span>Titel</span>
<input type="text" id="p-titel" maxlength="120" placeholder="Worum geht es?" />
</label>
<label class="pult__feld">
<span>YouTube</span>
<input type="text" id="p-video" placeholder="Adresse oder Kennung einfügen" />
</label>
</div>
<div class="pult__zeile">
<label class="pult__feld">
<span>Vorschaubild</span>
<input type="text" id="p-vorschau" placeholder="Adresse eines Bildes (freiwillig)" />
</label>
<label class="pult__feld pult__feld--kurz">
<span>Beginn</span>
<input type="datetime-local" id="p-beginn" />
</label>
<button type="button" class="knopf-still" id="p-sichern">Übernehmen</button>
</div>
</div>
<!-- Reihe 2: die Regler -->
<div class="pult__block">
<h3 class="pult__ueber">Ton</h3>
<div class="regler-reihe" id="regler"></div>
</div>
<!-- Reihe 3: Gäste -->
<div class="pult__block">
<h3 class="pult__ueber">Gäste <span class="pult__klein" id="gast-zahl">0 von 2</span></h3>
<div class="gast-reihe" id="gast-reihe"></div>
<div class="pult__zeile">
<select id="p-wen" class="pult__wahl" aria-label="Wen holen?"></select>
<button type="button" class="knopf-still" id="p-holen">Dazuholen</button>
</div>
</div>
<!-- Reihe 4: die Anordnung -->
<div class="pult__block">
<h3 class="pult__ueber">Anordnung</h3>
<div class="layout-reihe" id="layout-reihe"></div>
</div>
<!-- Reihe 5: der große Knopf -->
<div class="pult__block pult__block--tun">
<button type="button" class="tun tun--vor" id="p-vorbereiten">Vorbereiten</button>
<button type="button" class="tun tun--live" id="p-starten">Auf Sendung</button>
<button type="button" class="tun tun--aus" id="p-beenden">Beenden</button>
</div>
</div>
</section>
</main>
<!-- bereiche.js VOR kopf.js: Dort steht `window.Bereiche`, und kopf.js
liest daraus die Leitungsrollen. -->
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/reaktion.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</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=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/rechte.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" defer></script>
<script src="assets/js/rechte.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/report.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/report.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -106,17 +106,17 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/ki.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/report.js?v=202609271833" defer></script>
<script src="assets/js/ki.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" defer></script>
<script src="assets/js/report.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -173,14 +173,14 @@
</div>
</div>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/scouting.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/scouting.js?v=202609280151" defer></script>
</body>
</html>
+16 -16
View File
@@ -12,21 +12,21 @@
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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/heim.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -732,16 +732,16 @@
</p>
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/live.js?v=202609271833" defer></script>
<script src="assets/js/start.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/live.js?v=202609280151" defer></script>
<script src="assets/js/start.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -85,16 +85,16 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/ki.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/startcheck.js?v=202609271833" defer></script>
<script src="assets/js/ki.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/startcheck.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -150,16 +150,16 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/hochladen.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/hochladen.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/auskunft.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/steckbrief.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/auskunft.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/steckbrief.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/support.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/support.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -128,16 +128,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=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" 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=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/support.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/support.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/bewerbung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -111,15 +111,15 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/talente.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" defer></script>
<script src="assets/js/talente.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/team.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/team.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -110,15 +110,15 @@
</main>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/team.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" defer></script>
<script src="assets/js/team.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/team.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/team.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</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=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/teamlage.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" defer></script>
<script src="assets/js/teamlage.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -112,15 +112,15 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/teilen.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/teilen.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -126,14 +126,14 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/treff-moderation.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/treff-moderation.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/treff.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</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=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/auskunft.js?v=202609271833" defer></script>
<script src="assets/js/treff-regeln.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/auskunft.js?v=202609280151" defer></script>
<script src="assets/js/treff-regeln.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
Dialog "Creator anlegen". -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -149,16 +149,16 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/uebersicht.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/uebersicht.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/unsere-seiten.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/unsere-seiten.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -335,14 +335,14 @@
</main>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/live.js?v=202609271833" defer></script>
<script src="assets/js/unsere-seiten.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/live.js?v=202609280151" defer></script>
<script src="assets/js/unsere-seiten.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/unterstuetzen.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/unterstuetzen.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</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=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/holen.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/holen.js?v=202609280151" 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=202609271833" defer></script>
<script src="assets/js/unterstuetzen.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/unterstuetzen.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/werdegang.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/entwicklung.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/werdegang.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -144,14 +144,14 @@
</main>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/werdegang.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/werdegang.js?v=202609280151" defer></script>
</body>
</html>
+15 -15
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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/willkommen.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/willkommen.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<!-- KOERPERKLASSE "start", NICHT "gate" (berichtigt 21.09.2026).
`body.gate` ist die Anmeldewand: `display:flex; align-items:center`,
@@ -75,13 +75,13 @@
<p class="fehler" id="fehler" hidden></p>
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/willkommen.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/willkommen.js?v=202609280151" 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=202609271833" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609271833" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/start.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609271833" />
<link rel="stylesheet" href="assets/css/module.css?v=202609271833" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609280151" />
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609280151" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/start.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609280151" />
<link rel="stylesheet" href="assets/css/module.css?v=202609280151" />
<!-- 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=202609271833" />
<link rel="stylesheet" href="assets/css/haus.css?v=202609280151" />
</head>
<body class="start">
@@ -190,15 +190,15 @@
</main>
<script src="assets/js/meldung.js?v=202609271833" defer></script>
<script src="assets/js/hochladen.js?v=202609271833" defer></script>
<script src="assets/js/nachfrage.js?v=202609271833" defer></script>
<script src="assets/js/meldung.js?v=202609280151" defer></script>
<script src="assets/js/hochladen.js?v=202609280151" defer></script>
<script src="assets/js/nachfrage.js?v=202609280151" defer></script>
<script src="assets/js/wahl.js?v=202609271833" defer></script>
<script src="assets/js/bereiche.js?v=202609271833" defer></script>
<script src="assets/js/installieren.js?v=202609271833" defer></script>
<script src="assets/js/kopf.js?v=202609271833" defer></script>
<script src="assets/js/glocke.js?v=202609271833" defer></script>
<script src="assets/js/wissen.js?v=202609271833" defer></script>
<script src="assets/js/wahl.js?v=202609280151" defer></script>
<script src="assets/js/bereiche.js?v=202609280151" defer></script>
<script src="assets/js/installieren.js?v=202609280151" defer></script>
<script src="assets/js/kopf.js?v=202609280151" defer></script>
<script src="assets/js/glocke.js?v=202609280151" defer></script>
<script src="assets/js/wissen.js?v=202609280151" defer></script>
</body>
</html>