Commit Graph
39 Commits
Author SHA1 Message Date
oierandClaude Opus 5 8507ffd020 feat(files): gate files. browse behind FILES_BROWSE (on|off)
Per-host toggle for the read-only, no-auth browse of the shared upload dir,
so it can stay on for the LAN-party host (oxps) and be off on the
internet-facing one (cochi). Defaults to `on` — existing hosts unchanged.

Neither layer has conditionals, so each gets the value substituted in:
- caddy: docker-compose mounts 62-files.${FILES_BROWSE:-on}.caddy at the
  fixed conf.d target (62-files.caddy -> 62-files.on.caddy, plus a
  comments-only 62-files.off.caddy stub — caddy needs a parseable file
  there). Same trick as FILES_MOUNT_MODE.
- nginx: render-nginx.sh splices nginx/files-browse.server.tmpl into
  ${FILES_BROWSE_SERVER} and adds/omits the name in the port-80 redirect.
  This is the gate that matters; caddy is bound to 127.0.0.1.

Applying a change needs render-nginx.sh --install plus a caddy
--force-recreate (single-file bind mount pins the inode). With `off`,
drop `files` from LE_SUBDOMAINS on that host. upload. is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 17:08:33 +02:00
oierandClaude Opus 5 92c6f5fc8f feat(compose): mount mods, config and kubejs writable
Drop :ro from ./server-mods:/mods and the four kubejs source subdirs, and
add a new ./server-config:/data/config bind — mod config used to live only
inside the mc_data volume, unreachable from the host without root.

The config bind needs seeding: a bind mount shadows what is under it, so an
empty ./server-config hides the generated config and every mod falls back to
defaults. tooling/export-server-config.sh copies /data/config out of the
volume first, halting if the target is non-empty, if the container is still
running, or if the mc_data volume name is ambiguous.

Writable mounts also need the server's uid (itzg runs as 1000) to have write
access on the host dir; oxps' host user is 1002, hence MC_UID/MC_GID.

The kubejs ROOT still must not be mounted — BaseProperties.save() NPEs and
kubejs never initialises. That is about mounting the root at all, not about
the mode, so dropping :ro does not change it. It does mean kubejs writes now
land in $DISTRIBUTION_WEB_ROOT as uncommitted changes in that host's checkout.

Also documents how to reach the mc_data volume (exec/cp, resolved host path,
stop-before-write rule) in plan/05-minecraft.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:46:30 +02:00
oierandClaude Opus 4.8 16523e4f62 feat(files): move filestash to upload., serve files. as read-only static browse
Filestash cannot be served under a subpath (its SPA hardcodes /api, /assets,
/login at root), so it moves to its own subdomain upload.${BASE_DOMAIN}. The
apex-level ask was a static, no-auth file browser at files.${BASE_DOMAIN}: caddy
now file_server browse's the same ${FILES_DATA_DIR} share read-only there.

- filestash general.host -> upload.; nginx upload. vhost keeps the 2g body +
  upload streaming + /admin throttle; new files. vhost is a plain static proxy.
- caddy: 60-files.caddy split into 60-upload.caddy (filestash) + 62-files.caddy
  (file_server browse of /srv/files); caddy mounts FILES_DATA_DIR :ro.
- LE_SUBDOMAINS += upload (both upload. and files. are vhosts needing certs).

Security: files. is unconditionally public read (no auth), independent of
FILES_AUTH. The htpasswd login only gates writes via upload. Documented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:03:33 +02:00
oierandClaude Opus 4.8 243787c530 feat(files): run filestash as a chosen uid so share files are host-owned
Add an env-driven `user:`/`group_add` to the filestash service plus
tooling/filestash-set-run-user.sh. Running the container as a real host
account (oxps: oier=1002) makes every file it writes into the share owned
by that account, so scp/ssh and the web UI share one owner — no chown dance.

Defaults FILES_UID/FILES_GID=1000, leaving cochi (image's own uid) untouched.
group_add 1000 keeps the 0730 image binary + 0770 /app/data usable under any
uid; the helper also chowns the filestash_state volume (1000:1000) so a
non-1000 uid doesn't crash-loop unable to write its own sqlite/config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 16:25:03 +02:00
oierandClaude Opus 4.8 9322e39a8c docs(files): oxps runs filestash auth-off (LAN-only)
Two prod hosts now diverge on filestash auth: cochi stays htpasswd
(internet-facing), oxps runs FILES_AUTH=none/passthrough since it is
LAN-only, satisfying the internal-only gate. Document the per-host
status table and the flip-back rule for if oxps ever gains an internet
path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 02:57:56 +02:00
oierandClaude Opus 4.8 05a98efe57 docs(migration): fresh-world kubejs root-owned crash-loop + fix
On a fresh mc_data volume the minecraft service crash-loops with
"AccessDeniedException: /data/kubejs/config" (ConsoleJS.STARTUP null NPE):
Docker auto-creates the bind-mount parent /data/kubejs as root:root, but
the itzg container runs as uid 1000, so KubeJS can't create its writable
siblings (config/cache/generated/logs). Only bites fresh worlds — cochi's
aged volume predates the ro submounts and is already 1000-owned.

Fix (durable): chown 1000:1000 /data/kubejs once, then restart.

- plan/22-migration.md: Troubleshooting section with symptom + fix.
- migrate-import.sh: manual tail now flags it with the exact commands.

Hit + resolved during the cochi->oxps migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 02:38:13 +02:00
oierandClaude Opus 4.8 b9bcc13b73 feat(tooling): host migration scripts (export/import/check)
Move the stack to a new host keeping the same domain, carrying Drasl
users + skins (+ kuma monitors, filestash index) but not the world.

- migrate-check.sh: validate the target meets requirements. [restore]
  hard-fails (docker, compose v2, git, tar, ssh); [deploy] warns
  (node/pnpm, py>=3.11, nginx, acme.sh, RAM, disk, ports). --host runs
  it remotely by streaming itself over ssh.
- migrate-export.sh: workstation-run, pulls the source read-only over
  ssh into one 0600 bundle (.env, certs/, drasl_state/kuma_data/
  filestash_state volumes, checksummed MANIFEST). Certs in by default
  (--without-certs); world opt-in (--with-world).
- migrate-import.sh: target-run, sha256-verify, refuse existing volumes
  (--force), backup+restore .env with --path-prefix rewrite, check host
  paths resolve, restore certs+volumes, print the manual tail.
- plan/22-migration.md: human walkthrough (cochi -> oxps), rollback
  window warning, rehearsal notes.
- .gitignore: ignore ulicraft-bundle-*.tar.gz (holds secrets + LE keys).

Assumes Compose v2 on the target (no update-all.sh changes). Distribution
repo, TLS issuance, DNS and firewall stay manual. Round-trip tested
against prod: 9.2M bundle, drasl.db restored byte-exact, clobber guard
verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 00:41:39 +02:00
oier 00e9815307 docs(generators): an empty 200 is not a healthy service
Observed while staging generators. on cochi: with the nginx vhost installed
but the container not yet deployed, caddy answers an unmatched Host with an
EMPTY 200. Both / and the mirror path returned "HTTP 200, 0 bytes" while
nothing at all was running.

So the guidance to probe a mirror path instead of / was necessary but not
sufficient — a status-code-only check reports green on a completely absent
service. Assert the body.

Same failure family as the filestash green-monitor: the server answering
is not the app working.
2026-07-21 19:37:28 +02:00
oier b6b02398da feat(generators): offline data pack generators at generators.
Add generators.${BASE_DOMAIN}: a pinned fork of misode.github.io served
with its own mcmeta + vanilla-mcdoc mirror, so it works with zero internet
at the LAN party. Fork lives in ulicraft-generators (branch ulicraft),
pinned at upstream 2bfe5522.

The app is 100% client-side: every data fetch comes from the player's
browser, not from cochi. So offline capability is entirely about which
origins the bundle refers to, plus a mirror to serve them from. The vhost
sets Content-Security-Policy with connect-src 'self' as ENFORCEMENT: an
external origin that sneaks back in (or returns via an upstream merge)
then breaks loudly in prod, on an ordinary day, instead of silently at the
venue with no internet to debug it.

sync-generators-data.sh mirrors ~109M: the 1.21.1 summary/data/atlas tags,
assets-tiny (assets minus hundreds of MB of .ogg — hence no sounds
generator), vanilla-mcdoc symbols.json, and summary/versions/data.min.json,
which comes from a BRANCH head rather than a version tag and runs during
Spyglass init. It verifies a sentinel file per source instead of trusting a
clean clone exit, since a renamed upstream path would otherwise sync
"successfully" and only fail in the browser at the party.

The container serves the bundle and the mirror on one origin, so there is
no CORS, no second cert and no second DNS name.

DEPLOY ORDER MATTERS. GENERATORS_REPO must be set in cochi's .env BEFORE
pulling this: compose uses ${GENERATORS_REPO:?...}, and an unset required
variable makes docker compose fail to parse the file — taking down every
service including Minecraft, not just this one. Full runbook and the rest
of the prerequisites: plan/21-generators.md.

Verified offline in real Chromium with every non-origin request aborted:
zero off-origin attempts, zero same-origin failures, zero console errors,
generators rendering schemas and live JSON.
2026-07-21 18:59:01 +02:00
oierandClaude Opus 4.8 7164371d34 docs(mods): sync docs to the 164-jar kitchen-sink pack
The pack grew 76 -> 164 jars (133 both / 31 client) without any doc
update. Create + ~18 addons filled the tech gap, ars_nouveau + ars_creo
the magic gap, and the full YUNG's/Moog's/TerraBlender set the worldgen
gap, which also resolved the ponderjs orphan.

- 17-mod-shortlist: rewritten around the current pack; gap doc marked
  done, remaining gaps narrowed to map mod, Necronomicon orphan, C2ME.
- 04-mods: jar counts updated; the 24/52 cutover split is now labelled
  historical alongside the current 31/133.
- CLAUDE.md: WS6 partly done, stale "65 default both" line dropped, and
  the ~50-100 mod ceiling flagged as knowingly exceeded in both places.
  MAX_MEMORY 10G was sized for that old assumption - noted for re-check.
- 18-landing-rework: mods.json count 76 -> 164, "50+" stat tile noted
  stale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:29:43 +02:00
oierandClaude Opus 4.8 08fe8c9c53 docs(files): record the general.host trap + that a green monitor proves nothing
Propagate the bare-hostname fix (10b86ae) to the docs that still said "three
traps", and write down the meta-lesson it exposed.

The bug was invisible to every check we had: Filestash strips the scheme
before its own Host check, so `curl /` returned 200, the API answered, wrong
passwords were rejected, and the brand-new Uptime Kuma HTTP monitor stayed
green — while the SPA computed `http://https://files...` and no browser could
use the site. An HTTP monitor asserts "server returned 200", which is a much
weaker claim than "the app works".

- CLAUDE.md / README: four traps now, host-scheme first; add the post-change
  check (`/api/config` → origin must be the real https URL).
- 19-routes: login form only renders at ?action=redirect (a bare 303 is
  normal, not a bug); assert origin after config changes.
- 10-uptime-kuma: an HTTP monitor cannot see an SPA break — use a Keyword
  monitor if you want teeth, and don't read green as "users can log in".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:31:46 +02:00
oierandClaude Opus 4.8 10b86ae23e fix(files): general.host must be a bare hostname, not a URL
The SPA computes its redirect origin as (force_ssl ? "https://" : "http://")
+ general.host. With host set to "https://files.${BASE_DOMAIN}" that produced
"http://https://files.ulicraft.net", so every client-side redirect broke and
the page never routed anywhere.

Server-side this was invisible: SecureOrigin strips the scheme before the
Host check, so `curl /` returned 200, the API answered, and the uptime
monitor stayed green — only real browsers failed. The tell is a
`POST /report?...msg=Redirecting to http://https://...` line in the log.

Set host to the bare hostname and force_ssl=true (which the origin needs to
build https://, and which only emits an HSTS header — it does not redirect,
so it can't loop behind the plain-http nginx→caddy hop).

Verified: origin is now "https://files.ulicraft.net"; login + ls still work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:27:42 +02:00
oierandClaude Opus 4.8 cd79b0e540 docs(files): document filestash across plan/README/deploy skill
Fold files./filestash into the docs that carry the service list, the vhost
map and the deploy procedure — README stack table, 00-overview, 02-caddy,
12-build-order, 14-deploy, and the deploy skill.

Two real gaps the filestash deploy exposed, now guardrails in the skill:
- update-all.sh never touches host nginx, so a NEW SUBDOMAIN silently 404s
  after deploy. It needs issue-letsencrypt.sh + render-nginx.sh --install.
- a new service with ${FOO:?} guards fails the WHOLE compose file, so a
  missing .env var means `up` starts nothing — and --hard has already run
  `down`. Check the host's .env BEFORE tearing the stack down.

Also: filestash/config.json is a single-file bind mount re-rendered every
run, so a rolling update-all leaves it on a stale inode (same trap as the
caddy conf) — force-recreate after any FILES_* change.

Correct the cochi divergence section: `git diff HEAD` on the host is now
empty (verified this deploy — the ff-pull succeeded). The documented
docker-compose.yml/package.json divergence was converged; only untracked
dnsmasq/, caddy-root-ca.crt and a stale pack/ remain. The stash-pull-pop
procedure it prescribed is no longer needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:23:18 +02:00
oierandClaude Opus 4.8 25df9f9398 feat(files): add Filestash player file share at files.${BASE_DOMAIN}
One shared login (htpasswd) for all players, writable. Config is rendered
from a template and mounted read-only, so git stays authoritative — the
admin console loads but cannot save, by design.

Filestash's OIDC/SAML middlewares are enterprise-only, so Keycloak SSO is
out; FILES_AUTH keeps htpasswd/passthrough switchable for later. Auth-off
(passthrough) is only valid once the host is off the public internet —
render-config.sh warns loudly when rendering it.

Three traps, all found by testing against the pinned image:
- the `local` backend is admin-gated: without LOCAL_BACKEND_SECRET in the
  connection params every login fails with "backend error - Not Allowed"
- the htpasswd plugin only accepts $2a$ bcrypt, so a $2y$ hash from
  `htpasswd -B` fails every login silently — use `openssl passwd -6`.
  render-config.sh rejects the wrong format rather than shipping it
- APPLICATION_URL/ADMIN_PASSWORD env make filestash rewrite config.json at
  boot, which fails on the :ro mount — both live in the rendered config

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:08:46 +02:00
oierandClaude Opus 4.8 e45ce210ba docs(mc): document Simple Voice Chat voice_host gotcha + commit prod snapshot
voice_host lives in the mc_data volume and regenerates empty on world wipe,
silently breaking remote voice (secret sent, UDP handshake never completes →
red icon). Document the set/restore steps and the VPN-client false-positive
(Cloudflare One/WARP, FortiClient drop voice UDP). Add a known-good prod
snapshot at server-configs/voicechat-server.properties (voice_host=ulicraft.net).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:16:02 +02:00
oierandClaude Opus 4.8 d12071df04 fix(mc): ONLINE_MODE=true — authlib-injector needs online handshake for skins
online-mode=false made the server skip the online-auth handshake entirely, so
authlib-injector never validated sessions against Drasl: players got offline-hash
UUIDs and no skin textures (everyone Steve). authlib-injector's whole purpose is
to redirect the *online* handshake from Mojang to Drasl, so online-mode must be
true; only the 1.21+ secure profile stays off (ENFORCE_SECURE_PROFILE=false +
Drasl SignPublicKeys=false). Correct the conflation in CLAUDE.md and
plan/05-minecraft.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:34:47 +02:00
oierandClaude Opus 4.8 35be2e6b76 docs(plan): scrub stale pack subdomain + packwiz from live-arch docs
The pack./packwiz service was removed operationally long ago, but several
plan docs still presented pack. as a live caddy vhost / DNS subdomain and
render-pack.sh as a current tool. Remove those references from the
current-architecture docs (overview, caddy, tooling, uptime-kuma,
letsencrypt, mc-backup); leave the migration log (04-mods.md), superseded
banner (04-packwiz.md), commit history (12-build-order.md), and planned
landing rework (18) intact as deliberate history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:15:22 +02:00
oierandClaude Opus 4.8 07a4ae469d docs(minecraft): document OP-via-RCON, not itzg OPS env
OPS env resolves names via Mojang lookup, but the server is offline-mode
with Drasl auth (own UUIDs) → wrong UUID, inert op entry. Document the
RCON procedure that uses the cached Drasl UUID from usercache.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:00:22 +02:00
oierandClaude Opus 4.8 8a71709048 docs(deploy): caddy conf bind mounts are inode-pinned — recreate, not reload
Document the gotcha hit fixing distribution CORS: caddy/conf.d/*.caddy are
single-file bind mounts pinned to the host inode at container creation. git pull
rewrites tracked files via atomic rename (new inode), so a running caddy keeps
the old config; `restart`/`caddy reload` reuse the stale inode. Apply conf
changes with `docker compose up -d --force-recreate caddy` (or the --hard
down/up). Added to plan/14-deploy.md and the deploy skill guardrails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:26:42 +02:00
oierandClaude Opus 4.8 037b1777d6 feat(landing): rename crew to "El valle" + source downloads from launcher.json
Rename all "crew" mentions (en/es/eu) to the untranslated proper name
"El valle" across hero, members, and features copy in i18n/ui.ts.

Rewire the homepage launcher download list to the custom-launcher build
output launcher.json (productName/version/files[]) instead of the
never-produced launcher-manifest.json shape. site.ts now reads launcher.json
and normalizes files[] -> internal builds[]; launcher.example.json is the
committed fallback. Drop the stale launcher-manifest.example/schema, update
.gitignore and docs (CLAUDE.md, plan/18).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:55:05 +02:00
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oier 067e990306 mods 2026-06-09 18:29:48 +02:00
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 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
oierandClaude Opus 4.8 5172316919 feat(auth): Blessing Skin variant as Drasl alternative
Override stack that swaps Drasl for Blessing Skin Server + MariaDB,
talking to Minecraft via the yggdrasil-api plugin (authlib endpoint
/api/yggdrasil, ONLINE_MODE=TRUE). Caddy auth.* repointed to
blessing-skin:80; Drasl left idle (compose merges depends_on).

Run: docker compose -f docker-compose.yml -f docker-compose.blessingskin.yml up -d

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:48:54 +02:00
oierandClaude Opus 4.7 3c2ce72c2a docs(plan): add 13-dns-and-run-modes; mark dnsmasq optional
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 06:06:33 +02:00
oier c09da15bd0 initial commit 2026-05-24 04:03:42 +02:00