chore: update dependencies and biome rules
CI / check (push) Failing after 18s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / tests-imaging-browsers (push) Skipped
CI / tests-quality (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
CI / check (push) Failing after 18s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / tests-imaging-browsers (push) Skipped
CI / tests-quality (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
This commit is contained in:
1 parent
6ef92f57fc
commit
be4dc08c31
14 files changed
+4655
-2583
No files matched your search
+1
-1
@@ -27,7 +27,7 @@ This source change alone does not configure or deploy the production service.
|
|||||||
## Exported content
|
## Exported content
|
||||||
|
|
||||||
| Source | Repository destination |
|
| Source | Repository destination |
|
||||||
| --- | --- |
|
| ------------------------------------------------------------------------ | --------------------------------------------------- |
|
||||||
| Configured Nitro bundles, including furniture, figures, effects and pets | `Gamedata/bundled` |
|
| Configured Nitro bundles, including furniture, figures, effects and pets | `Gamedata/bundled` |
|
||||||
| Configured furni icons | `Gamedata/icons` |
|
| Configured furni icons | `Gamedata/icons` |
|
||||||
| Existing badge/catalog images | `Gamedata/c_images` |
|
| Existing badge/catalog images | `Gamedata/c_images` |
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Features a premium animated homepage (typewriter hero, floating orbs, scroll cou
|
|||||||
## System Requirements
|
## System Requirements
|
||||||
|
|
||||||
| Component | Version | Notes |
|
| Component | Version | Notes |
|
||||||
| --------------- | -------------- | ---------------------------------------- |
|
| --------------- | ------------ | --------------------------------------- |
|
||||||
| Node.js | 26.12.1 | Pinned in `.nvmrc` (`>=26.12.1 <27`) |
|
| Node.js | 26.12.1 | Pinned in `.nvmrc` (`>=26.12.1 <27`) |
|
||||||
| pnpm | 12.11.2 | Pinned via `packageManager` |
|
| pnpm | 12.11.2 | Pinned via `packageManager` |
|
||||||
| npm | >= 11.x | Supported alternative |
|
| npm | >= 11.x | Supported alternative |
|
||||||
@@ -293,7 +293,7 @@ cannot be changed simply by injecting a new variable into an old container.
|
|||||||
### Docker commands
|
### Docker commands
|
||||||
|
|
||||||
| Command | Purpose |
|
| Command | Purpose |
|
||||||
| --- | --- |
|
| ------------------------------- | ----------------------------------------- |
|
||||||
| `bash scripts/docker-update.sh` | Pull, build, migrate, recreate and verify |
|
| `bash scripts/docker-update.sh` | Pull, build, migrate, recreate and verify |
|
||||||
| `docker compose logs -f cms` | View CMS logs |
|
| `docker compose logs -f cms` | View CMS logs |
|
||||||
| `docker compose exec cms sh` | Open a shell in the running CMS |
|
| `docker compose exec cms sh` | Open a shell in the running CMS |
|
||||||
@@ -311,7 +311,7 @@ application build layer. Deleting all Docker cache is not an update mechanism.
|
|||||||
|
|
||||||
The image never installs a pinned pnpm of its own. The Dockerfile installs
|
The image never installs a pinned pnpm of its own. The Dockerfile installs
|
||||||
[corepack](https://nodejs.org/api/corepack.html) and lets it resolve the pnpm
|
[corepack](https://nodejs.org/api/corepack.html) and lets it resolve the pnpm
|
||||||
version *and* its sha256/sha512 hash from the `packageManager` field in
|
version _and_ its sha256/sha512 hash from the `packageManager` field in
|
||||||
`package.json`. One pin therefore drives the Docker build, the CI runner and
|
`package.json`. One pin therefore drives the Docker build, the CI runner and
|
||||||
the host — the image can no longer drift away from the lockfile, and a tampered
|
the host — the image can no longer drift away from the lockfile, and a tampered
|
||||||
package tarball is rejected instead of silently installed. Bumping pnpm is a
|
package tarball is rejected instead of silently installed. Bumping pnpm is a
|
||||||
@@ -319,7 +319,6 @@ single edit in `package.json` (plus the README version table, checked by
|
|||||||
`pnpm readme:versions:check`).
|
`pnpm readme:versions:check`).
|
||||||
|
|
||||||
Two caches make repeated builds cheap:
|
Two caches make repeated builds cheap:
|
||||||
|
|
||||||
- **pnpm store** — a BuildKit cache mount on `$PNPM_HOME/store` keeps the
|
- **pnpm store** — a BuildKit cache mount on `$PNPM_HOME/store` keeps the
|
||||||
downloaded packages between builds. A lockfile change then re-uses the store
|
downloaded packages between builds. A lockfile change then re-uses the store
|
||||||
(`reused 493, downloaded 0`) instead of re-downloading every package.
|
(`reused 493, downloaded 0`) instead of re-downloading every package.
|
||||||
@@ -352,20 +351,20 @@ error in test code break the image build and pulled `@playwright/test`, `vitest`
|
|||||||
Only the CMS (and the optional avatar imager container, see [Avatar Imaging](#avatar-imaging)) run in Docker. The heavy client assets and gamedata stay on the host and are shared into the container so imports and uploads persist:
|
Only the CMS (and the optional avatar imager container, see [Avatar Imaging](#avatar-imaging)) run in Docker. The heavy client assets and gamedata stay on the host and are shared into the container so imports and uploads persist:
|
||||||
|
|
||||||
| Container Path | Host Path | Mode | Purpose |
|
| Container Path | Host Path | Mode | Purpose |
|
||||||
| -------------------------- | -------------------------- | ---- | ------------------------------------ |
|
| -------------------------- | ----------------------- | ---- | ------------------------------------- |
|
||||||
| `/app/public/nitro-assets` | `./public/nitro-assets` | rw | Imported furni/pet/effect assets |
|
| `/app/public/nitro-assets` | `./public/nitro-assets` | rw | Imported furni/pet/effect assets |
|
||||||
| `/app/public/swf` | `./public/swf` | rw | SWF costumes / icons (imports) |
|
| `/app/public/swf` | `./public/swf` | rw | SWF costumes / icons (imports) |
|
||||||
| `/app/storage` | `./storage` | rw | Uploaded media (persistent) |
|
| `/app/storage` | `./storage` | rw | Uploaded media (persistent) |
|
||||||
| `/var/www/Gamedata` | `/var/www/Gamedata` | rw | Shared gamedata root (hardcoded path)|
|
| `/var/www/Gamedata` | `/var/www/Gamedata` | rw | Shared gamedata root (hardcoded path) |
|
||||||
|
|
||||||
**About the hardcoded `/var/www/Gamedata` path:** `src/lib/services/furni-asset-dirs.ts` defines `DEFAULT_GAMEDATA_ROOT = /var/www/Gamedata` as an absolute on-disk path the CMS reads and mirrors imported assets into. The compose file mounts the host `/var/www/Gamedata` at the identical path inside the container so `existsSync('/var/www/Gamedata')` succeeds and nginx keeps serving `/gamedata/` from the same directory.
|
**About the hardcoded `/var/www/Gamedata` path:** `src/lib/services/furni-asset-dirs.ts` defines `DEFAULT_GAMEDATA_ROOT = /var/www/Gamedata` as an absolute on-disk path the CMS reads and mirrors imported assets into. The compose file mounts the host `/var/www/Gamedata` at the identical path inside the container so `existsSync('/var/www/Gamedata')` succeeds and nginx keeps serving `/gamedata/` from the same directory.
|
||||||
|
|
||||||
**The Nitro/Octane client and renderer are NOT mounted** — nginx on the host serves them directly:
|
**The Nitro/Octane client and renderer are NOT mounted** — nginx on the host serves them directly:
|
||||||
|
|
||||||
| Component | Host path | How it's served |
|
| Component | Host path | How it's served |
|
||||||
| -------------------- | ----------------------- | ---------------------------------------------------------------------- |
|
| ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------- |
|
||||||
| Nitro/Octane client | `/var/www/Octane/dist` | nginx `location ^~ /client/` and `/nitro-client/` alias |
|
| Nitro/Octane client | `/var/www/Octane/dist` | nginx `location ^~ /client/` and `/nitro-client/` alias |
|
||||||
| Avatar imager | `/docker/Polaris-imager`| Docker container `avatar-imaging-pixinode` on port `8082` — see [Avatar Imaging](#avatar-imaging) |
|
| Avatar imager | `/docker/Polaris-imager` | Docker container `avatar-imaging-pixinode` on port `8082` — see [Avatar Imaging](#avatar-imaging) |
|
||||||
| Camera uploads | `/var/www/Camera` | nginx `/camera/` alias |
|
| Camera uploads | `/var/www/Camera` | nginx `/camera/` alias |
|
||||||
|
|
||||||
**Container user & write permissions:** the CMS container runs as `www-data` (UID/GID 33) by default to match the host owner of `/var/www/Gamedata`. Ensure the other write volumes (`./public/nitro-assets`, `./public/swf`, `./storage`) are also owned by `www-data` on the host:
|
**Container user & write permissions:** the CMS container runs as `www-data` (UID/GID 33) by default to match the host owner of `/var/www/Gamedata`. Ensure the other write volumes (`./public/nitro-assets`, `./public/swf`, `./storage`) are also owned by `www-data` on the host:
|
||||||
@@ -386,7 +385,7 @@ docker compose build --build-arg RUN_USER=1000
|
|||||||
|
|
||||||
The CMS container runs with `network_mode: host`. This lets the existing `127.0.0.1` references in `.env` keep working against host services — MariaDB (`3306`), Valkey/Redis (`6379`), the emulator RCON/API (`3003`/`3001`) and the avatar imager — without re-writing any environment variables. The container consequently listens **directly on the host's port 3002**, so stop any previous host-side CMS (e.g. `pm2 stop next`) that occupies that port before starting it.
|
The CMS container runs with `network_mode: host`. This lets the existing `127.0.0.1` references in `.env` keep working against host services — MariaDB (`3306`), Valkey/Redis (`6379`), the emulator RCON/API (`3003`/`3001`) and the avatar imager — without re-writing any environment variables. The container consequently listens **directly on the host's port 3002**, so stop any previous host-side CMS (e.g. `pm2 stop next`) that occupies that port before starting it.
|
||||||
|
|
||||||
The **build** deliberately does *not* use host networking. BuildKit (v0.26, Docker 29)
|
The **build** deliberately does _not_ use host networking. BuildKit (v0.26, Docker 29)
|
||||||
refuses to grant host networking unless every caller passes
|
refuses to grant host networking unless every caller passes
|
||||||
`--allow=network.host`, and `docker compose build` has no such flag — asking for
|
`--allow=network.host`, and `docker compose build` has no such flag — asking for
|
||||||
`build.network: host` therefore turned every rebuild into an immediate
|
`build.network: host` therefore turned every rebuild into an immediate
|
||||||
@@ -411,7 +410,6 @@ pnpm db:up # docker compose --profile db up -d (starts mariadb-turbo)
|
|||||||
```
|
```
|
||||||
|
|
||||||
Key settings (see the `command:` block in `docker-compose.yml`):
|
Key settings (see the `command:` block in `docker-compose.yml`):
|
||||||
|
|
||||||
- `max_allowed_packet = 512M` — 50 MB JSON batches no longer hit the 16 MB packet ceiling.
|
- `max_allowed_packet = 512M` — 50 MB JSON batches no longer hit the 16 MB packet ceiling.
|
||||||
- `innodb_flush_log_at_trx_commit = 2` + `innodb_doublewrite = 0` — durability relaxed for bulk writes.
|
- `innodb_flush_log_at_trx_commit = 2` + `innodb_doublewrite = 0` — durability relaxed for bulk writes.
|
||||||
- `innodb_buffer_pool_size = 2G`, `innodb_log_file_size = 1G`, `bulk_insert_buffer_size = 512M`.
|
- `innodb_buffer_pool_size = 2G`, `innodb_log_file_size = 1G`, `bulk_insert_buffer_size = 512M`.
|
||||||
@@ -424,7 +422,7 @@ The datadir lives in a **named volume** (`mariadb-turbo-data`), never a host bin
|
|||||||
|
|
||||||
#### node_modules & host-sync corruption
|
#### node_modules & host-sync corruption
|
||||||
|
|
||||||
pnpm's store is hard-linked and its `.bin` shims are symlinks, so **node_modules must never be a host bind-mount** (Docker Desktop gRPC-FUSE/VirtioFS, Unison and Syncthing all corrupt it). The production build bakes `node_modules` into the image at build time; it is never bind-mounted. For local dev, mount a *named volume* (`node_modules:/app/node_modules`) instead of the host directory, and keep `node_modules` / the pnpm store out of any shared-filesystem bind.
|
pnpm's store is hard-linked and its `.bin` shims are symlinks, so **node_modules must never be a host bind-mount** (Docker Desktop gRPC-FUSE/VirtioFS, Unison and Syncthing all corrupt it). The production build bakes `node_modules` into the image at build time; it is never bind-mounted. For local dev, mount a _named volume_ (`node_modules:/app/node_modules`) instead of the host directory, and keep `node_modules` / the pnpm store out of any shared-filesystem bind.
|
||||||
|
|
||||||
#### Loading a 50 MB JSON dump
|
#### Loading a 50 MB JSON dump
|
||||||
|
|
||||||
@@ -439,8 +437,7 @@ pnpm db:bulk --file=/data/products.json --table=docs --category=furni
|
|||||||
pnpm db:bulk --file=/data/external_texts.json --table=texts --category=default
|
pnpm db:bulk --file=/data/external_texts.json --table=texts --category=default
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer (`scripts/bulk-import-json.ts`) maps rows onto `src/db/schema-gamedata.ts`, batches them into **2000-row multi-row INSERTs** (one statement per chunk), uses `ON DUPLICATE KEY UPDATE` so re-runs are idempotent and interrupted loads resume, and prints rows/s + ETA. See ["ORM Setup & Type Generation"](#orm-setup--type-generation) → *Bulk JSON storage* for the design.
|
The importer (`scripts/bulk-import-json.ts`) maps rows onto `src/db/schema-gamedata.ts`, batches them into **2000-row multi-row INSERTs** (one statement per chunk), uses `ON DUPLICATE KEY UPDATE` so re-runs are idempotent and interrupted loads resume, and prints rows/s + ETA. See ["ORM Setup & Type Generation"](#orm-setup--type-generation) → _Bulk JSON storage_ for the design.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -455,15 +452,13 @@ import { db } from "@/lib/db";
|
|||||||
import { users } from "@/db/schema";
|
import { users } from "@/db/schema";
|
||||||
import { eq } from "drizzle-orm/expressions";
|
import { eq } from "drizzle-orm/expressions";
|
||||||
|
|
||||||
const found = await db.select()
|
const found = await db.select().from(users).where(eq(users.username, "hello"));
|
||||||
.from(users)
|
|
||||||
.where(eq(users.username, "hello"));
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Drizzle CLI** (`drizzle-kit`) is used for local development tasks — it is a devDependency and is never bundled in production.
|
**Drizzle CLI** (`drizzle-kit`) is used for local development tasks — it is a devDependency and is never bundled in production.
|
||||||
|
|
||||||
| Command | What it does |
|
| Command | What it does |
|
||||||
| ---------------------- | ---------------------------------------------------------------------- |
|
| ------------------------- | --------------------------------------------------------------------------------------- |
|
||||||
| `pnpm db:generate` | Draft SQL from Drizzle schema into `drizzle/drafts/` (review + copy) |
|
| `pnpm db:generate` | Draft SQL from Drizzle schema into `drizzle/drafts/` (review + copy) |
|
||||||
| `pnpm db:studio` | Open Drizzle Studio (dev only) |
|
| `pnpm db:studio` | Open Drizzle Studio (dev only) |
|
||||||
| `pnpm db:introspect` | Reverse-engineer an existing DB into a Drizzle schema draft |
|
| `pnpm db:introspect` | Reverse-engineer an existing DB into a Drizzle schema draft |
|
||||||
@@ -475,7 +470,6 @@ const found = await db.select()
|
|||||||
#### Bulk JSON storage (MariaDB)
|
#### Bulk JSON storage (MariaDB)
|
||||||
|
|
||||||
Emulator/hotel JSON dumps (furnidata, external texts, product data) are often >50 MB. MariaDB's `JSON` type is an alias for `LONGTEXT` and can **never be indexed directly**, so `src/db/schema-gamedata.ts` follows the "promote + index" pattern:
|
Emulator/hotel JSON dumps (furnidata, external texts, product data) are often >50 MB. MariaDB's `JSON` type is an alias for `LONGTEXT` and can **never be indexed directly**, so `src/db/schema-gamedata.ts` follows the "promote + index" pattern:
|
||||||
|
|
||||||
- The raw JSON document stays intact in a `longtext` / `mediumtext` column (`payload`, `value`).
|
- The raw JSON document stays intact in a `longtext` / `mediumtext` column (`payload`, `value`).
|
||||||
- The fields you actually `WHERE` / `ORDER BY` on are promoted into real columns (`sprite_id`, `class_name`, `kind`, `title`, `text_key`) and indexed.
|
- The fields you actually `WHERE` / `ORDER BY` on are promoted into real columns (`sprite_id`, `class_name`, `kind`, `title`, `text_key`) and indexed.
|
||||||
- Optional **VIRTUAL generated columns** extract indexed fields from the payload with `JSON_EXTRACT` (MariaDB supports secondary indexes on virtual columns, 10.2+), so no duplicate data has to be written by the importer.
|
- Optional **VIRTUAL generated columns** extract indexed fields from the payload with `JSON_EXTRACT` (MariaDB supports secondary indexes on virtual columns, 10.2+), so no duplicate data has to be written by the importer.
|
||||||
@@ -483,16 +477,16 @@ Emulator/hotel JSON dumps (furnidata, external texts, product data) are often >5
|
|||||||
Three tables are exported:
|
Three tables are exported:
|
||||||
|
|
||||||
| Table | Purpose | Unique key |
|
| Table | Purpose | Unique key |
|
||||||
| -------------------- | ---------------------------------------------- | --------------------------- |
|
| -------------------- | ------------------------------------------------------------ | ---------------------- |
|
||||||
| `gamedata_furnidata` | One row per furni/clothing item (habbo furnidata_json shape) | `(source, sprite_id)` |
|
| `gamedata_furnidata` | One row per furni/clothing item (habbo furnidata_json shape) | `(source, sprite_id)` |
|
||||||
| `gamedata_docs` | Generic JSON document store (per-key documents) | `(category, doc_key)` |
|
| `gamedata_docs` | Generic JSON document store (per-key documents) | `(category, doc_key)` |
|
||||||
| `gamedata_texts` | External-texts style key/value pairs | `(category, text_key)` |
|
| `gamedata_texts` | External-texts style key/value pairs | `(category, text_key)` |
|
||||||
|
|
||||||
The bulk importer (`scripts/bulk-import-json.ts`, run via `pnpm db:bulk`) is DB-driven and fast precisely because each chunk is a *single* multi-row `INSERT … VALUES () ()… ON DUPLICATE KEY UPDATE`, so a 50 MB dump is a few dozen statements instead of hundreds of thousands of round-trips. Flags:
|
The bulk importer (`scripts/bulk-import-json.ts`, run via `pnpm db:bulk`) is DB-driven and fast precisely because each chunk is a _single_ multi-row `INSERT … VALUES () ()… ON DUPLICATE KEY UPDATE`, so a 50 MB dump is a few dozen statements instead of hundreds of thousands of round-trips. Flags:
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| -------------------- | ----------- | ------------------------------------------------- |
|
| ---------------- | ------------ | ---------------------------------------------- |
|
||||||
| `--file=<path>` | — (required)| JSON file (habbo furnidata_json or flat array) |
|
| `--file=<path>` | — (required) | JSON file (habbo furnidata_json or flat array) |
|
||||||
| `--table=<name>` | `furnidata` | One of `furnidata` \| `docs` \| `texts` |
|
| `--table=<name>` | `furnidata` | One of `furnidata` \| `docs` \| `texts` |
|
||||||
| `--source=<x>` | `habbo` | `source` value for `furnidata` |
|
| `--source=<x>` | `habbo` | `source` value for `furnidata` |
|
||||||
| `--category=<x>` | `default` | `category` value for `docs` / `texts` |
|
| `--category=<x>` | `default` | `category` value for `docs` / `texts` |
|
||||||
@@ -744,7 +738,7 @@ For bulk-written tables (game-data JSON, imports) a containerized **`mariadb-tur
|
|||||||
### 3. Asset caching headers
|
### 3. Asset caching headers
|
||||||
|
|
||||||
| Path | Cache-Control |
|
| Path | Cache-Control |
|
||||||
| ------------------------------ | -------------------------------------------------------------- |
|
| ----------------------------- | -------------------------------------------------------- |
|
||||||
| `/swf/**`, `/nitro-assets/**` | `public, max-age=604800, stale-while-revalidate=2592000` |
|
| `/swf/**`, `/nitro-assets/**` | `public, max-age=604800, stale-while-revalidate=2592000` |
|
||||||
| `/gamedata/**`, `/client/**` | `public` + `expires 7–30 d` (nginx) |
|
| `/gamedata/**`, `/client/**` | `public` + `expires 7–30 d` (nginx) |
|
||||||
| `/imaging/*` | `public, max-age=300` |
|
| `/imaging/*` | `public, max-age=300` |
|
||||||
@@ -769,7 +763,7 @@ free of `auth()`, `cookies()`, `headers()` and request data, so the edge may
|
|||||||
cache them on their behalf.
|
cache them on their behalf.
|
||||||
|
|
||||||
| Class | Endpoints | TTL |
|
| Class | Endpoints | TTL |
|
||||||
| --- | --- | --- |
|
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
|
||||||
| 1 | `/api/online`, `/api/online/count` | 10s |
|
| 1 | `/api/online`, `/api/online/count` | 10s |
|
||||||
| 2 | `/api/photos`, `/api/leaderboard`, `/api/radio/current-dj`, `/api/radio/points/leaderboard` | 60s |
|
| 2 | `/api/photos`, `/api/leaderboard`, `/api/radio/current-dj`, `/api/radio/points/leaderboard` | 60s |
|
||||||
| 3 | `/api/staff`, `/api/teams`, `/api/guilds`, `/api/shop`, `/api/shop/categories`, `/api/values`, `/api/values/categories`, `/api/values/[0-9]+` | 300s |
|
| 3 | `/api/staff`, `/api/teams`, `/api/guilds`, `/api/shop`, `/api/shop/categories`, `/api/values`, `/api/values/categories`, `/api/values/[0-9]+` | 300s |
|
||||||
@@ -777,7 +771,6 @@ cache them on their behalf.
|
|||||||
`/api/radio/shouts` is intentionally excluded: it is live chat.
|
`/api/radio/shouts` is intentionally excluded: it is live chat.
|
||||||
|
|
||||||
Three details are easy to get wrong and are worth keeping intact:
|
Three details are easy to get wrong and are worth keeping intact:
|
||||||
|
|
||||||
- **`proxy_ignore_headers "Cache-Control" "Vary";` is what makes the cache work.**
|
- **`proxy_ignore_headers "Cache-Control" "Vary";` is what makes the cache work.**
|
||||||
`proxy_hide_header` only strips the header from the response to the client; the
|
`proxy_hide_header` only strips the header from the response to the client; the
|
||||||
cache module has already read the upstream's `no-store` by then and stores
|
cache module has already read the upstream's `no-store` by then and stores
|
||||||
@@ -799,7 +792,7 @@ with a redirect to `/me` and reads `headers()` for a per-request CSP nonce.
|
|||||||
### Files changed for stability and caching
|
### Files changed for stability and caching
|
||||||
|
|
||||||
| File | Change |
|
| File | Change |
|
||||||
| --- | --- |
|
| -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `/etc/nginx/nginx.conf` | `worker_connections 4096`, `multi_accept on`, TLS 1.2/1.3 only, `proxy_cache_path` for the micro-cache |
|
| `/etc/nginx/nginx.conf` | `worker_connections 4096`, `multi_accept on`, TLS 1.2/1.3 only, `proxy_cache_path` for the micro-cache |
|
||||||
| `/etc/nginx/sites-enabled/cms.conf` | single correct `Cache-Control` per path, micro-cache on the whitelist, session/Authorization bypass, SSE block with buffering off, `upstream cms_app` + `proxy_next_upstream` |
|
| `/etc/nginx/sites-enabled/cms.conf` | single correct `Cache-Control` per path, micro-cache on the whitelist, session/Authorization bypass, SSE block with buffering off, `upstream cms_app` + `proxy_next_upstream` |
|
||||||
| `/etc/nginx/snippets/cms_upstream_servers.conf` | backend list, rewritten by `ci-deploy.sh` during a cutover |
|
| `/etc/nginx/snippets/cms_upstream_servers.conf` | backend list, rewritten by `ci-deploy.sh` during a cutover |
|
||||||
@@ -824,7 +817,6 @@ with a redirect to `/me` and reads `headers()` for a per-request CSP nonce.
|
|||||||
|
|
||||||
Two layers work together to keep the origin alive during misuse, floods, and
|
Two layers work together to keep the origin alive during misuse, floods, and
|
||||||
repeat-offender attacks:
|
repeat-offender attacks:
|
||||||
|
|
||||||
1. **App-layer gate** (production only) — per-IP request buckets per category
|
1. **App-layer gate** (production only) — per-IP request buckets per category
|
||||||
(pages / API / auth), a whole-site safety valve, and escalation tiers that
|
(pages / API / auth), a whole-site safety valve, and escalation tiers that
|
||||||
temporarily hard-block offending IPs (`src/lib/ddos-guard.ts`).
|
temporarily hard-block offending IPs (`src/lib/ddos-guard.ts`).
|
||||||
@@ -882,7 +874,6 @@ Restart the container after changing them.
|
|||||||
|
|
||||||
Open **DevOps → Anti-DDoS protection**
|
Open **DevOps → Anti-DDoS protection**
|
||||||
(`/admin/devops/antiddos`, requires `settings.view`) and:
|
(`/admin/devops/antiddos`, requires `settings.view`) and:
|
||||||
|
|
||||||
- Check the **Cloudflare** card shows the request arrived proxied, and the
|
- Check the **Cloudflare** card shows the request arrived proxied, and the
|
||||||
green **API configured** badge.
|
green **API configured** badge.
|
||||||
- Click **Verify connection** — it confirms the token can read the zone and
|
- Click **Verify connection** — it confirms the token can read the zone and
|
||||||
@@ -918,19 +909,18 @@ read one thing, read the precedence table below.
|
|||||||
### Where the captcha is configured (and who wins)
|
### Where the captcha is configured (and who wins)
|
||||||
|
|
||||||
There are two places, and the **admin panel always wins**:
|
There are two places, and the **admin panel always wins**:
|
||||||
|
1. **Admin panel** — _Admin → CMS Settings → Captcha_ (stored in the database).
|
||||||
1. **Admin panel** — *Admin → CMS Settings → Captcha* (stored in the database).
|
|
||||||
2. **Environment variables** — used **only** as a fallback when the admin panel
|
2. **Environment variables** — used **only** as a fallback when the admin panel
|
||||||
has no value.
|
has no value.
|
||||||
|
|
||||||
Resolution order, highest first:
|
Resolution order, highest first:
|
||||||
|
|
||||||
| Admin panel `captcha_provider` | Env `CAPTCHA_PROVIDER` | What happens |
|
| Admin panel `captcha_provider` | Env `CAPTCHA_PROVIDER` | What happens |
|
||||||
| --- | --- | --- |
|
| ------------------------------ | ---------------------- | -------------------------------------- |
|
||||||
| `turnstile` | anything | **Turnstile** (panel wins) |
|
| `turnstile` | anything | **Turnstile** (panel wins) |
|
||||||
| `none` (set explicitly) | `turnstile` | **Off** — an explicit panel value wins |
|
| `none` (set explicitly) | `turnstile` | **Off** — an explicit panel value wins |
|
||||||
| *empty / never set* | `turnstile` | **Turnstile** (env fallback) |
|
| _empty / never set_ | `turnstile` | **Turnstile** (env fallback) |
|
||||||
| *empty / never set* | *empty / never set* | **Off** |
|
| _empty / never set_ | _empty / never set_ | **Off** |
|
||||||
|
|
||||||
In short: **enable it in the admin panel and the env vars are ignored; leave the
|
In short: **enable it in the admin panel and the env vars are ignored; leave the
|
||||||
panel empty and the env vars take over.** An explicit `none` in the panel turns
|
panel empty and the env vars take over.** An explicit `none` in the panel turns
|
||||||
@@ -941,7 +931,7 @@ the captcha **off** even when `CAPTCHA_PROVIDER=turnstile` is set.
|
|||||||
1. Create a Turnstile widget in the
|
1. Create a Turnstile widget in the
|
||||||
[Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/turnstile):
|
[Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/turnstile):
|
||||||
**Add widget**, name it, and add the hostnames your hotel is served on
|
**Add widget**, name it, and add the hostnames your hotel is served on
|
||||||
(e.g. `epicnabbo.nl`, `www.epicnabbo.nl`). *Managed* mode is fine. Copy the
|
(e.g. `epicnabbo.nl`, `www.epicnabbo.nl`). _Managed_ mode is fine. Copy the
|
||||||
**Site Key** and the **Secret Key**.
|
**Site Key** and the **Secret Key**.
|
||||||
2. Put them in your real `.env` (never commit this file):
|
2. Put them in your real `.env` (never commit this file):
|
||||||
|
|
||||||
@@ -1007,7 +997,7 @@ cms=2` therefore cannot work here: the replicas would collide on 3002. Instead
|
|||||||
there are two fixed slots, and each release lands in the idle one:
|
there are two fixed slots, and each release lands in the idle one:
|
||||||
|
|
||||||
| Slot | Host port | Container name |
|
| Slot | Host port | Container name |
|
||||||
| --- | --- | --- |
|
| ---- | --------- | -------------------- |
|
||||||
| A | 3002 | `epicnext-cms-app` |
|
| A | 3002 | `epicnext-cms-app` |
|
||||||
| B | 3003 | `epicnext-cms-green` |
|
| B | 3003 | `epicnext-cms-green` |
|
||||||
|
|
||||||
@@ -1027,7 +1017,6 @@ cron) parked an `epicnext-cms` container on the blue slot while nginx served the
|
|||||||
green slot, and every later release stopped on "Port 3002 is already in use" —
|
green slot, and every later release stopped on "Port 3002 is already in use" —
|
||||||
after the build, the migrations and the browser gate. Three guards now prevent
|
after the build, the migrations and the browser gate. Three guards now prevent
|
||||||
that:
|
that:
|
||||||
|
|
||||||
- The `cms` service sits behind the `cms` profile, so a bare `docker compose up
|
- The `cms` service sits behind the `cms` profile, so a bare `docker compose up
|
||||||
-d` starts only byparr and can never claim 3002. The service starts only when
|
-d` starts only byparr and can never claim 3002. The service starts only when
|
||||||
it is named explicitly (`docker compose up -d cms`), which is what the
|
it is named explicitly (`docker compose up -d cms`), which is what the
|
||||||
@@ -1038,7 +1027,7 @@ that:
|
|||||||
stopped and deleted, so the guard stopped firing while the host stayed
|
stopped and deleted, so the guard stopped firing while the host stayed
|
||||||
CI-managed. It now checks both slot containers and the nginx upstream, and
|
CI-managed. It now checks both slot containers and the nginx upstream, and
|
||||||
reports the skip with exit 0 instead of failing the cron.
|
reports the skip with exit 0 instead of failing the cron.
|
||||||
- `ci-deploy.sh` retires a compose replica of *this* checkout
|
- `ci-deploy.sh` retires a compose replica of _this_ checkout
|
||||||
(`com.docker.compose.project.config_files`) from the candidate port before
|
(`com.docker.compose.project.config_files`) from the candidate port before
|
||||||
starting the candidate — but never a slot container, and never the port nginx
|
starting the candidate — but never a slot container, and never the port nginx
|
||||||
currently serves. Anything else still fails loudly in `assert_port_free`.
|
currently serves. Anything else still fails loudly in `assert_port_free`.
|
||||||
@@ -1088,7 +1077,7 @@ means a single leak can starve MariaDB, nginx and Traefik.
|
|||||||
|
|
||||||
> **Watch this on the first heavy import.** The 4 GiB ceiling is not yet
|
> **Watch this on the first heavy import.** The 4 GiB ceiling is not yet
|
||||||
> exercised by a real bulk import. If the container is OOM-killed during a large
|
> exercised by a real bulk import. If the container is OOM-killed during a large
|
||||||
> furniture import, raise `mem_limit` in `docker-compose.yml` *and* `mem_limit` /
|
> furniture import, raise `mem_limit` in `docker-compose.yml` _and_ `mem_limit` /
|
||||||
> `mem_swap_limit` in `scripts/ci-deploy.sh` together.
|
> `mem_swap_limit` in `scripts/ci-deploy.sh` together.
|
||||||
|
|
||||||
### Manual deploys
|
### Manual deploys
|
||||||
@@ -1121,7 +1110,7 @@ OOM-killer. `MemorySwapMax=0` is set explicitly (this host has no swap). If
|
|||||||
fewer than 1 GB would remain, the script refuses to start.
|
fewer than 1 GB would remain, the script refuses to start.
|
||||||
|
|
||||||
| Script | Ceiling | Covers |
|
| Script | Ceiling | Covers |
|
||||||
| --------------------- | ------- | --------------------------------------- |
|
| ----------------------- | ------- | -------------------------- |
|
||||||
| `pnpm dev` | 8 GB | Dev server |
|
| `pnpm dev` | 8 GB | Dev server |
|
||||||
| `pnpm build` | 10 GB | Production build |
|
| `pnpm build` | 10 GB | Production build |
|
||||||
| `pnpm analyze` | 10 GB | Build + bundle-size report |
|
| `pnpm analyze` | 10 GB | Build + bundle-size report |
|
||||||
@@ -1158,7 +1147,7 @@ Use the capped build instead:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Fix: use `pnpm build`. If you are genuinely inside an isolated environment
|
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
|
where the container _is_ the boundary (the Docker build, a CI runner), set
|
||||||
`CMS_MEMORY_CAPPED=1` to opt out deliberately.
|
`CMS_MEMORY_CAPPED=1` to opt out deliberately.
|
||||||
|
|
||||||
### Backends
|
### Backends
|
||||||
@@ -1166,8 +1155,8 @@ where the container *is* the boundary (the Docker build, a CI runner), set
|
|||||||
`scripts/with-memory-cap.sh` picks its mechanism automatically:
|
`scripts/with-memory-cap.sh` picks its mechanism automatically:
|
||||||
|
|
||||||
| `CMS_MEMORY_CAP_BACKEND` | Mechanism | Notes |
|
| `CMS_MEMORY_CAP_BACKEND` | Mechanism | Notes |
|
||||||
| ------------------------ | ------------------------------------------- | ------------------------------------------------------- |
|
| ------------------------ | -------------------------- | ------------------------------------------------------ |
|
||||||
| `auto` *(default)* | systemd cgroup `MemoryMax` | Real RSS bound over the whole process tree. Used here. |
|
| `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. |
|
| `ulimit` | `ulimit -v`, per process | Virtual address space, **not** RSS. Fallback only. |
|
||||||
| `none` | None — warning only | Docker build and GitLab runner, each already isolated. |
|
| `none` | None — warning only | Docker build and GitLab runner, each already isolated. |
|
||||||
|
|
||||||
@@ -1188,7 +1177,7 @@ the headroom left for everything else on the machine; the effective ceiling is
|
|||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------------------------- | -------------------------------------------------- |
|
| ----------------------------- | ---------------------------------------------------------- |
|
||||||
| `pnpm dev` | Start development server (hot reload) |
|
| `pnpm dev` | Start development server (hot reload) |
|
||||||
| `pnpm build` | Production build (memory-capped) |
|
| `pnpm build` | Production build (memory-capped) |
|
||||||
| `pnpm start` | Start production server |
|
| `pnpm start` | Start production server |
|
||||||
@@ -1233,7 +1222,7 @@ the headroom left for everything else on the machine; the effective ceiling is
|
|||||||
## Performance Features
|
## Performance Features
|
||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
| ------------------------------ | -------------------------------------------------------------- |
|
| -------------------------------- | ------------------------------------------------------------------------- |
|
||||||
| **React Compiler** | Automatic memoization — reduces unnecessary re-renders |
|
| **React Compiler** | Automatic memoization — reduces unnecessary re-renders |
|
||||||
| **View Transitions API** | Native browser transitions between page navigations |
|
| **View Transitions API** | Native browser transitions between page navigations |
|
||||||
| **Lenis Smooth Scroll** | Fluid, customizable scrolling |
|
| **Lenis Smooth Scroll** | Fluid, customizable scrolling |
|
||||||
@@ -1302,7 +1291,6 @@ The `/client` page loads the Nitro client. Configure the client URL via **Admin
|
|||||||
### Radio
|
### Radio
|
||||||
|
|
||||||
Supports any streaming radio (e.g., Azuracast). Configure endpoints via CMS Settings:
|
Supports any streaming radio (e.g., Azuracast). Configure endpoints via CMS Settings:
|
||||||
|
|
||||||
- `radio_now_playing_api_url`
|
- `radio_now_playing_api_url`
|
||||||
- `radio_listeners_api_url`
|
- `radio_listeners_api_url`
|
||||||
|
|
||||||
@@ -1342,7 +1330,6 @@ BYPARR_URL=http://localhost:8191
|
|||||||
### Furniture Import with Auto-Translation
|
### Furniture Import with Auto-Translation
|
||||||
|
|
||||||
The CMS automatically translates furniture names and descriptions to **13 languages** on every import:
|
The CMS automatically translates furniture names and descriptions to **13 languages** on every import:
|
||||||
|
|
||||||
- **Native languages** (via official Habbo gamedata): Dutch, English, German, French, Spanish, Turkish, Italian
|
- **Native languages** (via official Habbo gamedata): Dutch, English, German, French, Spanish, Turkish, Italian
|
||||||
- **Additional languages** (via LibreTranslate Docker at `127.0.0.1:5000`): Portuguese, Finnish, Polish, Russian, Arabic, Japanese
|
- **Additional languages** (via LibreTranslate Docker at `127.0.0.1:5000`): Portuguese, Finnish, Polish, Russian, Arabic, Japanese
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"noDescendingSpecificity": "off"
|
"noDescendingSpecificity": "off"
|
||||||
|
},
|
||||||
|
"correctness": {
|
||||||
|
"noComponentHookFactories": "off",
|
||||||
|
"usePlaywrightValidDescribeCallback": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,4 +6,10 @@ services:
|
|||||||
HOSTNAME: 127.0.0.1
|
HOSTNAME: 127.0.0.1
|
||||||
PORT: "3002"
|
PORT: "3002"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3002/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"fetch('http://127.0.0.1:3002/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))",
|
||||||
|
]
|
||||||
+7
-1
@@ -61,7 +61,13 @@ x-cms: &cms
|
|||||||
pids_limit: 512
|
pids_limit: 512
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||'3002')+'/api/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"fetch('http://127.0.0.1:'+(process.env.PORT||'3002')+'/api/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))",
|
||||||
|
]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Operator changes
|
## Operator changes
|
||||||
|
|
||||||
| Area | Behavior and location |
|
| Area | Behavior and location |
|
||||||
| --- | --- |
|
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| Release | Deployment checks HTTP health, release identity and Chromium pages before marking the running image verified. Registry publication reuses that verified digest on the shared runner; independent hosts build and verify their own image. |
|
| Release | Deployment checks HTTP health, release identity and Chromium pages before marking the running image verified. Registry publication reuses that verified digest on the shared runner; independent hosts build and verify their own image. |
|
||||||
| Shared HK | Dialogs scroll within the available viewport. Table column preferences persist per page in the current browser session; filters already remain in the URL. No favorites added. |
|
| Shared HK | Dialogs scroll within the available viewport. Table column preferences persist per page in the current browser session; filters already remain in the URL. No favorites added. |
|
||||||
| Catalog Studio | Dedicated detail drawer and five separate completeness states: SQL, offers, furnidata, icon and Nitro. Sprite/type conflicts are consistently excluded from import and linked to audit. Missing source files are never represented as available. |
|
| Catalog Studio | Dedicated detail drawer and five separate completeness states: SQL, offers, furnidata, icon and Nitro. Sprite/type conflicts are consistently excluded from import and linked to audit. Missing source files are never represented as available. |
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
Local verification on 2026-09-09:
|
Local verification on 2026-09-09:
|
||||||
|
|
||||||
- Production build succeeded with fixture configuration and an intentionally unavailable database. Build-time fallback logs are expected in this check.
|
- Production build succeeded with fixture configuration and an intentionally unavailable database. Build-time fallback logs are expected in this check.
|
||||||
- Full Vitest run: 254 files passed, 4 skipped; 1,466 tests passed, 6 skipped. Coverage thresholds passed (19.89% lines); this does not imply exhaustive coverage.
|
- Full Vitest run: 254 files passed, 4 skipped; 1,466 tests passed, 6 skipped. Coverage thresholds passed (19.89% lines); this does not imply exhaustive coverage.
|
||||||
- Global Biome rules/import checks passed across 1,328 files; modified source/locales were formatted separately to avoid unrelated Windows line-ending changes.
|
- Global Biome rules/import checks passed across 1,328 files; modified source/locales were formatted separately to avoid unrelated Windows line-ending changes.
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ For each configured App Router route, resolve its exact app path using `app-path
|
|||||||
|
|
||||||
The **initial entry envelope** is the union of route bootstrap `rootMainFilesTree[appPath]` (or `rootMainFiles`) and every client chunk that route's client-reference manifest lists. This includes layout, page and boundary/loading entries.
|
The **initial entry envelope** is the union of route bootstrap `rootMainFilesTree[appPath]` (or `rootMainFiles`) and every client chunk that route's client-reference manifest lists. This includes layout, page and boundary/loading entries.
|
||||||
|
|
||||||
The manifest exposes those chunks differently per bundler. Turbopack emits an explicit per-segment `entryJSFiles` map; webpack emits no such field and records chunks only per client module, as `clientModules[*].chunks`, in `[chunkId, fileName, chunkId, fileName, …]` order. The report reads `entryJSFiles` when present and otherwise derives the same envelope from `clientModules`, which is the source Next's own `static-routes-info` uses. Numeric chunk ids are skipped; a malformed chunk *path* still fails rather than being dropped, so a broken manifest cannot quietly under-report a route.
|
The manifest exposes those chunks differently per bundler. Turbopack emits an explicit per-segment `entryJSFiles` map; webpack emits no such field and records chunks only per client module, as `clientModules[*].chunks`, in `[chunkId, fileName, chunkId, fileName, …]` order. The report reads `entryJSFiles` when present and otherwise derives the same envelope from `clientModules`, which is the source Next's own `static-routes-info` uses. Numeric chunk ids are skipped; a malformed chunk _path_ still fails rather than being dropped, so a broken manifest cannot quietly under-report a route.
|
||||||
|
|
||||||
> The build runs webpack (`next build --webpack`), so the `clientModules` path is the live one. An earlier revision only read `entryJSFiles`, and after the switch to webpack every route reported `unavailable` while the command still exited 0 — the budgets were silently not being measured. When a bundler switch changes the manifest layout again, re-check this section rather than trusting a clean exit.
|
> The build runs webpack (`next build --webpack`), so the `clientModules` path is the live one. An earlier revision only read `entryJSFiles`, and after the switch to webpack every route reported `unavailable` while the command still exited 0 — the budgets were silently not being measured. When a bundler switch changes the manifest layout again, re-check this section rather than trusting a clean exit.
|
||||||
|
|
||||||
The definition follows the data exposed by the installed Next 16.3.8 build and the `getLinkAndScriptTags` / `getRequiredScripts` renderer helpers; it is deliberately a build-artifact envelope, not a browser network trace. Conditional rendering, redirects, streaming and browser caches can change actual requests.
|
The definition follows the data exposed by the installed Next 16.3.8 build and the `getLinkAndScriptTags` / `getRequiredScripts` renderer helpers; it is deliberately a build-artifact envelope, not a browser network trace. Conditional rendering, redirects, streaming and browser caches can change actual requests.
|
||||||
|
|
||||||
- Raw bytes are filesystem byte lengths of unique JavaScript assets in that envelope.
|
- Raw bytes are filesystem byte lengths of unique JavaScript assets in that envelope.
|
||||||
- Gzip bytes are the **sum of independent gzip level 9 compressions** of those files using the recorded Node/zlib runtime. They are not gzip of concatenated source, nor observed CDN transfer sizes.
|
- Gzip bytes are the **sum of independent gzip level 9 compressions** of those files using the recorded Node/zlib runtime. They are not gzip of concatenated source, nor observed CDN transfer sizes.
|
||||||
- Deployment query strings and `/_next/` prefixes are normalized before deduplication. Shared files count once per route; each route is measured independently, with no misleading cross-route total.
|
- Deployment query strings and `/_next/` prefixes are normalized before deduplication. Shared files count once per route; each route is measured independently, with no misleading cross-route total.
|
||||||
@@ -35,7 +34,7 @@ Baselines were last recalibrated against a local webpack production build (Next
|
|||||||
The image build compiles the app, then runs `scripts/performance-report.mjs` in the same `RUN` layer (`Dockerfile:48-53`). In `mode: enforce` a failing route therefore fails the image build, which blocks both the PR `preflight` job and the `deploy` job. Recalibrate the table and `scripts/performance-budgets.json` **together** in the same commit whenever the envelope legitimately grows (new route dependency, Next upgrade).
|
The image build compiles the app, then runs `scripts/performance-report.mjs` in the same `RUN` layer (`Dockerfile:48-53`). In `mode: enforce` a failing route therefore fails the image build, which blocks both the PR `preflight` job and the `deploy` job. Recalibrate the table and `scripts/performance-budgets.json` **together** in the same commit whenever the envelope legitimately grows (new route dependency, Next upgrade).
|
||||||
|
|
||||||
| Route | Baseline raw bytes | Baseline gzip bytes | Raw limit | Gzip limit |
|
| Route | Baseline raw bytes | Baseline gzip bytes | Raw limit | Gzip limit |
|
||||||
| --- | ---: | ---: | ---: | ---: |
|
| --------------------- | -----------------: | ------------------: | --------: | ---------: |
|
||||||
| `/me` | 621460 | 195411 | 716800 | 225280 |
|
| `/me` | 621460 | 195411 | 716800 | 225280 |
|
||||||
| `/news` | 616712 | 193199 | 716800 | 225280 |
|
| `/news` | 616712 | 193199 | 716800 | 225280 |
|
||||||
| `/events` | 617121 | 193450 | 716800 | 225280 |
|
| `/events` | 617121 | 193450 | 716800 | 225280 |
|
||||||
@@ -46,4 +45,3 @@ The image build compiles the app, then runs `scripts/performance-report.mjs` in
|
|||||||
Configured limits are positive integer bytes; `null` explicitly means observe-only. `scripts/performance-budgets.json` runs `mode: enforce`: an `over-budget` route prints `Performance budgets enforced: <route> (over-budget)` and exits non-zero. Missing production `BUILD_ID`, unsupported manifests, missing routes or missing referenced assets produce `unavailable` with a reason and **no partial/zero total**; under `enforce` an `unavailable` route also fails the command, so a bundler change that breaks measurement cannot pass quietly. Malformed budget configuration or an unwritable output directory always fails the command. Set `mode` back to `informational` only while recalibrating, never as a permanent state.
|
Configured limits are positive integer bytes; `null` explicitly means observe-only. `scripts/performance-budgets.json` runs `mode: enforce`: an `over-budget` route prints `Performance budgets enforced: <route> (over-budget)` and exits non-zero. Missing production `BUILD_ID`, unsupported manifests, missing routes or missing referenced assets produce `unavailable` with a reason and **no partial/zero total**; under `enforce` an `unavailable` route also fails the command, so a bundler change that breaks measurement cannot pass quietly. Malformed budget configuration or an unwritable output directory always fails the command. Set `mode` back to `informational` only while recalibrating, never as a permanent state.
|
||||||
|
|
||||||
Synthetic tests cover shared-chunk deduplication, exact byte/gzip calculations, route bootstrap selection, missing data, safe parsing and CLI exit behavior in both modes. Run `pnpm exec vitest run --coverage.enabled=false scripts/performance-report.test.mjs`.
|
Synthetic tests cover shared-chunk deduplication, exact byte/gzip calculations, route bootstrap selection, missing data, safe parsing and CLI exit behavior in both modes. Run `pnpm exec vitest run --coverage.enabled=false scripts/performance-report.test.mjs`.
|
||||||
|
|
||||||
+7
-7
@@ -55,10 +55,10 @@
|
|||||||
"@dnd-kit/core": "6.3.1",
|
"@dnd-kit/core": "6.3.1",
|
||||||
"@dnd-kit/sortable": "10.0.0",
|
"@dnd-kit/sortable": "10.0.0",
|
||||||
"@dnd-kit/utilities": "3.2.2",
|
"@dnd-kit/utilities": "3.2.2",
|
||||||
"@formatjs/icu-messageformat-parser": "3.5.21",
|
"@formatjs/icu-messageformat-parser": "3.5.22",
|
||||||
"@hookform/resolvers": "6.0.0",
|
"@hookform/resolvers": "6.0.0",
|
||||||
"@tanstack/react-query": "5.104.4",
|
"@tanstack/react-query": "5.104.5",
|
||||||
"@tanstack/react-virtual": "3.14.21",
|
"@tanstack/react-virtual": "3.14.22",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
@@ -70,9 +70,9 @@
|
|||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"jszip": "3.10.2",
|
"jszip": "3.10.2",
|
||||||
"lucide-react": "1.59.0",
|
"lucide-react": "1.61.0",
|
||||||
"lzma-wasm": "1.0.7",
|
"lzma-wasm": "1.0.7",
|
||||||
"motion": "14.4.0",
|
"motion": "14.6.0",
|
||||||
"music-metadata": "12.2.3",
|
"music-metadata": "12.2.3",
|
||||||
"mysql2": "3.24.6",
|
"mysql2": "3.24.6",
|
||||||
"next": "16.4.2",
|
"next": "16.4.2",
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.14.0",
|
"@axe-core/playwright": "4.14.0",
|
||||||
"@babel/parser": "8.0.7",
|
"@babel/parser": "8.0.7",
|
||||||
"@biomejs/biome": "2.5.15",
|
"@biomejs/biome": "2.6.2",
|
||||||
"@playwright/test": "1.64.1",
|
"@playwright/test": "1.64.1",
|
||||||
"@tailwindcss/forms": "0.5.11",
|
"@tailwindcss/forms": "0.5.11",
|
||||||
"@tailwindcss/postcss": "4.3.3",
|
"@tailwindcss/postcss": "4.3.3",
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
"testcontainers": "12.2.1",
|
"testcontainers": "12.2.1",
|
||||||
"tsx": "4.23.15",
|
"tsx": "4.23.15",
|
||||||
"typescript": "7.0.2",
|
"typescript": "7.0.2",
|
||||||
"vite": "8.3.4",
|
"vite": "8.3.5",
|
||||||
"vitest": "5.0.5"
|
"vitest": "5.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+4577
-2505
File diff suppressed because it is too large.
Load diff
+8
-8
@@ -1,9 +1,9 @@
|
|||||||
packages:
|
packages:
|
||||||
- '.'
|
- "."
|
||||||
|
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
'@parcel/watcher': true
|
"@parcel/watcher": true
|
||||||
'@swc/core': true
|
"@swc/core": true
|
||||||
cpu-features: true
|
cpu-features: true
|
||||||
esbuild: true
|
esbuild: true
|
||||||
msw: true
|
msw: true
|
||||||
@@ -13,12 +13,12 @@ allowBuilds:
|
|||||||
minimumReleaseAge: 60
|
minimumReleaseAge: 60
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
glob: '^11.0.0'
|
glob: "^11.0.0"
|
||||||
'@esbuild-kit/core-utils': 'npm:tsx@^4.23.15'
|
"@esbuild-kit/core-utils": "npm:tsx@^4.23.15"
|
||||||
'@esbuild-kit/esm-loader': 'npm:tsx@^4.23.15'
|
"@esbuild-kit/esm-loader": "npm:tsx@^4.23.15"
|
||||||
source-map-js: '1.2.2'
|
source-map-js: "1.2.2"
|
||||||
# GHSA advisory: postcss-selector-parser <7.1.6. Pulled in only by
|
# GHSA advisory: postcss-selector-parser <7.1.6. Pulled in only by
|
||||||
# @tailwindcss/typography (build-time prose selectors), which pins 6.0.10
|
# @tailwindcss/typography (build-time prose selectors), which pins 6.0.10
|
||||||
# exactly and has no upstream bump. The API subset it uses is unchanged in
|
# exactly and has no upstream bump. The API subset it uses is unchanged in
|
||||||
# v7, so force the patched major.
|
# v7, so force the patched major.
|
||||||
postcss-selector-parser: '^7.1.6'
|
postcss-selector-parser: "^7.1.6"
|
||||||
+4
-4
@@ -5,7 +5,7 @@ This directory contains utility scripts for managing the EpicNext-Cms project.
|
|||||||
## Scripts Overview
|
## Scripts Overview
|
||||||
|
|
||||||
| Script | Description |
|
| Script | Description |
|
||||||
| :--- | :--- |
|
| :------------------------- | :---------------------------------------------------------------- |
|
||||||
| `dashboard.sh` | Main interactive menu to run all other scripts. |
|
| `dashboard.sh` | Main interactive menu to run all other scripts. |
|
||||||
| `deploy.sh` | Safely redeploys the application by freeing the port first. |
|
| `deploy.sh` | Safely redeploys the application by freeing the port first. |
|
||||||
| `logs.sh` | View logs for cms and mariadb services. |
|
| `logs.sh` | View logs for cms and mariadb services. |
|
||||||
@@ -28,6 +28,7 @@ This directory contains utility scripts for managing the EpicNext-Cms project.
|
|||||||
| `catalog-sync.sh` | Interactive catalog update against a clone source (see below). |
|
| `catalog-sync.sh` | Interactive catalog update against a clone source (see below). |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Run any script directly, or use the dashboard:
|
Run any script directly, or use the dashboard:
|
||||||
```bash
|
```bash
|
||||||
./scripts/dashboard.sh
|
./scripts/dashboard.sh
|
||||||
@@ -69,7 +70,7 @@ menu, so it is usable from cron.
|
|||||||
Every step the engine exposes is available as a flag:
|
Every step the engine exposes is available as a flag:
|
||||||
|
|
||||||
| Flag | What it does |
|
| Flag | What it does |
|
||||||
| :--- | :--- |
|
| :--------------------------------------------- | :------------------------------------------------------------------------- |
|
||||||
| `--sources=<a,b>` | Compare against these sources (id or display name) |
|
| `--sources=<a,b>` | Compare against these sources (id or display name) |
|
||||||
| `--icons` | Re-download missing furniture icons |
|
| `--icons` | Re-download missing furniture icons |
|
||||||
| `--nitros` | Re-download missing bundles (stored as `.hab`) |
|
| `--nitros` | Re-download missing bundles (stored as `.hab`) |
|
||||||
@@ -94,7 +95,6 @@ catalog page plus the offer, exactly like the app's own import. It skips
|
|||||||
anything already present, so it is safe to re-run.
|
anything already present, so it is safe to re-run.
|
||||||
|
|
||||||
Safety notes:
|
Safety notes:
|
||||||
|
|
||||||
- **An audit writes nothing** — no repair flag means read-only.
|
- **An audit writes nothing** — no repair flag means read-only.
|
||||||
- `--dry-run` together with `--import-new` lists what would be imported and
|
- `--dry-run` together with `--import-new` lists what would be imported and
|
||||||
imports nothing; use it first on a source before importing thousands of items.
|
imports nothing; use it first on a source before importing thousands of items.
|
||||||
@@ -106,7 +106,7 @@ Safety notes:
|
|||||||
- `--apply-sql` is the only flag that changes the catalog; `--apply` authorises
|
- `--apply-sql` is the only flag that changes the catalog; `--apply` authorises
|
||||||
the two heavyweight child scripts (`align-db-ids`, `migrate-nitro-to-hab`).
|
the two heavyweight child scripts (`align-db-ids`, `migrate-nitro-to-hab`).
|
||||||
Both only report without it.
|
Both only report without it.
|
||||||
- The engine is the same one the admin *Catalog audit* page streams
|
- The engine is the same one the admin _Catalog audit_ page streams
|
||||||
(`runCatalogAudit`), so a terminal run and an in-admin run agree.
|
(`runCatalogAudit`), so a terminal run and an in-admin run agree.
|
||||||
- `--json` prints one machine-readable report; `--fail-on-errors` exits `3` when
|
- `--json` prints one machine-readable report; `--fail-on-errors` exits `3` when
|
||||||
error-severity issues remain, which is what a scheduled check should use.
|
error-severity issues remain, which is what a scheduled check should use.
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default async function AdminAntiDdosPage() {
|
|||||||
const ip = key.replace("antiddos:block:", "");
|
const ip = key.replace("antiddos:block:", "");
|
||||||
return {
|
return {
|
||||||
ip,
|
ip,
|
||||||
ttlMs: ttlMs > 0 ? ttlMs : 0,
|
ttlMs: Math.max(ttlMs, 0),
|
||||||
count: violationCounts.get(ip) ?? 0,
|
count: violationCounts.get(ip) ?? 0,
|
||||||
source: "gate" as const,
|
source: "gate" as const,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default async function AdminRadioHistoryPage() {
|
|||||||
|
|
||||||
const liveCount = history.filter((h) => !h.endedAt).length;
|
const liveCount = history.filter((h) => !h.endedAt).length;
|
||||||
const peakListeners = history.reduce(
|
const peakListeners = history.reduce(
|
||||||
(max, h) => (h.listenersCount > max ? h.listenersCount : max),
|
(max, h) => Math.max(h.listenersCount, max),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const uniqueDjs = new Set(history.map((h) => String(h.userId))).size;
|
const uniqueDjs = new Set(history.map((h) => String(h.userId))).size;
|
||||||
|
|||||||
+2
-2
@@ -2092,9 +2092,9 @@ body:has([data-admin]) {
|
|||||||
|
|
||||||
/* Admin forms / boxes follow admin tokens in both light and dark. */
|
/* Admin forms / boxes follow admin tokens in both light and dark. */
|
||||||
body:has([data-admin])
|
body:has([data-admin])
|
||||||
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(
|
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(
|
||||||
[type="file"]
|
[type="file"]
|
||||||
):not([type="color"]),
|
):not([type="color"]),
|
||||||
body:has([data-admin]) select,
|
body:has([data-admin]) select,
|
||||||
body:has([data-admin]) textarea {
|
body:has([data-admin]) textarea {
|
||||||
background-color: var(--admin-surface-elevated);
|
background-color: var(--admin-surface-elevated);
|
||||||
|
|||||||
Reference in new issue
Block a user