Files
dogfather-universe/server/pruef-livepunkt.mjs
T
DogFatherGitandClaude Opus 5 39276ef2ee screen6/7/8: der LIVE-Punkt, und eine Benachrichtigung, die stimmt
Filipe: "in dieser kachel soll wenn ich live bin um 20h bis 22h ein
live button der richtig geil ist mit einem live roten punkt symbol am
besten, das soll aufblinken fuer zwei stunden. man soll nicht drauf
druecken koennen aber so dass es auffaellt. ... die leute sollen auch
automatisch eine benarichtigung bekommen um 20 uhr dass ich live bin."

EINE AUSKUNFT, EINE AUSLEGUNG (workspace/assets/js/live.js)
Drei Stellen sollen dasselbe wissen: die Kachel "Draussen" auf der
Startseite, die Kanalkarten auf der Draussen-Seite, die Pulskarte
darunter. Drei Abfragen waeren drei Auslegungen -- und spaetestens
beim naechsten Umbau steht auf der Kachel LIVE und daneben "wartet".

DER DRITTE AUSGANG IST HIER DIE EIGENTLICHE ARBEIT
Die oeffentliche Seite (assets/js/streamplan.js) faengt jeden Fehler
ab und setzt istLive = false. Aus "wir konnten nicht fragen" wird
dort "er sendet nicht" -- wer das liest, macht zu und verpasst den
Stream. Der Dienst SAGT sogar, ob er nachsehen konnte (autoHealthy);
gelesen wird es dort nicht.
Hier gibt es drei Antworten: live / wartet / weiss-nicht. Bei
"weiss nicht" blinkt nichts -- aber es behauptet auch niemand, dass
nichts laeuft. Sechs Antworten des Dienstes durchgespielt.

BLINKEN OHNE FLACKERN
Hausregel vom 12.08.2026: keine aggressiven Flacker-Effekte. Das ist
kein Widerspruch zu "richtig geil", sondern dieselbe Sache: Ein Rot,
das im Sekundentakt umspringt, liest man als "Alarm, ich sehe weg".
Eins, das atmet und einen Saum nach aussen schickt, liest man als
"jetzt gerade". 1,4 Sekunden, ueberall derselbe Takt -- zwei Rhythmen
nebeneinander waeren Unruhe, einer ist ein Herzschlag. Die Pruefung
verlangt ausdruecklich >= 1 s und einen einheitlichen Takt; nach dem
Wort "animation" zu suchen waere auch bei 0,1 s gruen.
Bei Bewegungsarmut steht alles still -- der rote Punkt bleibt
sichtbar, er bewegt sich nur nicht. Im Kontrastmodus sagt es ein
Rahmen, weil Farben dort nichts tragen.

KEIN KNOPF, UND ES SIEHT AUCH NICHT SO AUS
pointer-events: none ist die halbe Antwort; die andere ist die Form.
Etwas, das wie ein Knopf aussieht und nichts tut, ist eine Sackgasse.
Deshalb eine Marke wie auf einer Kamera: Punkt plus Wort. Das Wort
steht auch fuer Vorleseprogramme da -- ein stummer roter Kreis waere
die halbe Auskunft.

DIE KACHEL WIRD UEBER IHR ZIEL ERKANNT, NICHT UEBER DEN NAMEN.
Der Name hat sich in diesem Haus schon zweimal geaendert ("Unsere
Seiten" -> "Draussen"), das Ziel nie. Am 21.09. hat genau dieser
Unterschied eine ganze Hinweisspalte lahmgelegt.

DIE BENACHRICHTIGUNG ENTSCHEIDET DER DIENST, NICHT DIE UHR.
Eine Meldung "er ist live", waehrend er nicht sendet, funktioniert
genau einmal: Beim zweiten Mal weiss jeder, dass sie nichts bedeutet.
Deshalb prueft der Lauf (alle fuenf Minuten) den echten Status; bei
"weiss nicht" wird NICHTS verschickt. Ein Merkmal mit Datum haelt es
bei einer pro Abend -- sonst kaemen in zwei Stunden 24.
Eigene Art "dogfather_live", vorgegeben an und trotzdem abschaltbar:
Wer jeden Abend dieselbe Meldung bekommt und nie hinschaut, schaltet
sonst ALLES ab, und dann erreicht ihn auch keine Aufgabe mehr.

EIN FEHLER, DEN DIE PRUEFUNG GEFANGEN HAT: Der Versand las aus
"push_geraete" -- diese Tabelle gibt es nicht, sie heisst
push_anmeldungen. Der Block steht in einem try/catch; der Fehler
waere still geblieben, und die Benachrichtigung waere nie gekommen.
Die Pruefung verlangt jetzt ausdruecklich, dass die Tabelle im
Schema existiert.

NEU: pruef-livepunkt 49/0, mit drei Gegenproben.
Daneben gruen: pruef-css-klassen 30/0, pruef-push 24/0,
pruef-draussen 39/0, pruef-zwischenspeicher 27/0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 09:28:29 +02:00

254 lines
12 KiB
JavaScript

/* =====================================================================
pruef-livepunkt.mjs — Der LIVE-Punkt, wenn er sendet
---------------------------------------------------------------------
Filipe, 22.09.2026: „in dieser kachel soll wenn ich live bin um 20h
bis 22h ein live button der richtig geil ist mit einem live roten
punkt symbol am besten, das soll aufblinken für zwei stunden. man
soll nicht drauf drücken können aber so dass es auffällt. … die
leute sollen auch automatisch eine benarichtigung bekommen um 20
uhr dass ich live bin."
DIE WICHTIGSTE FRAGE IST NICHT „BLINKT ES"
Sie lautet: Was passiert, wenn der Live-Dienst NICHT antwortet?
Die öffentliche Seite macht daraus seit jeher ein „nicht live"
(`assets/js/streamplan.js`, `.catch()` setzt `istLive = false`).
Wer das liest, macht die Seite zu und verpasst den Stream — und
niemand merkt je, dass es an einer Zeile liegt.
Deshalb prüft diese Datei vor allem den dritten Ausgang: live /
wartet / weiß nicht, an sechs Antworten des Dienstes durchgespielt.
DIE ZWEITWICHTIGSTE: BLINKT ES AUCH DANN, WENN ES NICHT SOLL?
Wer Bewegungsarmut eingestellt hat, macht das meist wegen
Schwindel oder Migräne. Ein Punkt, der trotzdem pulst, ist dann
keine Information, sondern eine Zumutung — und man sieht es beim
Bauen nicht, weil der eigene Rechner die Einstellung nicht hat.
===================================================================== */
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const HIER = dirname(fileURLToPath(import.meta.url));
const W = join(HIER, "..", "workspace");
let ok = 0, fehl = 0;
const pruef = (name, bedingung, zusatz = "") => {
if (bedingung) { ok++; console.log(` OK ${name}`); }
else { fehl++; console.log(` FEHL ${name}${zusatz ? " — " + zusatz : ""}`); }
};
const lies = (p) => readFileSync(join(W, p), "utf8");
console.log("\n===== DER LIVE-PUNKT =====\n");
/* =====================================================================
1. DIE AUSLEGUNG — live / wartet / weiss nicht
===================================================================== */
console.log("=== Drei Antworten, nicht zwei ===");
{
/* Die Funktion wird aus der ausgelieferten Datei geholt, nicht
nachgebaut. Eine Nachbildung wäre die zweite Fassung, die anders
altert — dann prüfte diese Datei sich selbst. */
const quelle = lies("assets/js/live.js");
const fenster = {
window: {}, document: { addEventListener() {} },
fetch: () => { throw new Error("nicht benutzt"); },
setInterval: () => 0, clearInterval: () => {},
};
const fn = new Function("window", "document", "fetch", "setInterval", "clearInterval",
quelle + "\nreturn window.LiveStatus;");
const L = fn(fenster.window, fenster.document, fenster.fetch,
fenster.setInterval, fenster.clearInterval);
pruef("live.js liefert eine Auslegung", typeof L?.auswerten === "function");
const faelle = [
{ was: "er sendet", d: { ok: true, live: true, title: "Abendrunde" }, soll: "live" },
{ was: "er sendet nicht", d: { ok: true, live: false, source: "automatisch", autoHealthy: true }, soll: "wartet" },
{ was: "der Pruefer ist gestoert", d: { ok: true, live: false, source: "automatisch", autoHealthy: false }, soll: "weissnicht" },
{ was: "von Hand auf 'nicht live' gesetzt", d: { ok: true, live: false, source: "hand", autoHealthy: false }, soll: "wartet" },
{ was: "der Dienst meldet einen Fehler", d: { ok: false }, soll: "weissnicht" },
{ was: "gar keine Antwort", d: null, soll: "weissnicht" },
];
for (const f of faelle) {
const r = L.auswerten(f.d);
pruef(`${f.was} -> ${f.soll}`, r.zustand === f.soll, `kam: ${r.zustand}`);
}
/* DER KERN. Ohne diese Zeile wäre alles oben Dekoration. */
pruef('"weiss nicht" ist NICHT dasselbe wie "wartet"',
L.auswerten({ ok: true, live: false, source: "automatisch", autoHealthy: false }).zustand
!== L.auswerten({ ok: true, live: false, source: "automatisch", autoHealthy: true }).zustand);
pruef("der Titel kommt mit, wenn er sendet",
L.auswerten({ ok: true, live: true, title: "Abendrunde" }).titel === "Abendrunde");
/* Das Abzeichen -- Wort UND Punkt, damit ein Vorleseprogramm nicht
nur einen leeren Kreis ansagt. */
fenster.document.createElement = (tag) => ({
tagName: tag.toUpperCase(), className: "", textContent: "",
kinder: [], appendChild(k) { this.kinder.push(k); },
setAttribute(a, w) { this["_" + a] = w; },
});
const a = L.abzeichen("LIVE");
pruef("das Abzeichen traegt einen Punkt und ein Wort",
a.kinder.length === 2 && a.kinder.some((k) => /wort/.test(k.className))
&& a.kinder.some((k) => /punkt/.test(k.className)));
pruef("und einen lesbaren Text (nicht nur einen Kreis)",
a.kinder.some((k) => k.textContent === "LIVE"));
pruef("der Punkt allein ist fuer Vorleseprogramme verborgen",
a.kinder.some((k) => /punkt/.test(k.className) && k["_aria-hidden"] === "true"));
}
/* =====================================================================
2. ES IST KEIN KNOPF
===================================================================== */
console.log("\n=== Man kann nicht draufdruecken ===");
{
const css = lies("assets/css/module.css");
const block = css.slice(css.indexOf(".live-marke {"));
pruef("die Marke nimmt keine Klicks an (pointer-events: none)",
/\.live-marke\s*\{[^}]*pointer-events:\s*none/s.test(css));
pruef("und laesst sich nicht markieren (user-select: none)",
/\.live-marke\s*\{[^}]*user-select:\s*none/s.test(css));
pruef("sie ist kein <button> und kein <a>",
!/live-marke[^}]*\}\s*$/.test("") && !lies("assets/js/live.js").includes("createElement('button')")
&& !lies("assets/js/live.js").includes("createElement('a')"));
}
/* =====================================================================
3. BLINKEN OHNE FLACKERN
===================================================================== */
console.log("\n=== Es blinkt, aber es flackert nicht ===");
{
const css = lies("assets/css/module.css");
const draussen = lies("assets/css/unsere-seiten.css");
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG. Eine Prüfung, die nur nach dem
Wort "animation" sucht, wäre auch bei 0,1 Sekunden grün — also
genau bei dem Strobe, den die Hausregel verbietet. */
const dauern = [...css.matchAll(/animation:\s*live-[a-z]+\s+([\d.]+)s/g)]
.map((m) => Number(m[1]));
pruef(`der Punkt und der Saum pulsen (${dauern.length} Stellen: ${dauern.join(", ")}s)`,
dauern.length >= 2);
pruef("keine Stelle ist schneller als 1 Sekunde (kein Strobe)",
dauern.length > 0 && dauern.every((d) => d >= 1));
pruef("alle laufen im selben Takt (ein Herzschlag, nicht drei)",
dauern.length > 0 && new Set(dauern).size === 1);
const drDauern = [...draussen.matchAll(/animation:\s*drauss-live\s+([\d.]+)s/g)]
.map((m) => Number(m[1]));
pruef(`auch die Draussen-Seite pulst (${drDauern.length} Stellen)`, drDauern.length >= 2);
pruef("und im selben Takt wie der Punkt",
drDauern.length > 0 && dauern.length > 0 && drDauern.every((d) => d === dauern[0]),
`${drDauern.join(",")} gegen ${dauern[0]}`);
/* BEWEGUNGSARMUT. Das ist die Zeile, die man beim Bauen nie sieht. */
const ruhe = css.slice(css.indexOf("@media (prefers-reduced-motion: reduce)"));
pruef("bei Bewegungsarmut steht der Punkt still",
/prefers-reduced-motion[^}]*\}[\s\S]{0,400}?\.live-marke__punkt\s*\{[^}]*animation:\s*none/.test(css)
|| /\.live-marke__punkt\s*\{\s*animation:\s*none/.test(ruhe));
pruef(" aber er ist weiter zu sehen (nicht einfach weg)",
/\.live-marke__punkt\s*\{[^}]*opacity:\s*1/.test(ruhe));
pruef("auch die Kachel hoert dann auf zu pulsen",
/prefers-reduced-motion[\s\S]*?\.kachel\[data-live="ja"\]::after[^}]*animation:\s*none/.test(css));
pruef("und die Draussen-Seite",
/prefers-reduced-motion[\s\S]*?animation:\s*none/.test(draussen));
/* KONTRASTMODUS: Dort tragen Farben nichts. Ohne Rahmen waere die
Marke ein Wort ohne Aussage. */
pruef("im Kontrastmodus sagt es ein Rahmen statt der Farbe",
/forced-colors:\s*active[\s\S]{0,400}?\.live-marke\s*\{[^}]*border:/.test(css));
}
/* =====================================================================
4. WO ES ERSCHEINT
===================================================================== */
console.log("\n=== An den drei Stellen, und nur dort ===");
{
const start = lies("assets/js/start.js");
const seiten = lies("assets/js/unsere-seiten.js");
/* ÜBER DAS ZIEL, NICHT ÜBER DEN NAMEN. Der Name dieser Kachel hat
sich schon zweimal geändert; am 21.09. hat genau das eine ganze
Hinweisspalte lahmgelegt. */
pruef("die Kachel wird ueber ihr ZIEL erkannt, nicht ueber den Namen",
/unsere-seiten\.html/.test(start) && !/b\.name\s*===\s*['"]Drau/.test(start));
pruef("sie bekommt die Marke nur bei 'live'",
/zustand\s*===\s*['"]live['"]/.test(start));
pruef("und sie verschwindet wieder, wenn er aufhoert",
/alt\.remove\(\)/.test(start) && /delete li\.dataset\.live/.test(start));
pruef("die Draussen-Seite benutzt dieselbe Auslegung",
/window\.LiveStatus/.test(seiten));
pruef("sie setzt ein Merkmal am body, statt drei Stellen einzeln zu schalten",
/document\.body\.dataset\.live/.test(seiten));
for (const s of ["start.html", "unsere-seiten.html"]) {
const h = lies(s);
pruef(`${s} laedt live.js`, /assets\/js\/live\.js\?v=\d+/.test(h));
/* Reihenfolge: live.js muss VOR der Seite stehen, die es benutzt. */
const iLive = h.indexOf("assets/js/live.js");
const iSeite = h.indexOf(s === "start.html" ? "assets/js/start.js" : "assets/js/unsere-seiten.js");
pruef(` und zwar vor ${s === "start.html" ? "start.js" : "unsere-seiten.js"}`,
iLive > 0 && iSeite > 0 && iLive < iSeite);
}
}
/* =====================================================================
5. DIE BENACHRICHTIGUNG
===================================================================== */
console.log("\n=== Die Benachrichtigung, wenn er live geht ===");
{
const push = readFileSync(join(HIER, "workspace-push.js"), "utf8");
const draussen = readFileSync(join(HIER, "workspace-draussen.js"), "utf8");
const { ARTEN } = await import("./workspace-push.js");
const art = ARTEN.find((a) => a.schluessel === "dogfather_live");
pruef("es gibt eine eigene Art dafuer", !!art);
pruef(" mit einem Namen, den ein Mensch versteht",
!!art && /live/i.test(art.name) && !/_/.test(art.name), art?.name);
pruef(" sie ist vorgegeben an", art?.vorgabe === true);
pruef(" und trotzdem abschaltbar (sie steht in der Liste)",
ARTEN.filter((a) => a.schluessel === "dogfather_live").length === 1);
pruef("der Server kann selbst nachsehen, ob er live ist",
/export async function liveStatusHolen/.test(draussen));
pruef(" und kennt dabei den dritten Ausgang",
/weissnicht/.test(draussen));
pruef(" mit einer Frist, damit der Lauf nicht haengt",
/AbortSignal\.timeout/.test(draussen));
pruef("der Lauf verschickt nur bei 'live'",
/lage\.zustand === "live"/.test(push));
pruef(" hoechstens einmal am Abend (Merkmal mit Datum)",
/merkmal: `dogfather_live:\$\{heute\}`/.test(push));
pruef(" an alle, die ein Geraet angemeldet haben",
/FROM push_anmeldungen/.test(push));
/* EIN ERFUNDENER TABELLENNAME WAERE HIER STILL DURCHGEGANGEN --
der Block steht in einem try/catch. Genau das ist beim Bauen
passiert (push_geraete gibt es nicht). */
pruef(" und die Tabelle gibt es wirklich",
/CREATE TABLE IF NOT EXISTS push_anmeldungen/
.test(readFileSync(join(HIER, "workspace.js"), "utf8")));
pruef("ein Fehler dort haelt den restlichen Lauf nicht auf",
/\[push\] Live-Hinweis/.test(push));
}
/* =====================================================================
6. GEGENPROBE — kann diese Pruefung ueberhaupt rot werden?
===================================================================== */
console.log("\n=== Gegenprobe ===");
{
const strobe = "animation: live-atmen 0.2s ease-in-out infinite;";
const dauer = Number(strobe.match(/live-[a-z]+\s+([\d.]+)s/)[1]);
pruef(`ein Strobe von 0,2 s wuerde auffallen (${dauer} < 1)`, dauer < 1);
pruef("eine fehlende Bewegungsarmut-Regel wuerde auffallen",
!/prefers-reduced-motion/.test(".live-marke { color: red; }"));
pruef("eine Erkennung ueber den Namen wuerde auffallen",
/b\.name\s*===\s*['"]Drau/.test("if (b.name === 'Draußen') {}"));
}
console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen\n`);
process.exit(fehl ? 1 : 0);