The admin pages used bare inline-styled tables on the default page
background. Rebuilt the admin shell + added a scoped .admin CSS layer so
the whole panel matches the original AtomCMS Filament look:
- Dark #2d2d44 sidebar (the AtomCMS admin navbar colour) with the nav
grouped into Overview / Content / Users & access / Economy / Radio /
System (mirrors the Filament resource groups), an avatar + rank badge
header, and amber active-link highlighting via a client AdminNavLink
(usePathname).
- A topbar ("Housekeeping" + signed-in user) over a light content area.
- Carded tables (surface bg, rounded, shadow, tinted header, row hover)
and a page-title treatment, applied globally so every admin page is
styled without per-page edits.
Verified in a real authenticated admin session (production server,
amx_test): sidebar renders #2d2d44 sticky, active link amber on dark
text, /admin/users table carded with 7 real rows; dev server serves the
admin rules after a cache refresh. tsc 0, vitest 49/49, next build 0.
The live DB lacked 32 tables the conversion's own CMS features need
(radio_*, game_*/challenges, staff_activities, alert_logs,
email_templates, social_accounts, website_article_comments/reactions).
Generated idempotent CREATE TABLE DDL from `prisma migrate diff`
(CREATE statements ONLY — every ALTER/DROP excluded so no emulator-owned
table is ever touched), as migration 0004. Applied to amx_test via the
existing idempotent runner.
0005: radio_listener_points shipped as a stub (id+timestamps); the
/radio/leaderboard raw query needs user_id + points, so add them (model +
ALTER migration).
Verified against amx_test: drift check now reports 0 missing modeled
tables (live tables 255 -> 287), and a full route sweep logs ZERO
prisma errors (radio/leaderboard + all radio pages 200). tsc 0,
vitest 49/49, next build 0.
Introspected the live DB and fixed real drift between the hand-modeled
schema and the actual columns:
- Added missing @map("snake_case") on camelCase fields that silently
failed at query time: website_teams (rank_name/hidden_rank/...),
website_paypal_transactions/user_guestbooks/help_center_tickets(+replies)/
used_shop_vouchers/maintenance_tasks (user_id), website_rare_values
(currency_type).
- website_permissions was modeled as key/value/comment but is actually
permission/min_rank/description — fixed the model + the admin page/action.
- website_shop_articles.icon -> icon_url (+ added category_id, is_giftable);
updated the shop page + admin shop CRUD.
- website_shop_categories: dropped non-existent slug/timestamps, added the
real description/order columns; updated the shop page.
- website_shop_article_features.features(Json) -> content(Text).
Verified against amx_test: all website_* query errors gone; home renders
the real hotel_name ("Habbo"), rankings shows real users, /staff + /shop
200. tsc 0, vitest 49/49, next build 0. Remaining missing tables
(radio_*/game_*/staff_activities/alert_logs/article_comments+reactions)
don't exist in this DB and degrade gracefully via try/catch.
Verified against the live AtomCMS DB: users.password is varchar(64), so
argon2id (~97 chars) overflows the column and registration/upgrade fail
with 'value too long'. bcrypt (60-char $2y$) fits and matches the
existing accounts. hashPassword() now emits bcrypt by default; set
PASSWORD_HASH=argon2id to opt back in (needs a widened column).
verifyPassword() still accepts both, so existing logins keep working.
Verified end-to-end against the live DB: bcrypt $2y$ login round-trips
(correct=true, wrong=false). tsc 0, vitest 8/8 (password suite).
/news and six admin pages issued raw prisma reads with no try/catch, so
a DB outage rendered a 500 instead of an empty state. Wrap each read
(try/catch or .catch(() => fallback), preserving select() row types and
notFound() on the user-detail page). Matches the fail-soft pattern used
across the rest of the app.
Verified: tsc 0, next build 0, all public pages 200 against a dead DB.
Next 16 deprecated the middleware file convention in favour of proxy.ts
with an exported `proxy` function. Same header-forwarding logic and
matcher; clears the build-time deprecation warning.
Security (launch blockers):
- src/middleware.ts (edge): forwards x-pathname + real client IP.
- access-guard.ts (Node, from root layout): routes non-staff to /maintenance
when maintenance mode is on, banned users to /banned. New /banned + /maintenance
pages (the consumers the admin toggle was missing). Admin layout enforces
force_staff_2fa before /admin.
- staff-activity.ts audit log wired into ban/lift/give-currency/set-rank actions.
Infra (parallel agents): alert service (alert_logs + Discord embed + email),
PayPal top-up (create/capture API routes + /shop/topup), cron worker
(scripts/jobs-worker.ts via croner: emulator-ping->alert, maintenance-check,
bans-cleanup), social connections page, admin radio settings/banners/ranks.
Public radio subsystem: /radio (+schedule, shouts+post, contests, giveaways,
apply, leaderboard) and /apply/staff + /apply/team submission forms. Radio nav
link added. .env.example documents the new optional vars.
(radio song-requests dropped: its table is a stub in AtomCMS — columns added by
un-modeled alter-migrations.)
Verified: tsc exit 0, vitest 48/48, next build exit 0 (82 page routes).
Make the template match the atom theme, not just approximate it:
- Add Tailwind v4 (+forms/typography plugins, postcss) and port the real atom
CSS (global.css + atom app.css) into globals.css: nav-item underline, currency
+ navigation icon classes, site-bg, card-base/hover-lift, text utils — asset
paths adapted to /assets.
- Copy the real theme assets (backgrounds, icons/currency/navigation, profile,
leaderboards) into public/assets.
- Rebuild the shell 1:1 from the atom Blade: TopHeader (currency pills +
user/admin dropdowns, auth-only), SiteHeader (header image + black/50 overlay,
logo+online+Nitro client / guest Login+Create-account CTA), Navigation (white
bar, nav-item + Community/Assistance dropdowns), Footer. ThemeVars injects the
DB-driven CSS custom properties into :root like app.blade.php. Layout uses the
atom body/site-bg + grid-cols-12 max-w-7xl content wrapper.
Verified: tsc 0, next build exit 0; curl confirms the atom markup, compiled CSS
references the assets, and background-light.jpg + currency/navigation icons all
serve 200.
Faithful port of AtomCMS's "atom" theme look (light, Habbo-retro, golden
#eeb425 / amber #f59e0b accents, white cards, 12px radii, Nunito-first stack):
- globals.css: full token set + components (.site-header/.nav-item, .btn-*,
.card, .hero, currency pills, article cards, inputs, tables, footer).
- SiteHeader (brand + nav + currency pills + auth box, staff-aware) + SiteFooter;
layout wraps header/content/footer. Removed the bare SiteNav.
- Restyled home (hero + article cards), login (centered card).
- siteSettings now falls back to DEFAULTS on missing key OR DB error, so pages
render without a DB; DATABASE_CONNECT_TIMEOUT_MS env + pool acquireTimeout make
the no-DB fallback fast.
Verified in a real browser (computed styles): header white/sticky, golden logo
gradient, uppercase nav, amber primary button @12px radius, golden outline,
hero gradient — all correct. tsc exit 0, vitest 48/48, next build exit 0.
/admin/settings: list all website_settings, inline value edit, add/overwrite,
delete; staff-gated server actions that bust the siteSettings cache after each
write. Admin nav extended (Settings).
Verified: tsc exit 0, vitest 48/48, next build exit 0.
Faithful to AtomCMS's NitroController: requires a session (redirects to /login),
issues + persists the SSO ticket via issueSsoTicket (auth_ticket + ip_current
from x-forwarded-for), and embeds the configured client URL with ?sso=. Ties
auth + SSO + settings together.
Verified: tsc exit 0, next build exit 0 (/client route).
Real App Router pages reading the converted Prisma models:
- home: latest 4 articles (websiteArticles) + hotel_name from settings
- /news + /news/[slug]: article index and detail
- /u/[username]: profile (avatar via imager helper, motto, rank, credits, online)
- shared SiteNav (reads session via auth() + hotel_name), globals.css
- src/lib/format.ts: avatarImageUrl + excerpt helpers (unit-tested)
Verified: tsc exit 0, vitest 43/43, next build exit 0 (7 routes). Pages render
against a live DB (deferred until DATABASE_URL is provided). i18n to be layered
on next.
Minimal but real App Router app that builds (next build exit 0):
- src/lib/auth.ts: NextAuth v5 Credentials provider calling checkLogin()
(argon2id/bcrypt + md5->argon2id upgrade gated by CONVERT_PASSWORDS), JWT
session, /api/auth/[...nextauth] route handler.
- src/app: root layout, home (force-dynamic, reads hotel_name via siteSettings),
/login client form (signIn).
- next.config.ts: pinned turbopack.root, serverExternalPackages for the Prisma
MariaDB adapter; tsconfig set up for Next.
Routes: / (dynamic), /login, /api/auth. Verified: next build exit 0, 28 tests.
Still needs DB+APP_KEY to run auth end-to-end. i18n/middleware/pages to follow.
Pure, unit-tested primitives the AtomCMS->Next.js login must reproduce exactly
(verified now with round-trip + known vectors; full end-to-end check deferred
until a real DB + APP_KEY + live emulator are available):
- password.ts: argon2id (m=65536,t=4,p=1 via hash-wasm) + bcrypt ($2y$ accepted)
verify, and the md5->argon2id on-login upgrade gated by convert_passwords
(mirrors RedirectIfTwoFactorAuthenticatable).
- sso-ticket.ts: '{hotel_name without spaces}-{uuidv4}' written to auth_ticket +
ip_current (mirrors User::ssoTicket()).
- laravel-encrypter.ts: AES-256-CBC + HMAC-SHA256 payload compatible with
Laravel encrypt()/encryptString (for existing 2FA secrets) incl. PHP string
(de)serialization.
- totp.ts: otplib Google2FA-compatible TOTP verify (SHA1/6/30).
Libs: hash-wasm + bcryptjs + otplib (pure JS/WASM, no native build). 28 tests.
Models every Arcturus-owned table in default.sql (catalog, items, rooms,
guilds, pets, messenger, navigator, support, users_*, etc.) as flat Prisma
models — enum columns as String, @@id/@@unique/@@ignore per the real keys,
@@map to the exact table names. Assembled from a parallel modeling pass and
normalized (id de-dup, @db.Enum/Double/Decimal fixes).
123 models total (4 hand-authored + 119 generated). Verified: prisma validate
clean, prisma generate OK, tsc exit 0, vitest 6/6.