fix(catalog): decode WebP bundle textures so furniture icons resolve again
Every write path normalises a bundle's texture to WebP Lossless, so the catalog icon was being read back with a PNG-only decoder. decodePng throws on anything that is not a PNG, the callers caught that and returned null, and the user-visible result was "no icon (not in source or bundle)" for every furniture whose source does not serve a standalone icon. Measured against the production asset tree, all 18,505 bundles were WebP; icon extraction succeeded on 0 of them. src/lib/services/imager/ decode-texture.ts keeps PNG on the dependency-free decoder and routes WebP through sharp, which is already a dependency and already encodes these textures. extractFurniIconPng and getPetIconPng become async; the five call sites (upload, clone import, furni import, icon repair and both icon routes) already awaited their surrounding work. Same root cause, second bug: the spritesheet frame key. Converters disagree on packing — some keep a trailing ".png", and some lowercase the whole key while leaving the bundle name mixed-case, so "LTD_fashionistaf" looks up frame "LTD_fashionistaf_LTD_fashionistaf_icon_a" and never finds "ltd_fashionistaf_ltd_fashionistaf_icon_a". Any mixed-case classname could therefore never match, which is most of the catalogue. findFrame tries the two exact spellings, then falls back to one case-insensitive pass. Extraction now succeeds on 18,483 of 18,505 bundles (99.88%); the 22 remainder are data, not code — 9 bundles ship no icon asset, 11 do not parse. Third: three catalogue icons exist only as .gif while catalogueIconUrl hardcoded .png, so the picker offered icons that could only ever 404, and 291 icons that ship as both formats were listed twice. The API now dedupes per id and the two renderers retry with .gif before falling back to the placeholder, matching what catalog-image-picker already did. Verified live: /gamedata/.../icon_1542.png returns 404 while icon_1542.gif returns 200. Separately, close the last hole in the memory cap. Every script in package.json routes through scripts/with-memory-cap.sh, but invoking the builder directly — from a terminal, an IDE or an agent — skipped the wrapper and ran unbounded, on a host with no swap where the OOM killer picks its victim across the whole machine. next.config.ts now refuses a production build that the wrapper has not marked, before anything allocates. next dev and next start are deliberately unaffected. README gains a Memory-capped commands section covering the per-script ceilings, the backends and the ulimit -v trap, and its stale version and script tables are corrected.
This commit is contained in:
1 parent
6793f77733
commit
43742e8d99
18 files changed
+433
-67
No files matched your search
@@ -10,14 +10,21 @@ Features a premium animated homepage (typewriter hero, floating orbs, scroll cou
|
||||
|
||||
| Component | Version | Notes |
|
||||
| --------------- | -------------- | ---------------------------------------- |
|
||||
| Node.js | 26.9.0 | Current release pinned in `.nvmrc` |
|
||||
| pnpm | >= 11.25.0 | Recommended package manager |
|
||||
| Node.js | 26.10.0 | Pinned in `.nvmrc` (`>=26.10.0 <27`) |
|
||||
| pnpm | 12.10.1 | Pinned via `packageManager` |
|
||||
| npm | >= 11.x | Supported alternative |
|
||||
| yarn | >= 4.x | Supported alternative |
|
||||
| MySQL / MariaDB | 8.0+ / 10.6+ | Shared with the emulator |
|
||||
| Docker | 24+ | Optional — for containerized deployment |
|
||||
| Valkey | 8.x+ | Optional — caching, rate limiting, SSE |
|
||||
|
||||
Core stack: **Next.js 16.4.0** (App Router) · **React 19.3.0** · **TypeScript 7.0.2** · **Drizzle ORM 0.45.3** · **Zod 4.6.5** · **Vitest 5.0.3** · **Biome 2.5.15** · **Playwright 1.63.0**.
|
||||
|
||||
> Builds must run through the `pnpm` scripts. This host has no swap and
|
||||
> `vm.overcommit_memory=0`, so an uncapped `next build` gets OOM-killed by the
|
||||
> kernel and can take the database and the live release down with it. See
|
||||
> [Memory-capped commands](#memory-capped-commands).
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
@@ -102,6 +109,10 @@ pnpm build && pnpm start # or: npm run build && npm start
|
||||
|
||||
Open `http://localhost:3002` in your browser.
|
||||
|
||||
> Always go through these scripts. `next build` is refused outright when it is
|
||||
> not running under the memory cap — see
|
||||
> [Memory-capped commands](#memory-capped-commands).
|
||||
|
||||
### 6. First Login
|
||||
|
||||
1. Register an account at `/register`, or log in with an existing emulator account.
|
||||
@@ -951,15 +962,99 @@ branch guard inside `ci-deploy.sh` only accepts `main`/`master`.
|
||||
|
||||
---
|
||||
|
||||
## Memory-capped commands
|
||||
|
||||
This host runs with `vm.overcommit_memory=0` **and no swap**. When a process
|
||||
asks for more memory than is free, the kernel does not wait — it calls the
|
||||
OOM-killer immediately, and it picks its victim across the **whole machine**,
|
||||
not just the offending process. An uncapped build does not merely fail: it can
|
||||
take the MariaDB process, nginx and the live release down with it.
|
||||
|
||||
Every heavy command therefore runs inside its own cgroup with a hard
|
||||
`MemoryMax`, via `scripts/with-memory-cap.sh`. If the build outgrows its
|
||||
ceiling, only that cgroup is killed — the build fails, the site keeps serving.
|
||||
|
||||
| Script | Ceiling | Covers |
|
||||
| --------------------- | ------- | --------------------------------------- |
|
||||
| `pnpm dev` | 8 GB | Dev server |
|
||||
| `pnpm build` | 10 GB | Production build |
|
||||
| `pnpm analyze` | 10 GB | Build + bundle-size report |
|
||||
| `pnpm test` | 8 GB | Vitest |
|
||||
| `pnpm test:coverage` | 8 GB | Vitest with coverage |
|
||||
| `pnpm test:ui` | 8 GB | Playwright |
|
||||
| `pnpm test:e2e` | 8 GB | Playwright |
|
||||
| `pnpm test:integration` | 8 GB | Vitest integration config |
|
||||
| `pnpm typecheck` | 6 GB | `tsc --noEmit` |
|
||||
|
||||
### Running the builder by hand
|
||||
|
||||
```bash
|
||||
npx next build # ✗ refused before it allocates anything
|
||||
```
|
||||
|
||||
`next build` loads `next.config.ts`, which **refuses any production build that
|
||||
is not running under the memory cap**. This closes the one hole the `pnpm`
|
||||
scripts leave open: invoking the builder directly — from a terminal, an IDE, or
|
||||
an automated agent — would otherwise bypass the cgroup entirely and go
|
||||
unbounded.
|
||||
|
||||
The refusal looks like this:
|
||||
|
||||
```
|
||||
Error: Refusing to run an uncapped production build.
|
||||
|
||||
On this host an unbounded `next build` gets OOM-killed by the kernel,
|
||||
and the killer may take the database, nginx or the live release with it.
|
||||
|
||||
Use the capped build instead:
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Fix: use `pnpm build`. If you are genuinely inside an isolated environment
|
||||
where the container *is* the boundary (the Docker build, a CI runner), set
|
||||
`CMS_MEMORY_CAPPED=1` to opt out deliberately.
|
||||
|
||||
### Backends
|
||||
|
||||
`scripts/with-memory-cap.sh` picks its mechanism automatically:
|
||||
|
||||
| `CMS_MEMORY_CAP_BACKEND` | Mechanism | Notes |
|
||||
| ------------------------ | ------------------------------------------- | ------------------------------------------------------- |
|
||||
| `auto` *(default)* | systemd cgroup `MemoryMax` | Real RSS bound over the whole process tree. Used here. |
|
||||
| `ulimit` | `ulimit -v`, per process | Virtual address space, **not** RSS. Fallback only. |
|
||||
| `none` | None — warning only | Docker build and GitLab runner, each already isolated. |
|
||||
|
||||
On a host **without** systemd and without `CMS_MEMORY_CAP_BACKEND=none`, the
|
||||
script refuses to run rather than proceeding unbounded.
|
||||
|
||||
> Do not "fix" a cap failure by lowering `--max-old-space-size` or by raising
|
||||
> `CMS_MEMORY_CAP_VIRTUAL` (the default is `40g` on purpose). A `ulimit -v` of
|
||||
> 10g makes V8 clamp its own heap to ~2.25 GB and webpack dies with
|
||||
> `std::bad_alloc`. Measure first; raise the ceiling deliberately.
|
||||
|
||||
---
|
||||
|
||||
## Scripts
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------- | -------------------------------------------------- |
|
||||
| `pnpm dev` | Start development server (hot reload) |
|
||||
| `pnpm build` | Production build |
|
||||
| `pnpm build` | Production build (memory-capped) |
|
||||
| `pnpm start` | Start production server |
|
||||
| `pnpm typecheck` | Run TypeScript type checking |
|
||||
| `pnpm test` | Run all tests (Vitest) |
|
||||
| `pnpm test:coverage` | Run all tests with coverage thresholds enforced |
|
||||
| `pnpm test:ui` | Playwright UI tests (`playwright.ui.config.ts`) |
|
||||
| `pnpm test:ui:update` | Playwright UI tests, updating snapshots |
|
||||
| `pnpm test:e2e` | Playwright end-to-end tests |
|
||||
| `pnpm test:integration` | Integration tests (own Vitest config) |
|
||||
| `pnpm test:housekeeping` | Housekeeping feature tests |
|
||||
| `pnpm lint` | Lint and format check (Biome) |
|
||||
| `pnpm biome:lint` | Alias of `pnpm lint` |
|
||||
| `pnpm format` | Format files in place (Biome) |
|
||||
| `pnpm toolchain:check` | Verify the Node toolchain matches `.nvmrc` |
|
||||
| `pnpm i18n:check` | Audit CMS translation coverage |
|
||||
| `pnpm deps:audit` | Audit dependencies for high-severity advisories |
|
||||
| `pnpm db:migrate` | Apply pending SQL migrations |
|
||||
| `pnpm db:migrate:status` | Show migration status |
|
||||
| `pnpm db:schema:generate` | Regen `src/db/schema.ts` from prior schema + live DB |
|
||||
@@ -970,10 +1065,15 @@ branch guard inside `ci-deploy.sh` only accepts `main`/`master`.
|
||||
| `pnpm db:up` / `pnpm db:down` | Start / stop the `mariadb-turbo` container |
|
||||
| `pnpm gamedata:compress` | Pre-compress large gamedata JSON to `.gz` (gzip_static) |
|
||||
| `pnpm analyze` | Build + report per-route bundle sizes |
|
||||
| `pnpm performance:report` | Re-render the performance report from a build |
|
||||
| `pnpm jobs:worker` | Start background task worker |
|
||||
| `pnpm biome:check` | Lint and format code |
|
||||
| `pnpm assets:editor` | Copy TinyMCE editor assets into `public/` |
|
||||
|
||||
> Replace `pnpm` with `npm run` or `yarn` for other package managers.
|
||||
>
|
||||
> The heavy ones run memory-capped — see
|
||||
> [Memory-capped commands](#memory-capped-commands). A production `next build`
|
||||
> run outside the wrapper is refused rather than executed unbounded.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user