page.tsx131 lines · main
| 1 | import { ApiError, apiJson } from '@/lib/api'; |
| 2 | |
| 3 | import { EmptyState } from '../_components/empty-state'; |
| 4 | import { Section } from '../_components/section'; |
| 5 | import { StatCard } from '../_components/stat-card'; |
| 6 | |
| 7 | interface 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 | |
| 15 | export 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). |
| 19 | export const metadata = { |
| 20 | title: 'admin · realtime', |
| 21 | other: { refresh: '10' }, |
| 22 | }; |
| 23 | |
| 24 | function 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 | |
| 30 | export 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 | } |