# Landing rework — join flow, player rosters, account, mod list, footer > **Status: PLANNED — not started.** Five landing workstreams (WS1–WS5) 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 list (delivery = synced local file, option B):** - Source of truth: the `custom-launcher` build output `launcher.json` (shape `productName/version/baseUrl/files[]`, each file `{name,os,arch,ext,size,url}`), published to `https://distribution.${BASE}/launcher/launcher.json`. - The release flow syncs it into `landing/src/data/launcher.json` (gitignored, generated), same pattern as `sync-server-mods.sh`. `launcher.example.json` committed as the documented fallback shape. - Landing reads it at build (`site.ts` `loadLauncherManifest`), normalizes `files[]` → internal `builds[]`, renders per-OS buttons. 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}/?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** (4–10 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 `` 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/.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.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.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. - `launcher.json` — the `custom-launcher` build output (download list); landing syncs it to `landing/src/data/launcher.json`, `launcher.example.json` committed.