Add remaining public + admin pages (parallel build, 26 files)

Built via two parallel agent workflows reading the real Prisma schema, then
integrated + verified.

Public: /shop (categories + storefront), /community hub, /rankings (top by
credits), /staff, /photos (camera_web gallery), /help (categories + rules),
/help/tickets (auth-gated user tickets + create), /settings (auth-gated, update
motto via RCON).

Admin: /admin/catalog (+[id] items), /admin/radio (shouts/apps/schedules +
delete shout), /admin/teams (CRUD), /admin/permissions (read), /admin/wordfilter
(CRUD + RCON push), /admin/ip (whitelist/blacklist CRUD), /admin/applications
(list + dismiss), /admin/logs (chat/command/alert read), /admin/achievements
(read). Server actions all staff-gated.

Header + admin nav extended. Verified: tsc exit 0, vitest 48/48, next build
exit 0 (33 routes); curl-probed every route — public 200/<main>, auth+admin
correctly 307-redirect when unauthorized.
This commit is contained in:
Simo committed 2026-06-28 13:24:55 +02:00
1 parent 9a79acebe7
commit e96b606e1e
27 files changed
+2400

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
// NOTE: The emulator's standalone `permissions` table is not present in the
// Prisma schema (there is no `prisma.permissions` accessor). The real,
// queryable rank-permission table in this schema is `website_housekeeping_permissions`
// (model WebsiteHousekeepingPermissions), which gates each permission by the
// minimum rank that holds it — exactly the rank-permission data this page lists.
type PermissionRow = {
id: bigint;
permission: string;
minRank: number;
description: string | null;
};
export default async function AdminPermissions() {
let permissions: PermissionRow[] = [];
try {
permissions = await prisma.websiteHousekeepingPermissions.findMany({
select: { id: true, permission: true, minRank: true, description: true },
orderBy: [{ minRank: "asc" }, { permission: "asc" }],
});
} catch {
permissions = [];
}
return (
<main>
<h1>Permissions</h1>
<p className="muted">
Read-only view of rank permissions. Each entry is the minimum rank that
grants the permission.
</p>
{permissions.length === 0 ? (
<p className="muted">No permissions found.</p>
) : (
<table>
<thead>
<tr>
<th>ID</th>
<th>Permission</th>
<th>Min rank</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{permissions.map((p) => (
<tr key={String(p.id)}>
<td>{String(p.id)}</td>
<td>{p.permission}</td>
<td>{p.minRank}</td>
<td className="muted">{p.description ?? "—"}</td>
</tr>
))}
</tbody>
</table>
)}
</main>
);
}