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:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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"],
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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. */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user