feat(monitor): admin-only latest.log stream at monitor.

Phase 1 of plan/23-monitor.md: a small Go service that tails the Minecraft
server's latest.log into an in-memory ring buffer and serves it to an embedded
single-page UI (autoscroll, substring/regex filter, level chips).

Read-only by construction — /data is mounted :ro, there is no mutating route,
and the container holds no credentials. It publishes NO port: caddy's
basic_auth vhost is the only way in, which is the point, since the service has
no auth of its own and the log carries player IPs, chat and admin commands.

Two non-obvious choices, both to avoid prod-only failures:

- Polling, not SSE. The chain is host nginx -> caddy -> app, and nginx buffers
  proxied responses by default, so an EventSource would stall on cochi (whose
  vhost is an untracked per-host file) and not reproduce locally. Delay was
  accepted at design time, so polling is strictly better.

- The cursor is a sequence number, not a byte offset, and the tailer reopens on
  inode change or size shrink. itzg rotates latest.log on every server restart,
  i.e. every deploy — a naive tailer goes silent afterwards while still
  answering HTTP 200. Verified against rotate, truncate, delete/recreate,
  partial-line and ring-overflow cases.

MONITOR_ENABLED (on|off) gates both layers per host, mirroring FILES_BROWSE.

MONITOR_PASSWORD_HASH must be bcrypt AND single-quoted in .env: Compose
interpolates $-sequences in .env values, so a bare hash is silently mangled
($2a$14$abc... -> $$2a$$14...) and every login fails with no hint why.
Confirmed empirically, and it is the opposite of filestash, which needs $6$.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
oierandClaude Opus 5 committed 2026-08-01 22:51:39 +02:00
1 parent 8507ffd020
commit fca6747bcc
13 files changed
+1241 -4

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
# Monitor DISABLED (MONITOR_ENABLED=off in .env).
#
# This stub is mounted at /etc/caddy/conf.d/80-monitor.caddy instead of
# 80-monitor.on.caddy, so caddy defines NO vhost for monitor.${BASE_DOMAIN} and
# the log stream is unreachable on this host. The monitor container may still be
# running — it just has no ingress, and it publishes no port, so nothing can
# reach it.
#
# Caddy needs a parseable file here, not an absent one: the Caddyfile imports
# conf.d/*.caddy and the mount target is fixed. Comments only, on purpose.
+20
View File
@@ -0,0 +1,20 @@
# Monitor — admin-only latest.log stream (monitor service). Read-only: the UI
# only ever issues GETs, and the service exposes no mutating route at all.
# Full design: plan/23-monitor.md.
#
# basic_auth here IS the access control. The monitor container has no auth of
# its own and publishes no port, so caddy is the only path to it — there is no
# second door to also lock.
#
# The hash must be BCRYPT (`caddy hash-password`). Note this is the OPPOSITE of
# filestash (60-upload.caddy / plan/20-files.md), which only accepts $6$ and
# silently rejects bcrypt. Do not copy a hash between the two services.
#
# No path is exempted. Uptime Kuma probes /healthz as http://monitor:8080/healthz
# over mcnet, bypassing caddy entirely, so the whole vhost stays gated.
http://monitor.{$BASE_DOMAIN} {
basic_auth {
{$MONITOR_USER} {$MONITOR_PASSWORD_HASH}
}
reverse_proxy monitor:8080
}