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>
9.7 KiB
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):
- Register — create your Ulicraft account (link
/register). - Download the Ulicraft Launcher — per-OS download (from manifest); log in with your credentials. (No authlib URL — launcher is preconfigured.)
- 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-launcherrelease flow produceslauncher-manifest.jsonand syncs it intolanding/src/data/launcher-manifest.json(gitignored, generated), same pattern assync-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: deletepackwizUrl; replacelauncher(Fjord hardcode) with the manifest import; rename product display to "Ulicraft Launcher".ui.ts: rewritejoinblock (3 steps) in all three locales; eyebrow "3 Steps"; reword thefeaturesitem that name-drops packwiz.[...lang].astro: collapse the 4-step join markup → 3; remove packwizCopyChip, keep server-IP chip; render download buttons from manifest.- New
[...lang]/fjord.astro+ui.tsfjordblock +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 (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/loginwith admin creds → cacheapiToken(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:/playershandler (Drasl login + list, token cache, TTL cache, isolated from the status path).docker-compose.yml: passDRASL_ADMIN_*env to mc-status.- New
PlayerRoster.astro+ a "Members" section in[...lang].astro+ui.tskeys (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 fromregister.astro:250-307(readBase64, PATCH,apiMessage). ui.ts:accountblock (reuseregister.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: addstatusUrl.ui.ts: addfooter.status(3 langs).- Add
<a target="_blank" rel="noopener">in.footer-linksof[...lang].astro,register.astro, and the newaccount.astro/fjord.astrofooters.
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; extractlogoFilePNG → writelanding/public/mod-logos/<modid>.png(skip if absent →logo: null). - Write
landing/src/data/mods.json(gitignored, generated), sorted byname:
{
"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.tsheading keys (3 langs). site.ts: feedstats[0]count frommods.json.- Run the generator pre-
pnpm build; document in12-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 buildthenpnpm check. Must pass offline even iflauncher-manifest.json/mods.jsonare 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,/fjordpage. - Rosters (running stack): online tiles appear with players on;
/api/mcstatus/playersreturns[{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
deployskill; the manifest sync,build-modlist.py, and landing rebuild are host steps (not compose) — fold into14-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 + themods.jsondataset.landing/src/data/launcher-manifest.schema.json— the launcher manifest contract.