feat(nmsr): cut avatar refresh lag to ~1min + document HTTP routes
NMSR has no per-request avatar refresh (the `?skin=` override is ignored by this build), so the only lever for a changed skin to appear is the resolve cache. Drop resolve_cache_duration 15m -> 60s so account-page skin changes propagate within ~1 min; texture cache stays 48h (Drasl skin URLs are content-hashed, so a new skin is always a new URL — never stale). Cost is one extra internal Drasl profile lookup per avatar per minute, trivial for 4-10 players. Update the now-stale "~15 min" copy (skinLead + skinPreviewNote, en/es/eu) and code comments to "~1 min". Add plan/19-routes.md: a reference for the nmsr / landing / auth HTTP routes (public via caddy + browser->Drasl + internal service-to-service), indexed in plan/00-overview.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -174,7 +174,7 @@ export interface UI {
|
|||||||
skinUpload: string;
|
skinUpload: string;
|
||||||
skinUploading: string;
|
skinUploading: string;
|
||||||
skinOk: string;
|
skinOk: string;
|
||||||
skinPreviewNote: string; // "NMSR takes ~15 min to update" hint
|
skinPreviewNote: string; // "NMSR takes ~1 min to update" hint
|
||||||
resetSkin: string;
|
resetSkin: string;
|
||||||
resetConfirm: string; // window.confirm text
|
resetConfirm: string; // window.confirm text
|
||||||
resetOk: string;
|
resetOk: string;
|
||||||
@@ -356,7 +356,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
registerLink: "Register here",
|
registerLink: "Register here",
|
||||||
playerLabel: "Logged in as",
|
playerLabel: "Logged in as",
|
||||||
skinTitle: "Your skin",
|
skinTitle: "Your skin",
|
||||||
skinLead: "Upload a Minecraft skin PNG. The 3D preview below updates instantly; the in-game avatar takes ~15 minutes.",
|
skinLead: "Upload a Minecraft skin PNG. The 3D preview below updates instantly; the in-game avatar takes ~1 minute.",
|
||||||
skinChoose: "Choose PNG…",
|
skinChoose: "Choose PNG…",
|
||||||
modelLabel: "Model",
|
modelLabel: "Model",
|
||||||
modelClassic: "Classic",
|
modelClassic: "Classic",
|
||||||
@@ -364,7 +364,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
skinUpload: "Upload skin",
|
skinUpload: "Upload skin",
|
||||||
skinUploading: "Uploading…",
|
skinUploading: "Uploading…",
|
||||||
skinOk: "Skin updated!",
|
skinOk: "Skin updated!",
|
||||||
skinPreviewNote: "Avatar preview updates in ~15 min.",
|
skinPreviewNote: "Avatar updates in ~1 min.",
|
||||||
resetSkin: "Reset to default",
|
resetSkin: "Reset to default",
|
||||||
resetConfirm: "Reset your skin to the default? This cannot be undone.",
|
resetConfirm: "Reset your skin to the default? This cannot be undone.",
|
||||||
resetOk: "Skin reset to default.",
|
resetOk: "Skin reset to default.",
|
||||||
@@ -544,7 +544,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
registerLink: "Regístrate aquí",
|
registerLink: "Regístrate aquí",
|
||||||
playerLabel: "Sesión iniciada como",
|
playerLabel: "Sesión iniciada como",
|
||||||
skinTitle: "Tu skin",
|
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.",
|
skinLead: "Sube un PNG de skin de Minecraft. La vista previa 3D se actualiza al instante; el avatar en el juego tarda ~1 minuto.",
|
||||||
skinChoose: "Elegir PNG…",
|
skinChoose: "Elegir PNG…",
|
||||||
modelLabel: "Modelo",
|
modelLabel: "Modelo",
|
||||||
modelClassic: "Clásico",
|
modelClassic: "Clásico",
|
||||||
@@ -552,7 +552,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
skinUpload: "Subir skin",
|
skinUpload: "Subir skin",
|
||||||
skinUploading: "Subiendo…",
|
skinUploading: "Subiendo…",
|
||||||
skinOk: "¡Skin actualizada!",
|
skinOk: "¡Skin actualizada!",
|
||||||
skinPreviewNote: "La vista previa del avatar se actualiza en ~15 min.",
|
skinPreviewNote: "El avatar se actualiza en ~1 min.",
|
||||||
resetSkin: "Restablecer skin",
|
resetSkin: "Restablecer skin",
|
||||||
resetConfirm: "¿Restablecer tu skin a la predeterminada? Esta acción no se puede deshacer.",
|
resetConfirm: "¿Restablecer tu skin a la predeterminada? Esta acción no se puede deshacer.",
|
||||||
resetOk: "Skin restablecida.",
|
resetOk: "Skin restablecida.",
|
||||||
@@ -732,7 +732,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
registerLink: "Erregistratu hemen",
|
registerLink: "Erregistratu hemen",
|
||||||
playerLabel: "Saioa hasita:",
|
playerLabel: "Saioa hasita:",
|
||||||
skinTitle: "Zure azala",
|
skinTitle: "Zure azala",
|
||||||
skinLead: "Igo Minecraft azal PNG bat. 3D aurrebista berehala eguneratzen da; jokoko avatarra ~15 minutuan.",
|
skinLead: "Igo Minecraft azal PNG bat. 3D aurrebista berehala eguneratzen da; jokoko avatarra ~1 minutuan.",
|
||||||
skinChoose: "Aukeratu PNGa…",
|
skinChoose: "Aukeratu PNGa…",
|
||||||
modelLabel: "Eredua",
|
modelLabel: "Eredua",
|
||||||
modelClassic: "Klasikoa",
|
modelClassic: "Klasikoa",
|
||||||
@@ -740,7 +740,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
skinUpload: "Igo azala",
|
skinUpload: "Igo azala",
|
||||||
skinUploading: "Igotzen…",
|
skinUploading: "Igotzen…",
|
||||||
skinOk: "Azala eguneratuta!",
|
skinOk: "Azala eguneratuta!",
|
||||||
skinPreviewNote: "Avatararen aurrebista ~15 min barru eguneratzen da.",
|
skinPreviewNote: "Avatarra ~1 min barru eguneratzen da.",
|
||||||
resetSkin: "Berrezarri azala",
|
resetSkin: "Berrezarri azala",
|
||||||
resetConfirm: "Zure azala lehenetsi nahi duzu? Ezin da desegin.",
|
resetConfirm: "Zure azala lehenetsi nahi duzu? Ezin da desegin.",
|
||||||
resetOk: "Azala berrezarrita.",
|
resetOk: "Azala berrezarrita.",
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ const clientCfg = {
|
|||||||
showMsg(skinMsg, await apiMessage(res, cfg.errNetwork));
|
showMsg(skinMsg, await apiMessage(res, cfg.errNetwork));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Show the local file immediately as instant confirmation (NMSR lags ~15m).
|
// Show the local file immediately as instant confirmation (NMSR lags ~1m).
|
||||||
const dataUrl = URL.createObjectURL(file);
|
const dataUrl = URL.createObjectURL(file);
|
||||||
$("acc-avatar").src = dataUrl;
|
$("acc-avatar").src = dataUrl;
|
||||||
skinMsg.classList.add("ok");
|
skinMsg.classList.add("ok");
|
||||||
|
|||||||
@@ -16,7 +16,16 @@ port = 8080
|
|||||||
|
|
||||||
[caching]
|
[caching]
|
||||||
cleanup_interval = "1h"
|
cleanup_interval = "1h"
|
||||||
resolve_cache_duration = "15m"
|
# resolve_cache_duration caches the uuid -> profile (incl. skin URL) lookup, so
|
||||||
|
# it bounds how long a changed skin keeps showing the OLD avatar. Kept short so
|
||||||
|
# skin changes on the account page propagate within ~1 min (NMSR has no
|
||||||
|
# per-request force-refresh; a `?skin=` override is ignored by this build). Cost
|
||||||
|
# is one extra internal Drasl profile lookup per avatar per minute — trivial over
|
||||||
|
# mcnet for a 4-10 player server. For an instant global flush: restart nmsr.
|
||||||
|
resolve_cache_duration = "60s"
|
||||||
|
# texture cache stays long: Drasl skin URLs are content-hashed
|
||||||
|
# (/web/texture/skin/<sha256>.png), so a new skin = new URL = fresh fetch; this
|
||||||
|
# 48h cache never serves a stale skin.
|
||||||
texture_cache_duration = "48h"
|
texture_cache_duration = "48h"
|
||||||
|
|
||||||
[caching.cache_biases]
|
[caching.cache_biases]
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ and uptime-kuma.
|
|||||||
- `15-letsencrypt.md` — Let's Encrypt (OVH DNS-01) + host nginx ingress
|
- `15-letsencrypt.md` — Let's Encrypt (OVH DNS-01) + host nginx ingress
|
||||||
- `17-mod-shortlist.md` — kitchen-sink gap doc + landing mod-list dataset (PLANNED)
|
- `17-mod-shortlist.md` — kitchen-sink gap doc + landing mod-list dataset (PLANNED)
|
||||||
- `18-landing-rework.md` — join flow / rosters / account / mod list / footer (PLANNED)
|
- `18-landing-rework.md` — join flow / rosters / account / mod list / footer (PLANNED)
|
||||||
|
- `19-routes.md` — HTTP route reference (nmsr / landing / auth + internal)
|
||||||
|
|
||||||
## Boot/dependency order
|
## Boot/dependency order
|
||||||
|
|
||||||
|
|||||||
84
plan/19-routes.md
Normal file
84
plan/19-routes.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 19 — HTTP route reference (nmsr, landing, auth)
|
||||||
|
|
||||||
|
> Quick map of the public + internal HTTP routes. caddy (`caddy/conf.d/*.caddy`)
|
||||||
|
> is the internal router; the host nginx terminates TLS and forwards each vhost
|
||||||
|
> to it by Host header. All `${BASE_DOMAIN}` = `ulicraft.net` in prod.
|
||||||
|
|
||||||
|
## Vhost → service (caddy)
|
||||||
|
|
||||||
|
| Vhost | caddy file | Upstream |
|
||||||
|
|-------|-----------|----------|
|
||||||
|
| `${BASE_DOMAIN}` (apex) | `10-static.caddy` | static `/srv/www` + `/srv/launcher` + mc-status |
|
||||||
|
| `auth.${BASE_DOMAIN}` | `00-core.caddy` | `drasl:25585` |
|
||||||
|
| `avatar.${BASE_DOMAIN}` | `40-avatar.caddy` | `nmsr:8080` |
|
||||||
|
| `status.${BASE_DOMAIN}` | `50-status.caddy` | `uptime-kuma:3001` |
|
||||||
|
| `distribution.${BASE_DOMAIN}` | `30-distribution.caddy` | static `/srv/distribution` (other repo) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NMSR — `avatar.${BASE_DOMAIN}` (skin/avatar renderer)
|
||||||
|
|
||||||
|
`reverse_proxy nmsr:8080`. Stateless renderer; pulls profiles+skins from Drasl
|
||||||
|
over mcnet (no public round-trip).
|
||||||
|
|
||||||
|
- `GET /{mode}/{uuid}?size=N` — render an avatar PNG.
|
||||||
|
- `mode` = any NMSR render mode (`headiso` is the configured default via
|
||||||
|
`AVATAR_MODE`; others incl. `head`, `face`, `fullbody`, `fullbodyiso`).
|
||||||
|
- `uuid` = Drasl player UUID; `size` = px.
|
||||||
|
- Landing usage: `${avatarUrl}/${avatarMode}/<uuid>?size=64|96`.
|
||||||
|
- **No per-request refresh.** A `?skin=<url>` override is **ignored** by this
|
||||||
|
build (tested). Skin changes propagate via the resolve cache only.
|
||||||
|
- Caching (`nmsr/config.toml`): `resolve_cache_duration = 60s` (uuid→profile,
|
||||||
|
bounds stale-avatar window), `texture_cache_duration = 48h` (safe — Drasl skin
|
||||||
|
URLs are content-hashed, new skin = new URL). Instant global flush = restart
|
||||||
|
nmsr. See `plan/03-drasl.md` for the Drasl-backed mojank wiring.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Landing — `${BASE_DOMAIN}` (apex)
|
||||||
|
|
||||||
|
`10-static.caddy`. Three handlers:
|
||||||
|
|
||||||
|
- `handle_path /launcher/*` → `/srv/launcher` (file browse) — launcher downloads.
|
||||||
|
- `handle /api/mcstatus/*` → `uri strip_prefix /api/mcstatus` → `mc-status:8080`:
|
||||||
|
- `/api/mcstatus/v2/status/java/<addr>` → internal `/v2/status/java/` — live
|
||||||
|
server status JSON (mcstatus.io-compatible, `CORS: *`, `<addr>` ignored — it
|
||||||
|
only pings `MC_STATUS_TARGET`). Landing `statusApi`.
|
||||||
|
- `/api/mcstatus/players` → internal `/players` — registered-players roster,
|
||||||
|
sanitized `[{uuid,name}]`, ~60s cache (mc-status admin-logs-in to Drasl
|
||||||
|
`GET /players`). Landing `rosterApi`.
|
||||||
|
- `/healthz` — mc-status liveness (internal).
|
||||||
|
- `handle {}` → `/srv/www` static (Astro output). Pages: `/`, `/es`, `/eu`
|
||||||
|
(index), `/{lang}/register`, `/{lang}/account`, `/{lang}/fjord`.
|
||||||
|
|
||||||
|
Browser → Drasl **directly** (CORS-scoped to apex via Drasl `CORSAllowOrigins`),
|
||||||
|
base `https://auth.${BASE_DOMAIN}/drasl/api/v2` (`site.draslApiUrl`):
|
||||||
|
- `register.astro`: `POST /users` (register), `POST /login`, `PATCH /players/{uuid}` (set initial skin).
|
||||||
|
- `account.astro`: `POST /login`, `PATCH /players/{uuid}` with `{skinBase64,skinModel}` (upload) or `{deleteSkin:true}` (reset).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth / Drasl — `auth.${BASE_DOMAIN}`
|
||||||
|
|
||||||
|
`reverse_proxy drasl:25585`. One service exposes three surfaces:
|
||||||
|
|
||||||
|
- **Web UI:** `/` (login / register / profile pages), `/web/texture/skin/<sha256>.png`, `/web/texture/cape/<sha256>.png`.
|
||||||
|
- **Yggdrasil / authlib-injector:** `/authlib-injector` (API root — server JVM
|
||||||
|
agent + clients point here), `/session/minecraft/...`, mojang-compat
|
||||||
|
`/users/profiles/minecraft/{name}` and `/session/minecraft/profile/{uuid}`
|
||||||
|
(also what NMSR calls internally).
|
||||||
|
- **Drasl REST API v2** (`/drasl/api/v2`):
|
||||||
|
- `POST /login` `{username,password}` → `{apiToken}`
|
||||||
|
- `POST /users` `{username,password[,inviteCode]}` — register (invite-gated when `RequireInvite=true`; admins from `DefaultAdmins` are promoted at startup, see `plan/03-drasl.md`)
|
||||||
|
- `GET /players` — **admin only**, full roster
|
||||||
|
- `PATCH /players/{uuid}` — skin CRUD (own player, or admin)
|
||||||
|
- `POST /invites` — admin, mint invite codes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Internal (service → service, over mcnet)
|
||||||
|
|
||||||
|
- `minecraft` → `-javaagent authlib-injector=http://auth.${BASE_DOMAIN}/authlib-injector` (caddy mcnet alias → drasl).
|
||||||
|
- `nmsr` → `http://auth.${BASE_DOMAIN}` (mojang-compat session/textures/api).
|
||||||
|
- `mc-status` → `http://drasl:25585/drasl/api/v2` (`DRASL_API_URL`) for the roster; pings `MC_STATUS_TARGET` for live status.
|
||||||
|
- `mc-backup` → RCON `minecraft:25575`.
|
||||||
Reference in New Issue
Block a user