Commit Graph

91 Commits

Author SHA1 Message Date
4750c0d6c7 feat(mods): classify 59 new distribution jars (Create ecosystem)
Distribution grew 76→135 forgemods; classify-mods.py seeded the 59
new jars into mods-sides.json (57 both, 2 client). Unblocks the server
mod sync — server was stuck on the old 52-jar subset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:21:42 +02:00
220b43d007 docs(deploy): document update-all.sh as the post-pull orchestrator
Rewrite the routine-redeploy section around update-all.sh (rolling vs --hard),
list the steps it runs, note the python>=3.11 requirement for build-modlist on
cochi (default python3 is 3.10), and that www/ + mods.json are generated every
run. The deploy skill = pull + fetch-authlib + ./update-all.sh --hard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:06:39 +02:00
37d21cacf9 fix(update-all): run build-modlist.py under python >=3.11
The host's default python3 may predate stdlib tomllib (cochi/Ubuntu 22.04 ships
3.10), so the shebang-resolved python3 failed the version guard and strict-halt
aborted the run. Pick the first python3.11+ interpreter instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:05:03 +02:00
3af14d0144 feat(tooling): add update-all.sh post-pull orchestrator; deploy skill uses it
update-all.sh is the single source of truth for the on-host post-pull build +
restart steps: render drasl/nmsr configs, sync server mods, regen the landing
mod list, rebuild www/, then apply via docker compose. Two modes:
- default (rolling): up -d --build, then restart ONLY services whose mounted
  inputs changed since a pre-run snapshot — drasl/nmsr on a config re-render,
  minecraft on a mod change. Minimal downtime. (Cannot detect caddy static-conf
  changes — use --hard for those.)
- --hard: down --remove-orphans && up -d --build (full restart, MC downtime).

Strict halt on any error; landing build sources nvm + pnpm (never npm) and bakes
.env. fetch-authlib stays out (rarely changes) — the deploy skill runs it before
update-all. Refactor the deploy skill to `pull && fetch-authlib && update-all.sh
--hard` so the step list can't drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:03:57 +02:00
369b4dc0a0 style(landing): bump roster player name to 18px
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:28:01 +02:00
c3b2d89ddc feat(landing): switch heading font to Block Blueprint
Replace Pixelify Sans with Block Blueprint as the heading font (theme.headFont).
Block Blueprint isn't on Google Fonts, so it's vendored straight from 1001fonts
(License: 1001Fonts Free For Personal Use — fine for this private friends'
server) as a TTF, with a hand-written @font-face appended to fonts.css. The
vendor step is added to fetch-fonts.sh (runs after the Google rewrite so it
survives re-runs). Pixelify stays available as a HEAD_FONTS option + the
hard-coded fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:17:52 +02:00
362228f985 feat(landing): 230px fullbody roster, features goat column, trimmed header
- Members roster avatars render 230px tall (full body); NMSR size bumped to 256
  and tiles widened to fit the portrait.
- Features section is now two columns: the 4 feature cards stacked on the left, a
  Minecraft goat image on the right (public/Goat_JE1_BE1.webp). Stacks on narrow
  screens.
- Header nav links trimmed to How to Join + Status; the auth CTA cluster is now
  Register / Account / Play (the old "Log in" button relabeled Account →
  Cuenta/Kontua).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:13:58 +02:00
c8a6c77a8e feat(landing): rework — es default, 2-step join, roster + account polish
- Default language Spanish at root /, English moves to /en/ (eu unchanged):
  getStaticPaths {lang:undefined}->es + LANG_*_PATH maps swapped in ui.ts.
- Section order: hero -> join -> status -> members -> mods -> features.
- Join flow 3 -> 2 steps (drop the Connect/IP step; launcher handles the
  address). Remove the hero IP CopyChip. Header CTA renamed Play/Jugar/Jolastu
  and gains Register + Login links on all four pages.
- Account page: hide the whole login block once authenticated (not just the
  form), add an avatar Refresh button (cache-busted NMSR re-fetch), flatten the
  avatar (no bevel). New i18n key account.refresh.
- Register page: new REGISTRATION_SHOW_INVITE env (default false/hidden) toggles
  the invite-code field independently of the REGISTRATION_MODE backend gate.
- Members roster: drop the `admin` account, render full-body via a new
  ROSTER_AVATAR_MODE (default fullbodyiso); portrait tiles.
- Style: flatten emboss on feature icons (.picon) + player tiles (.roster-tile).
  PixelIcon gains optional image support (/icons/<name>.png|svg).
- Favicon now /favicon.png (committed) on all pages.
- Docs: plan/09-landing.md Assets section (logo/favicon/features-icons drop
  points) + default-language note; .env.example documents the two new vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:03:18 +02:00
c233b1bfbc docs(drasl): record admin bootstrap + config-key/CORS/NMSR gotchas
Capture what this session learned the hard way, in plan/03-drasl.md and CLAUDE.md:

- Correct admin key is DefaultAdmins (not DefaultAdminUsernames); wrong/unknown
  keys are silently ignored by drasl (logged as "unknown config option").
- CORSAllowOrigins must be top-level, before any [Section], or it's ignored.
- First-admin bootstrap under invite mode is a chicken-egg → temp-flip
  RequireInvite=false, register, revert.
- NMSR needs the auth. host-gateway pin + https mojank endpoints or avatars
  render the default skin (cross-ref plan/19-routes.md).
- Fix the stale example config and mark the admin-bootstrap task done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:20:54 +02:00
de1ac2ee17 fix(nmsr): render real skins, not default — pin auth. to host over https
Every avatar rendered the default skin. NMSR fetched the player profile from
Drasl fine, but the profile embeds an absolute HTTPS skin URL (Drasl BaseURL is
https), and NMSR could not connect to auth.${BASE_DOMAIN}:443: the mcnet alias
resolves auth. -> caddy, which only listens on :80 internally. Logs showed
`fetch_texture_from_mojang … client error (Connect)` for the skin URL, so NMSR
fell back to the default skin.

Mirror what the minecraft service already does: pin auth.${BASE_DOMAIN} to the
host via extra_hosts so NMSR reaches the host nginx on :443 (real LE cert,
publicly trusted — no private CA needed), and switch the nmsr mojank endpoints
from http:// to https:// so the profile/texture scheme matches the embedded
skin URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:12:59 +02:00
5d5602fbd7 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>
2026-06-10 20:58:57 +02:00
97347693b6 fix(drasl): correct admin key + CORS placement in config template
Two config keys were silently ignored by drasl (logged as "unknown config
option"), so neither took effect in production:

- `DefaultAdminUsernames` is not a drasl option; the correct top-level key is
  `DefaultAdmins`. With the wrong key, the `admin` user never got promoted —
  admin-only API routes (e.g. GET /players for the registered-players roster)
  returned 403.
- `CORSAllowOrigins` sat after the `[RegistrationNewPlayer]` table header, so
  TOML parsed it as `RegistrationNewPlayer.CORSAllowOrigins` (ignored → no CORS
  headers → landing register/account browser calls blocked). Moved it top-level,
  before any [Section], with a comment warning against re-nesting.

Verified against drasl master configuration.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:34:48 +02:00
a138bbfd72 fix(landing): add valid pnpm-workspace.yaml (packages + build approvals)
Host cochi carried an untracked landing/pnpm-workspace.yaml with a bogus
`allowBuilds:` key and no `packages:` field, which made pnpm 11 abort every
command with "packages field missing or empty" — breaking the deploy landing
rebuild. Track a correct file: `packages: ['.']` plus `onlyBuiltDependencies`
for esbuild/sharp. Verified install + astro build clean locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:27:30 +02:00
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
096ef82420 docs(04-mods): record first-cutover deploy gotchas
Document the two crash classes hit on first deploy: NeoForge pin must match
the distribution's version, and client-only mods that declare BOTH crash the
dedicated server with "invalid dist DEDICATED_SERVER" (tomllib can't catch
them — hand-set to client + re-sync). Mark the decommission checklist done;
flag the landing join-flow rework as the one remaining open item. Current
state: 24 client / 52 both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 01:16:48 +02:00
c249172baa fix(mods): mark client-only GUI mods as client in mods-sides.json
Drippy Loading Screen (and other client-only mods declaring BOTH in their
manifest) crashed the dedicated server with "Attempted to load class
net/minecraft/client/gui/screens/Screen for invalid dist DEDICATED_SERVER".
tomllib can't catch these (they declare BOTH), so hand-classify the leaf
client-only mods as `client`: BetterF3, JustEnoughResources, MouseTweaks,
Searchables, TravelersTitles, drippyloadingscreen, entityculling, fancymenu,
konkrete, melody, ponderjs, welcomescreen, yeetusexperimentus. Now 24 client
/ 52 both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:07:50 +02:00
fa8cede6ae docs(roadmap): track post-migration mod follow-ups
Two follow-ups from the packwiz→04-mods migration: curate cosmetic-client
entries in mods-sides.json, and rework the landing join flow off the dead
packwiz pack.toml URL onto the HeliosLauncher/distribution.json flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:03:07 +02:00
84196d59d0 fix(minecraft): bump NeoForge pin to 21.1.233 to match distribution
The distribution-fed modset is built against NeoForge 21.1.233
(distribution.json); several mods hard-require it (ferritecore ≥21.1.218,
farmersdelight ≥21.1.219, configured ≥21.1.211). The old 21.1.209 pin made
the server crash in pre-load with "Missing or unsupported mandatory
dependencies: neoforge".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:59:23 +02:00
27237bc7c1 feat(mods): replace packwiz with distribution-fed custom mod volume
Drop packwiz entirely. The server now loads a filtered mod subset from a
local ./server-mods volume instead of fetching a packwiz pack at boot.

New flow:
- tooling/classify-mods.py (tomllib only, no network) reads each jar's
  META-INF/neoforge.mods.toml and seeds mods-sides.json with a side
  (client|server|both; default both). Existing entries are preserved.
- mods-sides.json: committed, keyed by jar filename, hand-editable override
  surface. Initial seed: 65 both, 11 client, 0 server (76 jars).
- tooling/sync-server-mods.sh mirrors the both/server jars from
  $DISTRIBUTION_WEB_ROOT into ./server-mods/ (authoritative; prunes strays;
  halts on a missing jar). Replaces render-pack.sh.
- compose: minecraft mounts ./server-mods:/mods:ro with REMOVE_OLD_MODS=TRUE
  (itzg auto-syncs /mods -> /data/mods). Removed PACKWIZ_URL, the pack.
  extra_host (auth. kept for authlib), and depends_on caddy (drasl kept).

Both classify-mods.py and sync-server-mods.sh resolve their source dir as:
CLI arg / MODS_DIST_DIR env / $DISTRIBUTION_WEB_ROOT (in that order).

Removed: tooling/render-pack.sh + add-custom-mod.sh (packwiz tooling),
pack/ (packwiz source), custom/ (76 jars now sourced from the distribution),
the pack. caddy vhost + its mounts/alias, and the packwiz .gitignore block.

Client distribution is UNCHANGED: HeliosLauncher still installs the full
modset from distribution.json. Docs (CLAUDE.md, plan/04/05/14, runtime)
updated; plan/04-packwiz.md stubbed as superseded by plan/04-mods.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:34:38 +02:00
90c3be7bb5 https auth 2026-06-09 22:13:31 +02:00
d21a7f0e92 feat(landing): live server card backed by self-hosted mc-status
Add a featured live ServerCard to the landing (replaces the static
ServerListPanel in hero + status sections): server favicon, color MOTD,
online/offline pill, players online/max with fill bar, and a player-head
row rendered by our own NMSR from Drasl skins. Progressive enhancement —
SSG skeleton degrades gracefully when JS or the API is unavailable.

Back it with a self-hosted pinger instead of the public api.mcstatus.io:
mcstatus.io's API service is closed-source (only the mcutil library is
open), so docker/mc-status wraps mcutil and re-emits its v2 JSON shape,
keeping the frontend unchanged. The service ignores the path address and
only pings MC_STATUS_TARGET (no SSRF relay), with a 30s TTL cache.

Exposed same-origin via caddy at the apex /api/mcstatus/* path (no new DNS
subdomain or LE cert change, no CORS). Uptime Kuma stays the uptime
history + alerting backend; see plan/15-mc-status.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:08:30 +02:00
df8ffca53e feat(landing): self-serve registration page via Drasl API (B1)
Add a static /register page (en/es/eu) that calls the Drasl REST API v2
directly from the browser: register -> login -> optional skin upload, all
in the pixel design. Guests never touch Drasl's own web UI.

Drasl config gains the pieces this needs:
- CORSAllowOrigins scoped to the apex (the API has no CORS until set;
  never "*").
- [RateLimit] for the now public-facing anonymous POST /users.
- [RegistrationNewPlayer] with RequireInvite driven by a new
  REGISTRATION_MODE (invite|open) .env flag.

REGISTRATION_MODE has two consumers from one key: render-config.sh derives
the TOML boolean for Drasl (drasl is TOML-only, no env), and the landing
build reads it to show/hide the invite-code field. render-config.sh halts
on any value other than invite/open.

Security verified against drasl source: anonymous POST /users cannot set
privileged fields (isAdmin/isLocked/chosenUuid/maxPlayerCount are gated on
callerIsAdmin in CreateUser), so browser-direct registration is safe.

Docs: plan/16-landing-registration.md captures the design + the B1 vs fork
decision; build-order, deploy, and the deploy skill wire REGISTRATION_MODE
and the landing-rebuild requirement (www/ is gitignored, not updated by a
git pull).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:55:51 +02:00
067e990306 mods 2026-06-09 18:29:48 +02:00
673202e1e4 fix(compose): pin auth./pack. to host-gateway so HTTPS resolves in-container
Containers don't inherit the host's /etc/hosts; the LAN resolver returns a
dead address (192.168.0.3:443) for the public names, crash-looping the
packwiz install on boot. extra_hosts host-gateway routes HTTPS through the
host's nginx (valid LE cert) -> caddy -> service.

Also add plan/10-uptime-kuma.md (Minecraft monitor: internal + public probes)
and document the HTTPS resolution variant in plan/00-overview.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 04:11:53 +02:00
1ec3a9c1ee internals http 2026-06-09 03:55:54 +02:00
663b87a4e4 change to https 2026-06-09 03:37:48 +02:00
a2821f2bbc feat(tooling): fetch-authlib.sh to download authlib-injector.jar
The server JVM agent (runtime/authlib-injector.jar, mounted at /extras) is
gitignored and was previously fetched by the deleted build-stack.sh prep. A
fresh checkout had no jar, so minecraft crashlooped with "Error opening zip
file or JAR manifest missing". Add a standalone fetch tool: resolves the latest
release asset, downloads, and verifies it's a real zip/jar (PK magic) before
trusting it. Idempotent (skips if valid, --force to re-download). Wire it into
the deploy skill and README launch steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 03:06:59 +02:00
cfd60131fb fix(landing): https + correct subdomains, drop dead CA-cert step
The guest landing page advertised http:// service URLs (mixed-content on the
https origin) and the wrong packwiz subdomain:
- authUrl / authlibUrl: http -> https
- packwizUrl: http://packwiz. -> https://pack.  (correct vhost)
- serverAddress: mc. -> apex (connect to ${BASE_DOMAIN}:25565)
- remove caCertUrl + the "trust the local CA" join step (airgap-only, /ca.crt
  no longer exists) and its en/es/eu i18n strings + type.

Also fix the stale http BaseURL example in plan/03-drasl.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:56:59 +02:00
95c63e1ee0 fix(drasl): use https BaseURL to match TLS ingress
Public scheme is HTTPS (host nginx terminates TLS in front of caddy). Drasl
builds absolute URLs — texture URLs and the authlib-injector API location —
from BaseURL, so an http:// value caused mixed-content blocking and broken
login on the https origin. Point BaseURL at https://auth.${BASE_DOMAIN}.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:53:35 +02:00
46103495fb fix(compose): pre-create pack/custom mountpoint for nested ro bind
The ./custom:/srv/pack/custom mount nests inside the read-only ./pack:/srv/pack
mount. Docker can't mkdir the child mountpoint under a :ro parent, so on a fresh
checkout (no pack/custom/ dir) container init fails with "read-only file system".
Track an empty pack/custom/ so the mountpoint always exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:28:15 +02:00
1709bcd340 docs(claude): rewrite project context for unified stack
Drop stale Keycloak/OIDC, NetBird, AdGuard/.home.local, airgap, and
Prism-specific content. Reflect the current reality: single docker-compose.yml
(drasl, minecraft, mc-backup, caddy, nmsr, uptime-kuma), Drasl password-login
auth (no OIDC), public nginx+LE -> caddy ingress, DNS handled outside the repo,
FjordLauncher for guests. Point to plan/ as the source of truth; keep the
still-valid gotchas (secure-profile, authlib-injector, NeoForge pinning, mod
source) and carry-over comms prefs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:16:42 +02:00
67d1f82e6a refactor!: unify into one compose, drop airgap/mirror/dnsmasq/avahi/blessingskin
Collapse the override-file matrix into a single docker-compose.yml holding the
whole stack: drasl, minecraft, mc-backup, caddy, nmsr, uptime-kuma. Bring-up is
now just `docker compose up -d --build`.

Removed features (dead-ends for this deployment):
- airgap / full asset mirror (mirror-airgap.sh, mirror-mods.sh, 20-mirror.caddy,
  caddy local-CA export, /ca.crt)
- dnsmasq + avahi/mDNS + dns-records.sh + mdns-host-setup.sh + ENABLE_MDNS;
  DNS is now handled outside this repo (HOST_LAN_IP dropped)
- Blessing Skin auth variant (compose + 00-core-blessingskin.caddy + BS_* env)
- host-nginx-static variant (docker-compose.nginx.yml, nginx/ulicraft.conf.tmpl)
- build-stack.sh and its run modes (online/airgap/core)

Production ingress is the only path now: host nginx terminates TLS (LE certs)
in front of caddy on a localhost-only port; caddy routes every vhost by Host
header. Launcher downloads move mirror/launcher -> launcher/.

Docs: README, deploy skill, and plan/ rewritten to match; obsolete plan docs
(dnsmasq, blessing-skin, assets-mirror, full-airgap-mirror, dns-and-run-modes)
deleted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:14:09 +02:00
ff645de2c8 feat(status): Uptime Kuma monitoring at status.${BASE_DOMAIN}
Add docker-compose.status.yml (louislam/uptime-kuma) layered on the caddy
ingress, joining mcnet so it probes the stack by internal service name and
the public vhosts end-to-end. Host port bound to 127.0.0.1:3001 only; public
access via nginx -> caddy.

- caddy/conf.d/50-status.caddy: status. -> uptime-kuma:3001
- nginx caddy-front template: status. TLS vhost + websocket upgrade headers
  for Kuma's live UI; status. added to the HTTP->HTTPS redirect list
- status added to LE_SUBDOMAINS defaults (.env.example, issue-letsencrypt.sh)
- README: Status monitoring section + recommended monitor list (incl. native
  Minecraft-protocol ping)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:49:39 +02:00
6df885eb13 docs(readme): document NMSR avatar renderer + ingress vhosts
Add an Avatar renderer (NMSR) section: Drasl-backed config, internal mcnet
resolution, localhost-only host port, endpoints, lavapipe note. List nmsr +
distribution as optional layers, refresh the Public TLS vhost list (www/avatar,
HTTPS-only), and update the Layout tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:42:00 +02:00
6fac1647aa feat(avatar): NMSR skin renderer at avatar.${BASE_DOMAIN}, Drasl-backed
Add NMSR-as-a-Service (NickAcPT/nmsr-rs) rendering players' skins/avatars,
sourced from Drasl instead of Mojang.

- docker/nmsr/Dockerfile     build from source, pinned to commit 948ba4bc027b
                             (ears feature, lavapipe software Vulkan)
- nmsr/config.toml.tmpl       [mojank] -> http://auth.${BASE_DOMAIN}; Drasl
                              serves the Mojang-compatible routes at its bare
                              BaseURL and embeds absolute skin URLs that NMSR
                              fetches directly. allow_offline_mode_uuids=true.
- docker-compose.nmsr.yml     nmsr service (build), host bind 127.0.0.1:9898
                              only, config mount; mounts the caddy avatar snippet
- caddy/conf.d/40-avatar.caddy avatar.${BASE_DOMAIN} -> reverse_proxy nmsr:8080
- nginx-front + LE_SUBDOMAINS  public TLS vhost (HSTS) + cert for avatar
- render-config.sh / .gitignore render + ignore nmsr/config.toml

Skin resolution stays internal over mcnet (caddy alias -> drasl), no public
round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:40:50 +02:00
a6155819b6 feat(nginx): enforce HTTPS-only with HSTS
Add Strict-Transport-Security (max-age 1y, includeSubDomains) to every TLS
server block in both vhost templates. Combined with the existing port-80
301 redirect, browsers refuse plain HTTP to these names after first visit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:21:17 +02:00
3185fea4c1 feat(nginx): render-nginx.sh + www->apex redirect vhost
Add tooling/render-nginx.sh: pulls BASE_DOMAIN/CADDY_HTTP_PORT from .env,
defaults APP_DIR to the repo checkout, renders a vhost template (default the
caddy-front one) to stdout, or --install writes + symlinks + nginx -t + reloads.
Document it in the README, replacing the manual envsubst one-liner.

Also add the www.${BASE_DOMAIN} canonical 301->apex block and put www in the
LE_SUBDOMAINS default so its cert is issued.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:09:46 +02:00
f3c38da56c fix(nginx): use legacy listen-flag http2 for older nginx
The `http2 on;` directive needs nginx 1.25.1+; older builds reject it. Move
http2 onto the listen line (`listen 443 ssl http2;`) which works on old and
new nginx alike. Applied to both vhost templates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:03:39 +02:00
21e6080e78 docs(readme): document host-nginx-in-front-of-caddy public TLS path
Add a section covering cert issuance, the localhost CADDY_HTTP_PORT bind, the
caddy/static/distribution compose bring-up, and rendering/installing
nginx/ulicraft-caddy.conf.tmpl. Note DISTRIBUTION_WEB_ROOT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:40:02 +02:00
ab3d2c201b feat(ingress): public distribution.${BASE_DOMAIN} static vhost
Serve a static site at distribution.${BASE_DOMAIN} whose web root lives in
another repo (DISTRIBUTION_WEB_ROOT, bind-mounted read-only into caddy):
  - caddy/conf.d/30-distribution.caddy  file_server vhost
  - docker-compose.distribution.yml     mount snippet + external web root
  - issue-letsencrypt.sh / .env.example add distribution to LE_SUBDOMAINS
  - ulicraft-caddy.conf.tmpl            TLS-front block proxying to caddy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:30:27 +02:00
3486546fb3 feat(ingress): configurable caddy port so host nginx can front it
Make the caddy host-published port configurable (CADDY_HTTP_PORT /
CADDY_HTTP_BIND, default 0.0.0.0:80 unchanged) so the machine's own nginx
can terminate TLS and reverse-proxy to caddy on a localhost-only port.

Add nginx/ulicraft-caddy.conf.tmpl: a TLS-terminator vhost set that proxies
apex/auth/pack to caddy by Host header, letting caddy stay the single ingress
for all routing. Alternative to ulicraft.conf.tmpl (nginx-as-static + drasl
proxy). minecraft still reaches caddy internally over http via mcnet aliases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:34:42 +02:00
3d52951355 feat(landing): pixel design ported to static Astro + en/es/eu i18n
Adopt the "Carved from stone" pixel design (landing/design/) as the apex
onboarding page, ported from its React/Babel-via-CDN prototype to static
Astro so it works on an offline LAN. Real modded-LAN content replaces the
prototype's fictional public-SMP copy.

- Vendor web fonts locally (tooling/fetch-fonts.sh -> public/fonts/) so the
  page renders without the Google Fonts CDN at party time.
- Port the design system (main.css: mood/hero/bevels) and split markup into
  Astro components (Creeper, PixelIcon, CopyChip, ServerListPanel).
- site.ts holds language-neutral config + theme knobs (mood/hero/headFont/
  dust) that replace the design's in-browser TweaksPanel; LITERALS holds
  never-translated product/in-game terms.
- i18n: English (/), Spanish (/es/), Euskera (/eu/) generated from one
  [...lang].astro via getStaticPaths; copy lives in src/i18n/ui.ts. Nav has
  an EN/ES/EU switcher; html lang + hreflang set per page.
- Status panel shows a static server-list row + honest stat tiles (no fake
  live count). Copy + scroll-reveal are the only client JS.

Build emits to ../www (gitignored). Euskera strings pending a native review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:18:36 +02:00
5cec2993d7 feat(nginx): serve launcher downloads from apex vhost
Port the /launcher/* static route from caddy's 10-static.caddy to the
nginx apex server (alias -> ./mirror/launcher, autoindex). /ca.crt is
omitted — LE certs are publicly trusted, no guest CA import needed.
The 20-mirror.caddy upstream spoofs stay caddy/air-gap-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:08:43 +02:00
843347ad00 feat(tls): Let's Encrypt via OVH DNS-01 + host nginx vhost
tooling/issue-letsencrypt.sh: manual script, issues one cert per name
(apex + LE_SUBDOMAINS) with acme.sh + OVH DNS-01 (no inbound ports),
installs to certs/<name>/ and reloads nginx. nginx/ulicraft.conf.tmpl:
TLS vhosts for apex/pack (static) + auth (proxy to drasl). OVH creds and
LE_EMAIL in .env.example; certs/ gitignored. plan/15-letsencrypt.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:35:07 +02:00
4c874e20ed refactor(ingress): extract caddy to its own compose file; add nginx path
Caddy is no longer in the base stack. Two ingress paths now:
- docker-compose.caddy.yml — LAN/air-gap; build-stack.sh adds it for all
  up modes + the prep pre-bake. Holds the mcnet auth./pack. aliases.
- docker-compose.nginx.yml — production behind the host's nginx: publishes
  drasl on 127.0.0.1:25585, points minecraft at the host (extra_hosts) over
  HTTPS for pack + authlib.

Base loses the caddy service and minecraft's caddy depends_on (re-added by
caddy.yml). Blessing Skin override now requires caddy.yml (it only swaps
caddy's core conf).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:35:07 +02:00
67718d8e05 docs(deploy): cochi redeploy guide + deploy skill
plan/14-deploy.md: git pull main + hard restart (compose down ->
build-stack.sh --up online), drasl/online, verify + rollback.
.claude/skills/deploy: skill to run it (pre-check, pull, hard restart,
verify) with prod guardrails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:22:37 +02:00
6e91749f59 refactor(pack): template CustomSkinLoader + unify pack rendering
Add tooling/render-pack.sh: renders all pack/**/*.tmpl with ${BASE_DOMAIN}
(no dnsmasq HOST_LAN_IP coupling) and rebuilds the packwiz index. Both
render-config.sh and add-custom-mod.sh now delegate to it.

CustomSkinLoader.json's skin API roots are no longer frozen — it becomes
CustomSkinLoader.json.tmpl, rendered like the mod metadata. .packwizignore
broadened to *.tmpl; rendered .json gitignored as build output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:42:32 +02:00
853405afe7 chore(skins): point CustomSkinLoader at auth.ulicraft.net
Update apiRoot/sessionRoot from the old .lan domain to .net.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:38:30 +02:00
f7858d4ec4 refactor(pack): template custom mod metadata so BASE_DOMAIN isn't frozen
The download URL is the only domain-dependent field in the external-file
metadata. Make mods/*.pw.toml build output rendered from committed
mods/*.pw.toml.tmpl (url uses ${BASE_DOMAIN}); render-config.sh and
add-custom-mod.sh render them + packwiz refresh at deploy/add time.

- pack/.packwizignore: keep *.pw.toml.tmpl out of the index
- gitignore rendered pack/mods/*.pw.toml + pack/index.toml (build output)
- seed a minimal index.toml before refresh (packwiz won't bootstrap one)
- add-custom-mod renders inline (BASE_DOMAIN only) — no coupling to
  dnsmasq's HOST_LAN_IP

Domain changes now just need a re-render, not a metadata rewrite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:07:35 +02:00
39be74d028 fix(pack): regenerate custom mod URLs for current BASE_DOMAIN
The mods/*.pw.toml download URLs were frozen at ulicraft.lan; the stack
now uses ulicraft.net. Regenerate all nine with the current domain.

add-custom-mod.sh: skip the copy when source == hosted dest (`-ef`),
so `add-custom-mod.sh custom --force` works as an in-place regen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:52:00 +02:00