feat(portal): add SSR operations and exclusive groups
CI / validate (push) Successful in 5m20s
Release / release (push) Successful in 6m56s

This commit is contained in:
dmg
2026-08-01 19:21:23 -04:00
parent b88097c15a
commit b7c0083647
45 changed files with 2245 additions and 363 deletions
@@ -0,0 +1,56 @@
import { events } from "@minecraft-account-manager/database";
import { eq } from "drizzle-orm";
import Link from "next/link";
import { notFound } from "next/navigation";
import { db } from "@/lib/database";
import { eventIpSummary } from "@/lib/event-ip-summary";
export const dynamic = "force-dynamic";
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export default async function EventDetailPage({ params }: { params: Promise<{ eventId: string }> }) {
const { eventId } = await params;
if (!UUID_PATTERN.test(eventId)) notFound();
const [event] = await db.select().from(events).where(eq(events.id, eventId)).limit(1);
if (!event) notFound();
const network = eventIpSummary(event.data);
return (
<main className="mx-auto max-w-5xl px-6 py-12">
<Link className="font-mono text-[10px] font-bold uppercase tracking-widest text-muted underline underline-offset-4" href="/admin/events"> Event explorer</Link>
<header className="mt-7 border-b border-line pb-8">
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">CloudEvent detail</p>
<h1 className="mt-4 break-words font-display text-3xl font-black uppercase sm:text-5xl">{event.type}</h1>
<p className="mt-4 break-all font-mono text-xs text-muted">{event.id}</p>
</header>
<section className="mt-8 border border-line bg-panel p-6 shadow-[7px_7px_0_var(--color-shadow)]">
<h2 className="font-display text-2xl font-black uppercase">Envelope</h2>
<dl className="mt-5 grid gap-x-8 gap-y-5 sm:grid-cols-2">
<Detail label="Time"><time dateTime={event.time.toISOString()}>{event.time.toISOString()}</time></Detail>
<Detail label="Spec version">{event.specVersion}</Detail>
<Detail label="Source">{event.source}</Detail>
<Detail label="Subject">{event.subject ?? "Not provided"}</Detail>
<Detail label="Content type">{event.dataContentType}</Detail>
<Detail label="Data schema">{event.dataSchema ?? "Not provided"}</Detail>
<Detail label="Actor user">{event.actorUserId ? <Link className="underline underline-offset-4" href={`/admin/users/${event.actorUserId}`}>{event.actorUserId}</Link> : "Not provided"}</Detail>
<Detail label="Correlation ID">{event.correlationId ?? "Not provided"}</Detail>
<Detail label="IP address">{event.ipAddress ?? "Not provided"}</Detail>
<Detail label="Network">{network.classification || network.location ? `${network.classification ?? "unknown"} · ${network.location ?? "location unavailable"}` : "Not provided"}</Detail>
<Detail label="Published">{event.publishedAt ? event.publishedAt.toISOString() : "Pending publication"}</Detail>
<Detail label="Recorded">{event.createdAt.toISOString()}</Detail>
</dl>
</section>
<section className="mt-10">
<h2 className="font-display text-2xl font-black uppercase">Event data</h2>
<pre className="mt-4 overflow-x-auto border border-line bg-ink p-5 font-mono text-xs leading-6 text-canvas" tabIndex={0}>{JSON.stringify(event.data, null, 2)}</pre>
</section>
</main>
);
}
function Detail({ label, children }: { label: string; children: React.ReactNode }) {
return <div><dt className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">{label}</dt><dd className="mt-1 break-all text-sm">{children}</dd></div>;
}
@@ -1,34 +1,92 @@
import { events } from "@minecraft-account-manager/database";
import { desc } from "drizzle-orm";
import { desc, inArray } from "drizzle-orm";
import Link from "next/link";
import { db } from "@/lib/database";
import { eventCategory, eventCategoryValues, normalizeEventCategory, normalizeSelectedEventTypes } from "@/lib/event-filters";
import { eventIpSummary } from "@/lib/event-ip-summary";
export default async function EventsPage() {
const recentEvents = await db.select().from(events).orderBy(desc(events.time)).limit(100);
export const dynamic = "force-dynamic";
function values(value: string | string[] | undefined) {
return Array.isArray(value) ? value : value ? [value] : [];
}
export default async function EventsPage({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const query = await searchParams;
const typeRows = await db.select({ type: events.type }).from(events).groupBy(events.type).orderBy(events.type);
const availableTypes = typeRows.map((row) => row.type);
const category = normalizeEventCategory(values(query.category)[0]);
const selectedTypes = normalizeSelectedEventTypes(query.type, availableTypes);
const categoryTypes = category === "all"
? availableTypes
: availableTypes.filter((type) => eventCategory(type) === category);
const filteredTypes = selectedTypes.length
? selectedTypes.filter((type) => categoryTypes.includes(type))
: categoryTypes;
const recentEvents = filteredTypes.length
? await db.select().from(events).where(inArray(events.type, filteredTypes)).orderBy(desc(events.time)).limit(100)
: [];
return (
<main className="mx-auto max-w-6xl px-6 py-14">
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">CloudEvents ledger</p>
<h1 className="mt-4 font-display text-5xl font-black uppercase">Recent events</h1>
<div className="mt-10 overflow-x-auto border border-line bg-panel shadow-[8px_8px_0_var(--color-shadow)]">
<h1 className="mt-4 font-display text-5xl font-black uppercase">Event explorer</h1>
<p className="mt-4 max-w-2xl text-sm leading-6 text-muted">Filter the immutable audit ledger, then open an event to inspect its complete CloudEvents envelope and data.</p>
<form className="mt-8 border border-line bg-panel p-6" method="get">
<div className="grid gap-6 md:grid-cols-[0.45fr_1.55fr]">
<label className="font-mono text-xs font-bold uppercase tracking-wider" htmlFor="event-category">
View
<select className="mt-3 block w-full border border-line bg-canvas px-3 py-3 font-sans text-sm font-normal normal-case" defaultValue={category} id="event-category" name="category">
{eventCategoryValues.map((value) => <option key={value} value={value}>{value === "all" ? "All activity" : value}</option>)}
</select>
</label>
<fieldset>
<legend className="font-mono text-xs font-bold uppercase tracking-wider">Event types</legend>
<details className="mt-3 border border-line bg-canvas p-4" open={selectedTypes.length > 0}>
<summary className="cursor-pointer font-mono text-[10px] font-bold uppercase underline underline-offset-4">{selectedTypes.length ? `${selectedTypes.length} selected` : "All types in this view"}</summary>
<div className="mt-4 grid max-h-64 gap-3 overflow-y-auto sm:grid-cols-2">
{availableTypes.map((type) => (
<label className="flex items-start gap-2 font-mono text-[10px] leading-4" key={type}>
<input className="mt-0.5 size-4 accent-[var(--accent)]" defaultChecked={selectedTypes.includes(type)} name="type" type="checkbox" value={type} />
<span className="break-all">{type}</span>
</label>
))}
</div>
</details>
</fieldset>
</div>
<div className="mt-5 flex flex-wrap gap-4">
<button className="border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas" type="submit">Apply filters</button>
<Link className="self-center font-mono text-[10px] font-bold uppercase underline underline-offset-4" href="/admin/events">Clear filters</Link>
</div>
</form>
<p className="mt-8 font-mono text-[10px] uppercase tracking-widest text-muted" role="status">Showing {recentEvents.length} most recent matching events</p>
<div className="mt-3 overflow-x-auto border border-line bg-panel shadow-[8px_8px_0_var(--color-shadow)]">
<table className="w-full min-w-[760px] border-collapse text-left">
<caption className="sr-only">Filtered account manager events</caption>
<thead className="border-b border-line font-mono text-[10px] uppercase tracking-widest text-muted">
<tr><th className="p-4">Time</th><th className="p-4">Type</th><th className="p-4">Subject</th><th className="p-4">IP</th><th className="p-4">Network</th></tr>
<tr><th className="p-4" scope="col">Time</th><th className="p-4" scope="col">Type</th><th className="p-4" scope="col">Subject</th><th className="p-4" scope="col">IP</th><th className="p-4" scope="col">Network</th></tr>
</thead>
<tbody className="divide-y divide-line text-xs">
{recentEvents.map((event) => {
const ip = eventIpSummary(event.data);
return (
<tr key={event.id}>
<td className="whitespace-nowrap p-4 font-mono text-muted">{event.time.toISOString()}</td>
<td className="p-4 font-mono font-bold">{event.type}</td>
<tr className="hover:bg-canvas/60" key={event.id}>
<td className="whitespace-nowrap p-4 font-mono text-muted"><time dateTime={event.time.toISOString()}>{event.time.toISOString()}</time></td>
<th className="p-4 text-left font-mono font-bold" scope="row"><Link className="underline decoration-line underline-offset-4 hover:decoration-accent" href={`/admin/events/${event.id}`}>{event.type}</Link></th>
<td className="p-4 font-mono text-muted">{event.subject ?? "—"}</td>
<td className="p-4 font-mono text-muted">{event.ipAddress ?? "—"}</td>
<td className="p-4"><div className="font-mono text-[10px] font-bold uppercase">{ip.classification ?? "—"}</div><div className="mt-1 text-xs text-muted">{ip.location ?? "Location unavailable"}</div></td>
</tr>
);
})}
{!recentEvents.length && <tr><td className="p-8 text-muted" colSpan={5}>No events have been recorded.</td></tr>}
{!recentEvents.length && <tr><td className="p-8 text-muted" colSpan={5}>No events match these filters.</td></tr>}
</tbody>
</table>
</div>