Fill the remaining gaps: badge upload, radio tools, VPN, writeable boxes

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).
This commit is contained in:
Simo committed 2026-06-28 21:04:34 +02:00
1 parent e004dfedaf
commit 0cd4d06ff6
21 files changed
+2443 -61

No files matched your search

+17
View File
@@ -22,6 +22,15 @@ export default async function HomePage() {
// DB unavailable — show the shell without articles.
}
// Staff-authored content boxes (managed at /admin/writeable-boxes).
const boxes = await prisma.websiteWriteableBoxes
.findMany({
where: { isActive: true },
orderBy: [{ position: "asc" }, { id: "asc" }],
select: { id: true, title: true, icon: true, content: true },
})
.catch(() => []);
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard
@@ -81,6 +90,14 @@ export default async function HomePage() {
</div>
)}
</ContentCard>
{boxes.map((b) => (
<ContentCard key={String(b.id)} icon={b.icon ?? "📌"} title={b.title}>
{/* Content is authored by staff in housekeeping (trusted HTML). */}
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: staff-authored writeable box */}
<div dangerouslySetInnerHTML={{ __html: b.content }} />
</ContentCard>
))}
</main>
);
}