Built the admin tools previously listed as missing:
- Badge upload (/admin/badges): uploads a <code>.gif into the emulator's
badge dir via BADGE_UPLOAD_DIR (node:fs); validated code/type/size,
logged. Made configurable rather than skipped.
- Radio tools: /admin/radio/api-keys (CRUD, server-generated keys),
/admin/radio/autodj (Auto-DJ playlist CRUD), /admin/radio/embed (embed
snippet generator), /admin/radio/points (points settings),
/admin/radio/monitoring (live stream/now-playing/listeners status).
radio_api_keys + radio_auto_dj_playlist already had real columns.
- /admin/vpn: VPN/proxy detection config (block toggle + provider + key),
complementing /admin/ip's raw blacklist.
- Writeable boxes: new website_writeable_boxes table (model + migration
0006) + /admin/writeable-boxes CRUD; active boxes render on the public
home page. env: BADGE_UPLOAD_DIR.
Verified live (prod, amx_test): all 8 pages render with real data; a test
writeable box appeared on the public home and was reverted. tsc 0,
vitest 49/49, next build 0 (7 new admin routes).
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.