From d105fc9acf7e1b2d0e41e37580b4f026586326ea Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Sun, 2 Aug 2026 03:39:47 +0200 Subject: [PATCH] docs(monitor): document join/leave browser notifications Records why it is browser-only rather than chat push (a push channel needs a CA bundle in the scratch image, a token, and outbound egress this container does not have), why newness is a localStorage seen-set keyed on the log timestamp rather than a server-side sequence number (the counter resets on restart, the timestamp does not), and the unconditional-baseline subtlety. Also states plainly what the node harness does NOT cover: the real permission prompt and actual notification rendering are stubbed, so those are untested. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 3 +- README.md | 4 +++ plan/23-monitor.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9097aba..a3cea67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -251,7 +251,8 @@ Full design settled across WS1–WS6; all tasks unchecked, ready to execute. **outside mc-backup** and unquota'd (accepted — eyeballed). - [x] **Monitor** (`monitor.`, 2026-08-01) — admin-only `latest.log` stream + dashboard tiles (status, TPS via allowlisted RCON, players, disk, backups, - joins/leaves). Live on cochi, all three phases done. See `plan/23-monitor.md`. + joins/leaves) + opt-in browser notifications on join/leave (bell toggle, + client-side only). Live on cochi, all four phases done. See `plan/23-monitor.md`. Follow-up: add the Uptime Kuma monitor for `http://monitor:8080/healthz`. - [ ] **Verify NeoForge version** against current stable before first deploy. - [x] **Bootstrap Drasl admin** account (`admin`, 2026-06-10). Key is diff --git a/README.md b/README.md index 15cfffc..14af984 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,10 @@ ERROR/WARN/INFO chips, above a row of tiles: server status, TPS, players online (with NMSR heads), registered accounts, disk free, backup freshness and recent joins/leaves. +The bell in the header turns on **browser notifications for player join/leave**. +Opt-in, off by default, remembered per browser; it fires only while the page is +open (a background tab counts, a closed one doesn't). + **Admin only, and read-only.** The log carries player IPs, chat and admin commands verbatim and nothing sanitizes it, so the whole vhost sits behind caddy `basic_auth`. There are no controls in the UI — every route is a `GET`. diff --git a/plan/23-monitor.md b/plan/23-monitor.md index 1929218..526b97e 100644 --- a/plan/23-monitor.md +++ b/plan/23-monitor.md @@ -135,6 +135,58 @@ TTLs: status 15s, roster 60s, disk 60s, backup 60s, TPS 30s. The UI polls `/api/summary` every 10s and `/api/log` every 2s — tiles move far more slowly than the log does. +## Join/leave browser notifications (opt-in) + +A bell toggle in the header raises a desktop notification when a player joins or +leaves. It rides the **same `sessions[]`** the Recent joins tile renders — no +second data source, no re-parsing of the log in JS. Entirely client-side: +no server change, no new egress, no secret. The container still reaches nothing +outside `mcnet`. + +Deliberately **not** a push channel. Chat push (Telegram/Discord/ntfy) would +reach a phone with nothing open, but it needs a CA bundle in the `scratch` image +(this container currently makes no TLS calls at all), a token in `.env`, and +outbound internet egress it does not have today. Browser notifications cost none +of that, at the price of only firing while the page is open — a background tab +is fine, a closed tab or sleeping laptop is not. + +### Newness is a seen-set keyed on the log timestamp + +The whole difficulty is *not* notifying for history, and history arrives two +ways: the events ring is already populated when the page loads, and **the +cold-start backfill re-reads 5000 log lines on every deploy**, repopulating the +ring with the same events. + +So the client keeps a `time|player|action` seen-set in `localStorage` (capped at +the 500 newest keys). That key is stable across both a page reload and a +container restart. A server-side sequence number would **not** be — it resets to +zero on restart, so every historical event would look new and every deploy would +fire a notification burst. + +### The baseline flag must flip unconditionally + +The first response after a page load is history by definition, so it seeds the +seen-set and notifies nothing. The subtle part, caught by the tests rather than +by reading the code: that flag has to flip **whether or not any fresh events were +found**. A returning user already has every key in `localStorage`, so their first +response yields nothing fresh — flipping the flag lazily would burn the baseline +on their first *real* event instead and silently swallow it. + +### Other behaviour + +- **Opt-in, off by default.** Permission is requested from the bell click (a + user gesture), never on page load. +- **A denied permission shows a distinct blocked state**, because "I enabled it + and nothing happened" is otherwise indistinguishable from a bug. +- **Bursts coalesce**: more than 3 events in one poll (everyone rejoining after a + restart) becomes one "N joined, M left". +- **While enabled, `/api/summary` polls every 5s instead of 10s** — with this + feature on, the poll interval *is* the alert latency. +- Events are marked seen even while the toggle is off, so enabling it does not + replay a backlog. +- Each notification carries a `tag` derived from its event key, so a duplicate + cannot stack. + ### Running as root, on purpose `/data` is `drwxr-x---` owned by uid 1000 (itzg). A non-root container user @@ -322,6 +374,25 @@ one-container restart. - [x] Confirmed `neoforge tps` is the right command, and captured its real output format (see *RCON* above — it is not what this doc first assumed). +### Phase 4 — join/leave browser notifications — **DONE, live 2026-08-02** + +- [x] Bell toggle, opt-in, persisted in `localStorage`; permission requested from + the click gesture; blocked state surfaced. +- [x] Seen-set newness detection + unconditional first-response baseline. +- [x] Burst coalescing; 5s poll while enabled. +- [x] Fixed a bug this introduced: the level-chip handler bound to every + `.chip`, which now includes the bell. Scoped to `.chip[data-level]`. + +Verified by extracting the shipped block from `index.html` and running it in node +against stubbed `localStorage`/`Notification`: silent baseline, new event +notifies, re-polling silent, restart-replay silent (same instance *and* after a +reload), burst coalescing counts, the returning-user edge case, toggle-off still +tracking, denied permission silent, seen-set bounded at the 500 newest keys. + +**Not covered by that harness:** the actual browser permission prompt and whether +notifications render — those need a real browser and were not tested. Everything +downstream of `Notification.permission === "granted"` is stubbed. + ### Follow-ups - [ ] Uptime Kuma HTTP monitor against `http://monitor:8080/healthz` (internal,