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) <noreply@anthropic.com>
This commit is contained in:
1 parent
3eb20054a0
commit
d105fc9acf
3 files changed
+77
-1
No files matched your search
@@ -251,7 +251,8 @@ Full design settled across WS1–WS6; all tasks unchecked, ready to execute.
|
|||||||
**outside mc-backup** and unquota'd (accepted — eyeballed).
|
**outside mc-backup** and unquota'd (accepted — eyeballed).
|
||||||
- [x] **Monitor** (`monitor.`, 2026-08-01) — admin-only `latest.log` stream +
|
- [x] **Monitor** (`monitor.`, 2026-08-01) — admin-only `latest.log` stream +
|
||||||
dashboard tiles (status, TPS via allowlisted RCON, players, disk, backups,
|
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`.
|
Follow-up: add the Uptime Kuma monitor for `http://monitor:8080/healthz`.
|
||||||
- [ ] **Verify NeoForge version** against current stable before first deploy.
|
- [ ] **Verify NeoForge version** against current stable before first deploy.
|
||||||
- [x] **Bootstrap Drasl admin** account (`admin`, 2026-06-10). Key is
|
- [x] **Bootstrap Drasl admin** account (`admin`, 2026-06-10). Key is
|
||||||
|
|||||||
@@ -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
|
(with NMSR heads), registered accounts, disk free, backup freshness and recent
|
||||||
joins/leaves.
|
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
|
**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
|
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`.
|
`basic_auth`. There are no controls in the UI — every route is a `GET`.
|
||||||
|
|||||||
@@ -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
|
`/api/summary` every 10s and `/api/log` every 2s — tiles move far more slowly
|
||||||
than the log does.
|
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
|
### Running as root, on purpose
|
||||||
|
|
||||||
`/data` is `drwxr-x---` owned by uid 1000 (itzg). A non-root container user
|
`/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
|
- [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).
|
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
|
### Follow-ups
|
||||||
|
|
||||||
- [ ] Uptime Kuma HTTP monitor against `http://monitor:8080/healthz` (internal,
|
- [ ] Uptime Kuma HTTP monitor against `http://monitor:8080/healthz` (internal,
|
||||||
|
|||||||
Reference in new issue
Block a user