feat(landing): live server card backed by self-hosted mc-status
Add a featured live ServerCard to the landing (replaces the static ServerListPanel in hero + status sections): server favicon, color MOTD, online/offline pill, players online/max with fill bar, and a player-head row rendered by our own NMSR from Drasl skins. Progressive enhancement — SSG skeleton degrades gracefully when JS or the API is unavailable. Back it with a self-hosted pinger instead of the public api.mcstatus.io: mcstatus.io's API service is closed-source (only the mcutil library is open), so docker/mc-status wraps mcutil and re-emits its v2 JSON shape, keeping the frontend unchanged. The service ignores the path address and only pings MC_STATUS_TARGET (no SSRF relay), with a 30s TTL cache. Exposed same-origin via caddy at the apex /api/mcstatus/* path (no new DNS subdomain or LE cert change, no CORS). Uptime Kuma stays the uptime history + alerting backend; see plan/15-mc-status.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 10 — Uptime Kuma
|
||||
|
||||
> Distinct from **`plan/15-mc-status.md`**: Kuma is the uptime *history* +
|
||||
> alerting backend; mc-status is the live data feed for the landing's server
|
||||
> card. Both ping `minecraft` over `mcnet` but serve different purposes.
|
||||
|
||||
Status monitoring + public status page. Service `uptime-kuma`
|
||||
(`louislam/uptime-kuma:1`), web UI on `:3001`, reached only through caddy at
|
||||
`https://status.${BASE_DOMAIN}`. Joined to `mcnet`, so it can probe every other
|
||||
|
||||
95
plan/15-mc-status.md
Normal file
95
plan/15-mc-status.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 15 — mc-status (self-hosted live server card backend)
|
||||
|
||||
Self-hosted Minecraft Server List Ping → JSON service that feeds the landing's
|
||||
live **ServerCard**. Replaces the third-party `api.mcstatus.io` dependency.
|
||||
|
||||
## Why not just use api.mcstatus.io
|
||||
|
||||
The public API works (CORS `*`, no key), but mcstatus.io's **API service is not
|
||||
open source** — only its underlying Go library, `mcutil`, is. To self-host we
|
||||
run the *same library* behind our own thin HTTP shell that re-emits mcstatus.io's
|
||||
**v2 JSON shape**, so the frontend (`ServerCard.astro`) stays byte-identical —
|
||||
only the URL changes. itzg/mc-status is deprecated; mc-monitor only exports
|
||||
Prometheus metrics (no icon/MOTD/player sample), so neither fits the card.
|
||||
|
||||
## Service
|
||||
|
||||
`docker/mc-status/` — Go, built from source (multi-stage → scratch), image
|
||||
`ulicraft/mc-status:local`. Wraps `github.com/mcstatus-io/mcutil/v4`
|
||||
`status.Modern()` and maps `StatusModern` → v2 JSON:
|
||||
|
||||
| mcutil field | v2 field ServerCard reads |
|
||||
|---|---|
|
||||
| `Players.Online/Max` | `players.online` / `players.max` |
|
||||
| `Players.Sample[].ID` | `players.list[].uuid` |
|
||||
| `Players.Sample[].Name` (`formatting.Result`) | `players.list[].name_raw/clean/html` |
|
||||
| `MOTD` (`formatting.Result`) | `motd.raw/clean/html` |
|
||||
| `Favicon` (`data:image/png;base64,…`) | `icon` |
|
||||
|
||||
On any ping failure it returns `{"online": false}` so the card shows an offline
|
||||
state without erroring.
|
||||
|
||||
### Hardening
|
||||
|
||||
- **No open SSRF relay.** The `<addr>` segment in `/v2/status/java/<addr>` is
|
||||
**ignored** — the service only ever pings `MC_STATUS_TARGET`. The segment
|
||||
exists only to keep the URL drop-in compatible with the public API.
|
||||
- **TTL cache** (`MC_STATUS_CACHE_TTL`, default `30s`) so a busy landing page
|
||||
can't ping the Minecraft server once per visitor.
|
||||
|
||||
### Env
|
||||
|
||||
| Var | Default | Notes |
|
||||
|---|---|---|
|
||||
| `MC_STATUS_TARGET` | `minecraft:25565` | pinged internally over `mcnet` |
|
||||
| `MC_STATUS_CACHE_TTL` | `30s` | in-memory cache window |
|
||||
| `MC_STATUS_LISTEN` | `:8080` | bind address |
|
||||
|
||||
## Ingress
|
||||
|
||||
Path-on-apex (no new DNS subdomain, no Let's Encrypt cert change — DNS/TLS are
|
||||
managed outside this repo). `caddy/conf.d/10-static.caddy`:
|
||||
|
||||
```caddy
|
||||
handle /api/mcstatus/* {
|
||||
uri strip_prefix /api/mcstatus
|
||||
reverse_proxy mc-status:8080
|
||||
}
|
||||
```
|
||||
|
||||
Same-origin → the ServerCard fetch needs no CORS. `site.ts`:
|
||||
`statusApi: \`/api/mcstatus/v2/status/java/${BASE_DOMAIN}\``.
|
||||
|
||||
## Data flow
|
||||
|
||||
```
|
||||
guest browser ─ GET /api/mcstatus/v2/status/java/${BASE_DOMAIN}
|
||||
└► caddy (apex) ─ strip_prefix /api/mcstatus ─► mc-status:8080
|
||||
└► mcutil SLP ping ─► minecraft:25565 (internal, mcnet)
|
||||
◄── v2 JSON {online, players.list[uuid,name_*], motd.html, icon}
|
||||
ServerCard → player heads via NMSR: avatar.${BASE_DOMAIN}/headiso/<uuid>?size=64
|
||||
```
|
||||
|
||||
## Relationship to Uptime Kuma (plan/10)
|
||||
|
||||
Distinct roles. **Kuma** = uptime *history* + alerting backend (Minecraft Server
|
||||
monitor, status page). **mc-status** = live, public-facing data for the landing
|
||||
card (richer: icon/MOTD/player heads). Both ping `minecraft` over `mcnet`.
|
||||
|
||||
## Validation notes
|
||||
|
||||
Built + `go vet`/`gofmt` clean. The live ping path can't be exercised from the
|
||||
dev sandbox (egress proxy allows TCP connect but filters the raw SLP handshake →
|
||||
`context deadline exceeded`); it resolves on the prod host where `minecraft` is
|
||||
reachable over `mcnet`. After deploy, confirm:
|
||||
|
||||
```
|
||||
curl -s https://${BASE_DOMAIN}/api/mcstatus/v2/status/java/${BASE_DOMAIN} | jq .
|
||||
```
|
||||
expecting `online:true`, a populated `players.list`, and `motd.html`.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] `docker compose up -d --build mc-status` on prod, verify the curl above.
|
||||
- [ ] Confirm the SLP player sample carries real (Drasl) UUIDs so NMSR heads
|
||||
render; if empty, the card falls back to the count-only / empty state.
|
||||
Reference in New Issue
Block a user