b48f29a3b30b400d4e6e440bd51291d0cf54e91e
169
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
77a0d1010c |
feat(catalog-sync): report sources that can only be compared, not imported
CI / check (push) Successful in 47s
CI / tests-integration (push) Successful in 2m36s
CI / tests-unit (push) Successful in 2m52s
CI / preflight (push) Skipped
CI / tests-quality (push) Successful in 1m30s
CI / tests-imaging-browsers (push) Successful in 2m47s
CI / tests-ui (push) Successful in 3m28s
CI / deploy (push) Successful in 3m21s
The Habbo presets (and the other furnidata-only sources) carry no
`nitroBaseUrl`, so the clone importer's `${nitroBaseUrl}/${classname}.nitro`
download can never work against them — an `--import-new` run would have failed
for every single item instead of saying why. The SWF path that does work against
`images.habbo.com` lives in `furni-import.ts`, not in the clone importer this
CLI uses.
`stepImportNew` now skips those sources up-front, lists them under
"compared only" in the summary, and the menu prints the same warning when an
import step is chosen with such a source, pointing at the sources that do serve
bundles. Verified: Habbo (COM) reports compare-only, while Habboon and
VirtualCity list real candidates.
|
||
|
|
f56b345598 |
feat(catalog-sync): add import, reconcile, sprite-id and offer steps
CI / check (push) Successful in 37s
CI / tests-unit (push) Successful in 2m3s
CI / tests-integration (push) Successful in 2m54s
CI / preflight (push) Skipped
CI / tests-imaging-browsers (push) Failing after 3m23s
CI / tests-quality (push) Successful in 6m8s
CI / tests-ui (push) Successful in 2m52s
CI / deploy (push) Successful in 3m3s
The CLI could audit and repair what already existed, but adding furniture present at a source was still only possible from the admin UI item by item. The repo already ships every piece of that pipeline, so wire them together as further steps of the same run: - `--import-new` imports furniture the compared source has and this hotel lacks. It reuses `getClonableClassnames` for the diff and `cloneSingleFurni` per item, which downloads the icon and the bundle (sources serve `.nitro`, stored as `.hab`), appends the furnidata entry with `deferFurniData` and creates the catalog page plus the offer itself. Deferred entries are written in one `appendFurniEntriesBulk` call, so 26000 items cost one JSON write, not one per item. `--only`, `--limit` and `--concurrency` narrow/tune it, failures are isolated per item and never abort the run. - `--reconcile`, `--fix-sprite-ids` and `--rebuild-offers` run the existing `reconcileFurniDataWithItemsBase`, `verifyAndFixSpriteIds` and `rebuildCatalogOfferIds` passes — the post-import consistency work. - `--align-ids` and `--rename-bundles` delegate to the existing standalone scripts; both only report until `--apply` is given, matching their own contracts. - The menu grows options 6-8 plus a dry run, each with its own confirmation. `--everything` turns on every repair + import step but deliberately leaves the destructive ones off. A live dry run against Wibbo listed 26593 items on the source that this hotel does not have, and Habbo (COM) reported 2871. |
||
|
|
a4bcd40e7b |
docs(catalog-sync): surface the bundle extension and the page-writing nuance
CI / check (push) Successful in 34s
CI / tests-integration (push) Successful in 2m1s
CI / tests-imaging-browsers (push) Successful in 2m5s
CI / tests-unit (push) Successful in 2m11s
CI / preflight (push) Skipped
CI / tests-quality (push) Successful in 1m4s
CI / tests-ui (push) Successful in 2m46s
CI / deploy (push) Successful in 3m48s
`.hab` has been the stored bundle extension since the `FURNI_BUNDLE_EXT` switch, so the repair path already writes `.hab` (the sources serve `.nitro`), but nothing in the output said so. Print the resolved extension in the run header and correct the header comment: generating the catalog SQL also creates the `catalog_pages` sub-pages it references, so a "generate only" run is not entirely read-only — only the `catalog_items` rows wait for `--apply-sql`. |
||
|
|
4b575d3a1d |
feat(catalog): add scripts/catalog-sync.ts CLI + interactive menu
CI / check (push) Successful in 32s
CI / tests-integration (push) Successful in 1m55s
CI / tests-unit (push) Successful in 2m1s
CI / tests-imaging-browsers (push) Successful in 2m7s
CI / preflight (push) Skipped
CI / tests-quality (push) Successful in 1m46s
CI / tests-ui (push) Successful in 2m44s
CI / deploy (push) Successful in 3m28s
Updating the catalog was only possible from the admin "Catalog audit" page, which streams `runCatalogAudit` over SSE and always compares against every enabled clone source. This adds the same engine to the terminal so it can be run by hand or from cron: - `scripts/catalog-sync.ts` — CLI driver around `runCatalogAudit`. Prints the live progress, the per-source "on the source but not in this hotel" counts and a summary block; generated catalog SQL is saved to `storage/catalog-sync/*.sql` (gitignored). Supports `--list-sources`, `--sources=<a,b>`, `--icons`, `--nitros`, `--furnidata`, `--structure`, `--check-ids`, `--sql`, `--apply-sql`, `--organize-sql`, `--all`, `--json` and `--fail-on-errors`. - `scripts/catalog-sync.sh` — interactive menu (`pnpm catalog:sync`). Lists the configured sources (so the menu always matches `clone_sources`), lets Habbo (COM) be taken with Enter, and offers audit-only through full repair + applying the catalog SQL, each with a distinct confirmation. With no arguments it needs a terminal; with any flag it passes straight through to the CLI. - `runCatalogAudit` gains `sourceIds` to restrict which sites are compared; omitting it keeps the previous "compare against everything enabled" behaviour. An audit writes nothing, and `--apply-sql` is the only flag that changes the catalog. Verified against the live DB: 16370 items_base rows, 16479 catalog items, 2871 Habbo (COM) entries missing from this hotel. |
||
|
|
ef6c671903 |
test(ci): deterministic deploy gate + advisory quality suite; sync README versions
CI / check (push) Successful in 32s
CI / tests-imaging-browsers (push) Successful in 1m34s
CI / tests-integration (push) Successful in 1m59s
CI / tests-unit (push) Successful in 2m2s
CI / tests-quality (push) Successful in 1m1s
CI / preflight (push) Skipped
CI / tests-ui (push) Failing after 3m20s
CI / deploy (push) Skipped
The deploy gate ran e2e specs that depend on live content and data, so a missing upload (the /help article images) or a legacy malformed avatar figure could turn a safe code deploy red. Split the suites by what they actually guarantee: - Blocking gate (`pnpm test:e2e`, `playwright.config.ts`): only checks the CMS can guarantee itself — anonymous redirects, register form fields, the captcha DOM contract and the cache-control headers (deterministic origin-direct). - Advisory suite (`pnpm test:e2e:quality`, `playwright.quality.config.ts`): broken images, layout overflow and warm-page latency, which depend on live content/upload state and the host. It runs as a `continue-on-error` CI job against the public site so a real content problem stays visible without blocking the pipeline. Also add `scripts/sync-readme-versions.mjs` (+ tests, docs and a CI check) so the pinned versions in README.md can never drift from package.json / .nvmrc. |
||
|
|
3c8abb6fbf |
feat(assets): choose furniture bundle ext via FURNI_BUNDLE_EXT
CI / check (push) Successful in 39s
CI / tests-imaging-browsers (push) Successful in 2m9s
CI / tests-integration (push) Successful in 2m47s
CI / tests-unit (push) Failing after 3m22s
CI / preflight (push) Skipped
CI / tests-ui (push) Failing after 4m14s
CI / deploy (push) Skipped
Bundle the remaining imaging hardening and the host/CI fixes from the
"Doe alles" pass into one release.
Furniture:
- Resolve the local bundle extension from FURNI_BUNDLE_EXT (".hab" default,
".nitro" for a stock Nitro client). The configured extension is written by
imports/repairs and preferred on lookup; the other stays a legacy fallback.
Imaging:
- Avatar proxy sends s-maxage=86400 + stale-while-revalidate (300/3600 for the
fallback path) so the edge can hold a render a day.
- Cloudflare cache rule now covers /api/imaging/ (verified MISS->HIT live).
- Document that bare /imaging is shadowed by the edge proxy to the renderer.
Ops/CI:
- docker-update.sh skips a CI-managed host (exit 0) instead of failing cron.
- ci-deploy.sh runs the cross-browser imaging gate (chromium+firefox+webkit)
against the candidate before cutover; DEPLOY_SKIP_CROSS_BROWSER=1 as escape.
- scripts/imager-health-alert.sh alerts (Discord) on imager up/down transitions;
runs from cron every 5 minutes.
Tests:
- Furniture bundle-ext resolver + FURNI_BUNDLE_EXT wiring.
- Imaging performance budget (bytes + warm latency).
- classifyDdos: /api/imaging/* stays in the api bucket.
|
||
|
|
7b249a7222 |
P1-P3 admin cleanup: rank guard, SSRF guard, count caching, dead code removal, pagination rewrite
CI / check (push) Canceled after 0s
CI / tests-unit (push) Canceled after 0s
CI / tests-integration (push) Canceled after 0s
CI / preflight (push) Canceled after 0s
Gitea Actions Runner Test / test-job (push) Canceled after 0s
CI / tests-ui (push) Canceled after 0s
CI / deploy (push) Canceled after 0s
Gitea Runner Clean Test / test-job (push) Canceled after 0s
- P1: new src/lib/admin/rank-guard.ts with tryGuardRank/guardRank + RankGuardTarget/Result; extracted from users.ts; applied to admin-bans (create/lift + rank redirect) and admin-badges (caps + rank guard + error surfacing); import-url-safety.ts SSRF guard wired into clone save+icon fetch; logAudit await in help-tickets; antiddos mutations SETTINGS_EDIT; markAllAlertsRead NOTIFICATIONS_EDIT; rankError/saveError in all 25 message files; tests rew/new for bans/badges/rank-guard/import-url-safety - P2: ticket-inbox merged pagination reworked to single UNION ALL SQL with LIMIT/OFFSET (avoids O(offset) materialization); getCachedAdminCount with cacheFiltered option applied to devops/errors/photos/items/guilds/help-tickets/moderation pages; admin-theme catches→?error=1+banner+25 locales; logger in silent catches (rare-values/writeable-boxes); catalog-items-loader CAST→plain equality; count caching helper extended with opt-in cacheFiltered - P3: removed dead modules is-staff/notice/recent-furni-resync/log-filters plus load-user-sanctions/user-sanction-timeline; removed orphaned CATALOG_PAGE_COLUMNS re-export; removed getFurniIconUrl from studio-furni-icons Build: lint 0, typecheck 0, 3480 tests pass, i18n:check exit 0, colors exit 0. Circular chunk warning (9452/10535/66463) is webpack code-splitting artifact; build succeeds. |
||
|
|
2978483ea9 |
fix(assets): serve furniture icons from the gamedata tree, add stack-height SQL generator
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m52s
CI / tests-unit (push) Successful in 2m3s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m40s
CI / deploy (push) Successful in 3m47s
Commit the two changes that were live in the working tree but never recorded. The nginx change adds a location for /swf/dcr/hof_furni/icons/ that reads /var/www/Gamedata/icons/ off disk and falls back to the CMS bundle for anything missing. The CMS built its icon URLs from that path, but the icons actually live in the gamedata tree: of the 16,263 classnames in items_base, 15,338 are present there under the safe name, against 11,267 under public/swf/dcr/hof_furni/icons. The swf tree also keeps colour variants behind a literal "*" in the filename, so every request using the safe name missed, and hof.furni.url already points the Nitro client at the gamedata tree. A miss is a no-store 404, never a cached one — same reasoning as @gamedata_missing, since add_header without "always" says nothing about a 404 and Cloudflare would otherwise apply the zone TTL. scripts/generate-stack-height-sql.ts emits a SQL file that repairs items_base dimensions and interaction columns from the logic inside each bundle, which is where they actually live rather than in furnidata. It writes a file and touches no database, so the result is reviewed before it is run. |
||
|
|
43742e8d99 |
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. |
||
|
|
adffac7360 |
feat(catalog): store furniture bundles as .hab instead of .nitro
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 29s
CI / tests-unit (push) Failing after 1m37s
CI / tests-integration (push) Successful in 1m37s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m17s
CI / deploy (push) Skipped
Every bundle the CMS writes — upload, clone, sync, repair and the pet / effect / figure importers — now lands as `<classname>.hab`, the extension this deployment's renderer asks for. `.hab` and `.nitro` are the same container, so an upload of either extension is accepted. Resolution goes through one module, src/lib/furni/bundle-file.ts, so nothing has to know the extension twice. Every existence check probes `.hab` first and falls back to `.nitro`: the on-disk asset set is still predominantly `.nitro`, and without the fallback Studio would report every imported item as missing and the cleanup scan would classify 18k live bundles as fake leftovers. Downloads are unchanged — Habbo's CDN and every configured clone source still serve `.nitro`, so the conversion happens on write, not on request. Deliberately unchanged: the staged-attachment store in furni-attachment.ts keys on a UUID and never reaches the client, so renaming it would break in-flight recovery jobs. Adds scripts/migrate-nitro-to-hab.ts to rename the existing asset set. It refuses to run without --dry-run or --yes, never overwrites an existing .hab, never deletes, and is idempotent. Note: renderer-config.json lives outside this repo and was patched to .hab separately; that file is served with a 30-day max-age, so returning clients need a cms-client cache purge to pick the change up. |
||
|
|
fdb7af7ef5 |
fix(deploy): unblock every rebuild on BuildKit's host-network refusal
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 33s
CI / tests-unit (push) Failing after 2m4s
CI / tests-integration (push) Successful in 2m6s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m43s
CI / deploy (push) Skipped
Docker 29.1.3 ships BuildKit v0.26, which refuses to grant a build host networking unless each caller passes --allow=network.host. All three rebuild paths asked for it, and `docker compose build` has no flag to grant it, so a rebuild failed immediately with "additional privileges requested". The live container was never replaced, which is exactly the reported symptom: the site kept serving the previous release after a rebuild. Nothing in the build actually needs host networking. It uses the network only for apk, pnpm and next/font/google — all outbound internet, which the default bridge provides. Verified by building both the full runner image and the migrations stage with --no-cache after dropping the flag. Runtime `network_mode: host` stays: blue/green needs per-release host ports (3002/3003) and nginx reaches each slot over 127.0.0.1. The second gap is how a rebuild could still ship the wrong code. ci-deploy.sh stamped every image with HEAD's revision label, and verify-deployed-release.mjs only re-checks that same label, so a dirty working tree produced an image that claimed to be release $sha while containing uncommitted code. docker-update.sh already refused this; ci-deploy.sh now does too, before any build work. |
||
|
|
8561c3f85e |
fix(ci): accept any runtime the engines range supports
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 42s
CI / tests-unit (push) Successful in 1m42s
CI / tests-integration (push) Successful in 1m47s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m28s
CI / deploy (push) Successful in 3m12s
The active-runtime assertion required process.versions.node to equal .nvmrc exactly, so any Node.js patch release broke `toolchain:check` and the act CI run even though package.json engines (>=26.10.0 <27) supports the newer runtime. Keep .nvmrc and the Docker base image exactly pinned for reproducibility (both still asserted), but validate the running runtime against the engines range instead. Verified: toolchain:check, lint, typecheck, i18n:check, hk:matrix:check and all 3391 tests pass. |
||
|
|
0845f80768 |
fix(ops): run heavy commands under a hard memory cap to stop host OOM kills
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 35s
CI / tests-integration (push) Successful in 2m5s
CI / tests-unit (push) Successful in 2m30s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 3m3s
CI / deploy (push) Failing after 44s
The host runs with vm.overcommit_memory=0 and no swap, so a process that
grows past free memory makes the kernel OOM-kill across the whole machine
-- the Turbopack build (commit
|
||
|
|
5b2eb91c5c |
fix(ops): stop a compose replica from blocking the blue/green release
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m57s
CI / tests-unit (push) Successful in 2m3s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m49s
CI / deploy (push) Successful in 2m48s
The deploy failed after the build, the migrations and the browser gate:
"Port 3002 is already in use". The holder was `epicnext-cms`, a compose
replica of release
|
||
|
|
11ad6d4376 |
fix(ci): measure route bundles from webpack manifests
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 31s
CI / tests-integration (push) Successful in 1m41s
CI / tests-unit (push) Successful in 1m44s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m31s
CI / deploy (push) Failing after 2m25s
The performance report measured nothing. It only read `entryJSFiles` from
each route's client-reference manifest, a field Turbopack emits and webpack
does not. When the build moved to webpack (
|
||
|
|
6c3d81920e |
fix(ops): supervise the job worker and stop the health probe from lying
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 32s
CI / tests-unit (push) Successful in 1m49s
CI / tests-integration (push) Successful in 1m50s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m33s
CI / deploy (push) Failing after 2m26s
Four production defects, all found by auditing the running host rather than the code. Each one had a signature that looked like a network or permissions problem and was actually a configuration or ordering bug. jobs-worker never ran `import "./load-env"` sat on line 3 of scripts/jobs-worker.ts, but ESM evaluates a module's imports in source order and the first import reaches `@/env`, which validates process.env at import time. The ZodError on DATABASE_URL therefore fired before load-env ever executed, so the worker could only start from a shell that had already exported the configuration. Nothing supervised it either, so scheduled articles, catalog export, JAR and database backups, disk alerts and the ops health probe have all been dead; `cms:jobs-worker:heartbeat` did not exist. Moved the import to the top and added deployment/systemd/cms-jobs-worker.service with Restart=always. The JAR backup additionally pointed at './emulator/Arcturus.jar', which does not exist and would go stale on the next emulator upgrade. resolveEmulatorJar now accepts a file, a directory or a wildcard and picks the newest JAR, the same way emulator.service picks its build, and reports an unresolvable path once instead of logging an opaque copyFile ENOENT every night. /api/health answered 200 with the database down The route documented this as intentional, and ci-deploy.sh worked around it by grepping the body for '"database":true'. The container healthcheck did not, so Docker reported containers healthy while every page 500'd. The status is now load-bearing: 503 when the database is unreachable, 200 otherwise. Redis and the emulator deliberately do not fail the container — both have in-process fallbacks, so failing them would trade a slow site for an outage. The runtime had no V8 heap cap NODE_OPTIONS existed only in the builder stage. With no cap, V8 sized its heap from host memory (23.5 GB) while the container was limited to 4 GB, so the kernel OOM-killed the process mid-request — the same failure mode as the 14 host-wide `next-build` kills. docker-start.mjs now reads the cgroup limit (v2 with a v1 fallback) and sets 70% of it, respecting an explicit override. Storage ownership was only repaired for one path ci-deploy.sh chowned storage/imaging and nothing else, so storage/catalog-git/hotel-status.json kept coming back root:root and /api/admin/catalog/status kept throwing EACCES. All eight writable storage paths are repaired now. The silent-failure mode is the reason this mattered: these writes sit inside try/catch, so a wrong owner looks like a slow page rather than an error. nginx: robots.txt was a guaranteed 404, and TLS never resumed `index index.html` without a `root` left every try_files resolving against /etc/nginx/html, which sits behind a 0750 directory — the worker got EACCES on each stat and nginx logs a failed stat at crit, which is where 149 crit lines per scan came from. robots.txt answered from that same broken location, so crawlers were pointed at a file they could never read while sitemap.xml kept advertising it. Added `root`, proxied robots.txt to the CMS, added ssl_session_cache (there was no session resumption at all), and set Restart=on-failure in a systemd override, since the packaged unit ships Restart=no and nginx is the only thing serving the site. Verified against the running host: 3379 tests, typecheck and biome clean, nginx -t passes, health returns 200 with every check green, and the worker has run for hours at NRestarts=0 with a heartbeat refreshing each minute. |
||
|
|
108c6ce03d |
fix(ci): make the lint gate fail for real and stop byparr leaking disk
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 36s
CI / tests-integration (push) Successful in 2m3s
CI / tests-unit (push) Successful in 2m18s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 3m6s
CI / deploy (push) Failing after 3m14s
The CI lint step was `biome check . || true`, so it could never fail: 14 real
violations were passing unnoticed. Drop the `|| true` and fix what it found.
Lint fixes, none of which change behaviour:
- give list items their natural identity instead of the array index
(key={c} / key={char}, key={`skeleton-${i}`})
- document the two useEffect dependency lists that must keep their
function-declaration handlers, with the reasoning that dropping them broke
the tree and save-on-Ctrl+S once already (
|
||
|
|
c8b3054527 |
fix(deploy): free port 3002 and stop compose from competing for the slots
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 33s
CI / tests-integration (push) Successful in 1m46s
CI / tests-unit (push) Failing after 1m49s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m39s
CI / deploy (push) Skipped
The deploy could not start its candidate because port 3002 was held by `epicnext-cms`, a `docker compose up` replica built from the `local` image and serving no traffic. Everything else in the pipeline was healthy: the image built, the news browser gate passed and migrations were current. The container was unusable for this pipeline for two reasons. It ran a different image than any release, and its name did not match the slot the deploy script manages — docker-compose.yml pinned `container_name: epicnext-cms` while ci-deploy.sh expects `epicnext-cms-app` for slot A. Slot B happened to agree (`epicnext-cms-green`), which is why 3003 deployed fine and 3002 never could. deploy.sh already documents that compose "never managed the release that actually ran", so the service was stale by its own account. Removed the stray container and dropped the `cms` and `cms-green` services (plus the now-unused x-cms anchor) from docker-compose.yml, so a reboot cannot resurrect a replica that permanently occupies a blue/green slot. byparr is untouched. Also fixed the diagnostic from the previous commit, which blamed every running container. `docker ps --filter publish=` returns nothing for --net=host containers, so the fallback listed all of them and buried the real holder among seven innocent ones. It now resolves the listening PID from `ss` back to its container through /proc/<pid>/cgroup and names only that one, with the exact `docker rm -f` command to run. Verified: port 3002 free, live release on 3003 still serving (status ok, database and redis true), deploy simulation 26 passed, typecheck. |
||
|
|
8ec3df541e |
fix(deploy): name the container blocking a port and silence phantom cleanup
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m43s
CI / tests-unit (push) Successful in 1m47s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m33s
CI / deploy (push) Failing after 1m30s
Two follow-ups from the blocked deploy. The rollback path called `docker logs` and `docker rm -f` on the candidate unconditionally. When the port check refuses to start it, the container was never created, so both printed "No such container: epicnext-cms-app" — noise that looked like a second, unrelated failure and buried the real message. Both calls are now guarded by `docker inspect`. assert_port_free() now reports which container holds the port and flags it when it is not a blue/green slot this script manages. The previous output listed every container and said only "port already in use", which is a dead end: on this host the holder is `epicnext-cms` (a `docker compose up` replica on port 3002), while the deploy manages slot A as `epicnext-cms-app`. The names differ because docker-compose.yml pins `container_name: epicnext-cms` for the `cms` service; slot B happens to match, which is why 3003 deploys fine and 3002 never can. The message now names the squatter, explains that live traffic is unaffected, and gives the next action. Deploy simulation: 26 passed. |
||
|
|
8ee144745a |
fix(deploy): stub ss in the deploy harness and cover the port-conflict path
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m47s
CI / tests-unit (push) Successful in 1m54s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m40s
CI / deploy (push) Failing after 1m41s
The port-conflict guard added in the previous commit made the six existing blue/green deployment simulation tests fail. assert_port_free() shells out to ss, and the simulation harness stubs git, curl, docker, nginx, pnpm and node — but not ss. Because the runner is self-hosted and the containers use --net=host, the simulation saw the production CMS containers holding 3002 and 3003 and refused to start its own candidate. The harness now stubs ss. It reports no listener for every scenario except 'port-taken', which reserves whichever port the script asks about, so the simulation stays independent of the host it runs on. Also switched the ss probe from `command -v ss` to `type ss`. The stub is a shell function delivered through BASH_ENV; `command -v` happens to find it, but `type` is the reliable test for "is this resolvable", and the two differ across shells. Added a regression test for the guard itself: with the candidate port already occupied, the deploy must fail, must not have run `docker run`, and must leave the nginx upstream untouched on the old port — no half-finished cutover. Verified it fails when the assert_port_free call is removed. Deploy simulation: 26 passed. Full unit suite: 3316 passed, 12 skipped. |
||
|
|
64ad9baf39 |
fix(deploy): trust the nginx upstream when picking the live slot
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m48s
CI / tests-unit (push) Failing after 1m54s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m46s
CI / deploy (push) Skipped
The deploy failed with "Expected release never became healthy" after 30 attempts. Root cause: read_active_port() counted the slots answering /api/health and only consulted the nginx upstream when the count was not exactly one. On this host both slots were healthy, so it fell back to the upstream file, but a leftover epicnext-cms:local replica was holding slot A (3002). The candidate was assigned that occupied port, docker run died with EADDRINUSE, and the health probe then answered from the pre-existing container on that port. That container reports release "unknown" because it was built without NEXT_DEPLOYMENT_ID, so the release comparison could never match and the deploy timed out blaming a release that was never serving. read_active_port() now orders its sources by how well they describe reality: 1. The nginx upstream file. It is the only source that says where public traffic actually enters; everything below it is a consequence. 2. A healthy slot matching that pointer. 3. The other slot when the pointer names a dead port. 4. The pointer itself when nothing answers, so rollback still has a target. 5. Slot A when no upstream file exists at all. answers_health() was added as a retry-free sibling of healthy(); port detection should not spend 90 seconds per slot on a process that is either running now or never will. start_candidate() now calls assert_port_free() before docker run, so an occupied port fails immediately and names the listener and the containers involved, instead of surfacing later as a misleading health-check timeout. Added scripts/ci-deploy-ports.test.sh, which extracts the two functions from the real script rather than copying them, and covers the regression: with both slots healthy and nginx serving slot B, the result must not be slot A. Verified the test fails against the old logic and passes against the new. Wired into the check job so this is caught before an image is built. |
||
|
|
f99980052b |
perf: optimize cache layer for speed and stability
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 34s
CI / tests-integration (push) Failing after 33m57s
CI / tests-unit (push) Failing after 33m57s
CI / preflight (push) Skipped
CI / tests-ui (push) Failing after 33m56s
CI / deploy (push) Skipped
- Remove random TTL jitter to prevent unpredictable cache drops - Add deterministic LRU eviction with proper entry cleanup - Improve cache deduplication to prevent duplicate computations - Skip Redis I/O during tests for faster, more stable execution - Optimize depth calculation in catalog tree nodes - Maintain backward compatibility and full test coverage (3331 passed) |
||
|
|
8a6d92afd8 |
fix(cloudflare): cache the gamedata tree at the edge with respect_origin
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 32s
CI / tests-unit (push) Successful in 1m44s
CI / tests-integration (push) Successful in 1m44s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m31s
CI / deploy (push) Successful in 2m16s
Icons are plain .png, a cacheable extension by default, so the zone's "Browser Cache TTL = 1 year" pinned them to max-age=31536000 regardless of the 300/3600/604800 that nginx sends per class. Extend the edge rule to /gamedata/ and keep respect_origin, so the nginx header wins and a 404 (notably no-store from the gamedata 404 handler) is never pinned. |
||
|
|
a6cc3cafa9 |
fix(catalog): read furnidata from one cache, purge the gamedata edge on write
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 36s
CI / tests-integration (push) Successful in 1m52s
CI / tests-unit (push) Successful in 1m56s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m48s
CI / deploy (push) Successful in 2m27s
Furniture was not always loading completely because the same file was cached twice and nobody could reach the client. The catalog items loader kept its own 30s TTL copy of FurnitureData.json next to the mtime-validated cache in `furni-data.ts`. An import cleared only the second one, so the catalog table kept serving pre-import furnidata — empty descriptions and revisions — until the TTL ran out. The loader now reads through `readFurniData`, which revalidates on mtime+size and is reset by every write, so there is exactly one cache and it cannot go stale on its own. `invalidateFurniDataCache` and its single call site are gone with it. The client was worse: nginx served all of /gamedata/ with `max-age=604800`, and the `cms-gamedata` purge that would have fixed it hung off the catalog Git export, which is disabled in production. A freshly imported item was invisible in the client for up to seven days no matter how often you imported. - `writeFurniData` now purges the gamedata edge tag itself. One place covers import, batch, resync, regen, nitro-editor, translate and dedupe. It is fire-and-forget and swallowed at every level: a stale edge copy is bounded by the edge TTL, so a failed purge must never fail an import. - nginx splits /gamedata/ by how mutable the content is: config/ gets `max-age=300, must-revalidate`, bundled/ `max-age=3600, must-revalidate`, and the content-addressed trees (c_images, album*, clothes) keep the long TTL. `must-revalidate` is the point — the client now revalidates instead of replaying the old body. All three keep `Cache-Tag: cms-gamedata` so the purge still reaches them. - A 30-minute safety-net purge in the jobs worker covers the case where Cloudflare was unreachable at write time. |
||
|
|
7507c3b55c |
fix(deploy): detect the actually-live blue/green slot, stop nginx-sync clobbering the upstream
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 30s
CI / tests-unit (push) Successful in 1m40s
CI / tests-integration (push) Successful in 1m42s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m28s
CI / deploy (push) Successful in 2m5s
- ci-deploy.sh: read_active_port() now probes both slots on /api/health and picks the one that really answers; the upstream file only serves as a fallback when zero or both slots respond. A stray 'docker compose up' (or a clobbered snippet) can no longer derail the next deploy's cutover. - nginx-sync.sh: cms_upstream_servers.conf is runtime-owned by ci-deploy.sh; only seed it when missing, never overwrite what a deploy wrote. This is the root cause of tonight's 502: a nginx-sync run reset the snippet (written to green:3003 by the last cutover) back to the dead slot A:3002. - cms_upstream_servers.conf: restore the fresh-host seed default to slot A. |
||
|
|
90b65c92a2 |
feat(proxy): sync Cloudflare ranges at nginx+Traefik, block IP spoofing
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m51s
CI / tests-unit (push) Successful in 1m54s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m44s
CI / deploy (push) Successful in 20s
- cloudflare-ips.conf (new): geo $cms_trusted_edge + set_real_ip_from from live CF IPv4/IPv6 ranges plus Traefik bridge and loopback - nginx-cms.conf: forward real client IP only from trusted peers, strip incoming CF-Connecting-IP, 403 any other peer that presents one (spoof gate); direct game clients on :9443 stay unaffected - cf-ips-sync.sh (new): fetch cloudflare.com/ips-v4/-v6, regenerate the nginx snippet and Traefik websecure.forwardedHeaders.trustedIPs - nginx-sync.sh: install the cloudflare-ips.conf snippet - cms_upstream_servers.conf: point default at the live green slot 3003 |
||
|
|
7697728d07 |
feat(cache): single-owner caching across nginx, edge and content edits
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 28s
CI / tests-unit (push) Successful in 1m39s
CI / tests-integration (push) Successful in 1m41s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m27s
CI / deploy (push) Successful in 3m35s
Rebuild production nginx from the repo (deployment/proxy/*) with a single Cache-Control owner per route: the app stays the source, nginx only manages headers, and Cloudflare stores the public API allowlist at the edge. - deployment/proxy: nginx.conf, mime.types, nginx-cms.conf and the blue/green upstream snippet; config backed by scripts/nginx-sync.sh (idempotent install + reload, --check/--force). - nginx serves Cache-Tag headers on the public allowlist (cms-public), gamedata, client and camera responses so the edge and purge stay in sync. - src/lib/edge-cache.ts + tests: coalesced, fire-and-forget edge purges that no-op unless Cloudflare is configured; scripts/cf-purge.sh and cf-setup-cache.sh create and purge the cache rule. - src/lib/cloudflare-api.ts: purgeCacheByTags/purgeCacheByUrls. - Purge hooks after catalog exports (public + gamedata) and on shop, team, guild, photo and rare-values edits; ci-deploy purges after each release. - src/proxy.ts excludes the imaging/images docs from the middleware matcher. |
||
|
|
e4f83a8036 |
chore: upgrade to pnpm v12, vitest v5 and resolve deprecated subdependencies
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Failing after 21s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / preflight (push) Skipped
CI / tests-ui (push) Skipped
CI / deploy (push) Skipped
|
||
|
|
80d7ae14ba |
fix(ci): fail fast when the deploy dir has no DATABASE_URL
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 34s
CI / tests-unit (push) Successful in 1m42s
CI / tests-integration (push) Successful in 1m48s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m35s
CI / deploy (push) Failing after 1m37s
pnpm db:migrate runs on the host and reads DATABASE_URL from the deploy directory's .env. When that variable was missing the deploy had already built an image and run the browser gate before pnpm db:migrate aborted on an empty value, so a release was paid for in full and then thrown away. Check for the variable right after the .env is copied, before the build, and say plainly that the live release was not touched. The deploy test fixture gains a DATABASE_URL so it mirrors a working deploy directory instead of the broken one. |
||
|
|
306e209e29 |
fix(nitro): normalise uploaded bundles so meta.image matches the texture
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 39s
CI / tests-unit (push) Successful in 2m3s
CI / tests-integration (push) Successful in 2m7s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m50s
CI / deploy (push) Failing after 1m45s
An uploaded .nitro was written to disk byte-for-byte, so a bundle from a third-party tool that ships a WebP member while still pointing spritesheet.meta.image at a .png was accepted and stored as-is. The client resolves the spritesheet through that pointer, so the result was a file that validates fine and then renders nothing. Re-write the bundle through createNitroBundle on import, which labels the member from the actual bytes and repairs the pointer. No texture is re-encoded, so the bytes stay identical, and the member keeps the base name it arrived with so `chair*2` colour variants that share the `chair` library are not renamed. |
||
|
|
fe5a7a6185 |
fix(imaging): keep avatars rendering, cacheable and reliably timed
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m40s
CI / tests-unit (push) Successful in 1m51s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m43s
CI / deploy (push) Successful in 2m10s
Effect renders need a little over 4s, which the 4s primary timeout cut off, so every avatar with the default effect fell through to an unreachable public fallback and rendered as a placeholder. Raise the primary budget above the observed render cost and shorten the fallback budget. Also stop the proxy from stamping no-store over the avatar and media responses, so browsers keep the long-lived Cache-Control the route already sends, and recreate the imaging cache directories with the container user on every deploy, since root ownership made those cache writes fail silently. |
||
|
|
8486ac4053 | feat(security): add darklist.de source and raise the blocklist cap to 1M | ||
|
|
7f6febf906 |
fix(security): drop URLhaus feed, validate CIDR ranges, pass unknown client IPs
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 31s
CI / tests-integration (push) Successful in 1m42s
CI / tests-unit (push) Successful in 1m46s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m35s
CI / deploy (push) Successful in 1m40s
|
||
|
|
7392b843ad |
fix(security): LAPI-only engine boot, import via stdin, correct compose service
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 29s
CI / tests-integration (push) Successful in 1m46s
CI / tests-unit (push) Successful in 1m59s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m53s
CI / deploy (push) Failing after 1m54s
|
||
|
|
9562a75378 |
feat(security): external IP blocklist sync for the local CrowdSec engine
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m42s
CI / tests-unit (push) Successful in 1m43s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m35s
CI / deploy (push) Successful in 2m3s
|
||
|
|
84d53139a9 |
feat(security): opt-in local CrowdSec LAPI bouncer on the Docker engine
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 30s
CI / tests-unit (push) Successful in 1m37s
CI / tests-integration (push) Successful in 1m55s
CI / preflight (push) Skipped
CI / tests-ui (push) Successful in 2m23s
CI / deploy (push) Successful in 2m38s
|
||
|
|
7707722f4c | fix(build): remove deprecated middleware to fix Next.js build and update ci-deploy script | ||
|
|
6c53f4680c | feat(studio): run nitro scans in the background with cancel-re-attach and nightly auto-clean | ||
|
|
2c0439db6a |
refactor(auth): remove obsolete CONVERT_PASSWORDS env var
Legacy md5/argon2id hashes are now always upgraded to bcrypt on login, so the CONVERT_PASSWORDS flag is no longer used. Drop it from env schema, .env.example, the docker installer, and test mocks. |
||
|
|
faa37e7c58 |
fix(ci): restore preflight image cleanup marker and remove obsolete publish-container tests
- Restore build_attempted=1 in ci-preflight.sh so the exit trap
removes the temporary image tag
- Remove publish-container.test.ts and its harness (publication
workflow and script were removed in
|
||
|
|
da90b90c97 | fix(ci): guard browser context cleanup and export news e2e env before build | ||
|
|
fff284aaa0 | ci: remove container publication workflows | ||
|
|
6e0ffff94b | Restore docker-prune retention windows to match deploy contract | ||
|
|
1a9e1e791a |
Improve catalog studio UX and aggressive docker cleanup
Catalog Studio: - Cross-parent drag & drop now uses optimistic updates with rollback on failure (no more full tree reload / visible delay) - Subpage creation adds the node optimistically then refreshes parent only (was full tree reload) - Single page deletion refreshes only the affected parent (was full tree reload) - Root page creation replaces native prompt() with an inline input in the root tab bar - Escape key no longer closes the dialog when an input field is focused - TreeNodeUpdate type now supports parentId and orderNum for optimistic structural changes Docker: - docker-prune.sh default mode now aggressively cleans all unreferenced build cache, images >1h old, and stopped containers >1h old (was 72h/7d/24h which let cache grow past 80% on every push) |
||
|
|
349188af10 | Merge remote-tracking branch 'origin/main' into codex/news-ci-preflight | ||
|
|
d4e4711a77 | feat: add gitlab-ci, logrotate setup and deploy alerts | ||
|
|
b688760309 | feat: add documentation, alerts, cron setup and update dashboard | ||
|
|
bd977da3a5 | feat: add db-restore, maintenance, doctor scripts and update dashboard | ||
|
|
a605807c69 | feat: add perf-report, setup-dev, check-updates and update dashboard | ||
|
|
636fde523f | feat: add dashboard, security-scan, and monitor scripts |