page.tsx131 lines · main
1import { ApiError, apiJson } from '@/lib/api';
2
3import { EmptyState } from '../_components/empty-state';
4import { Section } from '../_components/section';
5import { StatCard } from '../_components/stat-card';
6
7interface RealtimeStats {
8 totalSubscriptions: number;
9 totalChannels: number;
10 limits: { perWs: number; perProject: number };
11 byProject: { projectId: string; subscriptions: number }[];
12 byChannel: { channel: string; subscriptions: number }[];
13}
14
15export const dynamic = 'force-dynamic';
16// Auto-refresh every 10s — operators keep this page open while watching a
17// noisy project hit its cap; meta refresh avoids a client polling component
18// and survives the next.js cache (force-dynamic re-fetches each refresh).
19export const metadata = {
20 title: 'admin · realtime',
21 other: { refresh: '10' },
22};
23
24function severityClass(pct: number): string {
25 if (pct >= 0.9) return 'inline-flex rounded-md bg-red-500/15 px-2 py-0.5 text-red-300';
26 if (pct >= 0.6) return 'inline-flex rounded-md bg-yellow-500/15 px-2 py-0.5 text-yellow-300';
27 return 'inline-flex rounded-md bg-[var(--color-surface)] px-2 py-0.5 text-[var(--color-text-subtle)]';
28}
29
30export default async function AdminRealtimePage() {
31 let stats: RealtimeStats | null = null;
32 let errorMsg: string | null = null;
33 try {
34 stats = await apiJson<RealtimeStats>('/v1/admin/realtime');
35 } catch (err) {
36 if (err instanceof ApiError && err.status === 503) {
37 errorMsg = 'realtime service is not configured or unreachable.';
38 } else {
39 throw err;
40 }
41 }
42
43 return (
44 <div className="flex flex-col gap-8">
45 <div>
46 <h2 className="font-mono text-lg">realtime · live snapshot</h2>
47 <p className="mt-1 max-w-2xl font-mono text-xs leading-relaxed text-[var(--color-text-muted)]">
48 per-project subscription counts pulled live from the realtime service — refreshes
49 every 10s. numbers reset on a realtime restart; durable usage lives in admin · usage.
50 </p>
51 </div>
52
53 {errorMsg ? (
54 <EmptyState title="realtime unreachable" message={errorMsg} />
55 ) : stats ? (
56 <>
57 <div className="grid grid-cols-2 gap-6 xl:grid-cols-4">
58 <StatCard label="total subs" value={stats.totalSubscriptions} />
59 <StatCard label="active channels" value={stats.totalChannels} />
60 <StatCard label="cap · per ws" value={stats.limits.perWs} />
61 <StatCard label="cap · per project" value={stats.limits.perProject} />
62 </div>
63
64 <Section title="top projects by subscription count">
65 {stats.byProject.length === 0 ? (
66 <EmptyState title="no active subscriptions" message="quiet on the wire right now." />
67 ) : (
68 <div className="overflow-x-auto rounded-xl border border-[var(--color-border-subtle)]">
69 <table className="w-full font-mono text-xs">
70 <thead className="bg-[var(--color-surface)] text-left text-[var(--color-text-muted)]">
71 <tr>
72 <th className="px-6 py-3 font-medium">project</th>
73 <th className="px-6 py-3 font-medium">subscriptions</th>
74 <th className="px-6 py-3 font-medium">% of cap</th>
75 </tr>
76 </thead>
77 <tbody>
78 {stats.byProject.map((row) => {
79 const pct = row.subscriptions / stats!.limits.perProject;
80 return (
81 <tr
82 key={row.projectId}
83 className="border-t border-[var(--color-border-subtle)]"
84 >
85 <td className="px-6 py-4 text-[var(--color-text)]">{row.projectId}</td>
86 <td className="px-6 py-4 text-[var(--color-text)]">
87 {row.subscriptions.toLocaleString()}
88 </td>
89 <td className="px-6 py-4">
90 <span className={severityClass(pct)}>{(pct * 100).toFixed(1)}%</span>
91 </td>
92 </tr>
93 );
94 })}
95 </tbody>
96 </table>
97 </div>
98 )}
99 </Section>
100
101 <Section title="top channels by subscriber count">
102 {stats.byChannel.length === 0 ? (
103 <EmptyState title="no active listen channels" message="nothing is subscribed yet." />
104 ) : (
105 <div className="overflow-x-auto rounded-xl border border-[var(--color-border-subtle)]">
106 <table className="w-full font-mono text-xs">
107 <thead className="bg-[var(--color-surface)] text-left text-[var(--color-text-muted)]">
108 <tr>
109 <th className="px-6 py-3 font-medium">channel</th>
110 <th className="px-6 py-3 font-medium">subscribers</th>
111 </tr>
112 </thead>
113 <tbody>
114 {stats.byChannel.map((row) => (
115 <tr key={row.channel} className="border-t border-[var(--color-border-subtle)]">
116 <td className="px-6 py-4 text-[var(--color-text)]">{row.channel}</td>
117 <td className="px-6 py-4 text-[var(--color-text)]">
118 {row.subscriptions.toLocaleString()}
119 </td>
120 </tr>
121 ))}
122 </tbody>
123 </table>
124 </div>
125 )}
126 </Section>
127 </>
128 ) : null}
129 </div>
130 );
131}