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>
This commit is contained in:
2026-06-10 18:17:14 +02:00
parent 096ef82420
commit 6c7e259fcb
24 changed files with 2319 additions and 117 deletions

View File

@@ -2,7 +2,7 @@
// locales; the page (src/pages/[...lang].astro) renders against it.
//
// Conventions:
// - "{name}" in step1 is replaced with the launcher name at render time.
// - "{name}" is replaced with the launcher name at render time.
// - Sentences containing links/kbd/literals are split into parts (a/b/pre/
// post/link) so the markup stays in the template, not in the strings.
// - Product / in-game literals (authlib-injector, Multiplayer, Add Server)
@@ -19,11 +19,23 @@ export const LANG_REGISTER_PATH: Record<Lang, string> = {
es: "/es/register",
eu: "/eu/register",
};
// Fjord (alternative path) page path per locale (mirrors LANG_REGISTER_PATH).
export const LANG_FJORD_PATH: Record<Lang, string> = {
en: "/fjord",
es: "/es/fjord",
eu: "/eu/fjord",
};
// Account page path per locale (mirrors LANG_REGISTER_PATH).
export const LANG_ACCOUNT_PATH: Record<Lang, string> = {
en: "/account",
es: "/es/account",
eu: "/eu/account",
};
export interface UI {
htmlLang: string;
copied: string; // transient copy-button feedback
nav: { status: string; features: string; join: string; play: string };
nav: { status: string; members: string; features: string; mods: string; join: string; play: string };
hero: {
eyebrow: string; // "Modded LAN · NeoForge" — version appended at render
tagline: string;
@@ -42,13 +54,46 @@ export interface UI {
upTo: string;
};
statLabels: [string, string, string, string];
members: {
eyebrow: string;
title: string;
lead: string;
empty: string; // shown when the roster is empty or unreachable
};
features: {
eyebrow: string;
title: string;
lead: string;
items: { h: string; p: string }[];
};
mods: {
eyebrow: string;
title: string;
lead: string;
empty: string; // shown when the catalogue is absent/empty (not yet generated)
};
join: {
eyebrow: string;
title: string;
lead: string;
s1: {
kicker: string;
h: string;
pa: string; // before the register link
pb: string; // after it
registerLink: string;
};
s2: { kicker: string; h: string; p: string };
s3: {
kicker: string;
h: string;
pa: string; // before the Multiplayer/Add Server kbds
pb: string; // after them
copy: string;
};
};
fjord: {
navHome: string; // "← Home" link in nav
eyebrow: string;
title: string;
lead: string;
@@ -59,7 +104,6 @@ export interface UI {
pa: string; // before the authlib-injector literal
pb: string; // after it
copy: string;
registerPre: string;
};
s3: { kicker: string; h: string; p: string };
s4: {
@@ -69,8 +113,9 @@ export interface UI {
pb: string; // after them
copy: string;
};
noPack: string; // shown when fjordPack is missing from the manifest
};
footer: { join: string; disc: string };
footer: { join: string; status: string; disc: string };
register: {
navHome: string; // "← Home" link in nav
eyebrow: string;
@@ -86,7 +131,7 @@ export interface UI {
submit: string;
submitting: string;
haveAccount: string; // "Already have an account?"
loginLink: string; // links to the Drasl web UI
loginLink: string; // links to the account page
successTitle: string;
successBody: string; // {name} = player name
uuidLabel: string;
@@ -104,13 +149,47 @@ export interface UI {
errSkinType: string;
errNetwork: string;
};
account: {
navHome: string; // "← Home" link in nav
eyebrow: string;
title: string;
lead: string;
// Login form
usernameLabel: string;
usernamePlaceholder: string;
passwordLabel: string;
passwordPlaceholder: string;
submit: string;
submitting: string;
noAccount: string; // "No account yet?"
registerLink: string; // links to /register
// Skin panel
playerLabel: string; // "Logged in as"
skinTitle: string;
skinLead: string; // reuses register.skinLead concept
skinChoose: string;
modelLabel: string;
modelClassic: string;
modelSlim: string;
skinUpload: string;
skinUploading: string;
skinOk: string;
skinPreviewNote: string; // "NMSR takes ~15 min to update" hint
resetSkin: string;
resetConfirm: string; // window.confirm text
resetOk: string;
// Errors
errFields: string;
errSkinType: string;
errNetwork: string;
};
}
export const ui: Record<Lang, UI> = {
en: {
htmlLang: "en",
copied: "Copied!",
nav: { status: "Status", features: "Features", join: "How to Join", play: "Play Now" },
nav: { status: "Status", members: "Members", features: "Features", mods: "Mods", join: "How to Join", play: "Play Now" },
hero: {
eyebrow: "Modded LAN · NeoForge",
tagline: "Kitchen-sink survival, built to outlast the weekend.",
@@ -132,6 +211,12 @@ export const ui: Record<Lang, UI> = {
upTo: "up to",
},
statLabels: ["Mods", "NeoForge", "Player slots", "World backups"],
members: {
eyebrow: "The crew",
title: "Who's on the server.",
lead: "Everyone registered on Ulicraft, with their own self-hosted skin.",
empty: "No members yet — be the first to register.",
},
features: {
eyebrow: "What makes it Ulicraft",
title: "Built for the crew.",
@@ -149,7 +234,7 @@ export const ui: Record<Lang, UI> = {
},
{
h: "One-click setup",
p: "packwiz installs and updates the whole modpack inside your launcher, so everyone stays in sync.",
p: "The Ulicraft Launcher installs and updates the whole modpack from our distribution, so everyone stays in sync.",
},
{
h: "Built to last",
@@ -157,27 +242,57 @@ export const ui: Record<Lang, UI> = {
},
],
},
mods: {
eyebrow: "The pack",
title: "Every mod in the pack.",
lead: "The full client modset, installed automatically by the launcher. No hunting, no version mismatches.",
empty: "Mod list not generated yet — check back soon.",
},
join: {
eyebrow: "How to Join · 4 Steps",
eyebrow: "How to Join · 3 Steps",
title: "From zero to spawning in.",
lead: "One-time setup. After this, the launcher keeps you in sync.",
s1: {
kicker: "Account",
h: "Register",
pa: "Create your Ulicraft account at",
pb: "— one username and password for the server and your skin.",
registerLink: "Register",
},
s2: {
kicker: "Launcher",
h: "Download {name}",
p: "Grab {name} for your system — a Prism-based launcher with authlib-injector support built in.",
p: "Grab {name} for your system and log in with your Ulicraft credentials — it installs the modpack and everything else automatically.",
},
s3: {
kicker: "Connect",
h: "Join the server",
pa: "Launch the launcher, open",
pb: ", paste the address, and join.",
copy: "Copy IP",
},
},
fjord: {
navHome: "← Home",
eyebrow: "Alternative · Fjord Launcher",
title: "Play with Fjord Launcher.",
lead: "Prefer a vanilla Prism-based launcher? Set it up manually with Fjord and import the Ulicraft pack.",
s1: {
kicker: "Launcher",
h: "Download Fjord",
p: "Grab Fjord Launcher for your system — a Prism-based launcher with authlib-injector support built in.",
},
s2: {
kicker: "Account",
h: "Add your account",
pa: "In the launcher, add an",
pa: "In Fjord, add an",
pb: "account using this URL, then log in with your Ulicraft credentials.",
copy: "Copy",
registerPre: "Need credentials? Register at",
},
s3: {
kicker: "Modpack",
h: "Import the modpack",
p: "Add a new instance from this packwiz URL — it installs and updates the whole pack.",
h: "Import the Ulicraft pack",
p: "Download the Ulicraft modpack and import it into Fjord as a new instance — it installs the whole pack.",
},
s4: {
kicker: "Connect",
@@ -186,9 +301,11 @@ export const ui: Record<Lang, UI> = {
pb: ", paste the address, and join.",
copy: "Copy IP",
},
noPack: "The pack download isn't available yet — check back soon.",
},
footer: {
join: "How to Join",
status: "Server status",
disc: "Not affiliated with Mojang or Microsoft. Minecraft is a trademark of Mojang AB.",
},
register: {
@@ -224,12 +341,43 @@ export const ui: Record<Lang, UI> = {
errSkinType: "Pick a PNG image.",
errNetwork: "Network error — try again.",
},
account: {
navHome: "← Home",
eyebrow: "Account",
title: "Manage your skin.",
lead: "Log in to upload or reset your Minecraft skin.",
usernameLabel: "Username",
usernamePlaceholder: "your in-game name",
passwordLabel: "Password",
passwordPlaceholder: "your password",
submit: "Log in",
submitting: "Logging in…",
noAccount: "No account yet?",
registerLink: "Register here",
playerLabel: "Logged in as",
skinTitle: "Your skin",
skinLead: "Upload a Minecraft skin PNG. The 3D preview below updates instantly; the in-game avatar takes ~15 minutes.",
skinChoose: "Choose PNG…",
modelLabel: "Model",
modelClassic: "Classic",
modelSlim: "Slim",
skinUpload: "Upload skin",
skinUploading: "Uploading…",
skinOk: "Skin updated!",
skinPreviewNote: "Avatar preview updates in ~15 min.",
resetSkin: "Reset to default",
resetConfirm: "Reset your skin to the default? This cannot be undone.",
resetOk: "Skin reset to default.",
errFields: "Fill in every field.",
errSkinType: "Pick a PNG image.",
errNetwork: "Network error — try again.",
},
},
es: {
htmlLang: "es",
copied: "¡Copiado!",
nav: { status: "Estado", features: "Características", join: "Cómo entrar", play: "Jugar ahora" },
nav: { status: "Estado", members: "Miembros", features: "Características", mods: "Mods", join: "Cómo entrar", play: "Jugar ahora" },
hero: {
eyebrow: "LAN con mods · NeoForge",
tagline: "Supervivencia todo incluido, para durar más que el finde.",
@@ -251,6 +399,12 @@ export const ui: Record<Lang, UI> = {
upTo: "hasta",
},
statLabels: ["Mods", "NeoForge", "Plazas", "Copias del mundo"],
members: {
eyebrow: "La cuadrilla",
title: "Quién está en el servidor.",
lead: "Todos los registrados en Ulicraft, cada uno con su skin autoalojada.",
empty: "Aún no hay miembros — sé el primero en registrarte.",
},
features: {
eyebrow: "Qué hace especial a Ulicraft",
title: "Hecho para la cuadrilla.",
@@ -268,7 +422,7 @@ export const ui: Record<Lang, UI> = {
},
{
h: "Configuración en un clic",
p: "packwiz instala y actualiza todo el modpack dentro de tu launcher, así todos vais sincronizados.",
p: "El Ulicraft Launcher instala y actualiza todo el modpack desde nuestra distribución, así todos vais sincronizados.",
},
{
h: "Hecho para durar",
@@ -276,27 +430,57 @@ export const ui: Record<Lang, UI> = {
},
],
},
mods: {
eyebrow: "El pack",
title: "Todos los mods del pack.",
lead: "El modset completo del cliente, instalado automáticamente por el launcher. Sin búsquedas ni versiones incompatibles.",
empty: "La lista de mods aún no está generada — vuelve pronto.",
},
join: {
eyebrow: "Cómo entrar · 4 pasos",
eyebrow: "Cómo entrar · 3 pasos",
title: "De cero a aparecer en el mundo.",
lead: "Configuración única. Después, el launcher te mantiene sincronizado.",
s1: {
kicker: "Cuenta",
h: "Regístrate",
pa: "Crea tu cuenta de Ulicraft en",
pb: "— un usuario y contraseña para el servidor y tu skin.",
registerLink: "Registro",
},
s2: {
kicker: "Launcher",
h: "Descarga {name}",
p: "Coge {name} para tu sistema: un launcher basado en Prism con soporte authlib-injector integrado.",
p: "Coge {name} para tu sistema e inicia sesión con tus credenciales de Ulicraft: instala el modpack y todo lo demás automáticamente.",
},
s3: {
kicker: "Conectar",
h: "Entra al servidor",
pa: "Lanza el launcher, abre",
pb: ", pega la dirección y entra.",
copy: "Copiar IP",
},
},
fjord: {
navHome: "← Inicio",
eyebrow: "Alternativa · Fjord Launcher",
title: "Juega con Fjord Launcher.",
lead: "¿Prefieres un launcher vanilla basado en Prism? Configúralo a mano con Fjord e importa el pack de Ulicraft.",
s1: {
kicker: "Launcher",
h: "Descarga Fjord",
p: "Coge Fjord Launcher para tu sistema: un launcher basado en Prism con soporte authlib-injector integrado.",
},
s2: {
kicker: "Cuenta",
h: "Añade tu cuenta",
pa: "En el launcher, añade una cuenta",
pa: "En Fjord, añade una cuenta",
pb: "con esta URL y luego inicia sesión con tus credenciales de Ulicraft.",
copy: "Copiar",
registerPre: "¿Sin credenciales? Regístrate en",
},
s3: {
kicker: "Modpack",
h: "Importa el modpack",
p: "Añade una instancia nueva desde esta URL de packwiz: instala y actualiza todo el pack.",
h: "Importa el pack de Ulicraft",
p: "Descarga el modpack de Ulicraft e impórtalo en Fjord como instancia nueva: instala todo el pack.",
},
s4: {
kicker: "Conectar",
@@ -305,9 +489,11 @@ export const ui: Record<Lang, UI> = {
pb: ", pega la dirección y entra.",
copy: "Copiar IP",
},
noPack: "La descarga del pack aún no está disponible — vuelve pronto.",
},
footer: {
join: "Cómo entrar",
status: "Estado del servidor",
disc: "No afiliado a Mojang ni Microsoft. Minecraft es una marca de Mojang AB.",
},
register: {
@@ -343,12 +529,43 @@ export const ui: Record<Lang, UI> = {
errSkinType: "Elige una imagen PNG.",
errNetwork: "Error de red: inténtalo de nuevo.",
},
account: {
navHome: "← Inicio",
eyebrow: "Cuenta",
title: "Gestiona tu skin.",
lead: "Inicia sesión para subir o restablecer tu skin de Minecraft.",
usernameLabel: "Usuario",
usernamePlaceholder: "tu nombre en el juego",
passwordLabel: "Contraseña",
passwordPlaceholder: "tu contraseña",
submit: "Iniciar sesión",
submitting: "Entrando…",
noAccount: "¿Aún no tienes cuenta?",
registerLink: "Regístrate aquí",
playerLabel: "Sesión iniciada como",
skinTitle: "Tu skin",
skinLead: "Sube un PNG de skin de Minecraft. La vista previa 3D se actualiza al instante; el avatar en el juego tarda ~15 minutos.",
skinChoose: "Elegir PNG…",
modelLabel: "Modelo",
modelClassic: "Clásico",
modelSlim: "Fino",
skinUpload: "Subir skin",
skinUploading: "Subiendo…",
skinOk: "¡Skin actualizada!",
skinPreviewNote: "La vista previa del avatar se actualiza en ~15 min.",
resetSkin: "Restablecer skin",
resetConfirm: "¿Restablecer tu skin a la predeterminada? Esta acción no se puede deshacer.",
resetOk: "Skin restablecida.",
errFields: "Rellena todos los campos.",
errSkinType: "Elige una imagen PNG.",
errNetwork: "Error de red: inténtalo de nuevo.",
},
},
eu: {
htmlLang: "eu",
copied: "Kopiatuta!",
nav: { status: "Egoera", features: "Ezaugarriak", join: "Nola sartu", play: "Jokatu orain" },
nav: { status: "Egoera", members: "Kideak", features: "Ezaugarriak", mods: "Mods", join: "Nola sartu", play: "Jokatu orain" },
hero: {
eyebrow: "Mod-dun LANa · NeoForge",
tagline: "Mod ugariko biziraupena, irauteko egina.",
@@ -370,6 +587,12 @@ export const ui: Record<Lang, UI> = {
upTo: "gehienez",
},
statLabels: ["Modak", "NeoForge", "Plazak", "Munduaren babeskopiak"],
members: {
eyebrow: "Koadrila",
title: "Nor dago zerbitzarian.",
lead: "Ulicraft-en erregistratutako guztiak, bakoitza bere azal auto-ostatatuarekin.",
empty: "Oraindik ez dago kiderik — izan zaitez lehena erregistratzen.",
},
features: {
eyebrow: "Zerk egiten du Ulicraft berezi",
title: "Koadrilarentzat eginda.",
@@ -387,7 +610,7 @@ export const ui: Record<Lang, UI> = {
},
{
h: "Konfigurazioa klik batean",
p: "packwiz-ek modpack osoa instalatu eta eguneratzen du zure launcherrean, denok sinkronizatuta egoteko.",
p: "Ulicraft Launcher-ek modpack osoa instalatu eta eguneratzen du gure banaketatik, denok sinkronizatuta egoteko.",
},
{
h: "Irauteko eginda",
@@ -395,27 +618,57 @@ export const ui: Record<Lang, UI> = {
},
],
},
mods: {
eyebrow: "Packa",
title: "Packeko mod guztiak.",
lead: "Bezeroaren mod-multzo osoa, launcherrak automatikoki instalatua. Bilaketarik gabe, bertsio-gatazkarik gabe.",
empty: "Mod zerrenda oraindik ez da sortu — itzuli laster.",
},
join: {
eyebrow: "Nola sartu · 4 urrats",
eyebrow: "Nola sartu · 3 urrats",
title: "Zerotik mundura agertzera.",
lead: "Behin bakarrik konfiguratu. Gero, launcherrak sinkronizatuta mantenduko zaitu.",
s1: {
kicker: "Kontua",
h: "Erregistratu",
pa: "Sortu zure Ulicraft kontua hemen:",
pb: "— erabiltzaile eta pasahitz bat zerbitzarirako eta zure azalerako.",
registerLink: "Erregistroa",
},
s2: {
kicker: "Launcherra",
h: "Deskargatu {name}",
p: "Hartu {name} zure sistemarako: Prism-en oinarritutako launcherra, authlib-injector euskarriarekin.",
p: "Hartu {name} zure sistemarako eta hasi saioa zure Ulicraft kredentzialekin: modpacka eta gainerako guztia automatikoki instalatzen du.",
},
s3: {
kicker: "Konektatu",
h: "Sartu zerbitzarira",
pa: "Abiarazi launcherra, ireki",
pb: ", itsatsi helbidea eta sartu.",
copy: "Kopiatu IPa",
},
},
fjord: {
navHome: "← Hasiera",
eyebrow: "Alternatiba · Fjord Launcher",
title: "Jokatu Fjord Launcher-ekin.",
lead: "Prism-en oinarritutako launcher vanilla nahiago duzu? Konfiguratu eskuz Fjord-ekin eta inportatu Ulicraft packa.",
s1: {
kicker: "Launcherra",
h: "Deskargatu Fjord",
p: "Hartu Fjord Launcher zure sistemarako: Prism-en oinarritutako launcherra, authlib-injector euskarriarekin.",
},
s2: {
kicker: "Kontua",
h: "Gehitu zure kontua",
pa: "Launcherrean, gehitu",
pa: "Fjord-en, gehitu",
pb: "kontu bat URL honekin, eta gero hasi saioa zure Ulicraft kredentzialekin.",
copy: "Kopiatu",
registerPre: "Kredentzialik ez? Erregistratu hemen:",
},
s3: {
kicker: "Modpacka",
h: "Inportatu modpacka",
p: "Gehitu instantzia berri bat packwiz URL honetatik: pack osoa instalatu eta eguneratzen du.",
h: "Inportatu Ulicraft packa",
p: "Deskargatu Ulicraft modpacka eta inportatu Fjord-en instantzia berri gisa: pack osoa instalatzen du.",
},
s4: {
kicker: "Konektatu",
@@ -424,9 +677,11 @@ export const ui: Record<Lang, UI> = {
pb: ", itsatsi helbidea eta sartu.",
copy: "Kopiatu IPa",
},
noPack: "Packaren deskarga ez dago oraindik eskuragarri — itzuli laster.",
},
footer: {
join: "Nola sartu",
status: "Zerbitzariaren egoera",
disc: "Ez dago Mojang edo Microsoft-ekin lotuta. Minecraft Mojang AB-ren marka da.",
},
register: {
@@ -462,5 +717,36 @@ export const ui: Record<Lang, UI> = {
errSkinType: "Aukeratu PNG irudi bat.",
errNetwork: "Sare-errorea: saiatu berriro.",
},
account: {
navHome: "← Hasiera",
eyebrow: "Kontua",
title: "Kudeatu zure azala.",
lead: "Hasi saioa zure Minecraft azala igotzeko edo berrezartzeko.",
usernameLabel: "Erabiltzailea",
usernamePlaceholder: "zure jokoko izena",
passwordLabel: "Pasahitza",
passwordPlaceholder: "zure pasahitza",
submit: "Hasi saioa",
submitting: "Sartzen…",
noAccount: "Oraindik konturik ez?",
registerLink: "Erregistratu hemen",
playerLabel: "Saioa hasita:",
skinTitle: "Zure azala",
skinLead: "Igo Minecraft azal PNG bat. 3D aurrebista berehala eguneratzen da; jokoko avatarra ~15 minutuan.",
skinChoose: "Aukeratu PNGa…",
modelLabel: "Eredua",
modelClassic: "Klasikoa",
modelSlim: "Mehea",
skinUpload: "Igo azala",
skinUploading: "Igotzen…",
skinOk: "Azala eguneratuta!",
skinPreviewNote: "Avatararen aurrebista ~15 min barru eguneratzen da.",
resetSkin: "Berrezarri azala",
resetConfirm: "Zure azala lehenetsi nahi duzu? Ezin da desegin.",
resetOk: "Azala berrezarrita.",
errFields: "Bete eremu guztiak.",
errSkinType: "Aukeratu PNG irudi bat.",
errNetwork: "Sare-errorea: saiatu berriro.",
},
},
};