Add superRefine rule in src/env.ts ensuring that if one PayPal credential (PAYPAL_CLIENT_ID or PAYPAL_SECRET) is set in production, the other is also required, catching configuration drift at startup.
Introduce getCachedAdminCount to cache un-filtered table count(*) queries in Redis for admin lists (starting with UsersPage), avoiding heavy full table scans on every request while keeping exact counts for search/filtered queries.
Add rateLimit protection to /api/paypal/create, /api/paypal/capture, /api/tokens, /api/radio/shouts, and /api/articles/[slug]/comment to prevent abuse and spamming.
The 5-minute disk probe now reclaims storage automatically: from 85% it runs the gentle age-windowed Docker prune, from 90% it drops the age windows (docker-prune.sh --force: all unused build cache and unreferenced images, all stopped containers) so a mount can never silently max out. Alerts still fire at 85/90/95% and their hint now points at non-Docker growth when reclaiming is not enough. Force mode is reserved for the worker; deploys keep the gentle mode. Volumes are off-limits in every path.
Add a pure df parser (disk-usage.ts) with 85/90/95% threshold classification, a diskPressure() alert (Discord/email/alert_logs, severity escalates with fill), and a 5-minute host-side probe in jobs-worker.ts that raises one alert per crossing mount, cooldown-gated per mount+level. Real mounts only: overlay/tmpfs pseudo filesystems are ignored.
Add scripts/docker-prune.sh (build cache >72h capped at 4g, unreferenced images >7d, stopped containers >24h; never volumes), run it after every CI deploy and compose update, and schedule a nightly prune from the host-side jobs-worker. Tighten the deployment contract tests to assert the scoped-prune boundaries.
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.
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.
Highlight the active search term in names/classnames (grid + table), add zebra striping and a left accent bar on selected table rows, fade the results list when switching grid/table or on first load, and swap the broken-icon fallback for a cleaner placeholder.
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.
- 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
- 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
The "Create" CTA used totalSelected (the sum of furni items across
approved groups) as its plural count, so with many imported items it
claimed to create thousands of pages. One approved group creates exactly
one page, so the label now counts approved groups instead.
- hero-ring: continuously shifting gradient hairline around the hero
frame (mask-drawn, reduced-motion safe).
- glass-chip: frosted floating pills under the CTAs that bob on a
staggered loop, live online counter keeps ticking for the Online chip.
- Taller hero for more presence on desktop.
The first cloud pass was to subtle: only five, up to 190s per crossing,
and they froze off-screen under prefers-reduced-motion. Rework:
- Eight clouds across the top 60% of the viewport with visible drift
(28s–66s loops, staggered by negative delays so they are always mid
scene on load).
- Higher opacity/steeper size contrast in light mode; dark mode dims
them slightly.
- Reduced motion now freezes a static, evenly-spread cloud field across
the width instead of pushing the clouds off-screen.
- New src/lib/site-icons.ts: base64 data URIs for the 13 tiny classic
icons actually referenced in markup (100–2000 bytes), replacing extra
requests with inline payloads. home.png, dynamic flags and currency
sets stay on the filesystem.
- Default favicon is now served server-side as a base64 SVG data URI
(memoized), while a DB-configured custom favicon still takes priority.
- Icons render through <Image unoptimized>, so data URIs pass through
untouched on all affected pages (home, login, register, settings,
navigation, auth top bar, client loading).
Add a fixed, decorative layer of soft clouds that slowly float across
the Habbo sky behind the content:
- Five clouds at staggered sizes, heights, opacities and loop timings
(60s–190s) so the drift feels organic.
- Dimmed further in dark mode; frozen by prefers-reduced-motion.
- Pure decoration: aria-hidden, pointer-events: none, no color
utilities in markup (cloud shapes live in globals.css).
- Slow Ken Burns drift on the hero artwork for cinematic depth.
- Gentle breathing pulse on the brand glows behind Frank and the hero.
- Silkier reveal easing (cubic-bezier .22/1/.36/1, 0.55s) site-wide.
- Eased, longer hover transitions on the hero CTAs.
- Smooth page scrolling, all guarded by prefers-reduced-motion.
Give the home page a clear, professional information hierarchy:
- Add eyebrow labels + headings for Features, Live stats and Community
sections using reusable, theme-aware .eyebrow / .section-title styles.
- Loosen the vertical rhythm (gap-8/10) so each block breathes.
- New messages resolve via the existing English fallback for all locales.
Professional tidy-up of the landing experience:
- SurfaceCard: unified rounded-xl radius for a crisper, consistent look.
- Hero: matches the new card radius and gains a dual-direction title
shadow so the headline stays readable over the header artwork.
- Login/register: drop the duplicated "no account / have an account"
paragraphs — the forms already ship an inline footer, so one clear CTA
cluster remains and the side column is cleaner.
Refine the public UI for a cleaner, more professional and scannable
landing experience without leaving the classic Habbo style:
- SurfaceCard: softer layered shadow, gradient accent hairline on the top
edge and a bolder header title across all public cards.
- Home: gradient hotel-name in the hero headline, shine effect on the
primary CTA, hairline on the top bar.
- Login/register: consistent avatar tiles with rounded corners, subtle
borders and a gentle hover lift; uniform username sizing.
- Add reusable theme-aware .card-hairline and .gradient-text utilities.
Replace the premium dark-gaming redesign of home, login and register with
the original classic landing (Habbo sky background, AuthTopBar, SurfaceCard
layout). Keeps the theme background visible again and adds a subtle
theme-aware brand halo behind the hero/Frank plus a soft primary glow on
card hover.
Also upgrades dependencies: next 16.3.5, vite 8.3.0 (typescript 7.0.2 was
already latest). Temporarily lowers pnpm minimumReleaseAge to 60 min so the
fresh 16.3.5 release can be installed; restore to 1440 once it is 24h old.
Replaced the classic Habbo landing style on the home, login and register pages with a modern premium dark-gaming look: always-dark hero canvas with brand glows, grid overlay and ambient orbs, glass panels, gradient text and floating art. Adds shared LandingTopBar, AuthShell and BrandFrank components plus reusable premium CSS utilities. Build, typecheck and lint pass.
resolveNitroFrame now matches spritesheet frame keys that carry a .png
suffix or namespaced naming, and isScale/scaleName preserve that suffix.
Broken source sprites (missing frames or references to icon artwork) are
skipped and reported instead of aborting the whole generation, and the
studio UI surfaces the skipped count.
Adds a second mode to the organize-imports dialog: instead of creating
one new page per approved group (which could produce dozens of tiny
pages), the user can pick an existing destination page and have every
approved item moved into it. No catalog page is created in this mode.
- organizeImportFurni: groups accept destinationPageId; when set, the
existing page is reused, new offers append after its current highest
order, moved offers keep their original name, and the real page
caption is used for logging and results
- OrganizeImportsDialog: mode toggle (create pages / move into page),
searchable destination picker via /api/admin/catalog/tree?search=,
name/icon/layout editors hidden in move mode, button shows a move
count, and the success toast reports moved/added instead of pages
- en + nl translations for the new mode, destination, and move keys
The organize-imports route defaulted to a 500-item limit, silently
hiding offers beyond the first batch of imported pages. Remove the
effective cap (limit now means 'all', guarded only by a 50k lint cap)
so every offer already sitting in the import tree is returned and
grouped.
The original GET route used a correlated NOT EXISTS / FIND_IN_SET
subquery over the entire catalog_items table for every recent import
audit entry, causing server timeouts when the audit log or catalog
grew large. The per-item host-page validation inside the create
action also issued one SELECT + one UPDATE per moved offer.
Changes:
- GET /api/admin/import/organize: replace the correlated subquery
with a bounded candidate list and a JS-side placed-set check, then
resolve all needed base items in a single indexed SELECT. This
bounds the query cost regardless of catalog or audit log size.
- organizeImportFurni action: validate mover ids in one SELECT, then
batch every move per group into a single UPDATE with a CASE
expression instead of one UPDATE per item.
- OrganizeImportsDialog: add a 45-second abort timeout on the fetch
and a distinct load-error state so the UI never silently hangs.
- Add 'loadError' translation key (en + nl).