Files
Lucas RayanandClaude Sonnet 5.5 a4a2584f37 #14: Gitea CI, registry composes, Node 22 image and changelog
- Gitea Actions: Trivy, Vitest in a Dockerfile test stage, arm64 image build, image
  scan and smoke test; releases reuse the image by content hash.
- docker-compose.<edition>.registry.yaml run the stack from the published image.
- Dockerfile: Node 22, a test stage, and no npm in the runtime image.
- README, CLAUDE.md and CHANGELOG describe the CI, the image and the release flow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 22:54:00 -03:00

128 lines
9.9 KiB
Markdown

# CLAUDE.md
Guidance for Claude Code when working in this repository.
## What CraftDock is
A lightweight, self-hosted web panel that manages a **single** Minecraft server (Java *or* Bedrock) running as a Docker container. Node.js + Express + Socket.io backend, vanilla-JS tabbed SPA frontend (Tailwind via CDN). The panel never modifies the Minecraft core: it controls the game container over the Docker socket and sends commands via RCON (Java) or container stdin (Bedrock).
## Commands
```bash
npm test # full Vitest suite — must pass before any commit
npm run test:watch
npm run dev # dev server (loads .env, watches files), panel on :3000
npm run hash -- <pw> # generate the admin bcrypt hash (raw + base64 forms)
docker compose -f docker-compose.bedrock.yaml up -d --build # full Bedrock stack
docker compose -f docker-compose.java.yaml up -d --build # full Java stack
docker build --target test . # Vitest inside the image (what CI runs)
scripts/smoke-imagem.sh <image> # image smoke test (what CI runs)
```
CI/CD lives in `.gitea/workflows` (Gitea runners): `ci.yml` on branches, `release.yml` on `main` and `vX.Y.Z` tags. The image is `git.lucasrguerra.dev.br/lucasrguerra/craftdock` (public). Remote: `gitea`. Keep `CHANGELOG.md` updated under *Unreleased* and release with `git tag vX.Y.Z && git push gitea vX.Y.Z`.
Run a single test file: `npx vitest run test/services/playerDetailService.test.js`.
## Git workflow (mandatory)
- **One numbered branch per feature/fix**: `feature/N-slug` or `fix/N-slug`, where N is sequential across the repo (check `git branch -a` for the highest existing N).
- **Every commit message is prefixed with the branch number**: `#N: message` (e.g. `#10: resolve Bedrock player detail via player_<uuid> ServerId mapping`). This makes it trivial to see which branch a change came from.
- **Merge into `main` with `--no-ff`** so the graph shows one merge bubble per feature. Never commit directly to `main`.
- **Squash small commits**: repeated touch-ups to the same file or tiny adjustments belong inside one larger, complete commit — don't inflate the tree.
- Messages in English, imperative, descriptive of the *why* when non-obvious.
## Architecture
### Composition root + dependency injection
`src/main.js` is the only place that wires real dependencies; `src/server.js` composes the Express app + Socket.io namespaces from injected pieces. Every service/route/adapter is a **factory function taking its collaborators as arguments** (`createXxx({ dep1, dep2 })`), so unit tests inject fakes — never module-level singletons for anything that touches I/O.
### Layers
```
src/
├── main.js # composition root (real deps wired here, and only here)
├── server.js # Express + Socket.io composition (takes deps)
├── config.js # env loading + validation (single source of env truth)
├── logger.js # structured JSON logger (ts/level/component/msg/meta)
├── appState.js # cached edition detection + adapter resolution
├── middleware/auth.js # session auth for HTTP routes AND socket handshake
├── adapters/ # ServerAdapter interface: java (RCON) / bedrock (stdin)
│ └── playerFile/ # read-only world-save readers (see below)
├── services/ # docker, rcon, stdin, properties, world, seed, auth,
│ # playerDirectory, playerDetailService, bedrockIdentityBridge
├── routes/ # auth, status, players, properties, world (Express routers)
├── sockets/ # logs stream + status push namespaces
└── public/ # vanilla-JS SPA: js/app.js shell + js/tabs/*.js (one file per tab)
```
### The adapter pattern (edition differences)
`ServerAdapter` abstracts the two editions behind one interface. Capability differences are declared, not special-cased in routes/UI:
| | Java | Bedrock |
|---|---|---|
| Command channel | RCON (`rconService`) | container stdin via `docker attach` (`stdinService`) |
| whitelist | `whitelist ...` | `allowlist ...` |
| ban/pardon | supported | **no native command** → `NotSupportedError`, buttons hidden |
| seed | `seed` command | **no command** — read `RandomSeed` from `level.dat` (`seedService`) |
| save snapshot | `save-all flush` | `save hold` → poll `save query` → read → `save resume` |
The frontend gets `capabilities` from `/api/players` and only renders controls the edition supports. When adding a command, add it to both adapters (or throw `NotSupportedError`) and update `CAPABILITIES` in `serverAdapter.js`.
### Player data: read-only from world saves
**Rule: ALL player data (position, health, food, inventory, gamemode, XP) is read directly from the world save files, read-only — never via console commands.** Per-edition file adapters in `src/adapters/playerFile/`:
- `nbtPlayer.js` — normalizes decoded NBT into one edition-agnostic `PlayerData` shape.
- `javaPlayerFile.js` — reads `world/playerdata/<uuid>.dat` (gzip NBT, big-endian).
- `bedrockPlayerFile.js` — reads the world **LevelDB** (`leveldb-zlib`, Mojang variant, little-endian NBT).
While the server runs, reads happen inside a save snapshot (`save hold`/`save-all flush`) orchestrated by `playerDetailService` — never open the live LevelDB without it.
### Bedrock identity model (critical, hard-won knowledge)
Bedrock never stores the gamertag or XUID readably inside the world DB. Three identity layers exist:
1. **XUID + gamertag** — only appear in the server log (`Player Spawned: <name> xuid: <n>`). `playerDirectory.js` scans docker logs for these lines and persists an XUID-keyed directory at `<dataRoot>/craftdock_players.json` (Java seeds from `usercache.json` instead).
2. **`uniqueId` UUID** (MsaId/SelfSignedId) — returned by the `querytarget` console command for an *online* player. ⚠️ `querytarget` also returns an int64 `id` (the entity UniqueID); do not confuse the two.
3. **LevelDB records** — `player_<uuid>` mapping records (keyed by MsaId or SelfSignedId) whose NBT `ServerId` field points to the actual data record `player_server_<leveldb-uuid>`.
`bedrockIdentityBridge.js` learns and persists the XUID → leveldbUuid binding at `<dataRoot>/craftdock_bedrock_ids.json` (learned once while the player is online, works offline forever after). Resolution order in `bedrockPlayerFile.findByUniqueId`: direct `player_<uuid>` mapping lookup first, NBT `UniqueID` scan as fallback.
### Properties (Options tab)
`PROPERTY_SCHEMA` in `src/routes/properties.js` is the single source of managed keys: type-based validation on PUT **and** vanilla defaults merged into GET (so the UI always starts fully populated; `motd` deliberately has no default). The frontend `options.js` renders from its own `CATEGORIES` list — when adding a property, update both the schema (backend) and CATEGORIES (frontend).
### Item icon assets (generated, never committed)
The player inspector renders Minecraft item icons served from `src/public/assets/mc/items/` — that directory is **gitignored and generated**: `scripts/extract-mc-assets.mjs` copies flat item PNGs out of the `minecraft-assets` devDependency. It runs automatically via `postinstall` for local dev, and in the Dockerfile `assets` stage at image build time (no runtime download — the panel stays offline-capable). Bump the game version with the `ASSETS_VERSION` build arg or `npm run assets -- <version>`. Unknown/modded items fall back to a text tile in the UI.
### CraftDock metadata files
Panel-owned files live at the data root, siblings of the world, so they ride along in world exports/backups: `craftdock_players.json` (XUID directory), `craftdock_bedrock_ids.json` (identity bridge).
## Testing rules
- **Vitest**, tests mirror `src/` under `test/`. TDD: write the failing test before the fix/feature.
- Unit tests inject fake collaborators (plain objects with `vi.fn()`); route tests use `supertest` against a minimal Express app.
- `test/fixtures/bedrock-player.nbt` is a **real captured Bedrock player NBT**; the bedrockPlayerFile tests build a real LevelDB in a temp dir around it. Prefer real-format fixtures over hand-mocked parsing.
- The whole suite must stay green (`npm test`) before committing.
## Environment & deployment facts
- Panel listens on **3000** (config default). The composes only `expose` it (reverse-proxy first); publish on the host only for local testing. Under Coolify the service's target port must be 3000.
- Auth: single admin password; prefer `ADMIN_PASSWORD_HASH_B64` under docker compose/Coolify (bcrypt `$` gets eaten by interpolation). `SESSION_SECRET` signs the cookie; `NODE_ENV=production` marks it `secure` (needs HTTPS).
- The Minecraft service **must** have `stdin_open: true` + `tty: true` (stdin channel + non-multiplexed logs) and `OVERRIDE_SERVER_PROPERTIES=false` (so the Options tab persists).
- The `mc-data` volume is shared panel ↔ game container; `MC_DATA_PATH` points at it inside the panel.
- Edition: `MC_EDITION=auto|java|bedrock` (auto reads the container's `TYPE` env). `MC_WORLD_NAME` is `world` (Java) / `worlds` (Bedrock).
- Local integration testing: the maintainer runs the stacks via **WSL docker**; the world lives in the `craftdock_mc-data` volume. To inspect real world data safely, stop the containers and copy it out (e.g. `docker run --rm -v craftdock_mc-data:/d alpine tar cf - -C /d worlds`).
## Code style
- ES modules (`"type": "module"`), Node ≥ 18, no build step, no TypeScript.
- Factory functions + closures over classes. Small focused files — split before a file grows into multiple responsibilities.
- Frontend: no framework, no bundler; each tab exports `render<Tab>(root)` and returns an optional cleanup function. Escape all user-derived strings with the local `esc()` before injecting into HTML.
- Comments explain *why*/constraints (protocol quirks, Bedrock oddities), not what the next line does.
- UI text is Brazilian Portuguese; code, comments and commits are English.