fix(frontend): finish the token migration — raw grey classes -> theme tokens

UI_REDESIGN chunk 4 was never finished: 39 app/component files still used
raw Tailwind greys, kept readable in light mode only by the !important
override sheet. In dark mode text-gray-600 is ~2.3:1 on surface, and the
light override maps it to #94a3b8 (~2.6:1 on white) — unreadable in both.

Mechanical mapping (886 classes): gray-100/200 -> ink, 300/400 -> ink-2,
500/600/700 -> ink-muted, bg 950/900/800 -> page/surface/raised,
border 800 -> line, 700/600 -> line-strong, plus hover/divide/placeholder.
Alpha suffixes kept (tokens are color-mix, so /60 works). bg-gray-700,
text-white and marketing pages left alone. text-[9px] -> text-[10px].

Frontend unverified: no Node on the authoring box (#207).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-25 21:29:13 -04:00
co-authored by Claude Opus 5.5
parent 3968b49b80
commit bf206334e3
39 changed files with 655 additions and 655 deletions
+109 -109
View File
@@ -85,7 +85,7 @@ function fmtDatetime(iso: string | null | undefined) {
const ROLE_COLORS: Record<UserRole, string> = {
admin: "bg-indigo-900 text-indigo-300",
operator: "bg-green-900 text-green-300",
viewer: "bg-gray-800 text-gray-400",
viewer: "bg-raised text-ink-2",
};
function RoleBadge({ role }: { role: UserRole }) {
@@ -136,14 +136,14 @@ function FeaturesTab() {
</div>
)}
{loading ? (
<p className="text-gray-500 text-sm font-mono">Loading…</p>
<p className="text-ink-muted text-sm font-mono">Loading…</p>
) : (
<div className="bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800">
<div className="bg-surface border border-line rounded-xl divide-y divide-line">
{FLAG_META.map(({ key, label, description }) => (
<div key={key} className="flex items-center justify-between gap-4 px-5 py-4">
<div className="min-w-0">
<p className="text-white text-sm font-semibold">{label}</p>
<p className="text-gray-500 text-xs mt-0.5 leading-snug">{description}</p>
<p className="text-ink-muted text-xs mt-0.5 leading-snug">{description}</p>
</div>
<Toggle
enabled={flags?.[key] ?? true}
@@ -238,30 +238,30 @@ function CorrelationDebugTab() {
return (
<div className="space-y-4">
<p className="text-xs text-gray-500 font-mono">
<p className="text-xs text-ink-muted font-mono">
Fetches recent incidents with per-call correlation debug fields, plus orphaned calls.
Copy the JSON and paste it to Claude to diagnose correlation problems.
</p>
<div className="flex flex-wrap items-end gap-4">
<div>
<label className="text-xs text-gray-400 block mb-1">Incidents (limit)</label>
<label className="text-xs text-ink-2 block mb-1">Incidents (limit)</label>
<input
type="number"
min={1} max={100}
value={limit}
onChange={(e) => setLimit(Math.min(100, Math.max(1, Number(e.target.value))))}
className="w-24 bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
className="w-24 bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
/>
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">Orphan scan window (hours)</label>
<label className="text-xs text-ink-2 block mb-1">Orphan scan window (hours)</label>
<input
type="number"
min={1} max={168}
value={orphanHours}
onChange={(e) => setOrphanHours(Math.min(168, Math.max(1, Number(e.target.value))))}
className="w-28 bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
className="w-28 bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
/>
</div>
<button
@@ -281,12 +281,12 @@ function CorrelationDebugTab() {
{meta && (
<div className="flex items-center justify-between">
<p className="text-xs text-gray-500 font-mono">
<p className="text-xs text-ink-muted font-mono">
{meta.incident_count} incidents · {meta.orphaned_call_count} orphaned calls · {meta.generated_at}
</p>
<button
onClick={handleCopy}
className="text-xs font-mono px-3 py-1.5 rounded-lg border border-gray-700 bg-gray-900 text-gray-300 hover:text-white transition-colors"
className="text-xs font-mono px-3 py-1.5 rounded-lg border border-line-strong bg-surface text-ink-2 hover:text-white transition-colors"
>
{copied ? "Copied!" : "Copy JSON"}
</button>
@@ -295,22 +295,22 @@ function CorrelationDebugTab() {
{meta && Object.keys(pathCounts).length > 0 && (
<div className="grid grid-cols-2 gap-3">
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-1">
<p className="text-xs text-gray-400 font-mono font-semibold mb-2">corr_path distribution</p>
<div className="bg-surface border border-line rounded-xl p-4 space-y-1">
<p className="text-xs text-ink-2 font-mono font-semibold mb-2">corr_path distribution</p>
{Object.entries(pathCounts).sort((a, b) => b[1] - a[1]).map(([path, n]) => (
<div key={path} className="flex justify-between text-xs font-mono">
<span className="text-gray-300">{path}</span>
<span className="text-ink-2">{path}</span>
<span className="text-indigo-400">{n}</span>
</div>
))}
</div>
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-1">
<p className="text-xs text-gray-400 font-mono font-semibold mb-2">fit_signal distribution</p>
<div className="bg-surface border border-line rounded-xl p-4 space-y-1">
<p className="text-xs text-ink-2 font-mono font-semibold mb-2">fit_signal distribution</p>
{Object.keys(signalCounts).length === 0
? <p className="text-xs text-gray-600 font-mono">No signal data yet — deploy correlator update first</p>
? <p className="text-xs text-ink-muted font-mono">No signal data yet — deploy correlator update first</p>
: Object.entries(signalCounts).sort((a, b) => b[1] - a[1]).map(([sig, n]) => (
<div key={sig} className="flex justify-between text-xs font-mono">
<span className={sig === "time_fallback" ? "text-yellow-400" : "text-gray-300"}>{sig}</span>
<span className={sig === "time_fallback" ? "text-yellow-400" : "text-ink-2"}>{sig}</span>
<span className="text-indigo-400">{n}</span>
</div>
))
@@ -320,13 +320,13 @@ function CorrelationDebugTab() {
)}
{meta?.orphans_by_talkgroup && meta.orphans_by_talkgroup.length > 0 && (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-1">
<p className="text-xs text-gray-400 font-mono font-semibold mb-2">orphans by talkgroup</p>
<div className="bg-surface border border-line rounded-xl p-4 space-y-1">
<p className="text-xs text-ink-2 font-mono font-semibold mb-2">orphans by talkgroup</p>
{meta.orphans_by_talkgroup.map((tg) => (
<div key={String(tg.talkgroup_id)} className="flex justify-between text-xs font-mono">
<span className="text-gray-300">{tg.talkgroup_name} <span className="text-gray-600">({tg.talkgroup_id})</span></span>
<span className="text-gray-400">
{tg.count} total · <span className="text-gray-500">{tg.no_type_count} no-type</span> · <span className="text-red-400">{tg.sweep_exhausted_count} exhausted</span>
<span className="text-ink-2">{tg.talkgroup_name} <span className="text-ink-muted">({tg.talkgroup_id})</span></span>
<span className="text-ink-2">
{tg.count} total · <span className="text-ink-muted">{tg.no_type_count} no-type</span> · <span className="text-red-400">{tg.sweep_exhausted_count} exhausted</span>
</span>
</div>
))}
@@ -336,7 +336,7 @@ function CorrelationDebugTab() {
{json && (
<pre
ref={preRef}
className="bg-gray-950 border border-gray-800 rounded-xl p-4 text-xs text-gray-300 font-mono overflow-auto max-h-[60vh] whitespace-pre"
className="bg-page border border-line rounded-xl p-4 text-xs text-ink-2 font-mono overflow-auto max-h-[60vh] whitespace-pre"
>
{json}
</pre>
@@ -455,13 +455,13 @@ function UserDetailPanel({
const isSelf = user.uid === currentUid;
return (
<div className="bg-gray-900 border border-gray-700 rounded-xl p-5 space-y-5 font-mono">
<div className="bg-surface border border-line-strong rounded-xl p-5 space-y-5 font-mono">
<div className="flex items-start justify-between">
<div>
<p className="text-white font-semibold">{detail.email}</p>
<p className="text-xs text-gray-500 mt-0.5">{detail.uid}</p>
<p className="text-xs text-ink-muted mt-0.5">{detail.uid}</p>
</div>
<button onClick={onClose} className="text-gray-600 hover:text-gray-300 transition-colors text-xl leading-none">×</button>
<button onClick={onClose} className="text-ink-muted hover:text-ink transition-colors text-xl leading-none">×</button>
</div>
{error && (
@@ -472,21 +472,21 @@ function UserDetailPanel({
<div className="space-y-3">
<div>
<label className="text-xs text-gray-400 block mb-1">Display Name</label>
<label className="text-xs text-ink-2 block mb-1">Display Name</label>
<input
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-indigo-500"
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-indigo-500"
placeholder="Full name"
/>
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">Role</label>
<label className="text-xs text-ink-2 block mb-1">Role</label>
<select
value={editRole}
onChange={(e) => setEditRole(e.target.value as UserRole)}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-indigo-500"
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-indigo-500"
>
<option value="admin">Admin — full access</option>
<option value="operator">Operator — owns nodes</option>
@@ -496,13 +496,13 @@ function UserDetailPanel({
{editRole === "operator" && (
<div>
<label className="text-xs text-gray-400 block mb-1">
Owned Node IDs <span className="text-gray-600">(comma-separated, required)</span>
<label className="text-xs text-ink-2 block mb-1">
Owned Node IDs <span className="text-ink-muted">(comma-separated, required)</span>
</label>
<input
value={editNodes}
onChange={(e) => setEditNodes(e.target.value)}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
placeholder="node-abc123, node-def456"
/>
</div>
@@ -517,10 +517,10 @@ function UserDetailPanel({
</button>
</div>
<div className="border-t border-gray-800 pt-4 space-y-2 text-xs">
<div className="border-t border-line pt-4 space-y-2 text-xs">
<div className="flex justify-between items-center gap-3">
<span className="text-gray-500">Organization</span>
<span className={`font-mono truncate ${detail.org_id && detail.org_id === myOrgId ? "text-gray-300" : "text-yellow-400"}`}>
<span className="text-ink-muted">Organization</span>
<span className={`font-mono truncate ${detail.org_id && detail.org_id === myOrgId ? "text-ink-2" : "text-yellow-400"}`}>
{!detail.org_id
? "None: sees no data"
: detail.org_id === myOrgId
@@ -538,34 +538,34 @@ function UserDetailPanel({
</button>
)}
<div className="flex justify-between">
<span className="text-gray-500">Status</span>
<span className="text-ink-muted">Status</span>
<span className={detail.disabled ? "text-red-400" : "text-green-400"}>
{detail.disabled ? "Disabled" : "Active"}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Discord</span>
<span className="text-gray-300">
<span className="text-ink-muted">Discord</span>
<span className="text-ink-2">
{detail.discord_linked
? `@${detail.discord_username ?? detail.discord_user_id}`
: "Not linked"}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Created</span>
<span className="text-gray-300">{fmtDate(detail.creation_time)}</span>
<span className="text-ink-muted">Created</span>
<span className="text-ink-2">{fmtDate(detail.creation_time)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Last sign-in</span>
<span className="text-gray-300">{fmtDate(detail.last_sign_in)}</span>
<span className="text-ink-muted">Last sign-in</span>
<span className="text-ink-2">{fmtDate(detail.last_sign_in)}</span>
</div>
</div>
{(detail.sessions?.length ?? 0) > 0 && (
<div className="border-t border-gray-800 pt-4">
<div className="border-t border-line pt-4">
<button
onClick={() => setShowSessions((v) => !v)}
className="text-xs text-gray-500 hover:text-gray-300 transition-colors flex items-center gap-1"
className="text-xs text-ink-muted hover:text-ink transition-colors flex items-center gap-1"
>
<span>{showSessions ? "▲" : "▼"}</span>
<span>Login history ({detail.sessions?.length} recent)</span>
@@ -573,9 +573,9 @@ function UserDetailPanel({
{showSessions && (
<div className="mt-3 space-y-1.5 max-h-48 overflow-y-auto">
{detail.sessions?.map((s) => (
<div key={s.session_id} className="text-xs text-gray-400 flex justify-between gap-4">
<div key={s.session_id} className="text-xs text-ink-2 flex justify-between gap-4">
<span>{fmtDatetime(s.timestamp)}</span>
<span className="text-gray-600 truncate">{s.ip ?? "—"}</span>
<span className="text-ink-muted truncate">{s.ip ?? "—"}</span>
</div>
))}
</div>
@@ -583,7 +583,7 @@ function UserDetailPanel({
</div>
)}
<div className="border-t border-gray-800 pt-4 flex gap-4 flex-wrap">
<div className="border-t border-line pt-4 flex gap-4 flex-wrap">
{!isSelf ? (
<>
<button
@@ -602,7 +602,7 @@ function UserDetailPanel({
</button>
</>
) : (
<p className="text-xs text-gray-600">Cannot disable or delete your own account.</p>
<p className="text-xs text-ink-muted">Cannot disable or delete your own account.</p>
)}
</div>
</div>
@@ -667,19 +667,19 @@ function CreateUserModal({
if (inviteLink) {
return (
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
<div className="bg-gray-900 border border-gray-700 rounded-xl w-full max-w-md p-6 space-y-4 font-mono">
<div className="bg-surface border border-line-strong rounded-xl w-full max-w-md p-6 space-y-4 font-mono">
<h2 className="text-white font-semibold">User Created</h2>
<p className="text-xs text-gray-400">
<p className="text-xs text-ink-2">
Share this one-time invite link with the new user so they can set their password.
It expires after use.
</p>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-3">
<div className="bg-raised border border-line-strong rounded-lg p-3">
<p className="text-xs text-indigo-300 break-all">{inviteLink}</p>
</div>
<div className="flex gap-3">
<button
onClick={copyLink}
className="flex-1 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg py-2 text-sm transition-colors"
className="flex-1 bg-raised hover:bg-gray-700 text-ink-2 rounded-lg py-2 text-sm transition-colors"
>
{copied ? "Copied!" : "Copy link"}
</button>
@@ -697,37 +697,37 @@ function CreateUserModal({
return (
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
<div className="bg-gray-900 border border-gray-700 rounded-xl w-full max-w-md p-6 font-mono">
<div className="bg-surface border border-line-strong rounded-xl w-full max-w-md p-6 font-mono">
<h2 className="text-white font-semibold mb-4">Create User</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="text-xs text-gray-400 block mb-1">Email</label>
<label className="text-xs text-ink-2 block mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
placeholder="user@example.com"
/>
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">
Display Name <span className="text-gray-600">(optional)</span>
<label className="text-xs text-ink-2 block mb-1">
Display Name <span className="text-ink-muted">(optional)</span>
</label>
<input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
placeholder="Jane Smith"
/>
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">Role</label>
<label className="text-xs text-ink-2 block mb-1">Role</label>
<select
value={role}
onChange={(e) => setRole(e.target.value as UserRole)}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
>
<option value="admin">Admin — full access</option>
<option value="operator">Operator — owns nodes</option>
@@ -736,14 +736,14 @@ function CreateUserModal({
</div>
{role === "operator" && (
<div>
<label className="text-xs text-gray-400 block mb-1">
Owned Node IDs <span className="text-gray-600">(comma-separated, required)</span>
<label className="text-xs text-ink-2 block mb-1">
Owned Node IDs <span className="text-ink-muted">(comma-separated, required)</span>
</label>
<input
value={nodeIds}
onChange={(e) => setNodeIds(e.target.value)}
required
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
placeholder="node-abc123, node-def456"
/>
</div>
@@ -760,7 +760,7 @@ function CreateUserModal({
<button
type="button"
onClick={onClose}
className="flex-1 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg py-2 text-sm transition-colors"
className="flex-1 bg-raised hover:bg-gray-700 text-ink-2 rounded-lg py-2 text-sm transition-colors"
>
Cancel
</button>
@@ -820,7 +820,7 @@ function UsersTab({ currentUid }: { currentUid: string }) {
)}
<div className="flex items-center justify-between">
<p className="text-xs text-gray-500 font-mono">{users.length} user{users.length !== 1 ? "s" : ""}</p>
<p className="text-xs text-ink-muted font-mono">{users.length} user{users.length !== 1 ? "s" : ""}</p>
<button
onClick={() => setShowCreate(true)}
className="bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors"
@@ -836,14 +836,14 @@ function UsersTab({ currentUid }: { currentUid: string }) {
)}
{loading ? (
<p className="text-gray-500 text-sm font-mono">Loading…</p>
<p className="text-ink-muted text-sm font-mono">Loading…</p>
) : users.length === 0 ? (
<p className="text-gray-600 text-sm font-mono">No users found.</p>
<p className="text-ink-muted text-sm font-mono">No users found.</p>
) : (
<div className="border border-gray-800 rounded-xl overflow-hidden">
<div className="border border-line rounded-xl overflow-hidden">
<table className="w-full text-xs font-mono">
<thead>
<tr className="bg-gray-900 text-gray-500 uppercase tracking-wider">
<tr className="bg-surface text-ink-muted uppercase tracking-wider">
<th className="px-4 py-2.5 text-left">Email</th>
<th className="px-4 py-2.5 text-left hidden lg:table-cell">Name</th>
<th className="px-4 py-2.5 text-left">Role</th>
@@ -857,17 +857,17 @@ function UsersTab({ currentUid }: { currentUid: string }) {
{users.map((u) => (
<tr
key={u.uid}
className={`border-t border-gray-800 transition-colors ${
selectedUid === u.uid ? "bg-gray-800/60" : "hover:bg-gray-900/60"
className={`border-t border-line transition-colors ${
selectedUid === u.uid ? "bg-raised/60" : "hover:bg-raised/60"
}`}
>
<td className="px-4 py-2.5 text-gray-200">{u.email ?? "—"}</td>
<td className="px-4 py-2.5 text-gray-400 hidden lg:table-cell">{u.display_name ?? "—"}</td>
<td className="px-4 py-2.5 text-ink">{u.email ?? "—"}</td>
<td className="px-4 py-2.5 text-ink-2 hidden lg:table-cell">{u.display_name ?? "—"}</td>
<td className="px-4 py-2.5"><RoleBadge role={u.role} /></td>
<td className="px-4 py-2.5 text-gray-500 hidden sm:table-cell">
<td className="px-4 py-2.5 text-ink-muted hidden sm:table-cell">
{u.discord_linked ? `@${u.discord_username ?? "linked"}` : "—"}
</td>
<td className="px-4 py-2.5 text-gray-500 hidden md:table-cell">{fmtDate(u.last_sign_in)}</td>
<td className="px-4 py-2.5 text-ink-muted hidden md:table-cell">{fmtDate(u.last_sign_in)}</td>
<td className="px-4 py-2.5">
{u.disabled
? <span className="text-red-500">Disabled</span>
@@ -952,15 +952,15 @@ function AuditLogTab() {
)}
{loading ? (
<p className="text-gray-500 text-sm font-mono">Loading…</p>
<p className="text-ink-muted text-sm font-mono">Loading…</p>
) : entries.length === 0 ? (
<p className="text-gray-600 text-sm font-mono">No audit entries yet.</p>
<p className="text-ink-muted text-sm font-mono">No audit entries yet.</p>
) : (
<>
<div className="border border-gray-800 rounded-xl overflow-hidden">
<div className="border border-line rounded-xl overflow-hidden">
<table className="w-full text-xs font-mono">
<thead>
<tr className="bg-gray-900 text-gray-500 uppercase tracking-wider">
<tr className="bg-surface text-ink-muted uppercase tracking-wider">
<th className="px-4 py-2.5 text-left">Time</th>
<th className="px-4 py-2.5 text-left">Action</th>
<th className="px-4 py-2.5 text-left hidden sm:table-cell">Actor</th>
@@ -970,12 +970,12 @@ function AuditLogTab() {
</thead>
<tbody>
{entries.map((e) => (
<tr key={e.log_id} className="border-t border-gray-800 hover:bg-gray-900/40">
<td className="px-4 py-2.5 text-gray-500 whitespace-nowrap">{fmtDatetime(e.timestamp)}</td>
<tr key={e.log_id} className="border-t border-line hover:bg-raised/40">
<td className="px-4 py-2.5 text-ink-muted whitespace-nowrap">{fmtDatetime(e.timestamp)}</td>
<td className={`px-4 py-2.5 whitespace-nowrap ${actionColor(e.action)}`}>{e.action}</td>
<td className="px-4 py-2.5 text-gray-400 hidden sm:table-cell">{e.actor_email}</td>
<td className="px-4 py-2.5 text-gray-400 hidden md:table-cell">{e.target_email ?? "—"}</td>
<td className="px-4 py-2.5 text-gray-600 max-w-xs truncate">
<td className="px-4 py-2.5 text-ink-2 hidden sm:table-cell">{e.actor_email}</td>
<td className="px-4 py-2.5 text-ink-2 hidden md:table-cell">{e.target_email ?? "—"}</td>
<td className="px-4 py-2.5 text-ink-muted max-w-xs truncate">
{Object.keys(e.details).length > 0
? Object.entries(e.details)
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
@@ -1029,26 +1029,26 @@ function StaleCallsTab() {
return (
<div className="space-y-5">
<p className="text-xs text-gray-500 font-mono">
Finds calls stuck in <span className="text-gray-300">active</span> status because a node rebooted before sending an end-call event.
<p className="text-xs text-ink-muted font-mono">
Finds calls stuck in <span className="text-ink-2">active</span> status because a node rebooted before sending an end-call event.
Preview first, then close.
</p>
<div className="flex flex-wrap items-end gap-4">
<div>
<label className="text-xs text-gray-400 block mb-1">Older than (minutes)</label>
<label className="text-xs text-ink-2 block mb-1">Older than (minutes)</label>
<input
type="number"
min={1} max={1440}
value={minutes}
onChange={(e) => setMinutes(Math.min(1440, Math.max(1, Number(e.target.value))))}
className="w-28 bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
className="w-28 bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
/>
</div>
<button
onClick={() => run(true)}
disabled={loading}
className="bg-gray-800 hover:bg-gray-700 disabled:opacity-50 border border-gray-700 text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors"
className="bg-raised hover:bg-gray-700 disabled:opacity-50 border border-line-strong text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors"
>
{loading ? "Working…" : "Preview"}
</button>
@@ -1068,18 +1068,18 @@ function StaleCallsTab() {
)}
{result && (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-2">
<div className="bg-surface border border-line rounded-xl p-4 space-y-2">
<p className="text-sm font-mono text-white">
{result.dry_run ? "Preview: " : "Closed: "}
<span className={result.count > 0 ? "text-amber-400" : "text-green-400"}>
{result.count} stale call{result.count !== 1 ? "s" : ""}
</span>
{result.count === 0 && <span className="text-gray-500"> — nothing to clear</span>}
{result.count === 0 && <span className="text-ink-muted"> — nothing to clear</span>}
</p>
{result.call_ids.length > 0 && (
<div className="max-h-40 overflow-y-auto space-y-0.5">
{result.call_ids.map((id) => (
<p key={id} className="text-xs font-mono text-gray-400">{id}</p>
<p key={id} className="text-xs font-mono text-ink-2">{id}</p>
))}
</div>
)}
@@ -1104,17 +1104,17 @@ function fmtPct(x: number | null | undefined): string {
function EvalStatsBar({ stats }: { stats: { eval_count: number; raw_wer: number | null; corrected_wer: number | null } | null }) {
return (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 flex flex-wrap gap-x-8 gap-y-2">
<div className="bg-surface border border-line rounded-xl p-4 flex flex-wrap gap-x-8 gap-y-2">
<div>
<p className="text-xs text-gray-500 font-mono">Calls verified</p>
<p className="text-xs text-ink-muted font-mono">Calls verified</p>
<p className="text-white text-lg font-mono">{stats?.eval_count ?? "—"}</p>
</div>
<div>
<p className="text-xs text-gray-500 font-mono">Raw WER (whisper-1)</p>
<p className="text-xs text-ink-muted font-mono">Raw WER (whisper-1)</p>
<p className="text-white text-lg font-mono">{fmtPct(stats?.raw_wer)}</p>
</div>
<div>
<p className="text-xs text-gray-500 font-mono">Corrected WER (shipped)</p>
<p className="text-xs text-ink-muted font-mono">Corrected WER (shipped)</p>
<p className="text-white text-lg font-mono">{fmtPct(stats?.corrected_wer)}</p>
</div>
{stats && stats.eval_count > 0 && stats.eval_count < 20 && (
@@ -1197,7 +1197,7 @@ function SttEvalTab() {
return (
<div className="space-y-4">
<p className="text-xs text-gray-500 font-mono">
<p className="text-xs text-ink-muted font-mono">
Listen to the audio, correct the transcript below until it matches what was actually said, then save.
This never touches the call&apos;s real transcript or re-runs anything — it only records ground truth
for measuring the pipeline. Do as many or as few as you have time for; it picks up where you left off.
@@ -1212,8 +1212,8 @@ function SttEvalTab() {
)}
{current ? (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-3">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-mono text-gray-400">
<div className="bg-surface border border-line rounded-xl p-4 space-y-3">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-mono text-ink-2">
<span>{new Date(current.started_at).toLocaleString()}</span>
<span>{current.talkgroup_name || (current.talkgroup_id ? `TGID ${current.talkgroup_id}` : "unknown talkgroup")}</span>
</div>
@@ -1222,18 +1222,18 @@ function SttEvalTab() {
// eslint-disable-next-line jsx-a11y/media-has-caption
<audio controls src={current.audio_url} className="w-full h-9" />
) : (
<p className="text-xs text-gray-500 italic">No audio on this call — skip it.</p>
<p className="text-xs text-ink-muted italic">No audio on this call — skip it.</p>
)}
<div>
<label className="text-xs text-gray-400 block mb-1">
<label className="text-xs text-ink-2 block mb-1">
Machine transcript (pre-filled) — correct it into what was actually said
</label>
<textarea
value={draft}
onChange={(e) => setDraft(e.target.value)}
rows={4}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-indigo-500"
/>
</div>
@@ -1248,15 +1248,15 @@ function SttEvalTab() {
<button
onClick={skip}
disabled={saving}
className="bg-gray-800 hover:bg-gray-700 disabled:opacity-50 border border-gray-700 text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors"
className="bg-raised hover:bg-gray-700 disabled:opacity-50 border border-line-strong text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors"
>
Skip
</button>
</div>
</div>
) : (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
<p className="text-sm font-mono text-gray-400">
<div className="bg-surface border border-line rounded-xl p-4">
<p className="text-sm font-mono text-ink-2">
{loadingBatch ? "Loading calls…" : exhausted ? "Nothing left to verify right now — check back after more calls come in." : "Loading…"}
</p>
</div>
@@ -1312,13 +1312,13 @@ export default function AdminPage() {
<div className={`space-y-6 ${wide ? "" : "max-w-2xl"}`}>
<h1 className="text-white text-xl font-bold font-mono">Admin</h1>
<div className="flex flex-wrap gap-1 bg-gray-900 border border-gray-800 rounded-lg p-1 w-fit">
<div className="flex flex-wrap gap-1 bg-surface border border-line rounded-lg p-1 w-fit">
{TAB_LABELS.map(({ key, label }) => (
<button
key={key}
onClick={() => setTab(key)}
className={`text-sm font-mono px-4 py-1.5 rounded-md transition-colors ${
tab === key ? "bg-gray-800 text-white" : "text-gray-500 hover:text-gray-300"
tab === key ? "bg-raised text-white" : "text-ink-muted hover:text-ink"
}`}
>
{label}