Commit Graph
153 Commits
Author SHA1 Message Date
oierandClaude Opus 5.5 f4bd786140 chore(uptime-kuma): bump image to :2
The :1 image runs Node 18 (EOL), which gets no fix for the October 2026
Node.js security release. :2 migrates kuma.db in place on first start;
back up kuma_data per host before pulling (see plan/10-uptime-kuma.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:33:37 +01:00
oierandClaude Opus 5 d457713186 chore(mods): classify owo-lib as both
Dependency library declaring side=BOTH; keep server-side so any
server-loaded dependent doesn't crash on a missing lib.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CAvxmMjspjDw7Fkkmy86EN
2026-09-12 17:00:04 +02:00
oierandClaude Opus 5 556f86f852 chore(mods): classify 2 new distribution jars as both
craftabletotemofundying (recipe addition) and friendsforlife (pet QoL)
both declare side=BOTH and need server-side loading. Pack now 192 jars,
160 server-side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CAvxmMjspjDw7Fkkmy86EN
2026-09-12 16:50:39 +02:00
oierandClaude Opus 5 6b9b8bd96c chore(mods): bump sodium + supplementaries jar names
Distribution updated sodium 0.8.12-alpha.4 → 0.8.13 and
supplementaries 3.5.26 → 3.7.7. Rename the mods-sides.json keys so
sync-server-mods.sh doesn't halt on the missing old jars.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E6PydPB5dvyQ7wxrzS7JvQ
2026-09-06 15:41:04 +02:00
oierandClaude Opus 5 fe91bf7778 chore(mods): resync mods-sides.json with 2026-09-06 distribution
The distro release dropped craftingstation and added create_jet_boots +
flightapi, but mods-sides.json still listed craftingstation as `both`.
sync-server-mods.sh halts on a selected jar missing from the source, so
update-all.sh would have failed — and under --hard that lands after
`docker compose down`, leaving the stack down.

classify-mods.py also picked up sawmill and supplementaries, which had no
entry at all and were therefore never synced to the server. All four new
entries default to `both`.

mods-sides.json entries now 190 = jars in distribution 190.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Wz4tizJzjivT4FhdsqAMb
2026-09-06 15:05:52 +02:00
oierandClaude Opus 5 0176a674e3 fix(tls): absolutise CERT_DIR so cron renewals actually install
acme.sh persists the --install-cert paths and replays them on every cron
renewal, where cwd is $HOME rather than the repo. The relative default
resolved to ~/certs/... so renewals wrote nothing: acme.sh kept renewing
cleanly while nginx served the stale copy until it expired. Apex, auth. and
pack. served expired certs on 2026-09-06.

Absolutise CERT_DIR in issue-letsencrypt.sh and document the failure mode,
the recovery command, and off-host verification in plan/15-letsencrypt.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UJzZrBaZUoU6qHMLpLz64
2026-09-06 00:36:23 +02:00
oier 671e6af05a doc: ip ban command 2026-08-15 13:03:52 +02:00
oierandClaude Opus 5 fbed9a5af7 chore(mods): classify 4 new distribution jars as both
craftingstation, dirigo, mechtrowel and smartfeedingtroughs landed in the
cochi distribution but were never classified, so sync-server-mods.sh left
the server on the old 151-jar set while clients pulled 155. All four
declare BOTH-side deps in neoforge.mods.toml and register blocks/items,
so the server needs them or joining clients hit a registry mismatch.

cochi is Python 3.10 (no tomllib), so classify-mods.py can't run there —
the tomls were read directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 21:37:52 +02:00
oier 40fb50e86f Cosas 2026-08-02 06:53:09 +02:00
oierandClaude Opus 5 d105fc9acf docs(monitor): document join/leave browser notifications
Records why it is browser-only rather than chat push (a push channel needs a CA
bundle in the scratch image, a token, and outbound egress this container does
not have), why newness is a localStorage seen-set keyed on the log timestamp
rather than a server-side sequence number (the counter resets on restart, the
timestamp does not), and the unconditional-baseline subtlety.

Also states plainly what the node harness does NOT cover: the real permission
prompt and actual notification rendering are stubbed, so those are untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 03:39:47 +02:00
oierandClaude Opus 5 3eb20054a0 feat(monitor): opt-in browser notifications for player join/leave
A bell toggle in the header. Rides the same sessions[] the "Recent joins" tile
already renders — no second data source and no re-parsing of the log in JS.
UI-only: no server change, no new egress, no secret, and the container still
reaches nothing but mcnet.

The hard part is not notifying for history, which arrives two ways: the events
buffer is already populated when the page loads, and the monitor's cold-start
backfill re-reads 5000 log lines on every deploy, repopulating the ring with the
SAME events. So newness is a seen-set in localStorage keyed on the log's own
timestamp (time|player|action). That key is stable across both a page reload and
a container restart, where a server-side sequence number would reset to zero and
make every historical event look new.

One subtle ordering bug worth calling out, caught while writing the tests: the
first-response baseline flag must flip unconditionally, not only when fresh
events were found. A returning user has everything already in localStorage, so
their first response yields nothing fresh — flipping the flag lazily would burn
the baseline on their first REAL event instead and silently swallow it.

Notifications are opt-in and off by default; permission is requested from the
bell click (a user gesture), never on load. A denied permission shows a distinct
blocked state, because "I enabled it and nothing happened" is otherwise
indistinguishable from a bug. Bursts (>3 in one poll, e.g. everyone rejoining
after a restart) collapse to one "N joined, M left". Summary polling tightens
from 10s to 5s only while enabled, since the poll interval is the alert latency.

Also fixes a bug this would have introduced: the level-chip click handler bound
to every .chip, which now includes the bell. Scoped to .chip[data-level].

Verified by extracting the shipped block and running it against stubs: silent
baseline, new event notifies, repolling silent, restart-replay silent (both
same-instance and after reload), burst coalescing, the returning-user edge case,
toggle-off still tracks (no backlog when re-enabled), denied permission silent,
and the seen-set staying bounded at 500 newest keys.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 03:38:33 +02:00
oierandClaude Opus 5 bac2372c95 docs: fold the monitor service into the stack-level docs
plan/23-monitor.md already covered the design; this makes the monitor
discoverable from the places someone actually starts reading.

- README: stack table, vhost list, DNS names, layout, and a setup section
  covering the bcrypt credential, the no-published-port model, and the
  --hard-on-first-deploy requirement.
- plan/00-overview.md: drop (PLANNED), add to the always-on service list, and
  note that it starts fine with Minecraft down (missing log and refused RCON
  are reported states, not fatal).
- plan/19-routes.md: vhost row, the four GET routes with their contracts, and
  the internal mcnet edges (mc-status for rosters so it needs no Drasl creds,
  RCON for TPS, Kuma probing healthz around caddy).
- caddy/Caddyfile: the snippet index had drifted — it listed five of nine
  vhosts. Added the four missing ones.

Two gotchas promoted to CLAUDE.md because they cost real time and will recur:

- Compose interpolates $-sequences in .env VALUES, so a bare bcrypt hash is
  silently corrupted before reaching the container, and the same bug bites
  again when writing one to a remote .env over ssh. Also records that caddy
  and filestash want opposite hash formats.
- The monitor container runs as root deliberately: /data is drwxr-x--- owned
  by uid 1000, so a USER directive would make the tailer silently read nothing
  while still returning 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:30:42 +02:00
oierandClaude Opus 5 bc99427ef9 docs(monitor): record phases 2+3 as deployed, with prod verification
Rolling deploy — only the monitor container was recreated, Minecraft was never
restarted, so no world downtime.

Records what was verified against real data rather than assumptions: the TPS
parse and RCON allowlist, the session parser against actual rotated-log join
lines, the level-regex fix (WARN/ERROR now distinguished where every line
previously read INFO), collector isolation with mc-status killed, and the
concurrent-refresh latency win.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:17:56 +02:00
oierandClaude Opus 5 00984e1a70 feat(monitor): dashboard tiles + TPS via allowlisted RCON
Phases 2 and 3 of plan/23-monitor.md. /api/summary adds: server online/MOTD/
version, players online with NMSR heads, registered-account count, disk free on
the world volume, backup freshness, join/leave history, and TPS.

Each tile has its own TTL cache and its own failure mode. A failed collector
keeps serving its last good value and reports the reason in a sibling *_err
field, so the page goes stale-with-a-warning rather than blank. The five
collectors refresh concurrently: sequentially, one request during an outage
would cost the sum of every timeout (~15s), and an outage is exactly when
someone is looking at this page. Measured with a dead dependency: 0.24s.

TPS needs RCON, so the monitor now holds RCON_PASSWORD. Read-only survives
because the allowlist is a compile-time constant and no HTTP input reaches a
command string — the UI cannot run commands because the server cannot be asked
to. The Source RCON client is implemented in-repo rather than added as a
dependency precisely to keep that property local and auditable.

Two things the real server corrected:

- `neoforge tps` prints "Overall: 20.000 TPS (0.357 ms/tick)", not the older
  Forge "Mean tick time / Mean TPS" wording the plan assumed. main_test.go
  pins the real captured output so a format change fails a test instead of
  silently zeroing the tile.

- Rosters come from mc-status, which already holds the Drasl admin credentials
  and exposes a sanitized list, so this container does not get a second copy.

Also fixes a phase-1 bug: the UI's level regex assumed itzg's console timestamp
([12:34:56]) but the file uses [01Aug2026 22:55:34.358], so every line was
classified INFO and the ERROR/WARN chips did nothing. It now matches on shape.

The container runs as root deliberately — /data is drwxr-x--- owned by uid 1000,
so a non-root user cannot traverse it and the tailer would silently read
nothing. The :ro mounts and absent port are what bound the damage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:16:11 +02:00
oierandClaude Opus 5 53604a6127 docs(monitor): record phase 1 as deployed + prod verification results
Phase 1 is live on cochi. The deploy exercised the rotation path on its own —
monitor came up against the pre-restart latest.log, Minecraft created a new
one, tailer reported rotations:1 and kept streaming — which was the single
failure this design was most likely to hit.

Also records the one known cosmetic gap: a delete-then-recreate rotation takes
the "file was gone" branch, so the rotations counter does not tick. Streaming
is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 22:56:26 +02:00
oierandClaude Opus 5 fca6747bcc feat(monitor): admin-only latest.log stream at monitor.
Phase 1 of plan/23-monitor.md: a small Go service that tails the Minecraft
server's latest.log into an in-memory ring buffer and serves it to an embedded
single-page UI (autoscroll, substring/regex filter, level chips).

Read-only by construction — /data is mounted :ro, there is no mutating route,
and the container holds no credentials. It publishes NO port: caddy's
basic_auth vhost is the only way in, which is the point, since the service has
no auth of its own and the log carries player IPs, chat and admin commands.

Two non-obvious choices, both to avoid prod-only failures:

- Polling, not SSE. The chain is host nginx -> caddy -> app, and nginx buffers
  proxied responses by default, so an EventSource would stall on cochi (whose
  vhost is an untracked per-host file) and not reproduce locally. Delay was
  accepted at design time, so polling is strictly better.

- The cursor is a sequence number, not a byte offset, and the tailer reopens on
  inode change or size shrink. itzg rotates latest.log on every server restart,
  i.e. every deploy — a naive tailer goes silent afterwards while still
  answering HTTP 200. Verified against rotate, truncate, delete/recreate,
  partial-line and ring-overflow cases.

MONITOR_ENABLED (on|off) gates both layers per host, mirroring FILES_BROWSE.

MONITOR_PASSWORD_HASH must be bcrypt AND single-quoted in .env: Compose
interpolates $-sequences in .env values, so a bare hash is silently mangled
($2a$14$abc... -> $$2a$$14...) and every login fails with no hint why.
Confirmed empirically, and it is the opposite of filestash, which needs $6$.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 22:51:39 +02:00
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
oier dfb8719074 enable command blocks 2026-07-25 16:56:49 +02:00
oierandClaude Opus 5 a3c97af374 chore(mods): drop superseded architectury/balm/defaultoptions + chat_heads keys
The distribution replaced architectury 13.0.8 -> 13.0.11, balm 21.0.58 ->
21.0.63 and defaultoptions 21.1.6 -> 21.1.7 (both versions of each were
briefly on disk, so both got classified and both were synced into /mods),
and dropped chat_heads. Removing the stale keys — sync-server-mods refuses
to run while a selected jar is missing from the distribution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 23:20:21 +02:00
oierandClaude Opus 5 5e5367e6d5 chore(mods): ftb-library 2101.1.33, add nirvana_lib + common-networking
Distribution bumped ftb-library to 2101.1.33 (satisfies ulicraftmod's
[2101.1.33,) requirement) and added Nirvana Lib + its Common Networking
dependency. Drops build_dim, removed from the distribution.

All three new jars declare side BOTH in neoforge.mods.toml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 22:47:46 +02:00
oierandClaude Opus 5 95f97a3b75 chore(mods): classify cupboard/collective, bump ftb-library key to 2101.1.32
Distribution added the two missing hard deps (cupboard for lootintegrations,
collective for starterkit) and replaced ftb-library 2101.1.31 with 2101.1.32.
Drops the 2101.1.31 key so sync-server-mods stops halting on the absent jar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 22:10:48 +02:00
oierandClaude Opus 5 e831d8b9f3 chore(mods): classify 23 new distribution jars, drop 3 stale entries
Distribution grew to 186 required jars (Create/Ars addons, YUNG's loot
integrations, easy_npc, ulicraftmod 1.1.3). classify-mods.py seeds the 23
new keys (3 client, 20 both).

Drops PuzzlesLib-v21.1.51 and ulicraftmod-1.0.0 (superseded versions still
sitting on the oxps dist disk — keeping them mapped `both` would sync two
copies of the same mod id into /mods) and iris (moved to forgemods/optionaloff).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 22:04:33 +02:00
oierandClaude Opus 5 6b317d179a feat(landing): link files. share from nav + hero
Read-only file browse was only reachable by typing the URL. Add
site.filesUrl (files.${BASE_DOMAIN}) plus nav/hero labels in en/es/eu,
and surface it as a nav link and a secondary hero CTA beside "Cómo entrar".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:01:32 +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 0bf42d5797 chore(mods): classify 3 new distribution mods
ars_nouveau, ars_creo and world_dimension_nexus all declare NeoForge
side BOTH and are content mods, so they stay on the server subset.
Create is already in the pack, so ars_creo's dependency is satisfied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 14:28:31 +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 1958a96acf chore(mods): classify 11 new distribution mods
Distribution added 11 jars. classify-mods.py seeds all as `both`
(none declare CLIENT in neoforge.mods.toml). Hand-flip the two
pure-client cosmetic ones so sync-server-mods.sh drops them:

- ExtremeSoundMuffler: client-side sound muffling UI
- ToastControl: client-side toast popup suppression

Rest stay `both` — Placebo/moonlight/caelus are libs, and
amendments/etched/fishingoverhaul/immersive_paintings/
createornithopterglider/emotecraft add server-side content or sync.

Also map iris as `client`. It is referenced by distribution.json but
absent from the local (gitignored) dist jar dir, so classify-mods.py
never sees it; mapping it keeps mods-sides.json aligned with the
manifest. Client-only, so the sync selection is unchanged.

server-mods/ now 130 jars (was 123).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:29:35 +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 a67b9cf59e fix(mc): mount only kubejs source subdirs ro, keep kubejs root writable
Mounting the whole kubejs dir read-only made KubeJS's BaseProperties.save()
throw NPE on boot (it writes config/cache/generated/logs under kubejs/),
so KubeJS never initialised. Bind only the source subdirs (server_scripts,
startup_scripts, data, assets) read-only and let the kubejs root live
writable in mc_data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:01:07 +02:00
oierandClaude Opus 4.8 d303d23d18 feat(mc): mount distribution kubejs + CustomSkinLoader into server
The minecraft container only received forgemods jars via ./server-mods.
The distribution's files/kubejs (server_scripts, data, startup_scripts)
and CustomSkinLoader never reached the server, so server-side kubejs
recipes/scripts were silently absent.

Bind-mount both from DISTRIBUTION_WEB_ROOT (read-only, to avoid the
server polluting the client distribution with kubejs-generated files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:53:47 +02:00
oierandClaude Opus 4.8 614e47598b chore(mods): repoint rechiseled (both) + fusion (client) to fixed jars
Distribution replaced the wrong-version jars: rechiseled now
mc1.21 (gate [1.21,1.21.2) — valid for 1.21.1, no longer the broken
mc1.21.11 build) so back to `both`; fusion bumped 1.3.2a→1.3.2b, stays
`client` (resource-pack texture feature). Dropped the two stale entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:39:09 +02:00
oierandClaude Opus 4.8 69871c8a93 fix(mods): drop rechiseled from server — wrong MC version
rechiseled-1.2.5-neoforge-mc1.21.11.jar targets Minecraft 1.21.11 /
NeoForge 21.11, not this server's 1.21.1 / 21.1.233. As `both` it crashed
the server at pre-load (FATAL ModLoader: requires minecraft 1.21.11). Mark
`client` to exclude it from server-mods so the server boots.

NOTE: the jar is still wrong-version for clients too (same gate) — it must
be removed from / replaced in the distribution to be usable at all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:36:03 +02:00
oierandClaude Opus 4.8 015bf7fbe3 chore(mods): classify fusion (client) + rechiseled (both)
Two new distribution mods. rechiseled adds decorative blocks → both
(server needs the block registry). fusion is a client-only resource-pack
texture/model feature (no blocks/logic) → client, so sync drops it from
server-mods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:31:25 +02:00
oierandClaude Opus 4.8 d0bc81f0f7 chore(mods): classify ulicraftmod as both
New custom mod ulicraftmod-1.21.1-1.0.0-6.0.10 added to the distribution;
content mod (server + client), so both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:07:26 +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 33b9ebc7f6 feat(landing): slim status section, hero, and server-card icon
- Remove the stat-grid from the status section (server card only); prune the
  now-orphaned site.stats, MOD_COUNT_KEY, statLabels (3 locales), and
  .stat-grid/.tile CSS.
- Empty-roster message -> "Nobody online", now localized (status.empty in
  en/es/eu) and wired to both ServerCard instances.
- Flatten the server-card icon (drop the inset emboss bevel on .sc-icon).
- Drop "Solo LAN" (hero metaLan) from the hero meta row; prune metaLan (3 locales).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:45:19 +02:00
oierandClaude Opus 4.8 b9929da54d chore(mods): add geckolib + terrablender (Yungs worldgen deps)
YUNG's Cave Biomes hard-requires geckolib>=4.7.6 and terrablender>=4.1.0.8;
both now in the distribution. Classified both (server worldgen libs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:21:56 +02:00
oierandClaude Opus 4.8 36c28362bc chore(mods): sync mods-sides.json with distribution
+21 new (Yungs* + Moogs* worldgen, lootjs, solcarrot, melter,
seamless-loading-screen), -8 removed (FramedBlocks, GnKinetics,
create_vibrant_vaults, createtransmission, interiors, picaxe,
bellsandwhistles, welcomescreen). seamless-loading-screen hand-edited
to client (pure client UI). Worldgen mods stay both — server generates
structures.

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