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>
Add a static /register page (en/es/eu) that calls the Drasl REST API v2
directly from the browser: register -> login -> optional skin upload, all
in the pixel design. Guests never touch Drasl's own web UI.
Drasl config gains the pieces this needs:
- CORSAllowOrigins scoped to the apex (the API has no CORS until set;
never "*").
- [RateLimit] for the now public-facing anonymous POST /users.
- [RegistrationNewPlayer] with RequireInvite driven by a new
REGISTRATION_MODE (invite|open) .env flag.
REGISTRATION_MODE has two consumers from one key: render-config.sh derives
the TOML boolean for Drasl (drasl is TOML-only, no env), and the landing
build reads it to show/hide the invite-code field. render-config.sh halts
on any value other than invite/open.
Security verified against drasl source: anonymous POST /users cannot set
privileged fields (isAdmin/isLocked/chosenUuid/maxPlayerCount are gated on
callerIsAdmin in CreateUser), so browser-direct registration is safe.
Docs: plan/16-landing-registration.md captures the design + the B1 vs fork
decision; build-order, deploy, and the deploy skill wire REGISTRATION_MODE
and the landing-rebuild requirement (www/ is gitignored, not updated by a
git pull).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The guest landing page advertised http:// service URLs (mixed-content on the
https origin) and the wrong packwiz subdomain:
- authUrl / authlibUrl: http -> https
- packwizUrl: http://packwiz. -> https://pack. (correct vhost)
- serverAddress: mc. -> apex (connect to ${BASE_DOMAIN}:25565)
- remove caCertUrl + the "trust the local CA" join step (airgap-only, /ca.crt
no longer exists) and its en/es/eu i18n strings + type.
Also fix the stale http BaseURL example in plan/03-drasl.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt the "Carved from stone" pixel design (landing/design/) as the apex
onboarding page, ported from its React/Babel-via-CDN prototype to static
Astro so it works on an offline LAN. Real modded-LAN content replaces the
prototype's fictional public-SMP copy.
- Vendor web fonts locally (tooling/fetch-fonts.sh -> public/fonts/) so the
page renders without the Google Fonts CDN at party time.
- Port the design system (main.css: mood/hero/bevels) and split markup into
Astro components (Creeper, PixelIcon, CopyChip, ServerListPanel).
- site.ts holds language-neutral config + theme knobs (mood/hero/headFont/
dust) that replace the design's in-browser TweaksPanel; LITERALS holds
never-translated product/in-game terms.
- i18n: English (/), Spanish (/es/), Euskera (/eu/) generated from one
[...lang].astro via getStaticPaths; copy lives in src/i18n/ui.ts. Nav has
an EN/ES/EU switcher; html lang + hreflang set per page.
- Status panel shows a static server-list row + honest stat tiles (no fake
live count). Copy + scroll-reveal are the only client JS.
Build emits to ../www (gitignored). Euskera strings pending a native review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Air-gap mirror uses tls internal; guests must trust Caddy's local CA. Serve it
at http://<domain>/ca.crt from a world-readable mount (the live pki dir is
root-only 0600). build-stack --up mirror|full exports the CA after bring-up;
landing page links it. Pre-creates the mount target to avoid a dir bind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Minecraft is a custom TCP protocol, not HTTP, so the no-port UX is solved with
a DNS SRV record (_minecraft._tcp.mc.<domain> -> :25565) rather than an HTTP
reverse proxy. Landing page now shows the bare address.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>