Files
ampelos-dashboard/src/app/(admin)/admin/users/page.tsx
T
Ryan acdc25c797 Ampelos dashboard: the web face, and the owner of the schema
Split out of the single Ampelos repository. Next.js app, Drizzle schema and
migrations, brand art, planning notes.

What left: scripts/, which was the agent's job library misfiled under web/ and
imported nothing from src/; and deploy/truenas, whose broadcast posts to the
scan listener on :3427 -- an agent script -- so it belongs beside the thing it
talks to.

This repository keeps the schema. The agent speaks raw SQL against the same
tables and holds no copy of it, so a rename here can break it silently where it
used to be one commit. The README says so, and the agent carries a snapshot to
check against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:12:08 +02:00

61 lines
2.5 KiB
TypeScript

import { db } from "@/db/client";
import { users, watchingNowItems, watchlistItems } from "@/db/schema";
import { count, desc, isNull } from "drizzle-orm";
export const dynamic = "force-dynamic";
function formatDate(value: Date) {
return value.toISOString().slice(0, 10);
}
export default async function AdminUsersPage() {
const [userRows, watchingCounts, watchlistCounts] = await Promise.all([
db.select().from(users).orderBy(desc(users.createdAt)),
db
.select({ userId: watchingNowItems.userId, activeCount: count() })
.from(watchingNowItems)
.where(isNull(watchingNowItems.removedAt))
.groupBy(watchingNowItems.userId),
db
.select({ userId: watchlistItems.userId, activeCount: count() })
.from(watchlistItems)
.where(isNull(watchlistItems.removedAt))
.groupBy(watchlistItems.userId),
]);
const watchingByUser = new Map(watchingCounts.map((row) => [row.userId, row.activeCount]));
const watchlistByUser = new Map(watchlistCounts.map((row) => [row.userId, row.activeCount]));
return (
<div className="space-y-6">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.32em] text-admin-accent">People</p>
<h1 className="mt-2 font-serif text-3xl font-semibold">Users</h1>
</div>
<section className="admin-panel overflow-hidden">
<div className="grid grid-cols-[1.4fr_1fr_0.6fr_0.6fr_0.7fr] gap-4 border-b border-admin-line px-4 py-3 text-xs font-semibold uppercase tracking-[0.2em] text-admin-muted">
<span>User</span>
<span>Email</span>
<span>Watch Now</span>
<span>Watchlist</span>
<span>Joined</span>
</div>
<div className="divide-y divide-admin-line">
{userRows.length ? userRows.map((user) => (
<div key={user.id} className="grid grid-cols-[1.4fr_1fr_0.6fr_0.6fr_0.7fr] gap-4 px-4 py-4 text-sm">
<div>
<p className="font-medium">{user.displayName}</p>
<p className="text-xs text-admin-muted">{user.isAdmin ? "Admin" : "User"}</p>
</div>
<p className="truncate text-admin-muted">{user.email}</p>
<p>{watchingByUser.get(user.id) ?? 0}/{user.watchingNowTvSlots + user.watchingNowMovieSlots}</p>
<p>{watchlistByUser.get(user.id) ?? 0}</p>
<p className="text-admin-muted">{formatDate(user.createdAt)}</p>
</div>
)) : <p className="p-5 text-sm text-admin-muted">No users found.</p>}
</div>
</section>
</div>
);
}