Phases 2 and 3 of plan/23-monitor.md. /api/summary adds: server online/MOTD/ version, players online with NMSR heads, registered-account count, disk free on the world volume, backup freshness, join/leave history, and TPS. Each tile has its own TTL cache and its own failure mode. A failed collector keeps serving its last good value and reports the reason in a sibling *_err field, so the page goes stale-with-a-warning rather than blank. The five collectors refresh concurrently: sequentially, one request during an outage would cost the sum of every timeout (~15s), and an outage is exactly when someone is looking at this page. Measured with a dead dependency: 0.24s. TPS needs RCON, so the monitor now holds RCON_PASSWORD. Read-only survives because the allowlist is a compile-time constant and no HTTP input reaches a command string — the UI cannot run commands because the server cannot be asked to. The Source RCON client is implemented in-repo rather than added as a dependency precisely to keep that property local and auditable. Two things the real server corrected: - `neoforge tps` prints "Overall: 20.000 TPS (0.357 ms/tick)", not the older Forge "Mean tick time / Mean TPS" wording the plan assumed. main_test.go pins the real captured output so a format change fails a test instead of silently zeroing the tile. - Rosters come from mc-status, which already holds the Drasl admin credentials and exposes a sanitized list, so this container does not get a second copy. Also fixes a phase-1 bug: the UI's level regex assumed itzg's console timestamp ([12:34:56]) but the file uses [01Aug2026 22:55:34.358], so every line was classified INFO and the ERROR/WARN chips did nothing. It now matches on shape. The container runs as root deliberately — /data is drwxr-x--- owned by uid 1000, so a non-root user cannot traverse it and the tailer would silently read nothing. The :ro mounts and absent port are what bound the damage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
54 lines
2.0 KiB
Go
54 lines
2.0 KiB
Go
package main
|
|
|
|
// Locks the two things that are easy to break silently: the RCON allowlist
|
|
// (the guarantee that this service cannot run commands) and the `neoforge tps`
|
|
// output format, which differs from the older Forge wording.
|
|
|
|
import "testing"
|
|
|
|
func TestParseTPSRealOutput(t *testing.T) {
|
|
// Captured verbatim from `docker exec minecraft rcon-cli neoforge tps` on prod,
|
|
// ANSI resets included.
|
|
raw := "Overworld: 20.000 TPS (0.298 ms/tick)\n\x1b[0mThe Nether: 19.100 TPS (0.014 ms/tick)\n" +
|
|
"\x1b[0mworld_dimension_nexus:creative: 20.000 TPS (0.009 ms/tick)\n" +
|
|
"\x1b[0mOverall: 18.500 TPS (0.357 ms/tick)\n\x1b[0m"
|
|
got, err := parseTPS(raw)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if got.TPS != 18.5 || got.MSPT != 0.357 {
|
|
t.Fatalf("overall wrong: %+v", got)
|
|
}
|
|
if len(got.Dimensions) != 3 {
|
|
t.Fatalf("want 3 dims, got %d: %+v", len(got.Dimensions), got.Dimensions)
|
|
}
|
|
}
|
|
|
|
func TestParseTPSUnknownCommand(t *testing.T) {
|
|
if _, err := parseTPS("Unknown or incomplete command, see below for error\nforge tps<--[HERE]"); err == nil {
|
|
t.Fatal("want error for unknown command")
|
|
}
|
|
}
|
|
|
|
func TestRCONAllowlist(t *testing.T) {
|
|
for _, bad := range []string{"op Oier", "stop", "say hi", "ban x", "", "neoforge tps ", "LIST"} {
|
|
if _, err := rconQuery("127.0.0.1:1", "pw", bad, 0); err != errRCONNotAllowed {
|
|
t.Fatalf("command %q was not rejected by the allowlist (err=%v)", bad, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionParse(t *testing.T) {
|
|
e := newEvents(10)
|
|
e.observe("[01Aug2026 22:55:34.358] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Oier joined the game")
|
|
e.observe("[01Aug2026 23:01:02.111] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Oier left the game")
|
|
e.observe("[01Aug2026 23:02:00.000] [Server thread/INFO] [x/]: something else entirely")
|
|
got := e.recent(10)
|
|
if len(got) != 2 {
|
|
t.Fatalf("want 2 events, got %d: %+v", len(got), got)
|
|
}
|
|
if got[0].Action != "left" || got[0].Player != "Oier" || got[0].Time != "01Aug2026 23:01:02.111" {
|
|
t.Fatalf("newest-first wrong: %+v", got[0])
|
|
}
|
|
}
|