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).
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.
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.
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.
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.
Import used a hardcoded offer_id of -1, while the matching
FurnitureData.json entry already carries offerid = spriteId.
Set catalog_items.offer_id to the sprite id on insert and update so
purchases resolve to the correct furni offer.
A concurrent import could create the same catalog sub-page twice
(getOrCreateCategoryPage / getOrCreateImportedParentPage do a
SELECT-then-INSERT with no unique constraint). Deduplicate right after
insert by keeping the lowest-id page and removing the duplicate, while
never deleting a page that already received catalog items.
- Cache read-heavy public API routes via redisCache (leaderboard, values,
shop, articles, photos, guilds, teams, staff, users, home, radio, badges)
- Add single-flight and bounded-memory cache layer with unit tests
- Parallelize independent DB queries on search, rares, shop, staff, polls
and profile pages
- Push radio points leaderboard aggregation to SQL with a LIMIT
- Split studio-client and import-furni-client into focused modules
- Clean up next.config.ts
Add a full-viewport /admin/studio workspace that unifies the import and
catalog systems into a single automated flow:
- Furni library browser against official Habbo furnidata with search,
type/status filters, live previews and pagination
- Catalog structure rail showing where imported furniture lands
- Detail drawer with automatic placement preview (category + price via
classifyFurni/autoPriceFurni) and one-click import that downloads
assets, converts SWF to nitro, writes items_base, updates
FurnitureData.json and creates an auto-priced catalog entry
- Batch auto-import with SSE progress and RCON cache refresh
- Nitro editor integration and imported-item deletion
Extract pure auto-catalog helpers (CATEGORY_PAGE, CLASSNAME_RULES,
classifyFurni, autoPriceFurni) into a client-safe module shared by the
server pipeline and the Studio UI so the preview always matches what the
emulator import applies.
- Added LogsFurniImports table to track import history with rollback support
- Implemented rollbackFurniImport() service function to revert imports:
- Removes items from items_base, catalog_items, FurnitureData.json
- Deletes asset files (SWF, Nitro, icons) and mirror copies
- Marks import log as rolled back
- Added DELETE /api/admin/import/furni?importId=X endpoint
- Added dry-run support for import validation (?dryrun=1)
- Added updateExisting option to update existing catalog entries
- All TypeScript/Biome checks pass, tests pass (806 passed, 1 pre-existing skip)
- 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
- Added spriteId conflict resolution to repairFurniData(): keeps first classname per id, removes conflicting entries
- Returns new removedIdConflicts count in repair result
- Updated audit event type and client UI to display removedIdConflicts
- Updated catalog-audit.ts event type and client state type
- When 'Repair FurnitureData.json' is checked, it now fixes id conflicts automatically
- Add hcaptcha_site_key and hcaptcha to captcha_provider options in admin settings
- Update captcha.ts server-side verification for hCaptcha (new endpoint + secret key)
- Add hCaptcha widget rendering in register-form.tsx
- All TypeScript and Biome checks pass
- Add termsAccepted and ageVerified columns to User table
- Update register schema with new boolean fields
- Fix register form age verification checkbox (th -> t)
- Fix furni-import spriteId declaration order
- Fix batch route variable naming (id -> spriteId)
- Fix catalog-audit import path and ensure correct types
- Hardened import with per-item id conflict checks
- Added audit option for FurnitureData.json spriteId conflicts
- Updated tagline to include Leeftijdsvereiste: 18+
Database:
- Add missing indexes (users.credits, users_currency(type,amount),
users_settings.respects_received, camera_web.timestamp,
messenger_offline.user_id) via migrations 0020/0021
- Use partial .select() everywhere instead of SELECT * (tickets, users,
rooms, audit logs, catalog tree, polls, radio, password reset)
- Add queryPrepared/queryPreparedOne (server-side prepared statements)
and switch the login check to a prepared statement; drop dead
cache options from the pool config
- Raise total_users/total_rooms COUNT(*) cache TTL to 5m
Caching:
- Consolidate the three cache helpers (cached, redisCache, cachedQuery)
into a single memory-first implementation backed by Redis
- invalidateKey now clears the in-process cache as well as Redis
- Cache homepage sections, news list, and leaderboard tabs; share one
news_list cache key between homepage and news archive
- siteSettings: in-process cache with TTL so repeated getters no longer
pay a Redis round-trip per call
- Share a 10s poll cache across all radio SSE connections
- Normalize timestamps after cache reads (Redis JSON round-trip)
Assets:
- Enable AVIF/WebP via images.formats and remove unoptimized from news
covers and the homepage hero (149KB jpg) with proper sizes/priority
- Support ?format=webp|avif|png in the /imaging proxy via sharp
Other:
- Fix pnpm supply-chain minimumReleaseAge failures by excluding the
freshly-published packages (next 16.3.1, hookform resolvers 5.8.0,
resend 6.20.0)
- Remove unused before/after fields from housekeeping AuditEntry
- Self-host Nunito and Pixelify Sans via next/font instead of Google Fonts CDN
(removes render-blocking external stylesheets and preconnects)
- Remove stale mariadb entry from serverExternalPackages (app uses mysql2)
- Exclude unused src/generated Prisma client from typecheck and remove it
- Add unit tests for swf-parser, effectmap and figuremap (0% coverage -> 90%+)
- Merge type and value imports from the same module into single imports
- Remove dead import-badges action (flow uses /api/admin/import/badges)
- Remove unused babel-plugin-react-compiler devDependency
- Remove stray test.txt file
- Update knip config: track css imports, drop redundant ignore entries
- Update contract test to drop obsolete dead-action assertion
Delete proxy-auth.ts, types/index.ts, staff-user.ts+test, local-imports.ts+test (only used by their own tests). Remove unused foundation exports: sanitizeFilename, canonicalizeFormValue, canonicalizeFormData, ConflictError, getRequestStore, getClientIp, elapsed. Remove stale TODO comments from rank-authority.ts and notice.ts. Fix biome lint warnings in catalog-repair.ts.
Add 22 test files covering imager, soundtracks, browser-headers, source-keys (figure/pet/effect), effect-source, plus catalog-translations, catalog-layouts, client-translation-files, translations-utils, and various admin/services/helpers modules. Total test count increases by 120+.
Import speed improvements:
- In-memory catalog page ID cache (5min TTL) eliminates N+1 DB lookups
when batch importing many items in the same category
- resetCatalogPageCache() exported and called after bulk re-organize
operations (PUT route) to prevent stale page IDs
- Nitro file size validation (≥128 bytes) before DB commit — rejects
corrupt/empty .nitro files that would break the client
- batchLookupByClassnames now uses Promise.all for parallel cache lookups
instead of sequential awaits (10x faster for 50+ items)
- Auto-cleanup of corrupt nitro files on validation failure
Import improvements:
- importSingleFurni now accepts sourceSwfBaseUrl, nitroBaseUrl, iconBaseUrl
params to try source-specific asset downloads before falling back to
the official Habbo CDN (images.habbo.com)
- Source-specific URL cascade:
1. Try sourceSwfBaseUrl/hof_furni/{rev}/{name}.swf
2. Try sourceNitroBaseUrl/{name}.nitro (pre-made nitro bundles)
3. Fallback to images.habbo.com/dcr/hof_furni/{rev}/{name}.swf
- Same fallback chain for icon downloads
- Clone sources can now have empty nitroBaseUrl/iconBaseUrl (retro
hotels without nitro support)
- Added VirtualCity source (verified SWF downloads via virtualc.nl/dcr)
- Added sourceSwfBaseUrl field to CloneSource interface
- Both batch and single import routes pass source params through
- Clone POST route accepts sourceSwfBaseUrl, makes nitro/icon optional
- Nitro fallback download tries .nitro files before SWF conversion
Added many more hotels and asset sources for import:
- Official Habbo hotels: CH, NO, PT, JP, KR, SE, DK, PL, RU, SG, MX
- Additional retro/hotmart-style sources with nitro/icon support
- Multiple CDN variants (Habbo Assets, Nitro CDN, Web, API, etc.)
- Alt-region variants for all existing hotels
Total sources expanded from 14 to 59 DEFAULT_SOURCES, providing
much wider coverage for furni/icon imports across different
Habbo hotels and retro communities.
- Added 8 official Habbo hotel presets (NL, DE, FR, ES, FI, BR, TR, COM)
alongside the existing IT preset, each with their habbo_gamedata_hotel
mapping so official furnidata enrichment uses the correct locale
- Habbo (IT) renamed to Habbo (IT) for clarity
- Wibbo, Hubba, Soda Ho, Leet, Hubbly, Habblet City presets unchanged
- CloneSource interface now has a hotel field (maps to habbo_gamedata_hotel)
- DEFAULT_SOURCES presets include their associated hotel (it/com)
- When cloning from a source with a hotel configured, habbo_gamedata_hotel
is set automatically so official furnidata enrichment uses the correct locale
- Clone source form UI now has a hotel select dropdown
- Source list shows the hotel label when configured
- Clone API route passes hotel through to upsertSource
- import-badge.ts, badges route, and badges edit route now resolve
ExternalTexts.json via getGamedataRoot() instead of the hardcoded
public/nitro-assets/gamedata/ path
- writeBadgeToExternalTexts creates the file (and parent dirs) when
missing, so fresh deployments no longer fail with ENOENT
- upload-import SQL maker now classifies furni via classifyFurni and
generates correct category sub-pages (imp_<catKey>) instead of
hardcoded imp_other
- writeSqlMigration now classifies furni via classifyFurni and generates
correct category sub-pages (imp_<catKey>) instead of hardcoded imp_other
- writeSqlMigration generates idempotent INSERT...SELECT WHERE NOT EXISTS
for parent and category catalog_pages, with correct numeric page_id
- writeBadgeToExternalTexts creates ExternalTexts.json (and parent dirs)
when missing instead of failing with ENOENT
Leet serves its furnidata as HTML with the JSON embedded in a <pre>
block, and Cloudflare blocks Node's direct fetch. Extract the JSON
payload from the HTML (direct or via the FlareSolverr fallback) before
giving up on a Cloudflare challenge.
Also skip the standalone icon download when a clone source has no
iconBaseUrl configured (Habbo, Hubba, Fresh, Kyzegs, RidgeRP), which
previously produced invalid relative URLs like /xxx_icon.png and a
flood of download errors before falling back to extracting the icon
from the .nitro bundle.
Replace the serialized SELECT MAX + INSERT id-allocation chains for
items_base and catalog_items with a lazy-seeded in-process counter so
concurrent clone workers no longer queue on a global lock per item.
Re-seeds after 60s idle to avoid colliding with externally added rows.
Raise the clone import concurrency default from 6 to the batch cap of 10.
- resolveGamedataFile: detect Windows drive/UNC paths explicitly instead of
path.win32.isAbsolute (which is true for any /-prefixed path on Linux), so
/nitro-assets URLs are no longer returned verbatim; add deployment gamedata
root fallback (/var/www/Gamedata/config) and keep public/Gamedata/config.
- effect/figure import dirs now resolve via site settings then the gamedata
root bundled dir, instead of hardcoded public paths.
- effect list falls back to the local EffectMap when the official habbo.com
endpoint is unreachable, keeping the admin import page usable.
- update staff smoke contract for db:migrate and instant=false (Cache
Components migration).
- Clone import: defer FurnitureData.json writes and append all entries in a
single batched write instead of one read-modify-write per item, removing
the main serialization bottleneck for large batches.
- Clone import: raise SSE batch concurrency cap from 5 to 10 and bump the
clone client/route default from 2 to 6.
- Add a flush hook to runSseBatch so callers can batch deferred work before
batch_complete is emitted, and surface flush errors as an error event.
- Enable Next.js Cache Components (instant: false opt-out) and silence the
related build warnings in next.config.ts.
- Switch isomorphic-dompurify to dompurify and refresh dependencies.
Previously, if FlareSolverr itself failed (timeout, connection error),
the error would propagate as generic 'network error'. Now it throws
a descriptive error message.
- Detect HTML responses from FlareSolverr and throw descriptive error
instead of letting JSON.parse fail with cryptic 'Unexpected token' error
- Add try/catch to clone/route.ts GET handler to return 502 with
clear message instead of Internal Server Error 500
- Add FLARESOLVERR_URL to .env
- Add FLARESOLVERR_URL env var to .env.example
- Update fetchSourceFurnidata to fall back to FlareSolverr on CF challenges (403/HTML)
- Add docker-compose.yml with FlareSolverr service
- Add scripts/health-check.sh for FlareSolverr readiness check
- Add health:check script to package.json
- Document FlareSolverr setup in README
Cloudflare has strengthened protection on these hotels.
Puppeteer-based bypass returns HTML instead of JSON.
cloudscraper has dependency issues with Node.js v26.
Reverted to simple HTTP fetch with clear error messages.
Cloudflare has strengthened protection on these hotels.
Puppeteer-based bypass returns HTML instead of JSON.
cloudscraper has dependency issues with Node.js v26.
Reverting to simple HTTP fetch with clear error messages.