Commit Graph
143 Commits
Author SHA1 Message Date
openhands 6cc45d7413 feat: harden atoms-nexst against review findings (37 items)
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
openhands 3933214953 feat(auth): implement all 16 homepage/login/register review items
- add countArticles() (published-only, mirrors news-list) and warm total_articles
- localize homepage metadata; bind articleCount to both stats; unique photo alts
- drop duplicate news date and the mascot preload priorities
- extract shared AuthPageFrame/AuthUsersCards used by /login and /register
- login: localized noindex metadata, session redirect via safeRedirectPath,
  ?from passthrough from proxy, unified auth roster cache keys, registered notice
- register: localized metadata, session redirect to /me, unified cache keys
- add resend-verification flow on /verify with rate-limited non-enumerable action
- add safeRedirectPath() with unit tests
- register form: live requirements checklist + password mismatch guard
- login form: unverified state with resend-link CTA
- honour prefers-reduced-motion in TypewriterText
- add 6 translations across all 25 locales
2026-10-08 18:49:27 +02:00
openhands 3265c149da style(landing): add micro-interactions and polish public pages 2026-10-06 22:59:03 +02:00
openhands 57710a7fe3 style(landing): polish the public index, login and register screens
- add theme-aware helpers: btn-brand, btn-glass-dark, auth-input, auth-label,
  auth-alert, explore-pill, avatar-tile, aurora blobs and a hero scroll cue
- reorder backdrop-filter declarations so the glass blur survives the
  production CSS optimizer in modern Chromium
- hero: aurora glow, frosted recent-users chip, premium CTA buttons and cue
- explore nav: icon pills with hover arrows; features: gradient icon tiles
- stats: single glass panel with column dividers; join CTA: aurora + ring
- auth forms: visible labels, icon inputs, eye/password toggle, gradient
  submit and pill footer links
- auth pages: gradient card frame, aurora accents on the intro panel and
  hover-lift avatar tiles; unified pill-shaped top bar
- drop the hard-coded register banner image in favor of the framed card
2026-10-06 22:00:02 +02:00
openhands 6bffc53779 refactor(auth): merge the duplicate login form and localize the auth screens
`home-login-form.tsx` and `login-form.tsx` were two ~240-line near-identical
components. Delete the former and give `LoginForm` a `variant` prop:

- `variant="page"`   sr-only labels plus the register/forgot footer (/login)
- `variant="compact"` visible labels, no footer (homepage sidebar)

Field ids now come from `useId()`, so the two usages can never collide, and the
hardcoded "Show"/"Hide"/"Loading" strings are translated.

Localization of the login and register screens:

- `home-login-form.tsx` was entirely hardcoded English.
- `passwordStrength()` returned hardcoded "Weak"/"Fair"/"Good"/"Strong".
- `register.ts` returned only English strings. It now returns a
  locale-independent `code` next to the message, and the form renders
  `t(code)` with the English string as a fallback.
- Backfilled the new keys across all 25 locales, plus the login/register
  strings that were still English in most of them. `ar`, `fi` and `ja` had
  their entire login/register namespace in English and are now filled in.
  Locale parity stays at 0 missing keys, as `i18n:check` requires.

Copy that did not match the enforced rules: the UI advertised "min 8 chars"
(EN) / "min 6 tekens" (NL) while registration requires 12 characters plus an
uppercase, a lowercase, a digit and a special character. Corrected in every
locale. `password-reset.ts` enforced only 6 characters and is raised to 12 to
match registration.

Accessibility: `login-form.tsx` had no `<label>`, no `id` and no `required` on
any field. All three are now present, and error banners are announced with
`role="alert"`.

Adds `src/i18n/auth-messages.test.ts`, which asserts every `RegisterErrorCode`
resolves to a non-empty message in all 25 locales; verified it fails when a key
is removed. The existing register tests now also assert the error `code`.
2026-10-04 18:50:23 +02:00
openhands f81b114b69 perf(cache): single-flight avatar renders, cacheable public reads, cheap row counts
Three separate things that were each costing more than they needed to on the
hot path.

- Single-flight avatar renders. The disk cache was checked first and a miss
  went straight to the upstream, with nothing shared between callers, so a page
  requesting dozens of avatars at once turned N concurrent requests for one
  figure into N renders. A render is the most expensive operation this app
  does, and the duplication happened exactly when the cache had nothing to
  offer. Eight concurrent requests now cause one render instead of eight. The
  map lives on globalThis because Next can evaluate the module more than once
  per process, and two copies would each start their own render.

- Let public read-only routes be cached by a shared cache. Every JSON response
  was `cache-control: no-store`, so a CDN in front of the app could not answer
  any of it and every request reached the origin. publicCacheControl() opts a
  route in with s-maxage and stale-while-revalidate, using the same TTL as the
  server-side cache so the two layers cannot disagree. The default stays
  no-store: most routes here are personalised, admin-only or auth-dependent.
  /api/badges/leaderboard is deliberately left alone because it returns
  per-viewer rank entries to signed-in callers.

  Note this only takes effect once a cache rule exists for /api/* at the CDN, or
  the explicit `cache: "no-store"` is dropped from the client fetches (24 files
  do that today, including the /api/online poll). The headers alone are inert
  until one of those happens.

- Take the homepage row counts from the storage engine estimate instead of
  COUNT(*), which walks an index and gets slower as the tables grow. A missing
  or zero estimate falls back to the exact count rather than ever showing a
  wrong zero. The online count stays exact: it is an indexed read over a small
  subset and a few seconds of drift reads as broken rather than approximate.

The counters move into one module because the homepage and the boot warm-up
populate the same cache keys, so two implementations would race to write
different values into the same entry.

3223 tests pass.
2026-09-25 18:42:23 +02:00
openhands 203399aab7 fix(cache): true LRU, stale-while-revalidate and cross-process invalidation
The in-process cache was a FIFO of 500 entries that was never touched on a
read, so a key polled on every request could be evicted by an unrelated burst
of dynamic keys. That looked exactly like the cache being cleared at random,
and it is what made the site fall back to the database unpredictably.

- Evict least-recently-used instead, and raise the default budget to 2000
  (CACHE_MEMORY_MAX_ENTRIES). Reading a key now marks it as used, so a hot key
  only leaves when a hotter one takes its place.
- Add opt-in stale-while-revalidate (CachedOptions.staleMs). The grace window
  lives on the entry, so one call site opting in protects every reader of that
  key. A failed background refresh keeps serving the last good value instead of
  falling through to the origin, and is reported once rather than per read.
- Invalidate across processes. invalidateKey() now clears memory, deletes the
  Redis key and publishes a signal, so a value written by one process is no
  longer served stale by the others for the rest of its TTL. A failed Redis
  delete no longer skips the broadcast.
- Guard against a refresh that started before an invalidation writing its
  outdated result back into the cache.
- Read the news revision at most once a second per process instead of on every
  call, with a pub/sub signal to drop the local copy when it rotates. A Redis
  outage now degrades to the in-process cache rather than to no cache at all.
- Warm the hot public keys on boot, so the first visitors after a deploy do not
  each pay for a miss.
- Count hits, misses, stale serves, errors and evictions per key, exposed at
  GET /api/admin/devops/cache. Without it a wrong REDIS_URL, a full budget and
  a dead origin all look identical from the outside.
- Enforce the imaging cache budget for real: records are .img/.json pairs, so
  the old cap counted files and never removed anything while entries were
  fresh. Sweeps are throttled per directory and prune to a low-water mark.
- Cap the JWT version map, and stop per-test scratch roots from littering the
  runtime imaging cache.

Public read-only endpoints get grace windows; admin, account and auth data
deliberately stays fresh. Redis TTLs get a little jitter so keys written
together no longer expire together.

3209 tests pass. next build could not be verified on this host: the optimized
build is OOM-killed before prerender, so this has not run in a real Next
runtime yet.
2026-09-25 18:26:45 +02:00
openhands b2777634f7 ci: run all workflows on the self-hosted host runner
- Switch test-runner and renovate workflows from ubuntu-latest to
  self-hosted now that a native host runner is running as a systemd service
- Replace remaining hardcoded color utilities in the homepage with theme
  tokens and inline rgba styles to satisfy the no-hardcoded-colors contract
- Restore dual UserAvatarThumbnail usage on the homepage (hero avatar stack
  plus community grid) to satisfy the public avatar presentation contract
2026-09-22 20:59:02 +02:00
openhands 628d24c0c7 feat(home): redesign landing page with cinematic hero and glass panels 2026-09-22 20:28:23 +02:00
openhands 8a66db4ed7 fix(imaging): make avatar and badge images resilient to upstream outages
- Add persistent disk cache for rendered avatars/badges (storage/imaging)
  so repeats never touch the flaky local renderer and cached renders
  survive upstream downtime
- Serve cache-first with stale-on-error; cut primary/fallback timeouts
  from 10s/6s to 4s/4s so failing images cannot stall pages
- Avatar proxy now returns a graceful 200 silhouette instead of 502 when
  no renderer can produce a figure, so no broken-image glyphs appear
- Badge endpoint becomes a caching proxy trying configured CDN, public
  Habbo CDN and local /swf copy in order, and drops the fragile IP rate
  limit that could blank badge streams
- Route all site badge images (profile, me, badges, apply pages) through
  the cached proxy instead of hot-linking images.habbo.com
2026-09-20 12:58:55 +02:00
openhands c3ff497050 feat(referrals): add referral attribution and daily login rewards
- Track referral attribution at registration via ?ref code with
  same-IP and duplicate-pair guards
- Add daily login rewards with streak tracking, claim flow and
  sendCurrency payout backed by RCON with DB fallback
- Add admin pages for referral settings and the daily reward schedule
- Add migration 0033 with tables, seed schedule, settings and ACL grants
- Add admin.referrals.* and admin.dailyrewards.* permission slugs
- Localize new copy in en, nl and it
2026-09-20 12:29:01 +02:00
openhands d6111387e4 feat(auth): align login and register with themed intro, translated social cards 2026-09-18 13:35:22 +02:00
openhands 469f69ddd7 feat(home): two-column hero with live status panel, explore nav, featured news 2026-09-18 13:27:08 +02:00
openhands d0db352f36 feat(home): make index clearer with decluttered hero and live stats panel 2026-09-18 13:06:26 +02:00
openhands d131124515 feat(theme): animate public background with aurora and particles, polish landing pages
Add background_effect (aurora/particles), background_overlay tint and
opacity to the theme manager, rendered site-wide by ThemeVars on every
public page. Polish the home and register pages (hero mascot, live stat
pulse, date pills, photo strip, CTA band, theme-aware register intro,
i18n for home/register section).
2026-09-18 12:44:39 +02:00
openhands 8638e81444 refactor(db): typed query helpers, shared test FormData helper
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
Simo f7b9b55700 fix(news): preserve recoverable reads and verify publication against real services 2026-09-13 19:31:32 +02:00
Simo 8abfe352ef fix(security): authorize site uploads and harden tokens, media and request identity 2026-09-13 19:24:43 +02:00
Simo 1eee6661f9 feat(notifications): add account-scoped inbox and persistent preferences 2026-09-13 17:59:22 +02:00
openhands d2572757cd perf(site): feature card tilt & news card zoom micro-interactions
- Feature cards: group hover zoom & rotate on icon badge, smoother
  card lift on hover (-translate-y-1.5).
- Stats cards: primary accent on numbers, pill badge for labels.
- News cards: smoother 500ms transition with scale-108 image zoom.
2026-09-12 18:16:29 +02:00
openhands fd7b29a5f8 feat(site): pro hero with animated ring and floating stat chips
- 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.
2026-09-12 18:08:09 +02:00
openhands e5e17d75ae perf(site): inline small classic icons and favicon as base64
- 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).
2026-09-12 17:28:01 +02:00
openhands 97012a78f6 feat(site): drifting cloud puffs across the public background
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).
2026-09-12 17:12:55 +02:00
openhands efd3b00075 feat(site): smoother motion across the landing pages
- 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.
2026-09-12 16:51:48 +02:00
openhands bf5b9918d9 feat(site): structured landing sections with eyebrow headings
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.
2026-09-12 16:44:21 +02:00
openhands 3265cf1fad feat(site): consistent card radius and remove duplicate login CTAs
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.
2026-09-12 16:35:28 +02:00
openhands 29b9a4b0dc feat(site): professional card system and tidy landing layout
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.
2026-09-12 16:29:55 +02:00
openhands d755ab7ce1 revert(site): restore classic Habbo landing with soft theme polish
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.
2026-09-12 16:14:21 +02:00
openhands cc58f16230 fix(site): satisfy theme-color and avatar contract tests for landing redesign 2026-09-12 15:51:22 +02:00
openhands 741f01e897 feat(site): redesign home, login and register with premium dark gaming theme
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.
2026-09-12 15:40:54 +02:00
Simo baeb54aeb5 feat(devops): separate public page server timing diagnostics 2026-09-11 10:49:08 +02:00
Simo 445ef13846 fix(public): distinguish unavailable data from empty results and missing pages 2026-09-11 00:39:53 +02:00
Simo 440ce4e556 test(me): use committed translations in dashboard tests 2026-09-11 00:34:21 +02:00
Simo 74223984dc feat(profile): add privacy controls and progressive photo gallery 2026-09-11 00:31:29 +02:00
Simo ba9c61d808 feat(search): search public news events users and open rooms 2026-09-11 00:29:54 +02:00
Simo 506e14783c test(i18n): validate nested housekeeping translation groups 2026-09-11 00:28:26 +02:00
Simo 96d3e3f602 feat(events): add weekly browsing and personal registrations 2026-09-11 00:27:41 +02:00
Simo 5d18af932d feat(news): add searchable paginated publication archive 2026-09-11 00:19:12 +02:00
Simo a537c55793 feat(me): show actionable personal tasks with unavailable states 2026-09-11 00:15:46 +02:00
Simo c389c3893d feat(cms): improve catalog, editorial recovery and operations 2026-09-09 19:36:15 +02:00
Simo 9b0ea2fb16 fix(news): restore publication flow and deduplicate dashboard friends 2026-09-06 18:32:43 +02:00
Simo b5dcadb67b feat(staff): present members in responsive profile cards 2026-09-06 11:53:05 +02:00
Simo 85a6df162b feat(me): organize personal dashboard and localize user actions 2026-09-06 11:30:24 +02:00
Simo 31691c1c7d feat(profile): organize profile sections and use native currency icons 2026-09-06 10:56:20 +02:00
Simo 6146995758 fix(profile): remove public role statistic 2026-09-06 10:44:58 +02:00
openhands 8c12fc6c60 refactor(cms): deduplicate shared admin components and fix studio batch completion
- 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 816e3875c2 feat(admin): add production error center and refresh CMS dependencies 2026-09-05 19:53:09 +02:00
Simo e17346ab9f refactor(cms): separate dashboard data and editor components with focused UX fixes 2026-09-05 18:46:03 +02:00
openhands 5e598a08b4 fix: remove ssr:false from dynamic imports in server components
Turbopack rejects ssr:false in Server Components.
Removed from 4 dynamic imports in:
- src/app/(site)/layout.tsx (RadioPlayerGate)
- src/app/admin/analytics/economy/page.tsx (RevenueChart)
- src/app/admin/analytics/page.tsx (DashboardChart, PeakHoursHeatmap)
- src/app/admin/media/page.tsx (AdminMediaGrid)
2026-09-03 16:04:48 +02:00
openhands 30c95b1a5c feat: comprehensive CMS improvements
- Fix DOMPurify SSR crash (use isomorphic-dompurify)
- Fix SanitizedHtml to sanitize by default
- Add auth guards to studio/catalog maintenance pages
- Add update/edit to vouchers CRUD
- Add update/edit to rare-values CRUD
- Add approve workflow to applications page
- Add edit form to guilds detail page
- Add SEO metadata to all public pages (21 pages)
- Fix mobile nav accessibility (focus trap, aria attributes)
- Fix missing labels and table accessibility
- Add dynamic imports for heavy client components (6 components)
- Fix silent error swallowing (40+ locations)
- Add content scheduling for articles (publishAt, status)
- Wire up 12 missing webhook notification triggers
- Add global search to admin panel
- Add bulk actions to admin users table
- Fix JSON formatting and a11y issues
2026-09-03 16:00:32 +02:00