Commit Graph
66 Commits
Author SHA1 Message Date
openhands a06db72c65 Update: admin panel fixes, badge icons, studio improvements, gitea runner v5
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Failing after 21s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
2026-10-11 18:44:02 +02:00
openhands 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 / tests-ui (push) Successful in 2m17s
CI / preflight (push) Skipped
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.
2026-10-10 17:09:36 +02:00
openhands 6cc45d7413 feat: harden atoms-nexst against review findings (37 items)
Gitea Actions Runner Test / test-job (push) Successful in 2s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m42s
CI / tests-unit (push) Failing after 1m49s
CI / tests-ui (push) Successful in 2m31s
CI / preflight (push) Skipped
CI / deploy (push) Skipped
Second review pass covering security, performance, admin tooling and the
public/room flows. All HIGH and MEDIUM findings from the audit are resolved;
nothing in this commit changes the visible feature set.

Authentication & session security
- CSP is now set on the request headers in the proxy, which is what Next.js
  uses to derive the render nonce, so the nonce is effective.
- 2FA: an already-enabled user cannot re-enroll, the setup endpoint is
  rate-limited per account, and confirmed codes are persisted so the second
  secret no longer silently never applies.
- Password reset revokes the ticket, authTicket and all personal access
  tokens, and bumps the token version so existing sessions die. The same
  revocation is now wired into the staff-side password reset.
- /reset and /verify return a stable error code instead of raw text; the
  mail lookups are ordered by id so duplicates cannot vary between runs.
- Resending the verification mail gets a per-address cooldown on top of the
  per-user limit.
- Issue API tokens with the narrower radio/ticket ability set instead of "*".

Authorization & input handling
- Mid-rank staff can no longer keep dynamically granted non-view admin.*
  permissions: existing grants are revoked by migration and the grant lookup
  is restricted to "%.view". Rank guards use the dynamic super-admin check.
- Alerting a user is permission-checked and audited like the other tools.
- Material mutations (giveCredits/giveDuckets/giveDiamonds, the admin user
  actions route, bulk user actions) are capped and rank-guarded, and bulk
  ids are bounded.
- updateRoom / updateRoomItem write through a field allowlist, and items
  may only be edited through their own room.
- Classnames reaching the filesystem are validated before use so a crafted
  value cannot escape the asset directories.
- The word filter now also covers offline mails, guild forum threads and
  replies, and user mottos.
- Media uploads are validated by magic bytes, /api/media requires the page
  edit permission, APP_URL must be configured once mail is enabled, and the
  diagnostics error route checks the fetch site header.

Admin tooling
- Secret settings render masked and cannot be overwritten with a blank or
  an arbitrary raw key; radio credentials are new password inputs.
- Commandocentrum balance changes are audited.
- Admin list pagination reads the caller's per-page instead of the max, and
  the log exporter caps offset and search length.

Performance
- Catalog translations are cached per module, with a cheap revision hash;
  the public online count uses a stale window instead of hammering the DB.
- The cache warmup now primes the payload the home route actually reads.
- TopHeader batches its queries into one round trip, and LCP avatars load
  eagerly.
- motion/react and sonner are no longer part of the root layout; the nav
  dropdown and mobile nav panels are lazy client chunks. Anonymous visitors
  again get the navigation chrome, and public pages get an edge cacheable
  response.

Accessibility
- Nested <main> elements in phase pages became <section>; the page entrance
  and route progress animations are pure CSS that respect reduced motion.
2026-10-09 16:19:48 +02:00
openhandsandClaude Opus 4.8 28ce0f911c fix(catalog): keep the live catalog truthful after every import path
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 29s
CI / tests-integration (push) Failing after 1m43s
CI / tests-unit (push) Successful in 1m47s
CI / tests-ui (push) Successful in 2m33s
CI / preflight (push) Skipped
CI / deploy (push) Skipped
The live catalog store only covered part of the import surface. A durable
job settled, a sync queue drained, a .nitro upload or a clone run left the
Studio rail and the stats bar showing pre-import numbers until the page was
reloaded, and the Catalog Manager kept a second tree that never saw writes
made elsewhere in the session.

Every one of those paths now pulls the tree again, and the refresh carries
the totals with it: importing writes catalog rows server-side, so the counts
the store holds were stale for the rest of the session.

- refreshCatalogTree shares one request between concurrent callers and queues
  a single follow-up read when a write lands mid-flight, so a burst of edits
  costs at most one extra read.
- useFurnitureJobs treats its first payload as a baseline, so a page load no
  longer replays every past import as "just settled", and hands the settled
  jobs to the callback.
- The Catalog Manager pushes its own mutations into the store and re-reads its
  active tab when the store changes.
- The 30s unstable_cache on the admin totals is now tagged and invalidated from
  every catalog write, including the import worker, so it no longer survives an
  import even across a hard reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-09-30 14:45:26 +02:00
openhands d73baf1458 fix(catalog): take furnidata values from the clone source for retro items
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 29s
CI / tests-integration (push) Successful in 1m33s
CI / tests-unit (push) Successful in 1m33s
CI / tests-ui (push) Successful in 2m21s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 1m53s
The resync route rebuilt every entry from items_base plus the *official
Habbo* furnidata. A classname that only exists on a retro hotel (leet.ws
and friends) is absent from the official set, so lookupOfficialHabboFurni
returned null and the entry was written with revision 0, category
"unknown" and an empty description — even though the clone import had
those exact values available at import time from the source's own
furnidata.

- resync/route.ts: when a classname is genuinely missing from official
  Habbo, fall back to the configured clone sources. Their furnidata is
  indexed by normalized classname and each entry is coerced into the
  OfficialHabboFurniEntry shape, which is the same JSON shape, so it drives
  the existing buildFurniEntry fallbacks for revision, category, name,
  description, defaultdir, partcolors, specialtype, furniline, environment,
  rare and bc. items_base stays authoritative for id, spriteId and dims,
  and public_name still wins over the source name, matching the import.

  The index is memoized per request, not at module scope: a module-level
  cache would pin the source list for the life of the process and a source
  added later would never be picked up. fetchSourceFurnidata already caches
  per URL, so this costs one parse rather than a network round-trip.

  Disabled sources and sources that fail to respond are skipped, so an
  unreachable hotel degrades to the previous items_base-only behaviour
  instead of failing the run. Official Habbo still wins whenever it has the
  classname, so existing behaviour is unchanged for everything but the
  retro-only case.

Applies to every resync mode, so the pre-existing ?missing=1 sweep picks
this up too.
2026-09-29 16:02:58 +02:00
openhands 2f7e557d5e feat(catalog): add a Studio button to fix missing furnidata entries
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 28s
CI / tests-integration (push) Successful in 1m33s
CI / tests-unit (push) Successful in 1m38s
CI / tests-ui (push) Successful in 2m26s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m1s
"Missing furnidata" was only a filter in the Studio status dropdown, so
imported items whose classname was absent from FurnitureData.json could
be found but not fixed from that screen. Only the Catalog Audit page could
repair them, and only globally.

Adds the same shape of quick action that "no nitro" already had:

- studio-client.tsx: a "N no furnidata" shortcut next to the "N no nitro"
  button that sets the missingFurnidata status filter, and a bulk "Add
  missing furnidata (N)" button for the selected rows. Both only appear
  when there is something to act on. Rows that come back repaired flip
  to hasFurnidata: true so the badges and counts update in place; rows
  the server reported in errors keep their state.
- resync/route.ts: accepts an optional { classnames: string[] } body to
  target exactly the selected rows. classnames are resolved through the
  same normalized local index the listing uses to decide hasFurnidata, so
  the rows written are the rows flagged as missing. The upsert is already
  idempotent, and RCON updateCatalog + updateItems run afterwards so the
  emulator picks the new entries up.
  Also clears the Studio furnidata cache after a write, which this route
  never did: without it the listing kept serving a stale hasFurnidata for
  up to the 30s cache TTL, so a repair looked like it had done nothing.
  PERMS is now imported from permission-slugs (identical re-export) so the
  route no longer pulls next-auth into tests.
- studio-filters.test.ts: pins the missingFurnidata branch, in particular
  that an unchecked item (hasFurnidata undefined) is not treated as missing.

The existing ?days / ?missing / ?broken / ?all modes are unchanged; the
body is only consulted when it carries a classnames array.
2026-09-29 15:48:32 +02:00
openhands 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 / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
2026-09-27 19:42:34 +02:00
openhands 17de94d984 feat(nitro): convert imported SWF bundles to WebP Lossless
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 31s
CI / tests-unit (push) Successful in 1m59s
CI / tests-integration (push) Successful in 2m19s
CI / tests-ui (push) Successful in 2m56s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m18s
Newly converted .nitro bundles now store their spritesheet as WebP VP8L
instead of PNG, so imports land much smaller without changing a single
pixel. The texture member and spritesheet.meta.image are both labelled
from the actual bytes, never from a caller's assumption.

- encode through sharp with lossless and exact, so colour hidden under
  alpha 0 survives; this mirrors ImageSharp's TransparentColorMode.Preserve
- detect PNG/WebP by magic bytes and reject anything the client cannot
  render, on create, download and upload paths
- keep the source format when deriving size-32 sheets, scaling composites
  and editing metadata, so existing bundles are never silently rewritten
- report fidelity in the studio: the compression panel re-encodes with the
  same options the importer uses, so it cannot drift and invent false
  warnings, and shows PNG/WebP size estimates

convertSwfToNitro and buildSpritesheet are now async, so the worker, the
main-thread fallback and every import call site await them. PNG stays
supported for existing bundles and icon sidecars are untouched.
2026-09-27 15:42:21 +02:00
openhands 420210ffa0 fix(build): make the production build pass, and stop it eating 20GB
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m39s
CI / tests-unit (push) Successful in 1m43s
CI / tests-ui (push) Successful in 2m31s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m17s
`next build` had never completed on this host, so three real defects were
sitting in the tree untested. All three are now fixed and the build is green.

- The build was not memory-bound the way it looked. Turbopack's builder reached
  20.5GB RSS and died, and raising `--max-old-space-size` could never have
  helped: that flag caps the V8 heap, while the 20GB sat in Turbopack's own Rust
  allocator. The first symptom was misleading because the process doing the
  allocating is a grandchild of `npx`, so watching the direct child shows a
  95MB shim the whole time. Building with `--webpack` puts the build back under
  the JS heap, where the flag actually applies: peak 5.9GB, 150s, exit 0.

- withAdmin's second parameter was typed `{ params?: ... }` and given a `= {}`
  default, which made it optional and `RouteContext | undefined`. Next's
  generated route types assert that argument against `ParamCheck<RouteContext>`
  and reject it, across 113 route files. `tsc --noEmit` cannot see this, because
  Next only adds `.next/types` to the project during a production build — so the
  type check that everyone runs locally was structurally incapable of catching
  the only type error that blocks a deploy. `params` is now required, which is
  also what the code already assumed: it is awaited with no guard. The 35 test
  call sites that invoked a handler with one argument now pass a real context,
  and the await got a guard so a direct internal call cannot turn a missing
  context into a 500.

- `src/app/api/admin/import/furni/route.ts` re-exported `ensureDirectories` and
  `importSingleFurni` for "backward compatibility" that nothing used; the batch
  route imports from `@/lib/services/furni-import` directly. Next rejects any
  value export from a route module that is not an HTTP verb or config, so this
  had been breaking the build for as long as it existed. Removed.

- `isomorphic-dompurify` builds its server-side DOM through jsdom. Bundled, that
  pulls jsdom's `browser/default-stylesheet.css` into the server chunk, where the
  path no longer resolves, and page-data collection dies with ENOENT on every
  page that sanitizes HTML. Marked external so Node resolves it from
  node_modules and the standalone tracer includes it.

The remaining build warning is a pre-existing circular dependency between
chunks that share the webpack runtime. It costs hash reuse, not correctness, and
is left alone rather than churned here.

Verified: build exit 0, 276 static pages generated, 3223 tests pass, tsc and
biome clean.
2026-09-25 19:47:10 +02:00
openhands 8638e81444 refactor(db): typed query helpers, shared test FormData helper
CI / check (push) Successful in 4m10s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m4s
Replace raw db.execute tuple casts with queryRows/rowsFrom/execResult/
affectedRows helpers from lib/db, drop redundant mysql2 casts on typed
query builders, and centralize per-test fakeForm into test/fake-form.
Update db mocks in tests so helpers resolve against mocked execute.
2026-09-17 21:02:57 +02:00
openhands fb68df3ba9 feat(import): apply translation to successful bulk items
CI / check (push) Successful in 2m27s
CI / deploy (push) Successful in 1m15s
CI / publish-container (push) Successful in 56s
The interactive batch ignored the client's Translate option, so translated names never landed in the language files during a bulk run. Patch each successful item through patchLocalizedFurniDataEntries (mutex-guarded, best-effort) when translation is requested, surfacing failures as item warnings instead of failing the import.
2026-09-13 12:54:00 +02:00
openhands ffcd4232d6 feat(import): durable batch checkpoint and transient auto-retry
CI / check (push) Successful in 2m24s
CI / deploy (push) Successful in 1m32s
CI / publish-container (push) Successful in 47s
Mirror interactive batch runs into the import-job store so interrupted imports (restart, time-out, disconnect) can be resumed from Import History. Items are checkpointed as they settle (coalesced, serialized saves) and the mirror starts 'running' so the boot-time worker marks it 'interrupted' instead of double-importing; done items are never re-imported. Add bounded backoff retry for transient download/connection failures before marking an item failed, and point the client's time-out/network toasts at Import History.
2026-09-13 12:50:17 +02:00
openhands 2a708b01b1 perf(import): adjustable concurrency and skip redundant SWF downloads
CI / check (push) Successful in 2m22s
CI / deploy (push) Successful in 1m12s
CI / publish-container (push) Successful in 44s
Raise batch concurrency (furni 3->12, clone 10->12) with a Speed control next to Translate. Skip the SWF download when a .nitro bundle already exists on disk (color variants share the base nitro), and stop flagging that as a failed download.
2026-09-13 12:14:52 +02:00
openhands b92f897ba2 perf(studio): virtualize the furniture table and throttle batch progress
CI / check (push) Successful in 2m33s
CI / deploy (push) Successful in 1m28s
CI / publish-container (push) Successful in 57s
- Render the table view through a virtualizer too, using a shared grid
  template so the sticky header and rows keep perfect column alignment
- Keep semantic table/row/cell elements while virtualizing
- Cap the batch item-details list to the latest 60 rows (newest first)
- Coalesce per-item progress events server-side (120ms throttle) in both
  the exact-import and clone SSE batch runners
2026-09-13 11:53:07 +02:00
openhands d02aaa0d87 perf(studio): cache furni imports and virtualize the furniture grid
CI / check (push) Failing after 22s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped
- Add TTL-based caching for local index lookup, furnidata classnames,
  catalog id set, nitro file presence and import stats
- Invalidate caches after furnace single/batch/clone imports
- Rewrite batch progress with elapsed time, rate and verification chips
- Virtualize the grid with @tanstack/react-virtual and replace the
  Load more button with infinite scroll via an IntersectionObserver
2026-09-13 11:40:55 +02:00
openhands 8c12fc6c60 refactor(cms): deduplicate shared admin components and fix studio batch completion
CI / check (push) Failing after 1m35s
CI / deploy (push) Skipped
CI / e2e (push) Skipped
- fix(studio): stop markBatchDone infinite recursion so batches complete
- refactor(api): merge api-response into api and drop the duplicate module
- refactor(media): extract shared media loader and URL validator
- refactor(ui): extract shared LoadingSpinner for site and admin groups
- refactor(dates): consolidate raw date formatting into formatDate util
- refactor(logs): share a single generic log-list loader across tables
- refactor(theme): merge both ColorField components and reuse contrast helpers
- refactor(import): extract shared ImportErrorBanner and SearchInput
- chore(): remove dead theme-editor-tabs after inlining tab components
2026-09-05 20:58:42 +02:00
Simo cd75361b9f feat(catalog): generate derived scale 32 with preview and backup restore
CI / check (push) Successful in 1m32s
CI / deploy (push) Successful in 1m12s
CI / e2e (push) Successful in 21s
2026-09-05 17:36:22 +02:00
Simo 33b6d1520e fix(catalog): stream single imports and preserve response errors
CI / check (push) Successful in 1m31s
CI / deploy (push) Successful in 1m13s
CI / e2e (push) Successful in 21s
2026-09-05 15:16:42 +02:00
Simo 8a919f85b2 Preserve furniture revisions when downloading source assets
CI / check (push) Successful in 1m31s
CI / deploy (push) Successful in 1m21s
CI / e2e (push) Successful in 21s
2026-09-05 14:29:48 +02:00
Simo a78d8256e0 Recognize imported furniture by normalized classname and local IDs
CI / check (push) Successful in 1m30s
CI / deploy (push) Successful in 1m21s
CI / e2e (push) Successful in 22s
2026-09-05 14:04:52 +02:00
Simo 9f791ba284 Support Gitea configuration and safe Catalog Studio furniture completion
CI / check (push) Successful in 1m31s
CI / deploy (push) Successful in 1m18s
CI / e2e (push) Successful in 21s
2026-09-05 13:33:40 +02:00
openhands 1374bd332f Studio: add clone-all-per-hotel, per-item status detail, retry, ETA, and auto-translate
CI / runtime-diagnostics (push) Skipped
CI / check (push) Successful in 36s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m14s
- Add 'Clone all missing' button for clone sources with confirmation dialog
- Show per-item status badges: nitro, furnidata, catalog entry
- Add status filters: missing furnidata, missing catalog entry
- Add ETA and percentage to batch progress bar
- Add retry button for failed items after batch import
- Auto-translate all languages after clone-all completes
- Fix statusFilter 'all' bug that incorrectly filtered imported items
- Bulk-load FurnitureData + catalog references for O(1) per-item checks
2026-09-01 17:29:12 +02:00
openhands 7556b1f3fa perf: prevent import hanging with timeouts and batching
CI / check (push) Successful in 31s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m1s
- verifyAndFixInteractionModesCount: paginated DB queries (500/batch)
  instead of loading all items into memory at once
- withFurniDataLock: add 60s chain timeout to prevent deadlocks
  when a lock holder stalls or crashes
- withGamedataLock: same timeout protection for gamedata locks
- conversion-pool: add 60s per-job timeout, fall back to main thread
- furni/batch: abort signal + post_import progress events + skip
  post-import steps when client disconnects
- furni/batch-regen: abort signal + early exit when disconnected
- clone/sync-all: pre-fetch furnidata once per source instead of
  per item (eliminates 2000+ redundant fetches)
- sse-client: add 60s idle timeout to prevent infinite hangs
2026-08-31 18:25:32 +02:00
openhands f70d96b81c fix: prevent import hanging by adding abort signals and idle timeouts
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 59s
- Furni batch: wire request.signal to abort controller, send post_import
  progress events, skip post-import steps when aborted
- Clone sync-all: pre-fetch furnidata once per source instead of per item
  (eliminates 2000+ redundant DB reads + HTTP requests)
- SSE client: add 60s idle timeout to prevent infinite hangs when server
  stops responding
2026-08-31 18:11:54 +02:00
openhands 6dced0fb54 feat: per-import translation toggle and language picker in studio
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 53s
Furni imports can now choose whether to translate (per import) and which
languages to build, instead of always rebuilding all 13 FurnitureData_<lang>
files. The studio header also has a global switch that persists the
furnidata_translate_enabled setting, seeding the per-import default.
2026-08-24 18:16:42 +02:00
openhands c7ba04bda1 feat: relocate import tools into studio and harden catalog/furnidata integrity
CI / check (push) Failing after 26s
CI / release (push) Skipped
CI / deploy (push) Skipped
- Move /admin/import/* tools under /admin/studio/* and add studio nav, layout and tabs
- Add shared catalog maintenance panel plus a /admin/studio/maintenance tab
- Make furnidata reconciliation overwrite conflicting entries with the
  DB-authoritative items_base classname/id (surfaced via fixedConflicts)
- Add furnidata_translate_enabled setting to skip the heavy localized
  furnidata build during import (manual build-languages still forces it)
- Update staff smoke contract test for the studio hub
2026-08-24 17:13:03 +02:00
openhands c2e61dc324 fix: reduce import concurrency to prevent 100% CPU during SWF→Nitro conversion
CI / check (push) Successful in 42s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m43s
- Default batch concurrency: 3→1 (max 5→3)
- Default batch-regen concurrency: 3→1 (max 5→3)
- Default IMPORT_BATCH_CONCURRENCY: 12→3
- Added setImmediate yields between items to prevent event loop blocking

Fixes gatje and other complex furniture imports consuming 100% CPU and stalling the catalog studio.
2026-08-23 19:51:04 +02:00
openhands 5311ee1fb8 Force catalog_items.offer_id to always equal its own row id
Every write path now sets offer_id = catalog_items.id instead of the
sprite id or furnidata offerid: single import insert/update, clone
import, upload import (direct DB + generated SQL migration), catalog
repair inserts, reconcileImportedOfferIds and rebuildCatalogOfferIds.

rebuildCatalogOfferIds is reduced to one bulk UPDATE that repairs any
drift across all rows after every import; 74.838 legacy rows were
repaired on the live database with this change.
2026-08-23 13:42:47 +02:00
openhands f24426c1fa Enable FurnitureData.json writes during furniture import
CI / check (push) Successful in 29s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m3s
- Change skipFurniDataWrite from true to false in both import routes
- Previously: FurnitureData.json entries were deferred/skipped during import
- Now: Entries are written immediately after each furniture item import
- Result: FurnitureData.json is updated directly, translations work again, offer_id fix preserves correctly set values
2026-08-22 21:12:06 +02:00
openhands 85b5792d40 feat: RCON API transport, full furnidata translation with LibreTranslate fallback, remove organize feature, bump per-page limit to 500
CI / check (push) Failing after 7s
CI / release (push) Skipped
CI / deploy (push) Skipped
- Add EMULATOR_MODE=api option with HTTP transport mirroring RCON payload
- buildLocalizedFurniDataFiles now falls back to LibreTranslate for customs (incl. pl/ru/ar/ja)
- Remove Organiseer import button from studio and Reorganize Catalog from import page
- Delete organize API route and PUT handler; remove organizeCatalogAll from catalog-repair.ts
- Increase imported-furni list perPage from 50 to 500
2026-08-22 14:28:06 +02:00
openhands 1d8672ccc5 perf: race furniture asset download candidates instead of trying sequentially
CI / check (push) Successful in 47s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m7s
tryDownloadCandidates now fires every candidate URL concurrently and keeps
the first valid response, removing the old 15s×retry sequential waits on
slow/unreachable sources that made import speed uneven. Batch concurrency
raised 8 -> 12 to absorb the now-faster downloads.
2026-08-22 13:38:00 +02:00
openhands ac7c427c3d feat: synthesize size 32 (catalog icon) frame for all furniture .nitro bundles
CI / check (push) Successful in 35s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m18s
AddSize32ToBundle downscales the largest existing sprite (usually size 64)
to generate a 32 visualization, so every bundle — including the
source-provided gamedata furniture — carries a 32 frame without needing the
original SWF. AddSize32AllNitros rewrites every existing .nitro across all
nitro directories (incl. /var/www/Gamedata/bundled/furniture) and is
idempotent (bundles that already have 32 are skipped). Expose it via a Studio
button and the background admin endpoint POST /api/admin/import/furni/add-size32.
2026-08-22 13:31:50 +02:00
openhands 6843af235e perf: speed up batch furniture import
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 57s
- Run batch imports with bounded concurrency (IMPORT_BATCH_CONCURRENCY = 8)
  instead of fully sequentially, parallelizing network downloads and DB writes.
- Collect FurnitureData.json entries and write the whole file once per batch
  (appendFurniEntriesBulk) instead of rewriting it on every single item.

Catalog id allocation stays serialized and FurnitureData writes remain
lock-guarded, so concurrent imports are safe.
2026-08-22 12:40:43 +02:00
openhands bb7ec3db02 feat: add automatic catalog organization for all imported furniture
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 53s
Add a fully automatic "Organize all" action in the Studio that organizes
the entire database into catalog pages:

- Create a dedicated sub-page per furniture line (e.g. weebz, habbox) from
  FurnitureData.json, falling back to the auto-detected category.
- Add missing purchasable items to the catalog on their line/category page.
- Re-home and re-price existing catalog entries onto their correct page.
- Pages are created automatically when missing; no manual selection needed.

Includes the organizeCatalogAll service, a POST /api/admin/import/furni/organize
endpoint, and the Studio button.
2026-08-22 11:27:22 +02:00
openhands 0b0cf4f37f feat: Auto-fix cost_credits <= 0 to 3 on every furniture import
CI / check (push) Successful in 29s
CI / release (push) Skipped
CI / deploy (push) Successful in 44s
- Extended fixDatabaseConsistencyAfterImport() to also set cost_credits = 3 where <= 0
- Integrated into both single and batch import routes
- API responses now include costCreditsFixed count

Ensures 100% of catalog_items have cost_credits > 0 after import
- Typecheck, lint, tests all pass
- Pushed to GitLab
2026-08-21 21:41:54 +02:00
openhands 86f3d82c5d feat: Add automatic database consistency fix to furniture imports
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 55s
- Added fixDatabaseConsistencyAfterImport() function in furni-import.ts
  * Fills empty catalog_name from items_base.public_name
  * Sets have_offer = '1' where it was '0'
  * Returns counts of fixes applied

- Integrated fix into import flows:
  * src/app/api/admin/import/furni/route.ts (single & batch POST)
  * src/app/api/admin/import/furni/batch/route.ts (SSE batch POST)

- API responses now include:
  * catalogNameFixed: number of catalog names filled
  * haveOfferFixed: number of have_offer values fixed

- Ensures 100% catalog_name coverage and have_offer=1 after every import
- Typecheck, lint, and all 827 tests pass
2026-08-21 21:31:39 +02:00
openhands 8bb339cc49 feat: auto-translate furnidata per import (nl/en/de/fr/es/tr/it)
CI / check (push) Successful in 29s
CI / release (push) Skipped
CI / deploy (push) Successful in 54s
Generate localized FurnitureData_<lang>.json files from the master
FurnitureData.json + official Habbo hotel translations. Fetches per-hotel
furnidata with in-memory cache (6h TTL), maps by classname (* variant
aware), and overrides name/description where a translation exists.

Full rebuild after every batch/batch-regen/clone import and as admin
GET ?action=build-languages; single imports patch incrementally via
patchLocalizedFurniDataEntries. Failures are best-effort and never fail
the import itself. Files are written to every configured gamedata write
path (primary + mirrors) so /gamedata/config/FurnitureData_<lang>.json
is available for per-language Nitro clients.

New service: src/lib/services/furni-data-i18n.ts with
buildLocalizedFurniDataFiles, patchLocalizedFurniDataEntries,
FURNIDATA_LANGUAGES (nl/en/de/fr/es/tr/it) and applyTranslationsForTest.
2026-08-21 14:16:37 +02:00
openhands 984b5bf793 feat: auto-repair sprite_id drift after every import
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 54s
The emulator sends items_base.sprite_id to the client and Nitro resolves
the furniture by looking up that id in FurnitureData.json. Legacy rows
where sprite_id drifted from the id made the client render a completely
different item (e.g. a wired or custom asset instead of the purchased
furniture).

Add verifyAndFixSpriteIds() which repairs any row whose sprite_id no
longer matches its id while the local furnidata carries the item under
that id, and run it after every import path (single, batch, batch-regen,
clone) next to the offer_id rebuild.
2026-08-21 14:02:42 +02:00
openhands 4b7cb519df fix: furniture interaction detection and automatic catalog offer_id rebuild
Interaction detection:
- Trust SWF-derived sit/lay/stand flags only when the logic XML actually
  contains action data (new hasActions metadata); otherwise fall back to
  keyword detection so custom furni without <action> nodes are still
  classified correctly
- Add French/Dutch/German/Spanish/Italian keywords (chaise, banquette,
  stoel, silla, sedia, stuhl, tafel, mesa, ...) to sit/lay/stand detection
  with token-boundary matching to avoid false positives like bedside_table
- Unify interaction_modes_count priority: mechanic fixed modes, then raw
  animation state count, then sit/lay fallback, then keyword default
- Detect mechanic type even when real flags are not used

Catalog integrity:
- Skip duplicate catalog_items inserts in clone and upload imports
- Re-check live catalog rows before applying generated repair SQL
- Add rebuildCatalogOfferIds() which rebuilds every catalog_items.offer_id
  from the local FurnitureData.json (matched by entry id, then classname)
  and run it after every import path (single, batch, batch-regen, clone)
2026-08-21 13:39:21 +02:00
openhands 652d331402 Add live furnidata reload without hotel reload and notImported status filter
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 57s
- Add /api/admin/import/furni?action=live-reconcile POST endpoint that reconciles FurnitureData.json with items_base without triggering RCON hotel reload
- Add 'notImported' status filter in studio to show all non-imported furniture items
- Useful for verifying imports and seeing what's missing after furniture import
2026-08-20 16:58:26 +02:00
openhands 6021a91ef7 fix: harden furni import pipeline for production
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 57s
- importSingleFurni: actually update an existing item instead of failing on a primary key INSERT collision; refuse to reassign a spriteId owned by a different classname
- reconcileFurniDataWithItemsBase: also sync each entry's offerid to the DB sprite id, not just the id
- Move the full-table interaction_modes_count verification out of the per-item import hot path and run it once per batch/single import
- clone-import: apply the auto-detected interaction_type (was hardcoded 'default'), set FurnitureData offerid and catalog_items.offer_id to the new local sprite id (was the source hotel's id / -1)
- clone batch route: run the same post-import reconcile/verify/ownership/RCON consolidation as the furni batch route
- upload-import: set offer_id to the allocated id in both the direct insert and the generated SQL migration (was -1)
- generateCatalogSql: use the classname as catalog_name and the item id as offer_id so rows match the app-managed import convention
- stats + missing-nitro endpoints: match catalog membership via item_ids instead of the catalog_name join, so translated display names no longer break counts
- deleteImportedItem/rollback: delete catalog rows by the canonical item_ids link only
- classifyFurni: wall items always classify as 'walls' before prefix rules (rare_/val_/xmas_) can misfile them
2026-08-20 11:48:51 +02:00
openhands 2b9a015afb feat: add manual 'verify & fix all interactions' admin tool
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 54s
New POST /api/admin/import/furni/verify re-runs the full interaction
verification over items_base at any time, plus a Tools dropdown entry
in the furni import admin. It corrects interaction_type,
interaction_modes_count and allow_sit/lay/walk against real furniture
data (furnidata flags + .nitro animation states); items without real
data are skipped and existing emulator handler types are preserved.
2026-08-19 21:11:38 +02:00
openhands b4968a9967 Reconcile FurnitureData.json with items_base after import
CI / check (push) Successful in 45s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m16s
After every single import, batch import and batch-regen, cross-check all
items_base rows against the local FurnitureData.json: entries whose
spriteId drifted are corrected to the DB id, and missing entries plus
real id conflicts are reported in the API/SSE response. Entries that are
missing entirely are counted (rebuilding them needs SWF/nitro metadata).
2026-08-18 20:43:41 +02:00
openhands b4021f6b42 Backfill icons too in the Gamedata bundle sync
CI / check (push) Successful in 38s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m28s
Extend the post-import backfill to also copy missing _icon.png files
into /var/www/Gamedata/icons (in addition to nitros into
bundled/furniture) so all imported furniture shows an icon in-game.
Report copied nitros and icons separately in the API/SSE responses.
2026-08-18 20:35:48 +02:00
openhands bcd24bfca4 Backfill missing nitros into the Gamedata bundle on import
CI / check (push) Successful in 43s
CI / release (push) Skipped
CI / deploy (push) Failing after 1m19s
After each single import, batch import and batch-regen, copy every
.nitro file that is missing from /var/www/Gamedata/bundled/furniture so
the emulator can render all imported furniture even when the mirror write
was skipped. Reports the copied files in the API/SSE response.
2026-08-18 20:29:30 +02:00
openhands 99e77d9872 Fix ownership reconcile on the batch import route
CI / check (push) Successful in 54s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m27s
The Studio's batch import uses /api/admin/import/furni/batch, which never
ran the offer_id reconciliation or the www-data ownership fix (only the
single/legacy-batch route did). Run both after the batch finishes and
report the counts in the batch_complete SSE event.
2026-08-18 20:01:18 +02:00
openhands 18825115a4 Auto-fix furni asset ownership after import
CI / check (push) Successful in 44s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m17s
After each single or batch import, chown the swf/icon/nitro asset
directories and the FurnitureData.json folders to www-data:www-data so
nginx and the emulator can read newly written files. Best-effort and
non-blocking; the API response reports which folders were fixed.
2026-08-18 19:52:32 +02:00
openhands c95ad4a37f Reconcile offer_id after every import
CI / check (push) Successful in 40s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m12s
After each single or batch import, walk all catalog items under the
Imported Furniture tree and set offer_id to the furnidata sprite id,
repairing any stale values (e.g. legacy -1 rows). Reports how many
rows were fixed in the API response.
2026-08-18 19:40:17 +02:00
openhands 54f2bc5e0c Add clone source selection to Studio furni browser
CI / check (push) Successful in 32s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m16s
Allow the admin Studio to browse and import furniture from custom retro
hotel sources, not just official Habbo furnidata.

- GET /api/admin/import/furni?source=<id> lists a clone source's
  furnidata (via getCloneList) and returns a per-item iconUrl from the
  source's iconBaseUrl so previews render correctly
- Studio client gets a source selector dropdown (official Habbo plus all
  configured clone_sources) that resets the selection and reloads the
  list when switched
- Single and batch imports now send sourceId so SWF/nitro/icon assets
  are fetched from the selected hotel's CDN
- Preview images prefer the source iconUrl with the existing fallback
  chain; header shows the active source name
2026-08-15 15:43:51 +02:00
openhands 7ef5038a9e Add updateExisting option to furniture import
CI / check (push) Successful in 1m14s
CI / release (push) Skipped
CI / deploy (push) Successful in 2m15s
- Added updateExisting parameter to importSingleFurni and API routes
- When updateExisting=true, existing items are updated (catalog price/page) instead of failing
- Added catalogUpdated flag to ImportSingleResult
- Updates existing catalog entries (price, page) instead of skipping duplicates
2026-08-14 18:35:00 +02:00