From 2c3b90d75c75f04675d2f358aae4234d4e4c00a9 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Tue, 29 Sep 2026 23:48:46 +0200 Subject: [PATCH] Benachrichtigungen: die Reaction laedt ein, jedes Haus bekommt sein Gesicht -- und die Videos haengen nicht mehr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filipe, 29.09.2026: „ich will dass du die benarichtigungen perfektionierst. dogfather und alle anderen sollen die benachrichtigungen perfekt von dieser seite hier bekommen." Und dazu, mit einer Bildschirmaufnahme: „wieso haengen die videos immer." ===================================================================== 1. WARUM DIE VIDEOS HAENGEN -- zwei Zeilen, eine Rueckkopplung ===================================================================== Die Aufnahme zeigt den YouTube-Zaehler bei 0:13 von 31:34, vier Bilder lang unbewegt, in der Mitte der Ladering. a) `hostMelden()` rechnete `laeuft = getPlayerState() === 1`. Zustand 3 heisst PUFFERN -- „ich will spielen, mir fehlen gerade Daten". Der Host meldete in diesem Moment „laeuft nicht", und zwar sofort, weil `onStateChange` bei jedem Zustandswechsel meldet. b) `empfaenger()` fuegt den Host ausdruecklich hinzu, und `folgen(d)` lief im Ereignisstrom fuer alle -- auch fuer ihn. Seine eigene Meldung kam zurueck und traf dort auf if (!stand.laeuft && getPlayerState() === 1) pauseVideo(); Der Host puffert eine halbe Sekunde, laeuft weiter -- und wird vom Echo seines eigenen Pufferers angehalten. Bei einem 31-Minuten-Video passiert das in den ersten Sekunden zuverlaessig. Und alle Zuschauer bekamen bei JEDEM Pufferer des Hosts ein Pause-Play-Paar. Das ist das Ruckeln, das man fuer die eigene Leitung haelt. GEMESSEN, NICHT HERGELEITET (mess-reaktion): vorher Host=laeuft -> Pufferer gemeldet -> Host=pause nachher Host=laeuft -> Pufferer gemeldet -> Host=laeuft Drei neue Messungen, jede mit Gegenprobe: · ein gemeldeter Pufferer haelt den Host nicht an · ein ECHTES Anhalten (Knopf gedrueckt) haelt alle an -- sonst waere das Erste mit kaputtem Gleichlauf bezahlt · beim Puffern meldet der Server `laeuft=true`; ist der Pufferer nicht herzustellen, sagt die Messung das (dritter Ausgang) Beide Behebungen einzeln zurueckgenommen: beide Male rot. Die erste Fassung der Gegenprobe war selbst falsch -- sie hielt das Selbstheilen des Systems (der Host meldet alle fuenf Sekunden die Wahrheit) fuer einen Fehler. Deshalb drueckt sie jetzt den Knopf, statt eine Meldung zu faelschen. ===================================================================== 2. DIE REACTION LAEDT EIN ===================================================================== Nachgemessen war `workspace-reaktion.js` STUMM: kein einziges `benachrichtige`. Beim Einschalten lief `melden("reaktion", ...)` ueber den Ereignisstrom -- also nur an Leute, die die Seite ohnehin offen haben. Das Kino machte auf, und die Einladung verliess das Haus nie. Dasselbe Muster wie bei den Bewerbungen am 23.09. Neue Art `reaktion_live`, eigene neben `dogfather_live`: Das eine ist sein Stream auf TikTok, das andere das Kino hier im Haus. Eingeladen wird, wer ein Geraet hat und NICHT der Agentur gehoert (`AGENTUR_ROLLEN` -- keine neue Liste). Nicht der, der eingeschaltet hat. Und nicht zweimal: Eine Bremse von zwei Stunden faengt den Neustart ab, denn das Merkmal haengt an `gestartet_am` und das wird bei jedem Wechsel nach live neu gesetzt. Geprueft Ende zu Ende in pruef-reaktion (+10): Sendung geht auf Sendung, danach stehen genau fuenf Einladungen in `push_verschickt` -- rechte Hand, linke Hand, Modi, zweimal Community. Nicht der Host, nicht die Creatorin. Einladung abgeschaltet: sechs Fehlschlaege. ===================================================================== 3. DARF DAS NACHTS KOMMEN? -- drei Antworten statt zwei ===================================================================== Hier stand `art === "test" || art === "anruf"`, 230 Zeilen von der Artenliste entfernt. Am 18.09. hat das eine Nacht lang alle Anrufe verschluckt; behoben wurde damals dieser eine Fall. GEMESSEN AM LIVE-BESTAND: `dogfather_live` ging an allen sieben Abenden vom 22. bis 28.09. zwischen 20:28 und 21:06 raus -- jedes Mal knapp vor der Sperre um 22 Uhr. Geht Filipe einmal um 22:05 live, bekommt niemand etwas, und niemand erfaehrt warum. `ruhe` steht jetzt an der ART: "immer" (Vorgabe) 22-7 gesperrt -- Erinnerungen "spaet" nur 1-7 -- etwas laeuft GERADE "nie" nie -- ein Mensch wartet am Hoerer ===================================================================== 4. JEDES HAUS BEKOMMT SEIN GESICHT ===================================================================== Im Service Worker stand fest `workspace-192.png`. Von acht Menschen mit angemeldetem Geraet gehoeren fuenf ins Crew-Haus -- die sahen auf jeder Benachrichtigung das Symbol des Hauses, in dem sie nicht arbeiten. Entschieden wird es auf dem SERVER: Im Browser stuende sonst die verborgene Crew-Adresse in einer Datei ohne Anmeldung (der Befund vom 21.09. bei crew-haus.css) -- und es waere eine zweite Fassung der Hausteilung. Zweistufig, beide Stufen gibt es schon: die Zielseite, wenn sie in genau ein Haus gehoert (GEHOERT_ZU_ADRESSE), sonst `AGENTUR_ROLLEN`. ===================================================================== 5. DAS ABZEICHEN WAR EIN KLOTZ ===================================================================== `badge` ist das winzige Zeichen in der Statusleiste; Android benutzt davon NUR den Alphakanal. Dort stand dasselbe `workspace-192.png` -- ein vollflaechiges Quadrat ohne Transparenz, also ein ausgefuellter Klotz. Es stuerzt nichts ab, deshalb faellt es niemandem auf. `assets/img/abzeichen-96.png`: der gefuellte Umriss des Huskys, weiss auf durchsichtig, 3,5 KB. Drei Fassungen gebaut und bei ECHTER Groesse (24 px) verglichen -- Strichbild zerfaellt, Flaeche traegt. `server/helfer-png-alpha.mjs` liest den Alphakanal wirklich (PNG auspacken, Zeilenfilter zuruecknehmen). Eine Pruefung auf „die Datei gibt es" haette den Klotz nie gefunden -- die Datei gab es ja. Die Gegenprobe ist der alte Zustand selbst: dieselbe Rechnung meldet auf `workspace-192.png` „kein Alphakanal, Deckung 100 %". Das Abzeichen liegt NICHT bei den App-Symbolen -- `pruef-struktur` verlangt dort zu jedem Namen den vollen Satz und hielt es fuer eine neue App. Der Waechter hat recht: Ein Abzeichen ist kein App-Symbol. ===================================================================== Gemessen: mess-reaktion 0, pruef-reaktion 393/0 (+10), pruef-push-ziel 38/0 (+27), pruef-push 24/0, pruef-push-weg 20/0, pruef-glocke 36/0, pruef-struktur 44/0, pruef-haus-trennung 100/0. Co-Authored-By: Claude Opus 5 --- assets/img/abzeichen-96.png | Bin 0 -> 3572 bytes server/helfer-png-alpha.mjs | 157 +++++++++++++++++++++++++ server/mess-reaktion.mjs | 144 +++++++++++++++++++++++ server/pruef-push-ziel.mjs | 201 ++++++++++++++++++++++++++++++++ server/pruef-reaktion.mjs | 135 +++++++++++++++++++++ server/workspace-push.js | 188 ++++++++++++++++++++++++++--- server/workspace-reaktion.js | 110 +++++++++++++++++ tools/abzeichen-bauen.mjs | 172 +++++++++++++++++++++++++++ workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/js/reaktion.js | 46 +++++++- workspace/aufgaben.html | 38 +++--- workspace/automation.html | 32 ++--- workspace/befinden.html | 34 +++--- workspace/bereich.html | 44 +++---- workspace/bewerben.html | 38 +++--- workspace/bewerbungen.html | 36 +++--- workspace/buehne.html | 6 +- workspace/calls.html | 34 +++--- workspace/chat.html | 40 +++---- workspace/content.html | 34 +++--- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 +++--- workspace/entwicklung.html | 36 +++--- workspace/hilfe.html | 30 ++--- workspace/index.html | 10 +- workspace/kalender.html | 34 +++--- workspace/leistung.html | 32 ++--- workspace/material.html | 32 ++--- workspace/notizen.html | 28 ++--- workspace/personen.html | 32 ++--- workspace/profil.html | 36 +++--- workspace/reaktion.html | 38 +++--- workspace/rechte.html | 34 +++--- workspace/report.html | 36 +++--- workspace/scouting.html | 32 ++--- workspace/start.html | 32 ++--- workspace/startcheck.html | 34 +++--- workspace/steckbrief.html | 36 +++--- workspace/support.html | 28 ++--- workspace/sw.js | 50 +++++++- workspace/tafel.html | 6 +- workspace/talente.html | 36 +++--- workspace/team.html | 32 ++--- workspace/teamlage.html | 34 +++--- workspace/teilen.html | 30 ++--- workspace/treff-moderation.html | 32 ++--- workspace/treff-regeln.html | 32 ++--- workspace/uebersicht.html | 32 ++--- workspace/unsere-seiten.html | 30 ++--- workspace/unterstuetzen.html | 32 ++--- workspace/werdegang.html | 32 ++--- workspace/willkommen.html | 30 ++--- workspace/wissen.html | 34 +++--- 55 files changed, 1851 insertions(+), 692 deletions(-) create mode 100644 assets/img/abzeichen-96.png create mode 100644 server/helfer-png-alpha.mjs create mode 100644 tools/abzeichen-bauen.mjs diff --git a/assets/img/abzeichen-96.png b/assets/img/abzeichen-96.png new file mode 100644 index 0000000000000000000000000000000000000000..010d39d2c7b1ca5800d80aa8dad95a048ad8583f GIT binary patch literal 3572 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 kanaele ? zeile[i - kanaele] : 0; // links + const b = vorige[i]; // oben + const c = i >= kanaele ? vorige[i - kanaele] : 0; // oben links + let w = zeile[i]; + if (filter === 1) w += a; + else if (filter === 2) w += b; + else if (filter === 3) w += (a + b) >> 1; + else if (filter === 4) { + const s = a + b - c; + const da = Math.abs(s - a), dbb = Math.abs(s - b), dc = Math.abs(s - c); + w += (da <= dbb && da <= dc) ? a : (dbb <= dc ? b : c); + } + zeile[i] = w & 0xff; + } + for (let x = kanaele - 1; x < zeileBytes; x += kanaele) { + if (zeile[x] > 127) voll++; + } + vorige = zeile; + } + + return { + lesbar: true, hatAlpha: true, + breite: kopf.breite, hoch: kopf.hoch, + deckung: voll / (kopf.breite * kopf.hoch), + }; +} diff --git a/server/mess-reaktion.mjs b/server/mess-reaktion.mjs index 0ff25e9f..c8da797e 100644 --- a/server/mess-reaktion.mjs +++ b/server/mess-reaktion.mjs @@ -393,6 +393,150 @@ if (stehen) { ausgang = 1; } +/* ===================================================================== + HAENGT DAS VIDEO? -- der Host darf sich nicht selbst anhalten + ===================================================================== */ +console.log(""); +console.log("=== Der Gleichlauf des Videos ==="); +{ + /* Wie ein Pufferer aussieht, wenn er gemeldet wird: `laeuft:false` + bei einem Spieler, der in Wahrheit laeuft. Genau das schickt + `hostMelden()` im YouTube-Zustand 3. */ + const pufferer = (seite) => seite.evaluate(async () => { + const stelle = document.getElementById("video-platz")?.dataset.spieler; + const a = await fetch("/workspace/api/reaktion/video", { + method: "POST", credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ laeuft: false, sekunde: 7 }), + }); + return { vorher: stelle, code: a.status }; + }); + + const stand = (seite) => seite.evaluate( + () => document.getElementById("video-platz")?.dataset.spieler || "(nichts)"); + + /* Erst sicherstellen, dass ueberhaupt etwas laeuft -- sonst misst + die Pruefung ein stehendes Video und nennt das Erfolg. */ + const vorHost = await stand(host.seite); + const vorZu = await stand(zuschauer.seite); + console.log(` Vorher: Host=${vorHost} Zuschauerin=${vorZu}`); + const laeuftVorher = (w) => w === "laeuft" || w === "puffert"; + if (!laeuftVorher(vorHost) || !laeuftVorher(vorZu)) { + console.log(" ACHTUNG: Vor der Messung laeuft schon nichts -- " + + "die Frage laesst sich so nicht beantworten."); + ausgang = 1; + } else { + const p = await pufferer(host.seite); + console.log(` Ein Pufferer wird gemeldet: ${p.code}`); + await host.seite.waitForTimeout(2000); + await zuschauer.seite.waitForTimeout(500); + + const nachHost = await stand(host.seite); + const nachZu = await stand(zuschauer.seite); + console.log(` Danach: Host=${nachHost} Zuschauerin=${nachZu}`); + + /* DER BEFUND. Der Host hat nichts angehalten -- er hat nur + gemeldet, dass gerade Daten fehlen. Steht sein Video danach, + hat sein eigenes Echo es angehalten. */ + if (!laeuftVorher(nachHost)) { + console.log(" ACHTUNG: Der Host haelt sich selbst an. Ein kurzer " + + "Pufferer meldet 'laeuft nicht', das Echo kommt zurueck und " + + "drueckt Pause -- deshalb haengt das Video."); + ausgang = 1; + } + + /* ---- DIE GEGENPROBE: ein ECHTES Anhalten wirkt weiter ------ + Ohne sie waere „der Host folgt sich nicht selbst" womoeglich + mit einem kaputten Gleichlauf fuer alle bezahlt. + + GEDRUECKT WIRD DER KNOPF, nicht eine Meldung gefaelscht. Bei + einem echten Anhalten meldet der Host durchgehend „steht" -- + die Antwort haengt damit nicht am Zufall seines + Fuenf-Sekunden-Taktes. Die erste Fassung dieser Gegenprobe hat + genau daran Alarm geschlagen: Sie hielt das Selbstheilen des + Systems fuer einen Fehler. */ + await host.seite.evaluate(() => document.getElementById("v-spiel")?.click()); + await host.seite.waitForTimeout(1800); + const haltHost = await stand(host.seite); + const haltZu = await stand(zuschauer.seite); + console.log(` Host haelt wirklich an: Host=${haltHost} Zuschauerin=${haltZu}`); + if (haltHost === "laeuft" || haltZu === "laeuft") { + console.log(" ACHTUNG: Ein echtes Anhalten des Hosts kommt nicht an -- " + + "dann laeuft niemand mehr synchron."); + ausgang = 1; + } + + /* Und wieder los, mit demselben Knopf. */ + await host.seite.evaluate(() => document.getElementById("v-spiel")?.click()); + await host.seite.waitForTimeout(2000); + const weiterHost = await stand(host.seite); + const weiterZu = await stand(zuschauer.seite); + console.log(` Und wieder los: Host=${weiterHost} Zuschauerin=${weiterZu}`); + if (!laeuftVorher(weiterHost) || !laeuftVorher(weiterZu)) { + console.log(" ACHTUNG: Nach dem Anhalten laeuft es nicht wieder an."); + ausgang = 1; + } + + /* ---- DER KERN: was meldet der Host, WAEHREND er puffert? ---- + Die beiden Teile darueber pruefen die Folge. Sie blieben auch + gruen, wenn der Host beim Puffern weiter „steht" meldete -- + die Zuschauer ruckelten dann bei jedem Pufferer, und niemand + saehe es hier. + + Puffern wird hergestellt: weit nach vorn springen, wohin noch + nichts geladen ist. Ob das GELINGT, ist nicht sicher -- auf + einer schnellen Leitung kann der Sprung fertig sein, bevor + jemand hinsieht. Deshalb der dritte Ausgang. */ + let gesehen = null; + for (let i = 0; i < 8 && !gesehen; i++) { + await host.seite.evaluate(() => { + for (let n = 0; n < 12; n++) document.getElementById("v-vor")?.click(); + }); + for (let t = 0; t < 14 && !gesehen; t++) { + const w = await host.seite.evaluate(() => ({ + spieler: document.getElementById("video-platz")?.dataset.spieler, + })); + if (w.spieler === "puffert") { + /* SOFORT NACHSEHEN, was der Server jetzt weiss. Eine + Sekunde spaeter ist der Pufferer vorbei und die Antwort + nichts mehr wert. */ + gesehen = await host.seite.evaluate(async () => { + const a = await fetch("/workspace/api/reaktion", { credentials: "same-origin" }); + const j = await a.json(); + return { laeuft: j.laeuft, stand: document.getElementById("video-platz")?.dataset.spieler }; + }); + } + await host.seite.waitForTimeout(120); + } + } + if (!gesehen) { + console.log(" HINWEIS: Ein Pufferer war hier nicht herzustellen " + + "(schnelle Leitung) -- was der Host dabei meldet, konnte ich " + + "nicht nachsehen."); + offenePunkte.push("Was der Host beim Puffern meldet, war in diesem " + + "Lauf nicht messbar -- der Sprung war zu schnell geladen."); + } else { + console.log(` Beim Puffern meldet der Server: laeuft=${gesehen.laeuft} ` + + `(Spieler: ${gesehen.stand})`); + if (!gesehen.laeuft) { + console.log(" ACHTUNG: Der Host meldet beim Puffern 'laeuft nicht'. " + + "Dann bekommt jeder Zuschauer bei jedem Stocken der Hostleitung " + + "ein Pause-Play-Paar -- das ist das Ruckeln."); + ausgang = 1; + } + } + + /* Und wieder anlaufen lassen, damit die folgenden Messungen ein + laufendes Video vorfinden. */ + await host.seite.evaluate(() => fetch("/workspace/api/reaktion/video", { + method: "POST", credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ laeuft: true, sekunde: 7 }), + })); + await host.seite.waitForTimeout(1200); + } +} + /* ---- Die Regie --------------------------------------------------- NICHT UMSCHALTEN, SONDERN HERSTELLEN. Ein Klick auf „Griff" heisst „das Gegenteil von dem, was gerade ist" -- und setzt damit voraus, diff --git a/server/pruef-push-ziel.mjs b/server/pruef-push-ziel.mjs index 2292181a..a38e8572 100644 --- a/server/pruef-push-ziel.mjs +++ b/server/pruef-push-ziel.mjs @@ -48,6 +48,27 @@ process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; +/* ---- DIE RUHEZEIT WIRD HERGESTELLT, NICHT ABGEWARTET ------------- + + Ab dem 29.09.2026 traegt jede Art ihre eigene Ruheregel. Um zu + pruefen, dass sie wirkt, muss Ruhezeit sein -- und darauf bis 22 + Uhr zu warten waere keine Pruefung, sondern ein Kalendereintrag. + + PUSH_RUHE_AB = 0 „gesperrt ab Stunde 0“ -> „immer“ ist IMMER + Ruhezeit, zu jeder Tageszeit. + PUSH_SPAET_AB = 24 „gesperrt ab Stunde 24" -- die es nicht gibt + -> „spaet“ ist NIE gesperrt. + + Beides zusammen macht den Unterschied zwischen den Regeln + unabhaengig von der Uhr sichtbar. Ohne das waere diese Pruefung + eine Zeitbombe: gruen am Nachmittag, rot um halb drei nachts, und + beides ohne Aussage ueber die Software. + + DIE VORHANDENEN ELF PRUEFUNGEN BLEIBEN UNBERUEHRT -- sie benutzen + alle die Art „test“, und die traegt `ruhe: "nie"`. */ +process.env.PUSH_RUHE_AB = "0"; +process.env.PUSH_SPAET_AB = "24"; + 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 }); }; @@ -213,6 +234,186 @@ melde("=== 4. Der Weg ==="); } /* ------------------------------------------------------------------- */ +/* ======================================================================= + DAS GESICHT DES HAUSES (29.09.2026) + ======================================================================= + + Im Service Worker stand fest `workspace-192.png` -- das Symbol der + Agentur. Gemessen am Live-Bestand gehoeren fuenf von acht Menschen + mit angemeldetem Geraet ins Crew-Haus; die sahen auf JEDER + Benachrichtigung das Symbol des Hauses, in dem sie nicht arbeiten. + + Entschieden wird es auf dem Server und nicht im Browser: Dort + stuende sonst die verborgene Crew-Adresse in einer Datei, die ohne + Anmeldung abrufbar ist -- genau der Befund vom 21.09. bei + crew-haus.css. + ======================================================================= */ +melde(""); +melde("=== Das Gesicht des Hauses ==="); +{ + const m = await push.benachrichtige(idModi, "test", + { titel: "x", text: "y", ziel: "/workspace/start.html" }); + ok(m.haus === "crew", `ein Modi bekommt das Crew-Gesicht (${m.haus})`); + + const g = await push.benachrichtige(idMana, "test", + { titel: "x", text: "y", ziel: "/workspace/start.html" }); + ok(g.haus === "agentur", `eine Managerin das der Agentur (${g.haus})`); + + /* Dass die beiden VERSCHIEDEN sind, ist die eigentliche Aussage -- + eine Funktion, die immer „agentur“ sagt, haette die zweite Zeile + bestanden und die erste nicht. */ + ok(m.haus !== g.haus, "die beiden Haeuser bekommen nicht dasselbe Gesicht"); + + /* DIE SEITE SCHLAEGT DIE ROLLE. DogFather arbeitet in beiden + Haeusern; eine Einladung ins Crew-Kino traegt trotzdem das + Crew-Gesicht, weil `/workspace/reaktion.html` in genau ein Haus + gehoert (GEHOERT_ZU_ADRESSE). */ + const r = await push.benachrichtige(idDogi, "test", + { titel: "x", text: "y", ziel: "/workspace/reaktion.html" }); + ok(r.haus === "crew", + `eine Einladung zur Reaction traegt das Crew-Gesicht (${r.haus})`); + + /* Gegenprobe zur Zeile darueber: Dieselbe Person, ein neutrales + Ziel -- die Seite entscheidet also wirklich, nicht die Person + allein. Waere es umgekehrt, stuende hier zweimal dasselbe. */ + const n = await push.benachrichtige(idMana, "test", + { titel: "x", text: "y", ziel: "/workspace/reaktion.html" }); + ok(n.haus === "crew", + `auch fuer die Agentur, wenn das ZIEL im Crew-Haus liegt (${n.haus})`); +} + +/* ======================================================================= + DAS ABZEICHEN -- ein Zeichen oder ein Klotz? + ======================================================================= + + Das `badge` ist das winzige Zeichen oben in der Statusleiste. Android + liest davon AUSSCHLIESSLICH den Alphakanal und malt ihn einfarbig + weiss. + + Bis zum 29.09.2026 stand dort dasselbe `workspace-192.png` wie beim + Symbol -- ein vollflaechiges Quadrat OHNE Alphakanal. In der + Statusleiste erschien deshalb ein ausgefuellter Klotz. Es stuerzt + nichts ab, es sieht nur billig aus, und genau deshalb faellt so + etwas niemandem als Fehler auf. + + EINE PRUEFUNG AUF „DIE DATEI GIBT ES“ HAETTE DAS NIE GEFUNDEN -- die + Datei gab es ja. Gefragt ist der INHALT. + ======================================================================= */ +melde(""); +melde("=== Das Abzeichen ==="); +{ + const { readFileSync } = await import("node:fs"); + const { dirname, join: fuege } = await import("node:path"); + const { fileURLToPath } = await import("node:url"); + const { pngDeckung } = await import("./helfer-png-alpha.mjs"); + const HIER = dirname(fileURLToPath(import.meta.url)); + const REPO = fuege(HIER, ".."); + const sw = readFileSync(fuege(REPO, "workspace", "sw.js"), "utf8"); + + /* ABGELESEN, NICHT ABGESCHRIEBEN: Der Pfad kommt aus dem Service + Worker selbst. Stuende hier der Name noch einmal, pruefte die + Zeile ihre eigene Kopie. */ + const wo = /badge:\s*"([^"]+)"/.exec(sw); + ok(!!wo, `der Service Worker nennt ein Abzeichen (${wo?.[1] || "keins"})`); + + const datei = fuege(REPO, (wo?.[1] || "").replace(/^\//, "")); + const e = pngDeckung(datei); + ok(e.lesbar, `es liegt als lesbares PNG vor (${e.grund || `${e.breite}x${e.hoch}`})`); + ok(e.hatAlpha === true, "es hat einen Alphakanal -- sonst ist es ein Klotz"); + /* ZWISCHEN STAUBKORN UND KLOTZ. Unter 10 % ist in der Statusleiste + nichts mehr zu sehen, ueber 80 % ist es wieder die Flaeche, die + wir gerade abgeschafft haben. */ + ok(e.deckung > 0.10 && e.deckung < 0.80, + `und deckt ${(e.deckung * 100).toFixed(0)} % seiner Flaeche (10-80 erwartet)`); + + /* ---- DIE GEGENPROBE, und sie ist der alte Zustand selbst ------- + Dieselbe Rechnung auf das App-Symbol, das bis zum 29.09. als + Abzeichen diente. Meldet sie dort nicht „kein Alphakanal, 100 %", + kann sie den Unterschied nicht sehen -- und alles darueber waere + wertlos. */ + const alt = pngDeckung(fuege(REPO, "assets/img/app-symbole/workspace-192.png")); + ok(alt.lesbar && alt.hatAlpha === false && alt.deckung === 1, + `Gegenprobe: das alte Abzeichen wird als Klotz erkannt ` + + `(Alpha ${alt.hatAlpha}, Deckung ${alt.deckung})`); + + /* Und die zwei Gesichter muss es wirklich geben -- ein Symbol, das + 404 antwortet, zeigt Android gar nicht an. */ + for (const n of ["crew-192.png", "workspace-192.png"]) { + const g = pngDeckung(fuege(REPO, "assets/img/app-symbole", n)); + ok(g.lesbar, `${n} liegt vor (${g.grund || `${g.breite}x${g.hoch}`})`); + ok(sw.includes(n), `und der Service Worker nennt es`); + } +} + +/* ======================================================================= + DARF DAS NACHTS KOMMEN? -- drei Antworten statt zwei + ======================================================================= + + Bis zum 29.09.2026 stand die Antwort als fest verdrahtete + Aufzaehlung in `benachrichtige`, 230 Zeilen von der Artenliste + entfernt: + + const dringend = art === "test" || art === "anruf"; + + Am 18.09. hat genau das eine Nacht lang alle Anrufe verschluckt. + Behoben wurde damals dieser eine Fall; die Frage dahinter blieb + stehen, und `dogfather_live` fiel bis heute in dieselbe Falle. + ======================================================================= */ +melde(""); +melde("=== Darf das nachts kommen? ==="); +{ + /* FESTE STUNDEN ALS EINGABE, nicht als Annahme ueber „jetzt". Ein + Datum, das der Kalender einholt, waere eine Zeitbombe -- so wie + die Weckerpruefung am 07.09. */ + const um23 = new Date(2026, 8, 29, 23, 0, 0); + const um3 = new Date(2026, 8, 30, 3, 0, 0); + const um15 = new Date(2026, 8, 29, 15, 0, 0); + /* Die Umgebung ist oben umgestellt; fuer die reine Rechnung wird + hier die Vorgabe gefragt -- die drei Regeln, nicht die Zahlen. */ + ok(push.ruheRegel("aufgabe_faellig") === "immer", + "eine Erinnerung traegt „immer“"); + ok(push.ruheRegel("anruf") === "nie", "ein Anruf traegt „nie“"); + ok(push.ruheRegel("dogfather_live") === "spaet", + "„DogFather ist live“ traegt „spaet“"); + ok(push.ruheRegel("reaktion_live") === "spaet", + "die Reaction-Einladung auch"); + ok(push.ruheRegel("gibtsnicht") === "immer", + "eine unbekannte Art bekommt die strengste Regel"); + + /* Die Rechnung selbst -- mit den Grenzen dieses Laufs (ab 0 bzw. + nie). Dass die Vorgaben 22 und 1 sind, steht in der Artenliste; + hier wird geprueft, dass die REGELN verschieden wirken. */ + ok(push.istRuhezeit(um23, "immer") === true, "um 23 Uhr ist „immer“ gesperrt"); + ok(push.istRuhezeit(um23, "nie") === false, "„nie“ nicht"); + ok(push.istRuhezeit(um15, "immer") === true, + "in diesem Lauf ist „immer“ rund um die Uhr gesperrt (hergestellt)"); + ok(push.istRuhezeit(um3, "spaet") === false, + "und „spaet“ nie (hergestellt)"); + + /* ---- UND JETZT DAS VERHALTEN, nicht die Rechnung -------------- + Eine Prüfung der Formel allein sagt nichts darueber, ob + `benachrichtige` sie auch BENUTZT. Genau diese Luecke war der + Fehler: Die Formel war richtig, die Aufzaehlung daneben nicht. */ + const erinnerung = await push.benachrichtige(idHand, "aufgabe_faellig", + { titel: "x", text: "y", ziel: "/workspace/aufgaben.html" }); + ok(erinnerung.grund === "ruhezeit", + `eine Erinnerung wird nachts zurueckgehalten (${erinnerung.grund})`); + + const live = await push.benachrichtige(idHand, "dogfather_live", + { titel: "x", text: "y", ziel: "/workspace/unsere-seiten.html" }); + ok(live.verschickt === 1, + `„DogFather ist live“ geht trotzdem raus (${live.verschickt}, ${live.grund})`); + + const kino = await push.benachrichtige(idHand, "reaktion_live", + { titel: "x", text: "y", ziel: "/workspace/reaktion.html" }); + ok(kino.verschickt === 1, + `die Einladung zur Reaction auch (${kino.verschickt}, ${kino.grund})`); + + const ruf = await push.benachrichtige(idHand, "anruf", + { titel: "x", text: "y", ziel: "/workspace/chat.html" }); + ok(ruf.verschickt === 1, `und ein Anruf erst recht (${ruf.verschickt})`); +} + melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler`); melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs index f978f45d..1d887a93 100644 --- a/server/pruef-reaktion.mjs +++ b/server/pruef-reaktion.mjs @@ -45,6 +45,19 @@ process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; den niemand prueft, weil das Pruefen zu lange dauert, ist ein ungeprueftes Verhalten. */ process.env.REAKTION_LISTE_MAX = "3"; +/* DIE EINLADUNG DARF NICHT AN DER UHR SCHEITERN. + + `reaktion_live` traegt die Ruheregel „spaet" -- gesperrt ist damit + nur zwischen 1 und 7 Uhr. Liefe diese Pruefung um halb drei nachts, + waere sie rot, ohne dass an der Software etwas kaputt ist: genau die + Zeitbombe, die in den Hausregeln steht und die am 07.09. schon die + Weckerpruefung getroffen hat. + + `PUSH_SPAET_AB = 24` heisst: „gesperrt ab Stunde 24" -- die es nicht + gibt. Damit ist „spaet" hier NIE gesperrt, zu jeder Tageszeit. Die + Grenze selbst wird in pruef-push-ziel mit festen Stunden geprueft, + wo sie hingehoert: als EINGABE, nicht als Annahme ueber „jetzt". */ +process.env.PUSH_SPAET_AB = "24"; const express = (await import("express")).default; const ec = express.response.cookie; @@ -2424,6 +2437,128 @@ melde("=== 21. Alles zuruecksetzen ==="); }); } +/* ======================================================================= + DIE EINLADUNG -- geht sie raus, und an die Richtigen? + ======================================================================= + + Filipe, 29.09.2026: „dogfather und alle anderen sollen die + benachrichtigungen perfekt von dieser seite hier bekommen." + + Bis zu diesem Tag verliess die Reaction das Haus nie. Sie rief beim + Einschalten `melden("reaktion", ...)`, und das laeuft ueber den + Ereignisstrom -- also NUR zu Leuten, die die Seite ohnehin offen + haben. Wer nicht hinschaut, erfuhr nichts. + + GEPRUEFT WIRD DAS VERHALTEN, NICHT DER QUELLTEXT. Eine Textsuche + nach „benachrichtige" waere gruen gewesen, sobald das Wort irgendwo + steht -- auch in einem Zweig, den nichts aufruft. Hier geht die + Sendung wirklich auf Sendung, und danach wird nachgesehen, was in + `push_verschickt` steht. + ======================================================================= */ +melde(""); +melde("=== Die Einladung ==="); +{ + const { createServer } = await import("node:http"); + const { createECDH } = await import("node:crypto"); + + /* Ein Push-Dienst, der alles annimmt. Er muss nichts entschluesseln + -- gefragt ist, WER eine bekommt. Dass der Inhalt unterwegs heil + bleibt, beweist pruef-push-weg mit einem nachgebauten Browser. */ + const dienstPort = await eigenerPort(import.meta, "pruef-reaktion (Push-Dienst)", 1); + const dienst = createServer((q, a) => { + q.on("data", () => {}); + q.on("end", () => { a.writeHead(201); a.end(); }); + }); + await new Promise((r) => dienst.listen(dienstPort, "127.0.0.1", r)); + + /* Erst zu -- sonst waere der naechste Schritt kein UEBERGANG nach + live, und eingeladen wird nur beim Uebergang. */ + await roh(CREW, "/workspace/api/reaktion/stand", + { method: "POST", keks: k.admin, rumpf: { stand: "zu" } }); + + const db2 = new DatabaseSync(process.env.WORKSPACE_DB); + /* Je ein Geraet fuer JEDEN -- auch fuer die, die keine Einladung + bekommen sollen. Ohne ihr Geraet waere „hat nichts bekommen" die + Antwort auf die falsche Frage: Man wuesste nicht, ob die Auswahl + greift oder ob nur kein Telefon da war. */ + for (const rolle of ["admin", "hand", "linke", "modi", "gast", "gast2", "creator"]) { + const kk = createECDH("prime256v1"); + kk.generateKeys(); + db2.prepare(`INSERT INTO push_anmeldungen (person_id, endpunkt, p256dh, auth, erstellt) + VALUES (?,?,?,?,?)`) + .run(IDS[rolle], `http://127.0.0.1:${dienstPort}/push/${rolle}`, + kk.getPublicKey().toString("base64url"), + randomBytes(16).toString("base64url"), nun); + } + db2.close(); + + const anSendung = await roh(CREW, "/workspace/api/reaktion/stand", + { method: "POST", keks: k.admin, rumpf: { stand: "live" } }); + ok(anSendung.code === 200, `auf Sendung (${anSendung.code})`); + + /* Die Einladung wird losgeschickt und nicht abgewartet -- der Knopf + „Auf Sendung" soll sofort reagieren. Also hier warten, wo Warten + nichts kostet. */ + await new Promise((r) => setTimeout(r, 2500)); + + const lies = () => { + const db3 = new DatabaseSync(process.env.WORKSPACE_DB); + const z = db3.prepare( + `SELECT person_id FROM push_verschickt WHERE merkmal LIKE 'reaktion_live:%'`).all(); + db3.close(); + return new Set(z.map((x) => x.person_id)); + }; + const bekommen = lies(); + + const sollen = [["hand", "die rechte Hand"], ["linke", "die linke Hand"], + ["modi", "ein Modi"], ["gast", "die Community"], ["gast2", "noch jemand aus der Community"]]; + for (const [rolle, wort] of sollen) { + ok(bekommen.has(IDS[rolle]), `${wort} wird eingeladen`); + } + + /* WER EINGESCHALTET HAT, BEKOMMT KEINE. Er hat gerade den Knopf + gedrueckt; eine Einladung zu etwas, das man selbst aufgemacht + hat, ist Rauschen. */ + ok(!bekommen.has(IDS.admin), + "wer eingeschaltet hat, bekommt keine Einladung"); + + /* DAS ANDERE HAUS BLEIBT AUSSEN VOR. Fuer eine Creatorin gibt es + `reaktion.html` nicht -- sie bekaeme eine Meldung, die sie auf + ihre Startseite wirft. Eine Benachrichtigung, die nirgendwohin + fuehrt, ist schlimmer als keine. */ + ok(!bekommen.has(IDS.creator), + "die Agentur bekommt keine Einladung ins Crew-Kino"); + + ok(bekommen.size === 5, + `genau fuenf Einladungen, nicht mehr (${bekommen.size})`); + + /* ---- Die Bremse gegen den Neustart --------------------------- + Wer aus Versehen auf „Vorbereitung" und gleich wieder auf „Auf + Sendung" drueckt, laedt sonst zweimal ein. Fuenf Menschen + bekaemen zwei Meldungen fuer dieselbe Sendung -- der schnellste + Weg, dass jemand die Art abschaltet, und dann fehlt auch die + naechste. */ + const db4 = new DatabaseSync(process.env.WORKSPACE_DB); + const vorher = db4.prepare( + `SELECT COUNT(*) AS n FROM push_verschickt WHERE merkmal LIKE 'reaktion_live:%'`).get().n; + db4.close(); + + await roh(CREW, "/workspace/api/reaktion/stand", + { method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" } }); + await roh(CREW, "/workspace/api/reaktion/stand", + { method: "POST", keks: k.admin, rumpf: { stand: "live" } }); + await new Promise((r) => setTimeout(r, 2000)); + + const db5 = new DatabaseSync(process.env.WORKSPACE_DB); + const nachher = db5.prepare( + `SELECT COUNT(*) AS n FROM push_verschickt WHERE merkmal LIKE 'reaktion_live:%'`).get().n; + db5.close(); + ok(nachher === vorher, + `ein Neustart laedt nicht noch einmal ein (${vorher} -> ${nachher})`); + + await new Promise((r) => dienst.close(r)); +} + melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler`); melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG"); diff --git a/server/workspace-push.js b/server/workspace-push.js index c64388d4..7b906240 100644 --- a/server/workspace-push.js +++ b/server/workspace-push.js @@ -58,6 +58,10 @@ import { gehoertAufDieseAdresse, TEAM_DOGI_ROLLEN, } from "./workspace.js"; import { schluesselErzeugen, schicken } from "./workspace-push-krypto.js"; +/* Die Hausteilung kommt aus der Datei, in der sie steht -- nicht aus + einer zweiten Fassung hier. `crew-adresse.js` haengt von nichts ab + (nur von node:fs/path), ein Kreis entsteht also nicht. */ +import { AGENTUR_ROLLEN, GEHOERT_ZU_ADRESSE, istCrewAdresse } from "./crew-adresse.js"; import { rhythmus as befindenRhythmus } from "./workspace-befinden.js"; import { SEITEN } from "./rechte.js"; @@ -70,6 +74,41 @@ const jetzt = () => new Date().toISOString(); eine bewusste Auswahl, keine Nutzdaten. Wer eine neue Art braucht, traegt sie hier ein und sie erscheint von selbst in den Einstellungen. + + --------------------------------------------------------------------- + `ruhe` -- DARF DAS NACHTS KOMMEN? (29.09.2026) + + Bis hierher stand das als fest verdrahtete Aufzaehlung 230 Zeilen + weiter unten in `benachrichtige`: + + const dringend = art === "test" || art === "anruf"; + + Wer eine neue Art eintrug, sah sie nicht -- und diese Liste sagte + ueber die Ruhezeit kein Wort, obwohl sie darueber entscheidet, ob + eine Meldung jemals ankommt. Am 18.09. hat genau das eine Nacht + lang alle Anrufe verschluckt; damals wurde EINE Art ausgenommen, + statt die Frage zu stellen. + + DREI ANTWORTEN, WEIL ES DREI FAELLE GIBT: + + "immer" (Vorgabe) 22-7 gesperrt. + Erinnerungen. Der Server schickt sie von sich aus, weil + eine Frist naeher rueckt -- die kann bis morgen warten. + + "spaet" nur 1-7 gesperrt. + Etwas LAEUFT GERADE und ist in zwei Stunden vorbei. Eine + Einladung um 22:30 will man haben; um vier Uhr nachts + ist die Sendung laengst aus, und die Meldung waere nur + noch eine Stoerung. + + "nie" immer durch. + Ein Mensch drueckt gerade auf den Hoerer und wartet. + Ein Telefon, das nachts stumm bleibt, ist kein Telefon. + + GEMESSEN, WARUM "spaet" gebraucht wird: An allen sieben Abenden vom + 22. bis 28.09.2026 ging der Live-Ruf zwischen 20:28 und 21:06 raus. + Jedes Mal knapp vor der Sperre. Geht Filipe einmal um 22:05 live, + bekommt niemand etwas -- und niemand erfaehrt, warum. ------------------------------------------------------------------ */ export const ARTEN = [ { schluessel: "aufgabe_faellig", name: "Aufgabe wird morgen fällig", vorgabe: true }, @@ -166,7 +205,41 @@ export const ARTEN = [ Eine eigene Art und nicht "termin_gleich": Das eine ist ein Kalendereintrag, das andere ein Stream, der JETZT laeuft. Wer das eine stumm stellt, meint selten das andere. */ - { schluessel: "dogfather_live", name: "DogFather ist live", vorgabe: true }, + { schluessel: "dogfather_live", name: "DogFather ist live", vorgabe: true, + /* „spaet", nicht „immer": Ein Stream, der um 22:05 anfaengt, ist um + 22:05 interessant. Mit der alten Regel waere er stumm geblieben, + und die Meldung, die Filipe ausdruecklich bestellt hat („die + leute sollen auch automatisch eine benarichtigung bekommen"), + haette an genau den Abenden nicht funktioniert, an denen es + spaeter wurde. */ + ruhe: "spaet" }, + + /* ---- DIE REACTION LAEDT EIN (29.09.2026) ------------------------ + + Filipe: „ich will dass du die benarichtigungen perfektionierst. + dogfather und alle anderen sollen die benachrichtigungen perfekt + von dieser seite hier bekommen." + + NACHGEMESSEN WAR DIE REACTION STUMM. `workspace-reaktion.js` + enthielt kein einziges `benachrichtige`. Geht die Sendung auf + Sendung, ruft sie `melden("reaktion", ...)` -- und das geht ueber + den Ereignisstrom, also NUR an Leute, die die Seite ohnehin schon + offen haben. Das Kino macht auf, und die Einladung verlaesst das + Haus nie. + + Dasselbe Muster wie bei den Bewerbungen am 23.09.: beide Wege + gebaut, beide stumm. + + EIGENE ART UND NICHT `dogfather_live`. Das eine ist sein Stream + auf TikTok (gemessen ueber `liveStatusHolen` beim Postfach), das + andere das Kino hier im Haus. Wer abends nicht auf TikTok geht, + aber zur Reaction kommt, will genau das eine und nicht das + andere -- und umgekehrt. + + `ruhe: "spaet"` aus demselben Grund wie oben: Eine Reaction um + 22:30 ist der Normalfall, keine Ausnahme. */ + { schluessel: "reaktion_live", + name: "Die Reaction geht auf Sendung", vorgabe: true, ruhe: "spaet" }, /* ---- ANRUFE SIND EINE EIGENE ART (18.09.2026) ------------------ Filipe: „sie bekommt nur eine benarichtigung das eine neue @@ -191,7 +264,7 @@ export const ARTEN = [ ist kein Telefon. Wer nachts nicht gestoert werden will, schaltet jetzt diese Art ab -- eine Entscheidung, die man selbst trifft, statt einer Regel, die man nicht kennt. */ - { schluessel: "anruf", name: "Jemand ruft an", vorgabe: true }, + { schluessel: "anruf", name: "Jemand ruft an", vorgabe: true, ruhe: "nie" }, /* DER TAGESRUF (08.09.2026, screen10). Filipe: "eine taetige meldung einmal am tag zu aktivieren wenn noch aufgaben auf sind." @@ -232,7 +305,9 @@ export const ARTEN = [ IM TEXT STEHT NIE, WORUM ES GEHT. Eine Benachrichtigung erscheint auf einem Sperrbildschirm, den auch andere sehen. */ { schluessel: "hilfe", name: "Vertrauliche Meldung: neu oder beantwortet", vorgabe: true }, - { schluessel: "test", name: "Probe-Benachrichtigung", vorgabe: true }, + /* Die Probe beantwortet „kommt ueberhaupt etwas an?" -- und die + stellt man sich auch um 23 Uhr. */ + { schluessel: "test", name: "Probe-Benachrichtigung", vorgabe: true, ruhe: "nie" }, ]; const ARTEN_SCHLUESSEL = new Set(ARTEN.map((a) => a.schluessel)); @@ -274,8 +349,32 @@ export function vorlauf(minuten) { eine Umgebungsvariable und kein Programmierauftrag. */ const RUHE_AB = Number(process.env.PUSH_RUHE_AB ?? 22); const RUHE_BIS = Number(process.env.PUSH_RUHE_BIS ?? 7); -export function istRuhezeit(d = new Date()) { +/* Wann eine „spaet"-Meldung aufhoert. Ein Uhr und nicht zwei: Was um + ein Uhr anfaengt, ist um drei vorbei -- eine Einladung dazu erreicht + niemanden mehr, sie weckt nur. */ +const SPAET_AB = Number(process.env.PUSH_SPAET_AB ?? 1); + +/** Welche Ruheregel gilt fuer diese Art? + * + * ABGELEITET AUS DER ARTENLISTE, nicht danebengeschrieben. Eine + * zweite Aufzaehlung waere genau die, die beim naechsten Eintrag + * vergessen wird -- so ist es am 18.09. beim Anruf gewesen. */ +export function ruheRegel(art) { + return ARTEN.find((a) => a.schluessel === art)?.ruhe || "immer"; +} + +/** Ist gerade Ruhezeit -- fuer diese Regel? + * + * `regel` steht hinten und hat eine Vorgabe, damit vorhandene + * Aufrufer (und die Pruefungen) unveraendert weiterlaufen. */ +export function istRuhezeit(d = new Date(), regel = "immer") { + if (regel === "nie") return false; const s = d.getHours(); + /* „spaet" ist EIN Bereich und nicht zwei -- SPAET_AB liegt vor + RUHE_BIS. Die Rechnung von „immer" (zwei Bereiche um Mitternacht + herum) gilt hier also nicht, und sie hier mitzubenutzen waere ein + Fehler, den man erst um ein Uhr nachts sieht. */ + if (regel === "spaet") return s >= SPAET_AB && s < RUHE_BIS; return s >= RUHE_AB || s < RUHE_BIS; } @@ -420,25 +519,67 @@ function zielFuer(personId, ziel) { } } +/** Welches Haus traegt diese Meldung? -> "crew" oder "agentur" + * + * Nur fuer das GESICHT der Benachrichtigung (Symbol und Abzeichen). + * Ueber Rechte entscheidet das hier nichts -- das tut `zielFuer` eine + * Funktion weiter oben, und zwar unveraendert. + * + * ZWEISTUFIG, UND BEIDE STUFEN GIBT ES SCHON: + * + * 1. Die Zielseite, wenn sie in genau ein Haus gehoert. Damit + * traegt die Einladung zur Reaction das Crew-Gesicht -- auch + * fuer DogFather, der in beiden Haeusern arbeitet und sonst + * ueberall dasselbe Symbol saehe. + * + * 2. Sonst das Haus der Person. `AGENTUR_ROLLEN` ist die + * vorhandene Menge; wer nicht darin steht, arbeitet im + * Crew-Haus. Wortgleich zu `nurAnderesHaus()`. + * + * IM ZWEIFEL "agentur" -- das ist der Stand von vor dem 29.09.2026. + * Ein Fehler hier darf das Gesicht aendern, nie die Zustellung. */ +export function hausFuerMeldung(personId, ziel) { + try { + const pfad = String(ziel || "").split("?")[0].split("#")[0]; + const eintrag = GEHOERT_ZU_ADRESSE.find((e) => e.datei === pfad); + if (eintrag) return eintrag.zuhause === istCrewAdresse ? "crew" : "agentur"; + const p = db().prepare("SELECT rolle FROM personen WHERE id = ?").get(personId); + if (!p) return "agentur"; + return AGENTUR_ROLLEN.has(p.rolle) ? "agentur" : "crew"; + } catch (f) { + console.error("[push] Haus bestimmen:", f?.message); + return "agentur"; + } +} + export async function benachrichtige(personId, art, { titel, text, ziel, merkmal }) { if (!ARTEN_SCHLUESSEL.has(art)) return { verschickt: 0, grund: "unbekannte_art" }; if (!willHaben(personId, art)) return { verschickt: 0, grund: "abgeschaltet" }; - /* Die Probe darf immer durch -- sie beantwortet die Frage "kommt - ueberhaupt etwas an?", und die stellt man sich auch um 23 Uhr. + /* WELCHE RUHEREGEL GILT, STEHT AN DER ART (29.09.2026). Hier stand + bis dahin `art === "test" || art === "anruf"` -- eine Aufzaehlung + 230 Zeilen von der Artenliste entfernt, die niemand sieht, der + eine Art eintraegt. - UND EIN ANRUF AUCH (18.09.2026). Die Ruhezeit ist gegen - Erinnerungen gebaut: Dinge, die der Server von sich aus schickt, - weil eine Frist naeher rueckt. Ein Anruf ist das Gegenteil -- ein - Mensch drueckt gerade auf den Hoerer und wartet. - - Gefunden hat das Filipes Satz „sie bekommt nur eine + Gefunden hat den Fall Filipes Satz „sie bekommt nur eine benarichtigung ... irgendwas laeuft da gewaltig schief", um 23:44 Uhr. Serverseitig war alles in Ordnung; der Anruf wurde nur nie - ausgeliefert, weil es nach 22 Uhr war. Wer nachts seine Ruhe will, - schaltet die Art „Jemand ruft an" ab. */ - const dringend = art === "test" || art === "anruf"; - if (!dringend && istRuhezeit()) return { verschickt: 0, grund: "ruhezeit" }; + ausgeliefert, weil es nach 22 Uhr war. Behoben wurde damals genau + dieser eine Fall -- die Frage dahinter blieb stehen, und + `dogfather_live` faellt bis heute in dieselbe Falle. + + WER NACHTS SEINE RUHE WILL, schaltet die Art ab. Das ist eine + Entscheidung, die man selbst trifft, statt einer Regel, die man + nicht kennt. */ + const regel = ruheRegel(art); + if (istRuhezeit(new Date(), regel)) return { verschickt: 0, grund: "ruhezeit" }; + + /* DRAENGELN IST ETWAS ANDERES ALS NACHTS STOEREN. Der Push-Dienst + kennt eine Dringlichkeit, die darueber entscheidet, ob ein + schlafendes Geraet dafuer aufwacht. Das gilt fuer den Anruf und + die Probe -- nicht fuer eine Einladung: Die soll ankommen, wenn + das Geraet ohnehin wach wird, und keinen Akku dafuer verbrennen. */ + const dringend = regel === "nie"; /* Schon einmal geschickt? */ if (merkmal) { @@ -456,7 +597,13 @@ export async function benachrichtige(personId, art, { titel, text, ziel, merkmal nachsehen kann, wohin wirklich geschickt wurde, ohne die Nachricht aufmachen zu muessen. */ const zielEcht = zielFuer(personId, ziel); - const inhalt = JSON.stringify({ titel, text, art, ziel: zielEcht }); + /* DAS HAUS WIRD AUS DEM ECHTEN ZIEL BESTIMMT, nicht aus dem + gewuenschten. Wird jemand von `zielFuer` auf die Startseite + umgeleitet, weil die Seite fuer ihn nicht existiert, soll auch das + Gesicht dazu passen -- sonst steht das Crew-Symbol ueber einer + Meldung, die auf der Agenturstartseite landet. */ + const haus = hausFuerMeldung(personId, zielEcht); + const inhalt = JSON.stringify({ titel, text, art, ziel: zielEcht, haus }); let raus = 0; for (const a of anmeldungen) { @@ -487,7 +634,12 @@ export async function benachrichtige(personId, art, { titel, text, ziel, merkmal db().prepare("INSERT OR REPLACE INTO push_verschickt (person_id, merkmal, zeit) VALUES (?,?,?)") .run(personId, merkmal, jetzt()); } - return { verschickt: raus, grund: raus ? "ok" : "nicht_zugestellt", ziel: zielEcht }; + /* `haus` kommt mit zurueck, damit eine Pruefung nachsehen kann, + welches Gesicht verschickt wurde, ohne die verschluesselte + Nachricht aufmachen zu muessen -- derselbe Gedanke wie beim + Ziel. */ + return { verschickt: raus, grund: raus ? "ok" : "nicht_zugestellt", + ziel: zielEcht, haus }; } /* --------------------------------------------------------------------- diff --git a/server/workspace-reaktion.js b/server/workspace-reaktion.js index d5998515..ec69db4a 100644 --- a/server/workspace-reaktion.js +++ b/server/workspace-reaktion.js @@ -66,6 +66,10 @@ import { Sendung weiterschreibt -- genau die Luecke, die man erst bemerkt, wenn sie benutzt wird. */ import { treffSperreLesen } from "./treff-tabellen.js"; +/* Wer zum Crew-Haus gehoert, steht in `crew-adresse.js` -- die + Einladung liest dieselbe Menge wie die Hausweiche, statt eine + zweite danebenzustellen. */ +import { AGENTUR_ROLLEN } from "./crew-adresse.js"; export const reaktionRouter = express.Router(); @@ -677,6 +681,13 @@ reaktionRouter.post("/workspace/api/reaktion/stand", nurHost, express.json({ lim }); const l = lage(req.person); melden("reaktion", { was: "stand", lage: oeffentlich(l) }); + + /* ERST JETZT EINLADEN, nach dem Schreiben und nach `melden`. + Wer die Meldung antippt, landet auf einer Seite, die den + neuen Stand schon kennt -- sonst sieht der Schnellste einen + Saal, der noch zu ist. */ + if (neu === "live" && s.stand !== "live") einladen(sendung(), req.person.id); + res.json(l); } catch (fehler) { console.error("[reaktion] Stand:", fehler?.message); @@ -1495,6 +1506,105 @@ reaktionRouter.post("/workspace/api/reaktion/werbung", nurHost, } }); +/* ===================================================================== + DIE EINLADUNG -- wenn die Sendung aufgeht (29.09.2026) + + Filipe: „ich will dass du die benarichtigungen perfektionierst. + dogfather und alle anderen sollen die benachrichtigungen perfekt + von dieser seite hier bekommen." + + NACHGEMESSEN WAR DIESE DATEI STUMM: kein einziges `benachrichtige`. + Geht die Sendung auf Sendung, ruft sie `melden("reaktion", ...)` -- + und das laeuft ueber den Ereignisstrom, also NUR zu Leuten, die die + Seite ohnehin schon offen haben. Wer nicht hinschaut, erfaehrt + nichts. Das Kino macht auf, und die Einladung verlaesst das Haus + nie. + + Genau dasselbe Muster wie bei den Bewerbungen am 23.09.: beide Wege + gebaut, beide stumm. + ===================================================================== */ + +/** Wer wird eingeladen? + * + * Alle mit einem angemeldeten Geraet, deren Rolle NICHT der Agentur + * gehoert -- das ist woertlich die Hausteilung aus `crew-adresse.js` + * und keine neue Liste. Ein Scout bekaeme sonst eine Einladung in + * ein Kino, dessen Tuer fuer ihn nicht existiert: `zielFuer` schickt + * ihn auf die Startseite, und er stuende vor einer Meldung, die + * nirgendwohin fuehrt. + * + * OHNE DEN, DER EINGESCHALTET HAT. Er hat gerade den Knopf + * gedrueckt. */ +function eingeladene(ausser) { + return db().prepare(`SELECT DISTINCT a.person_id AS id, p.rolle + FROM push_anmeldungen a JOIN personen p ON p.id = a.person_id`).all() + .filter((z) => z.id !== ausser && !AGENTUR_ROLLEN.has(z.rolle)) + .map((z) => z.id); +} + +/** Lief eben schon eine Einladung raus? + * + * DIE BREMSE GEGEN DEN NEUSTART. Das Merkmal allein reicht nicht: + * Es haengt an `gestartet_am`, und das wird bei jedem Wechsel nach + * „live" neu gesetzt. Wer aus Versehen auf „Vorbereitung" und gleich + * wieder auf „Auf Sendung" drueckt -- oder wem der Rechner + * abstuerzt --, laedt sonst zweimal ein. Acht Menschen bekommen dann + * zwei Meldungen fuer dieselbe Sendung, und das ist der schnellste + * Weg, dass jemand die Art abschaltet. + * + * ZWEI STUNDEN, weil eine Reaction etwa so lange dauert. Wer danach + * eine zweite, wirklich neue Sendung aufmacht, laedt zu Recht neu + * ein. + * + * Gefragt wird `push_verschickt` -- also das, was WIRKLICH rausging. + * Wurde die erste Einladung verschluckt (Ruhezeit, kein Geraet), + * steht dort nichts, und der zweite Versuch darf. */ +function ebenSchonEingeladen() { + const z = db().prepare( + `SELECT MAX(zeit) AS zuletzt FROM push_verschickt + WHERE merkmal LIKE 'reaktion_live:%'`).get(); + if (!z?.zuletzt) return false; + return (Date.now() - Date.parse(z.zuletzt)) < 2 * 60 * 60 * 1000; +} + +/** Die Einladung verschicken -- ohne die Antwort aufzuhalten. + * + * Der Knopf „Auf Sendung" muss sofort reagieren. Acht + * Verschluesselungen und acht Netzwege dauern laenger als die + * Antwort dauern darf, und wenn ein Push-Dienst gerade klemmt, + * haenge die ganze Sendung daran. Deshalb losgeschickt und nicht + * abgewartet -- ein Fehler dabei steht im Protokoll, nicht im Weg. */ +function einladen(sendungJetzt, hostId) { + (async () => { + try { + if (ebenSchonEingeladen()) { + console.log("[reaktion] Einladung uebersprungen -- vor weniger als " + + "zwei Stunden lief schon eine."); + return; + } + const { benachrichtige } = await import("./workspace-push.js"); + const wer = eingeladene(hostId); + const titel = String(sendungJetzt.video_titel || sendungJetzt.titel || "").trim(); + let raus = 0; + for (const id of wer) { + const e = await benachrichtige(id, "reaktion_live", { + titel: "Die Reaction läuft", + /* WAS GESCHAUT WIRD, STEHT DRIN -- das ist der Unterschied + zwischen „komm rein" und „komm rein, wir schauen X". Ist + kein Titel da, wird keiner erfunden. */ + text: titel ? `Wir schauen: ${titel}` : "DogFather hat die Sendung aufgemacht.", + ziel: "/workspace/reaktion.html", + merkmal: `reaktion_live:${sendungJetzt.gestartet_am || jetzt()}`, + }); + if (e?.verschickt) raus++; + } + console.log(`[reaktion] Einladung an ${raus} von ${wer.length} verschickt.`); + } catch (f) { + console.error("[reaktion] Einladung:", f?.message); + } + })(); +} + /* Der Stand der Einblendung -- an EINER Stelle gerechnet. Drei Aufrufer (setzen, anlegen, aendern) brauchen dasselbe Ergebnis; dreimal abgeschrieben waere die Gelegenheit, dass einer davon diff --git a/tools/abzeichen-bauen.mjs b/tools/abzeichen-bauen.mjs new file mode 100644 index 00000000..1f6a94af --- /dev/null +++ b/tools/abzeichen-bauen.mjs @@ -0,0 +1,172 @@ +/* ===================================================================== + tools/abzeichen-bauen.mjs — das Abzeichen der Benachrichtigungen. + + WAS EIN ABZEICHEN IST, UND WAS NICHT + + Eine Benachrichtigung hat unter Android ZWEI Bilder: + + `icon` — das bunte Symbol im aufgeklappten Meldungskasten. + `badge` — das winzige Zeichen oben in der Statusleiste, 24 dp. + + Vom `badge` benutzt das System AUSSCHLIESSLICH den Alphakanal und + malt ihn einfarbig weiß. Alles Bunte daran geht verloren. + + Bis zum 29.09.2026 stand dort `workspace-192.png` — ein + vollflächiges, quadratisches Bild OHNE Transparenz. Dessen + Alphakanal ist überall voll. In der Statusleiste erschien deshalb + ein ausgefüllter Klotz und kein Zeichen. Das fällt nicht als Fehler + auf, weil nichts abstürzt; es sieht nur billig aus. + + --------------------------------------------------------------------- + WARUM EINE FLÄCHE UND KEIN STRICHBILD + + Drei Fassungen gebaut und bei echter Größe verglichen, statt zu + raten: + + A) die Tinte des Logos (Strichbild) — bei 24 px ein Gekrissel + B) dieselbe, verdickt — besser, immer noch unruhig + C) der geschlossene Umriss, gefüllt — trägt + + Die Ohrkerbe des Huskys ist bei 512 px zwei Prozent der Breite. Bei + 24 px ist sie ein halber Pixel und wird zu Grau. Eine Fläche + überlebt das Verkleinern, ein Strich nicht. + + --------------------------------------------------------------------- + EIN ABZEICHEN FÜR BEIDE HÄUSER — UND ZWAR ABSICHTLICH + + Crew und Agentur sind getrennt, und das gilt auch hier: Das bunte + `icon` ist je Haus ein anderes (crew-192 / workspace-192). Das + Abzeichen ist es NICHT, und das ist keine vergessene Trennung. + + Es ist 24 Pixel groß und einfarbig. Der Unterschied zwischen den + beiden Symbolen ist die Chili der Agentur — bei 24 px ein Fleck von + zwei Pixeln, der nicht als Chili lesbar ist, sondern als Schmutz + neben dem Kopf. Der Husky ist die Marke beider Häuser; ihn zu + teilen verrät nichts über das jeweils andere. + + --------------------------------------------------------------------- + WARUM DAS EIN SKRIPT IST UND KEIN EINMALIGER HANDGRIFF + + Ändert sich das Logo, wird das Abzeichen mit einem Befehl neu + gebaut. Ein von Hand zugeschnittenes PNG wäre in dem Moment eine + Kopie, die niemand mehr pflegt — dieselbe Falle wie eine + abgeschriebene Liste. + + node tools/abzeichen-bauen.mjs + ===================================================================== */ + +import { spawnSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { existsSync, statSync } from "node:fs"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const REPO = join(HIER, ".."); +const QUELLE = join(REPO, "assets", "img", "marke", "logo-kopf-frei.png"); +const ZIEL = join(REPO, "assets", "img", "abzeichen-96.png"); + +/* Die Rechnung steht in Python, weil dort Pillow liegt — Node hat im + Haus keine Bildbibliothek, und eine dafür zu installieren wäre eine + Abhängigkeit für 40 Zeilen. */ +const RECHNUNG = String.raw` +import sys +from collections import deque +from PIL import Image +import numpy as np + +quelle, ziel = sys.argv[1], sys.argv[2] +i = Image.open(quelle).convert("RGBA") + +# DIE ZEICHNUNG STECKT IM ALPHAKANAL. Gemessen: RGB ist in dieser +# Datei ueberall schwarz, das weisse Fell ist DURCHSICHTIG, nicht +# weiss. Wer hier auf Helligkeit schwellt, bekommt ein leeres Bild. +tinte = np.array(i.split()[3]) > 90 +h, w = tinte.shape +if not tinte.any(): + sys.exit("Die Quelle hat keine Zeichnung im Alphakanal.") + +# ---- Von aussen fluten: was der Rand erreicht, ist aussen ---------- +aussen = np.zeros((h, w), bool) +st = deque() +for x in range(w): + for y in (0, h - 1): + if not tinte[y, x] and not aussen[y, x]: + aussen[y, x] = True; st.append((y, x)) +for y in range(h): + for x in (0, w - 1): + if not tinte[y, x] and not aussen[y, x]: + aussen[y, x] = True; st.append((y, x)) +while st: + y, x = st.pop() + for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)): + ny, nx = y + dy, x + dx + if 0 <= ny < h and 0 <= nx < w and not tinte[ny, nx] and not aussen[ny, nx]: + aussen[ny, nx] = True; st.append((ny, nx)) +voll = ~aussen + +# ---- Nur die groesste zusammenhaengende Flaeche ------------------- +# Im Logo liegen unter dem Kinn 18 lose Sprenkel. Bei 512 px sind sie +# Beiwerk, bei 24 px sind sie Schmutz neben dem Zeichen. Gezaehlt +# statt weggefiltert: Ein Filter muesste raten, wie dick ein Sprenkel +# hoechstens ist. +marke = np.zeros((h, w), np.int32) +nr, gr = 0, {} +for sy in range(h): + for sx in range(w): + if voll[sy, sx] and marke[sy, sx] == 0: + nr += 1; n = 0 + s2 = deque([(sy, sx)]); marke[sy, sx] = nr + while s2: + y, x = s2.pop(); n += 1 + for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)): + ny, nx = y + dy, x + dx + if 0 <= ny < h and 0 <= nx < w and voll[ny, nx] and marke[ny, nx] == 0: + marke[ny, nx] = nr; s2.append((ny, nx)) + gr[nr] = n +best = max(gr, key=gr.get) +kopf = Image.fromarray(((marke == best) * 255).astype(np.uint8), "L") + +# ---- Zuschneiden, mittig, etwas Luft ------------------------------ +k = kopf.getbbox() +aus = kopf.crop(k) +s = max(aus.size); rand = int(s * 0.06) +feld = Image.new("L", (s + 2 * rand, s + 2 * rand), 0) +feld.paste(aus, (rand + (s - aus.width) // 2, rand + (s - aus.height) // 2)) + +# ---- 96 px, weiss, Form im Alphakanal ----------------------------- +maske = feld.resize((96, 96), Image.LANCZOS) +bild = Image.new("RGBA", (96, 96), (255, 255, 255, 255)) +bild.putalpha(maske) +bild.save(ziel, optimize=True) + +deckung = (np.array(maske) > 127).sum() / (96 * 96) +print(" %d Flaechen, %d Sprenkel verworfen, Deckung %.0f %%" + % (nr, nr - 1, 100 * deckung)) +# DIE ZAHL GEHOERT IN DIE BEDINGUNG. Ein Abzeichen, das 2 % deckt, ist +# ein Staubkorn; eines mit 90 % ist der Klotz, den wir gerade +# abschaffen. Beides faellt hier auf, nicht erst auf dem Telefon. +if not (0.15 <= deckung <= 0.75): + sys.exit("Deckung %.0f %% liegt ausserhalb des Sinnvollen." % (100 * deckung)) +`; + +if (!existsSync(QUELLE)) { + console.error(`Die Quelle fehlt: ${QUELLE}`); + process.exit(2); +} + +const lauf = spawnSync("python", ["-c", RECHNUNG, QUELLE, ZIEL], { encoding: "utf8" }); +if (lauf.error?.code === "ENOENT") { + /* DER DRITTE AUSGANG. „Python ist nicht da" ist etwas anderes als + „das Abzeichen ist falsch" — und darf nicht wie ein Befund + aussehen. */ + console.error("Python ist auf diesem Rechner nicht im Pfad — das Abzeichen " + + "wurde NICHT neu gebaut. Die vorhandene Datei bleibt, wie sie ist."); + process.exit(2); +} +if (lauf.stdout) process.stdout.write(lauf.stdout); +if (lauf.status !== 0) { + console.error(lauf.stderr?.trim() || "Der Bau ist gescheitert."); + process.exit(1); +} +const gross = statSync(ZIEL).size; +console.log(` ${ZIEL.replace(REPO, "").replace(/\\/g, "/")} — ${gross} Bytes`); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 7eb492d3..4e7fda57 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +