Files
ulicraft-server-v1/plan/18-landing-rework.md
Oier Bravo Urtasun 6c7e259fcb feat(landing): join-flow rework, player rosters, account page, mod list
Execute plan/18 (WS1-6) + plan/17.

- WS1: homepage join = 3 steps off packwiz (register -> UlicraftLauncher
  -> join); per-OS downloads from a synced launcher-manifest.json (schema +
  example committed); Fjord moved to its own /fjord page. Drop packwizUrl.
- WS2/3: mc-status gains an isolated /players endpoint (admin login ->
  Drasl GET /players, own client+TTL+token cache, never blocks the status
  path); online + registered rosters render shared name+avatar tiles;
  AVATAR_MODE env (default headiso).
- WS4: /account skin CRUD via browser-direct Drasl (login -> upload ->
  reset), in-memory token, players[0]; register relinks "Manage it here".
- WS5: footer link to status.${BASE_DOMAIN} in every footer.
- WS6: tooling/build-modlist.py generates mods.json + extracted logos from
  the distribution forgemods; ModList.astro renders a flat list; stat tile
  fed from the count.

Manifest/mods loaders read at build with a process.cwd() fallback (the
import.meta.url-only path does not resolve in Astro's bundled build).

New env: AVATAR_MODE, DRASL_ADMIN_USERNAME, DRASL_ADMIN_PASSWORD (mc-status
only). Generated mods.json / launcher-manifest.json / logos are gitignored;
.example.json files document the shapes. Build: 12 pages; mc-status builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:17:14 +02:00

205 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Landing rework — join flow, player rosters, account, mod list, footer
> **Status: PLANNED — not started.** Five landing workstreams (WS1WS5) plus the
> mod-list component from `17-mod-shortlist.md` (WS6). Each is independent; suggested
> order: **WS5 → WS1 → WS2/3 → WS4 → WS6** (trivial first; rosters share avatar
> plumbing; account reuses register's skin JS; mods last).
The landing is an **Astro static site** (`landing/`, pnpm, output → `../www/`), built
on the host with `.env` sourced. Three locales (en/es/eu) in `i18n/ui.ts`, identical
shape; non-translatable config in `data/site.ts`. Runtime data (status, rosters, Drasl
API) is fetched client-side; everything else is SSG.
## New env keys (`.env` + `.env.example`)
| Key | Consumer | Purpose |
|---|---|---|
| `AVATAR_MODE` (default `headiso`) | landing build → `site.ts` | NMSR render mode for avatar tiles |
| `DRASL_ADMIN_USERNAME` / `DRASL_ADMIN_PASSWORD` | **mc-status** (compose env) | admin login for the registered-players roster — **never** reaches the browser |
Use a **dedicated, rotatable** Drasl admin account for these creds (Drasl has no
read-only role; full-admin, but isolates blast radius from your personal admin).
---
## WS1 — Join flow off packwiz (homepage = UlicraftLauncher, 3 steps)
The modpack now ships via **UlicraftLauncher** (rebranded HeliosLauncher in the
`custom-launcher` repo) — it installs mods (packwiz), auth (Drasl, preconfigured) and
all files itself. So the homepage join flow drops the packwiz step and the manual
authlib step.
**Homepage steps (3):**
1. **Register** — create your Ulicraft account (link `/register`).
2. **Download the Ulicraft Launcher** — per-OS download (from manifest); log in with
your credentials. (No authlib URL — launcher is preconfigured.)
3. **Join the server** — launch, connect to `site.serverAddress` (IP copy chip).
**Launcher manifest (delivery = synced local file, option B):**
- Contract: `landing/src/data/launcher-manifest.schema.json` (already written;
JSON-Schema draft 2020-12).
- The `custom-launcher` release flow **produces** `launcher-manifest.json` and syncs
it into `landing/src/data/launcher-manifest.json` (gitignored, generated), same
pattern as `sync-server-mods.sh`.
- Landing **imports** it at build → renders per-OS buttons from `builds[]`. Missing
file → hide the buttons (build must not fail).
**Fjord — alternative path, OWN page, off the homepage:** new `/fjord` (+ `/es`,
`/eu`). Download Fjord (existing `/launcher/` mirror), add authlib-injector account
(`site.authlibUrl` chip), import `manifest.fjordPack` (`.mrpack`), connect.
Tasks:
- [ ] `site.ts`: delete `packwizUrl`; replace `launcher` (Fjord hardcode) with the
manifest import; rename product display to "Ulicraft Launcher".
- [ ] `ui.ts`: rewrite `join` block (3 steps) in **all three** locales; eyebrow "3
Steps"; reword the `features` item that name-drops packwiz.
- [ ] `[...lang].astro`: collapse the 4-step join markup → 3; remove packwiz `CopyChip`,
keep server-IP chip; render download buttons from manifest.
- [ ] New `[...lang]/fjord.astro` + `ui.ts` `fjord` block + `LANG_FJORD_PATH`.
- [ ] Document the synced-manifest step in `12-build-order.md` / `14-deploy.md`.
---
## WS2 — Live server status: online players with names + 3D avatars
`ServerCard.astro` already fetches mc-status (`/api/mcstatus/v2/status/java/${BASE_DOMAIN}`,
mcstatus.io v2 JSON: `players.online/max/list[{uuid,name_clean}]`) and renders NMSR
`headiso` heads (filters the zero-UUID placeholder). Enhancement: labeled avatar tiles
(name + avatar) using `site.avatarMode`.
Tasks:
- [ ] `ServerCard.astro`: per online player render a **shared avatar tile** (name +
`${avatarUrl}/${avatarMode}/<uuid>?size=N`). Keep count + fill bar.
- [ ] `main.css`: `.roster`/tile styles (shared with WS3).
---
## WS3 — Registered players roster (live, via mc-status)
`GET /drasl/api/v2/players` (list all) is **admin-only** → cannot be a browser fetch.
Serve it **live** through mc-status (the trusted internal proxy that already emits
sanitized public JSON). **Coupling accepted** (410 friend server) with two mitigations:
isolate the roster fetch (own timeout + cache; the status path never blocks on Drasl),
and use a dedicated rotatable admin account.
mc-status (`docker/mc-status/main.go`) gains a `/players` endpoint:
- On request (cache TTL ~60s): if no cached token, `POST http://drasl:25585/drasl/api/v2/login`
with admin creds → cache `apiToken` (re-login on 401). `GET …/players` (Bearer) →
map to `[{uuid,name}]` → cache → serve JSON, CORS `*` (like the status route).
- Caddy already `handle /api/mcstatus/*` → mc-status, so the public path is
`/api/mcstatus/players` (browser fetches **same-origin**, no CORS, no admin token in
the page).
`PlayerRoster.astro` (new) fetches `/api/mcstatus/players` client-side → live grid of
shared avatar tiles. Roster is **public** (uuid+name) — accepted.
Tasks:
- [ ] `main.go`: `/players` handler (Drasl login + list, token cache, TTL cache,
isolated from the status path).
- [ ] `docker-compose.yml`: pass `DRASL_ADMIN_*` env to mc-status.
- [ ] New `PlayerRoster.astro` + a "Members" section in `[...lang].astro` + `ui.ts`
keys (3 langs).
---
## WS4 — Player account page (skin CRUD)
New `/account` (+ `/es`, `/eu`), browser-direct Drasl (proven by `/register`; CORS
already scoped to the apex). **Skins only** (capes out). One player per user
(`players[0]`, no switcher). Token **in-memory only** (refresh = re-login; no
localStorage → no XSS token theft).
- **Read:** `POST /login {username,password}``{apiToken, user.players[0]{uuid,name,skinUrl}}`.
- **Create/Update:** `PATCH /players/{uuid} {skinBase64, skinModel}` (Bearer).
- **Delete:** `PATCH /players/{uuid} {deleteSkin:true}` → reverts to default.
UI: login form → reveal skin panel (current avatar via NMSR `site.avatarMode`, model
radio classic/slim, file picker, [Upload], [Reset to default]). After upload, NMSR lags
~15m → show the **just-picked file** locally (FileReader dataURL) as instant
confirmation, not a fresh NMSR render.
Tasks:
- [ ] New `[...lang]/account.astro` — lift skin JS near-verbatim from
`register.astro:250-307` (`readBase64`, PATCH, `apiMessage`).
- [ ] `ui.ts`: `account` block (reuse `register.skin*`) + `LANG_ACCOUNT_PATH`.
- [ ] `main.css`: reuse `.reg-*` tokens.
- [ ] `register.astro`: repoint the "Manage it here" footer link from the Drasl web UI
to `/account`.
---
## WS5 — Footer status link
Add a link to `https://status.${BASE_DOMAIN}` in every footer. Nav unchanged.
Tasks:
- [ ] `site.ts`: add `statusUrl`.
- [ ] `ui.ts`: add `footer.status` (3 langs).
- [ ] Add `<a target="_blank" rel="noopener">` in `.footer-links` of `[...lang].astro`,
`register.astro`, and the new `account.astro` / `fjord.astro` footers.
---
## WS6 — Mod-list component (auto-generated, see `17-mod-shortlist.md`)
Shows the **installed pack** (full client set from the distribution forgemods), flat
alphabetical, pretty names + extracted logos, no categories, no external links.
Build-time baked.
New `tooling/build-modlist.py` (mirrors `classify-mods.py`: `tomllib` + `zipfile`,
offline) reads `$DISTRIBUTION_WEB_ROOT/servers/ulicraft-1.21.1/forgemods/required/*.jar`:
- Per jar: parse `neoforge.mods.toml`, take the primary `[[mods]]``displayName`
(fallback modId), `version`, `description`, `authors`; extract `logoFile` PNG → write
`landing/public/mod-logos/<modid>.png` (skip if absent → `logo: null`).
- Write `landing/src/data/mods.json` (gitignored, generated), sorted by `name`:
```json
{
"schemaVersion": 1,
"count": 76,
"mods": [
{ "id": "jei", "name": "Just Enough Items (JEI)", "version": "19.27.0.340",
"authors": "mezz", "description": "…", "logo": "/mod-logos/jei.png" }
]
}
```
`ModList.astro` (new) imports `mods.json` → flat grid (logo + name + version). The
homepage stat tile (`site.ts` `stats[0].key "50+"`) is fed from `mods.json.count`.
Tasks:
- [ ] `tooling/build-modlist.py` (jar parse + logo extract + `mods.json`).
- [ ] New `ModList.astro` + a "Mods" section in `[...lang].astro` + `ui.ts` heading
keys (3 langs).
- [ ] `site.ts`: feed `stats[0]` count from `mods.json`.
- [ ] Run the generator pre-`pnpm build`; document in `12-build-order.md` /
`14-deploy.md`. `.gitignore`: `landing/src/data/mods.json`,
`landing/src/data/launcher-manifest.json`, `landing/src/data/players.json` (if
any baked), `landing/public/mod-logos/`.
---
## Verification (whole rework)
- **Build:** `cd landing && set -a && . ../.env && set +a && pnpm run build` then
`pnpm check`. Must pass offline even if `launcher-manifest.json` / `mods.json` are
absent (buttons/list hidden).
- `grep -ri packwiz landing/` → no live join references remain (Fjord page may mention
its own import).
- **Local preview** (`pnpm dev`): all three locales — 3-step join, online roster, members
grid, account page, footer status link, mod grid, `/fjord` page.
- **Rosters (running stack):** online tiles appear with players on; `/api/mcstatus/players`
returns `[{uuid,name}]`; the members grid renders avatars.
- **Account (running stack):** login → upload PNG → instant local preview → in-game/NMSR
reflects after ~15m → reset reverts to default.
- Deploy via the `deploy` skill; the manifest sync, `build-modlist.py`, and landing
rebuild are **host** steps (not compose) — fold into `14-deploy.md`.
## Related
- `09-landing.md` — landing stack, i18n, theme.
- `15-mc-status.md` — the mc-status service (now also serves the roster).
- `16-landing-registration.md` — the browser-direct Drasl pattern WS4 reuses.
- `17-mod-shortlist.md` — the mod gap doc + the `mods.json` dataset.
- `landing/src/data/launcher-manifest.schema.json` — the launcher manifest contract.