Files
dogfather-universe/server/pruef-kamera-richtlinie.mjs
DogFatherGit 5d89d108f9 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.
2026-09-28 01:52:22 +02:00

139 lines
5.8 KiB
JavaScript

/* =====================================================================
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);