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:
co-authored by
Claude Opus 5.5
parent
3968b49b80
commit
bf206334e3
+113
-113
@@ -438,17 +438,17 @@ function RRImportModal({
|
||||
|
||||
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-lg max-h-[90vh] flex flex-col font-mono">
|
||||
<div className="bg-surface border border-line-strong rounded-xl w-full max-w-lg max-h-[90vh] flex flex-col font-mono">
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-5 pb-4 border-b border-gray-800">
|
||||
<div className="px-5 pt-5 pb-4 border-b border-line">
|
||||
<h2 className="text-white font-semibold">{system.name}</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
<p className="text-xs text-ink-muted mt-0.5">
|
||||
{system.systemType}{system.location ? ` · ${system.location}` : ""}
|
||||
</p>
|
||||
{system.sysIds && (
|
||||
<p className="text-xs text-gray-600 mt-0.5">System IDs: {system.sysIds}</p>
|
||||
<p className="text-xs text-ink-muted mt-0.5">System IDs: {system.sysIds}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
<p className="text-xs text-ink-muted mt-1">
|
||||
{system.categories.length} categor{system.categories.length !== 1 ? "ies" : "y"} · {total} talkgroups
|
||||
</p>
|
||||
</div>
|
||||
@@ -456,7 +456,7 @@ function RRImportModal({
|
||||
{/* Category list */}
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-1.5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-xs text-gray-400 uppercase tracking-wider">Talkgroup Categories</p>
|
||||
<p className="text-xs text-ink-2 uppercase tracking-wider">Talkgroup Categories</p>
|
||||
<div className="flex gap-3 text-xs text-indigo-400">
|
||||
<button
|
||||
type="button"
|
||||
@@ -480,18 +480,18 @@ function RRImportModal({
|
||||
type="checkbox"
|
||||
checked={selected.has(cat.name)}
|
||||
onChange={() => toggle(cat.name)}
|
||||
className="rounded border-gray-600 bg-gray-800 accent-indigo-500"
|
||||
className="rounded border-line-strong bg-raised accent-indigo-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-200 flex-1 group-hover:text-white transition-colors truncate">
|
||||
<span className="text-sm text-ink flex-1 group-hover:text-white transition-colors truncate">
|
||||
{cat.name}
|
||||
</span>
|
||||
<span className="text-xs text-gray-600 shrink-0">{cat.talkgroups.length} TGs</span>
|
||||
<span className="text-xs text-ink-muted shrink-0">{cat.talkgroups.length} TGs</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-5 pb-5 pt-4 border-t border-gray-800 flex gap-3">
|
||||
<div className="px-5 pb-5 pt-4 border-t border-line flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleImport}
|
||||
@@ -503,7 +503,7 @@ function RRImportModal({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
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>
|
||||
@@ -533,10 +533,10 @@ function LocalKnowledgeField({
|
||||
multiline?: boolean;
|
||||
}) {
|
||||
const cls =
|
||||
"w-full mt-0.5 bg-gray-900 border border-gray-700 rounded px-2 py-1 text-white text-xs focus:outline-none focus:border-indigo-500";
|
||||
"w-full mt-0.5 bg-surface border border-line-strong rounded px-2 py-1 text-white text-xs focus:outline-none focus:border-indigo-500";
|
||||
return (
|
||||
<label className="block">
|
||||
<span className="text-xs text-gray-500 font-sans">{label}</span>
|
||||
<span className="text-xs text-ink-muted font-sans">{label}</span>
|
||||
{multiline ? (
|
||||
<textarea
|
||||
value={value}
|
||||
@@ -656,8 +656,8 @@ function TalkgroupEditor({
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-gray-400">
|
||||
Talkgroups{talkgroups.length > 0 && <span className="text-gray-600 ml-1">({talkgroups.length})</span>}
|
||||
<label className="text-xs text-ink-2">
|
||||
Talkgroups{talkgroups.length > 0 && <span className="text-ink-muted ml-1">({talkgroups.length})</span>}
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
{/* RadioReference file import */}
|
||||
@@ -704,10 +704,10 @@ function TalkgroupEditor({
|
||||
)}
|
||||
|
||||
{showPaste && (
|
||||
<div className="space-y-2 p-3 bg-gray-800 rounded-lg border border-gray-700">
|
||||
<p className="text-xs text-gray-500">
|
||||
<div className="space-y-2 p-3 bg-raised rounded-lg border border-line-strong">
|
||||
<p className="text-xs text-ink-muted">
|
||||
Paste rows from RadioReference — tab- or comma-separated:{" "}
|
||||
<span className="text-gray-400">ID, Name, Tag</span>
|
||||
<span className="text-ink-2">ID, Name, Tag</span>
|
||||
<br />Tags: fire · police · ems · transit · public works · other
|
||||
</p>
|
||||
<textarea
|
||||
@@ -715,7 +715,7 @@ function TalkgroupEditor({
|
||||
onChange={(e) => setPasteText(e.target.value)}
|
||||
rows={6}
|
||||
placeholder={"1234\tFire Dispatch\tfire\n5678\tPolice Zone 1\tpolice\n9012\tEMS\tems"}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-white text-xs font-mono focus:outline-none focus:border-indigo-500"
|
||||
className="w-full bg-surface border border-line-strong rounded px-3 py-2 text-white text-xs font-mono focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -729,10 +729,10 @@ function TalkgroupEditor({
|
||||
)}
|
||||
|
||||
{talkgroups.length > 0 ? (
|
||||
<div className="border border-gray-800 rounded-lg overflow-hidden">
|
||||
<div className="border border-line rounded-lg overflow-hidden">
|
||||
<table className="w-full text-xs font-mono">
|
||||
<thead>
|
||||
<tr className="bg-gray-800 text-gray-400">
|
||||
<tr className="bg-raised text-ink-2">
|
||||
<th className="px-3 py-1.5 text-left w-20">Dec ID</th>
|
||||
<th className="px-3 py-1.5 text-left">Name</th>
|
||||
<th className="px-3 py-1.5 text-left w-28">Tag</th>
|
||||
@@ -742,7 +742,7 @@ function TalkgroupEditor({
|
||||
<tbody>
|
||||
{talkgroups.map((tg, i) => (
|
||||
<Fragment key={i}>
|
||||
<tr className="border-t border-gray-800 hover:bg-gray-800/30">
|
||||
<tr className="border-t border-line hover:bg-raised/30">
|
||||
<td className="px-2 py-1">
|
||||
<input
|
||||
value={tg.id}
|
||||
@@ -763,7 +763,7 @@ function TalkgroupEditor({
|
||||
<select
|
||||
value={tg.tag}
|
||||
onChange={(e) => updateRow(i, "tag", e.target.value)}
|
||||
className="w-full bg-gray-900 text-gray-300 focus:outline-none rounded px-1"
|
||||
className="w-full bg-surface text-ink-2 focus:outline-none rounded px-1"
|
||||
>
|
||||
{TG_TAGS.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
@@ -776,7 +776,7 @@ function TalkgroupEditor({
|
||||
title="Local knowledge for the transcript corrector"
|
||||
onClick={() => setExpanded(expanded === i ? null : i)}
|
||||
className={`transition-colors font-bold mr-2 ${
|
||||
hasLocalKnowledge(tg) ? "text-indigo-400" : "text-gray-600 hover:text-gray-300"
|
||||
hasLocalKnowledge(tg) ? "text-indigo-400" : "text-ink-muted hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
{expanded === i ? "▾" : "▸"}
|
||||
@@ -784,7 +784,7 @@ function TalkgroupEditor({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRow(i)}
|
||||
className="text-gray-600 hover:text-red-400 transition-colors font-bold"
|
||||
className="text-ink-muted hover:text-red-400 transition-colors font-bold"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -794,11 +794,11 @@ function TalkgroupEditor({
|
||||
talkgroups and most inherit the system's context rather than
|
||||
override it. */}
|
||||
{expanded === i && (
|
||||
<tr className="border-t border-gray-800 bg-gray-900/60">
|
||||
<tr className="border-t border-line bg-surface/60">
|
||||
<td colSpan={4} className="px-3 py-3">
|
||||
<p className="text-xs text-gray-500 mb-2 font-sans">
|
||||
<p className="text-xs text-ink-muted mb-2 font-sans">
|
||||
Given to the transcript corrector for this talkgroup only, ranked
|
||||
<span className="text-gray-300"> above</span> the system's own list.
|
||||
<span className="text-ink-2"> above</span> the system's own list.
|
||||
Leave blank to inherit the system's.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
@@ -849,7 +849,7 @@ function TalkgroupEditor({
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-600 italic py-1">
|
||||
<p className="text-xs text-ink-muted italic py-1">
|
||||
No talkgroups — add rows, paste from RadioReference, or use the RadioReference import button.
|
||||
</p>
|
||||
)}
|
||||
@@ -864,52 +864,52 @@ function P25Form({ value, onChange }: { value: P25Config; onChange: (v: P25Confi
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">NAC (hex)</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">NAC (hex)</label>
|
||||
<input
|
||||
value={value.nac}
|
||||
onChange={(e) => onChange({ ...value, nac: e.target.value })}
|
||||
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="0x293"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">System ID</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">System ID</label>
|
||||
<input
|
||||
value={value.system_id}
|
||||
onChange={(e) => onChange({ ...value, system_id: e.target.value })}
|
||||
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="50513"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">WACN (hex)</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">WACN (hex)</label>
|
||||
<input
|
||||
value={value.wacn}
|
||||
onChange={(e) => onChange({ ...value, wacn: e.target.value })}
|
||||
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="0xBEE00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">
|
||||
Control Channels <span className="text-gray-600">(MHz, comma-separated)</span>
|
||||
<label className="text-xs text-ink-2 block mb-1">
|
||||
Control Channels <span className="text-ink-muted">(MHz, comma-separated)</span>
|
||||
</label>
|
||||
<input
|
||||
value={value.control_channels}
|
||||
onChange={(e) => onChange({ ...value, control_channels: e.target.value })}
|
||||
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="851.0125, 851.5125, 852.0125"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">
|
||||
Voice Channels <span className="text-gray-600">(MHz, comma-separated — leave blank for auto-discovery)</span>
|
||||
<label className="text-xs text-ink-2 block mb-1">
|
||||
Voice Channels <span className="text-ink-muted">(MHz, comma-separated — leave blank for auto-discovery)</span>
|
||||
</label>
|
||||
<input
|
||||
value={value.voice_channels}
|
||||
onChange={(e) => onChange({ ...value, voice_channels: e.target.value })}
|
||||
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="Optional"
|
||||
/>
|
||||
</div>
|
||||
@@ -923,7 +923,7 @@ function P25Form({ value, onChange }: { value: P25Config; onChange: (v: P25Confi
|
||||
|
||||
// ── Fixed-channel editors (node-26#24) ────────────────────────────────────────
|
||||
|
||||
const INPUT = "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";
|
||||
const INPUT = "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";
|
||||
|
||||
function ConventionalForm({ value, onChange }: { value: P25Config; onChange: (v: P25Config) => void }) {
|
||||
const chs = value.channels;
|
||||
@@ -931,13 +931,13 @@ function ConventionalForm({ value, onChange }: { value: P25Config; onChange: (v:
|
||||
onChange({ ...value, channels: chs.map((ch, j) => (j === i ? { ...ch, [field]: v } : ch)) });
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-ink-muted">
|
||||
Fixed voice channels with no control channel — RadioReference lists them as conventional
|
||||
frequencies with mode P25 (or "DE"). A node decodes up to {MAX_CONVENTIONAL_CHANNELS} on
|
||||
one SDR, within about 1.5 MHz of each other. Each channel's calls are labelled with its name.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-[1fr_1.5fr_0.8fr_auto] gap-2 text-xs text-gray-400">
|
||||
<div className="grid grid-cols-[1fr_1.5fr_0.8fr_auto] gap-2 text-xs text-ink-2">
|
||||
<span>Frequency (MHz)</span>
|
||||
<span>Channel name</span>
|
||||
<span>NAC (hex, optional)</span>
|
||||
@@ -992,7 +992,7 @@ function ConventionalForm({ value, onChange }: { value: P25Config; onChange: (v:
|
||||
talkgroups={value.talkgroups}
|
||||
onChange={(tgs) => onChange({ ...value, talkgroups: tgs })}
|
||||
/>
|
||||
<p className="text-xs text-gray-600 -mt-2">
|
||||
<p className="text-xs text-ink-muted -mt-2">
|
||||
Talkgroups are optional here: only needed if the radios send distinct talkgroup IDs you want named.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1004,7 +1004,7 @@ function AnalogForm({ value, onChange }: { value: AnalogConfig; onChange: (v: An
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label htmlFor="analog-freq" className="text-xs text-gray-400 block mb-1">Frequency (MHz)</label>
|
||||
<label htmlFor="analog-freq" className="text-xs text-ink-2 block mb-1">Frequency (MHz)</label>
|
||||
<input
|
||||
id="analog-freq"
|
||||
value={ch.freq}
|
||||
@@ -1016,7 +1016,7 @@ function AnalogForm({ value, onChange }: { value: AnalogConfig; onChange: (v: An
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="analog-name" className="text-xs text-gray-400 block mb-1">Channel name</label>
|
||||
<label htmlFor="analog-name" className="text-xs text-ink-2 block mb-1">Channel name</label>
|
||||
<input
|
||||
id="analog-name"
|
||||
value={ch.name}
|
||||
@@ -1124,27 +1124,27 @@ function SystemForm({
|
||||
const title = initial && !createOnly ? "Edit System" : createOnly ? "Duplicate System" : "New System";
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="bg-gray-900 border border-gray-700 rounded-xl p-5 space-y-4 font-mono">
|
||||
<form onSubmit={handleSubmit} className="bg-surface border border-line-strong rounded-xl p-5 space-y-4 font-mono">
|
||||
<h3 className="text-white font-semibold">{title}</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Name</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">Name</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(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="Westchester County P25"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="system-kind" className="text-xs text-gray-400 block mb-1">Type</label>
|
||||
<label htmlFor="system-kind" className="text-xs text-ink-2 block mb-1">Type</label>
|
||||
<select
|
||||
id="system-kind"
|
||||
value={kind}
|
||||
onChange={(e) => handleKindChange(e.target.value as SystemKind)}
|
||||
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"
|
||||
>
|
||||
{KIND_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
@@ -1165,12 +1165,12 @@ function SystemForm({
|
||||
<AnalogForm value={analog} onChange={setAnalog} />
|
||||
) : (
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Config (JSON)</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">Config (JSON)</label>
|
||||
<textarea
|
||||
value={rawJson}
|
||||
onChange={(e) => setRawJson(e.target.value)}
|
||||
rows={10}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-xs 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-xs font-mono focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -1179,7 +1179,7 @@ function SystemForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleRaw}
|
||||
className="text-xs text-gray-600 hover:text-gray-400 transition-colors"
|
||||
className="text-xs text-ink-muted hover:text-ink-2 transition-colors"
|
||||
>
|
||||
{showRaw ? "← Use structured form" : "Edit raw JSON →"}
|
||||
</button>
|
||||
@@ -1198,7 +1198,7 @@ function SystemForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
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>
|
||||
@@ -1267,10 +1267,10 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
|
||||
const currentToken = tokens?.find((t) => t.token_id === preferredId);
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-gray-800 pt-3">
|
||||
<div className="mt-3 border-t border-line pt-3">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 font-mono transition-colors flex items-center gap-1"
|
||||
className="text-xs text-ink-muted hover:text-ink font-mono transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>{open ? "▲" : "▼"}</span>
|
||||
<span>
|
||||
@@ -1282,14 +1282,14 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
|
||||
{open && (
|
||||
<div className="mt-3 space-y-2 font-mono text-xs">
|
||||
{tokens === null ? (
|
||||
<p className="text-gray-600 italic">Loading tokens…</p>
|
||||
<p className="text-ink-muted italic">Loading tokens…</p>
|
||||
) : error ? (
|
||||
<p className="text-red-400">{error}</p>
|
||||
) : tokens.length === 0 ? (
|
||||
<p className="text-gray-600 italic">No tokens in pool.</p>
|
||||
<p className="text-ink-muted italic">No tokens in pool.</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-ink-muted">
|
||||
When a node on this system joins a voice channel, this token is tried first.
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
@@ -1303,7 +1303,7 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
|
||||
disabled={saving}
|
||||
className="accent-indigo-500"
|
||||
/>
|
||||
<span className={`flex-1 ${t.in_use && preferredId !== t.token_id ? "text-gray-600" : "text-gray-300"}`}>
|
||||
<span className={`flex-1 ${t.in_use && preferredId !== t.token_id ? "text-ink-muted" : "text-ink-2"}`}>
|
||||
{t.name}
|
||||
{t.in_use && <span className="ml-1.5 text-green-600">in use</span>}
|
||||
</span>
|
||||
@@ -1314,13 +1314,13 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
|
||||
<button
|
||||
onClick={handleClear}
|
||||
disabled={saving}
|
||||
className="text-gray-600 hover:text-gray-400 transition-colors disabled:opacity-50"
|
||||
className="text-ink-muted hover:text-ink-2 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Clear preference (use any free token)
|
||||
</button>
|
||||
)}
|
||||
{!preferredId && (
|
||||
<p className="text-gray-700">No preference — any free token will be used.</p>
|
||||
<p className="text-ink-muted">No preference — any free token will be used.</p>
|
||||
)}
|
||||
{currentToken && (
|
||||
<p className="text-indigo-500">Preferred: {currentToken.name}</p>
|
||||
@@ -1371,10 +1371,10 @@ function AiFlagsPanel({ systemId, initial }: { systemId: string; initial: System
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-gray-800 pt-3">
|
||||
<div className="mt-3 border-t border-line pt-3">
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 font-mono transition-colors flex items-center gap-1"
|
||||
className="text-xs text-ink-muted hover:text-ink font-mono transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>{open ? "▲" : "▼"}</span>
|
||||
<span>AI Flags</span>
|
||||
@@ -1400,23 +1400,23 @@ function AiFlagsPanel({ systemId, initial }: { systemId: string; initial: System
|
||||
>
|
||||
<span className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform ${isOn ? "translate-x-4" : "translate-x-0.5"}`} />
|
||||
</button>
|
||||
<span className="text-gray-300 flex-1">{label}</span>
|
||||
<span className="text-ink-2 flex-1">{label}</span>
|
||||
{isSet ? (
|
||||
<button
|
||||
onClick={() => handleClear(key)}
|
||||
disabled={saving === key}
|
||||
className="text-gray-600 hover:text-gray-400 transition-colors"
|
||||
className="text-ink-muted hover:text-ink-2 transition-colors"
|
||||
title="Clear override (inherit global)"
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-gray-700">inherits global</span>
|
||||
<span className="text-ink-muted">inherits global</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<p className="text-gray-700 pt-1">Overrides apply on top of global AI flags. "reset" restores global default.</p>
|
||||
<p className="text-ink-muted pt-1">Overrides apply on top of global AI flags. "reset" restores global default.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1456,15 +1456,15 @@ function SourceCallPlayer({ callId }: { callId: string }) {
|
||||
{loading ? "loading…" : open ? "▲ source" : "▶ source"}
|
||||
</button>
|
||||
{open && call && (
|
||||
<div className="mt-1.5 space-y-1 pl-2 border-l border-gray-700">
|
||||
<div className="mt-1.5 space-y-1 pl-2 border-l border-line-strong">
|
||||
{call.audio_url ? (
|
||||
<audio src={call.audio_url} controls className="w-full" style={{ height: "1.75rem" }} />
|
||||
) : (
|
||||
<p className="text-gray-600 italic">No audio</p>
|
||||
<p className="text-ink-muted italic">No audio</p>
|
||||
)}
|
||||
{transcript && (
|
||||
<>
|
||||
<p className="text-gray-500 italic line-clamp-2">{transcript}</p>
|
||||
<p className="text-ink-muted italic line-clamp-2">{transcript}</p>
|
||||
{/* Gate A / A2 (server-26#46) — this is a raw pipeline transcript. */}
|
||||
<MachineOutputNotice variant="inline" className="text-[10px]" />
|
||||
</>
|
||||
@@ -1528,16 +1528,16 @@ function AreaContextPanel({ systemId }: { systemId: string }) {
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-gray-800 pt-3">
|
||||
<div className="mt-3 border-t border-line pt-3">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 font-mono transition-colors flex items-center gap-1"
|
||||
className="text-xs text-ink-muted hover:text-ink font-mono transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>{open ? "▲" : "▼"}</span>
|
||||
<span>
|
||||
Local Area
|
||||
{area !== null && (
|
||||
<span className="text-gray-600 ml-1">
|
||||
<span className="text-ink-muted ml-1">
|
||||
({filled > 0 ? `${filled} field${filled === 1 ? "" : "s"} set` : "not set"})
|
||||
</span>
|
||||
)}
|
||||
@@ -1546,16 +1546,16 @@ function AreaContextPanel({ systemId }: { systemId: string }) {
|
||||
|
||||
{open && (
|
||||
<div className="mt-3 space-y-3 text-xs">
|
||||
{loading && <p className="text-gray-600 italic font-mono">Loading…</p>}
|
||||
{loading && <p className="text-ink-muted italic font-mono">Loading…</p>}
|
||||
{area && (
|
||||
<>
|
||||
<p className="text-gray-500">
|
||||
<p className="text-ink-muted">
|
||||
Given to the transcript corrector for every talkgroup on this system.
|
||||
Whisper mishears local names constantly — naming them here is what lets
|
||||
them be put back.
|
||||
</p>
|
||||
<p className="text-gray-500">
|
||||
Fill this in <span className="text-gray-300">only if it is true of every
|
||||
<p className="text-ink-muted">
|
||||
Fill this in <span className="text-ink-2">only if it is true of every
|
||||
talkgroup</span> on the system. One town, one department — fill it once here.
|
||||
A system spanning several counties — leave it blank and describe each
|
||||
talkgroup in the edit form instead.
|
||||
@@ -1592,7 +1592,7 @@ function AreaContextPanel({ systemId }: { systemId: string }) {
|
||||
{/* The anchor is the backend's answer to what was typed above, and
|
||||
it decides whether location checking runs at all — so it is
|
||||
worth showing rather than leaving as invisible state. */}
|
||||
<p className="text-gray-600 font-mono">{anchorLabel(area)}</p>
|
||||
<p className="text-ink-muted font-mono">{anchorLabel(area)}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={save}
|
||||
@@ -1659,16 +1659,16 @@ function TalkgroupPendingPanel({ systemId }: { systemId: string }) {
|
||||
const total = (rows ?? []).reduce((n, r) => n + r.pending.length, 0);
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-gray-800 pt-3">
|
||||
<div className="mt-3 border-t border-line pt-3">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 font-mono transition-colors flex items-center gap-1"
|
||||
className="text-xs text-ink-muted hover:text-ink font-mono transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>{open ? "▲" : "▼"}</span>
|
||||
<span>
|
||||
Proposed Terms
|
||||
{rows !== null && (
|
||||
<span className={total > 0 ? "text-indigo-400 ml-1" : "text-gray-600 ml-1"}>
|
||||
<span className={total > 0 ? "text-indigo-400 ml-1" : "text-ink-muted ml-1"}>
|
||||
({total > 0 ? `${total} awaiting review` : "none"})
|
||||
</span>
|
||||
)}
|
||||
@@ -1677,9 +1677,9 @@ function TalkgroupPendingPanel({ systemId }: { systemId: string }) {
|
||||
|
||||
{open && (
|
||||
<div className="mt-3 space-y-3 text-xs">
|
||||
{rows === null && <p className="text-gray-600 italic font-mono">Loading…</p>}
|
||||
{rows === null && <p className="text-ink-muted italic font-mono">Loading…</p>}
|
||||
{rows !== null && total === 0 && (
|
||||
<p className="text-gray-600 italic">
|
||||
<p className="text-ink-muted italic">
|
||||
Nothing proposed yet. Terms appear here when a corrected place name
|
||||
turns out to be real nearby, or when the induction loop spots one in
|
||||
recent traffic.
|
||||
@@ -1687,19 +1687,19 @@ function TalkgroupPendingPanel({ systemId }: { systemId: string }) {
|
||||
)}
|
||||
{(rows ?? []).map((row) => (
|
||||
<div key={row.talkgroup_id}>
|
||||
<p className="text-gray-400 font-mono mb-1">
|
||||
<p className="text-ink-2 font-mono mb-1">
|
||||
{row.talkgroup_name || `TGID ${row.talkgroup_id}`}
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{row.pending.map((p) => (
|
||||
<li
|
||||
key={p.term}
|
||||
className="flex items-start gap-2 bg-gray-900/60 border border-gray-800 rounded px-2 py-1"
|
||||
className="flex items-start gap-2 bg-surface/60 border border-line rounded px-2 py-1"
|
||||
>
|
||||
<span className="flex-1">
|
||||
<span className="text-white font-mono">{p.term}</span>
|
||||
{p.meaning && <span className="text-gray-500"> — {p.meaning}</span>}
|
||||
<span className="text-gray-700 ml-2">{p.source}</span>
|
||||
{p.meaning && <span className="text-ink-muted"> — {p.meaning}</span>}
|
||||
<span className="text-ink-muted ml-2">{p.source}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1713,7 +1713,7 @@ function TalkgroupPendingPanel({ systemId }: { systemId: string }) {
|
||||
type="button"
|
||||
disabled={busy === `${row.talkgroup_id}:${p.term}`}
|
||||
onClick={() => act(row.talkgroup_id, p.term, false)}
|
||||
className="text-gray-600 hover:text-red-400 disabled:opacity-40 font-semibold"
|
||||
className="text-ink-muted hover:text-red-400 disabled:opacity-40 font-semibold"
|
||||
>
|
||||
Drop
|
||||
</button>
|
||||
@@ -1802,16 +1802,16 @@ function VocabularyPanel({ systemId }: { systemId: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-gray-800 pt-3">
|
||||
<div className="mt-3 border-t border-line pt-3">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 font-mono transition-colors flex items-center gap-1"
|
||||
className="text-xs text-ink-muted hover:text-ink font-mono transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>{open ? "▲" : "▼"}</span>
|
||||
<span>
|
||||
Vocabulary
|
||||
{vocab !== null && (
|
||||
<span className="text-gray-600 ml-1">
|
||||
<span className="text-ink-muted ml-1">
|
||||
({vocab.length} terms{pending.length > 0 ? `, ${pending.length} pending` : ""})
|
||||
</span>
|
||||
)}
|
||||
@@ -1820,7 +1820,7 @@ function VocabularyPanel({ systemId }: { systemId: string }) {
|
||||
|
||||
{open && (
|
||||
<div className="mt-3 space-y-3 font-mono text-xs">
|
||||
{loading && <p className="text-gray-600 italic">Loading…</p>}
|
||||
{loading && <p className="text-ink-muted italic">Loading…</p>}
|
||||
|
||||
{!loading && vocab !== null && (
|
||||
<>
|
||||
@@ -1832,19 +1832,19 @@ function VocabularyPanel({ systemId }: { systemId: string }) {
|
||||
>
|
||||
{bootstrapping ? "Bootstrapping…" : bootstrapped ? "Re-bootstrap with AI" : "Bootstrap with AI"}
|
||||
</button>
|
||||
<span className="text-gray-600">GPT-4o generates local knowledge (agencies, units, streets)</span>
|
||||
<span className="text-ink-muted">GPT-4o generates local knowledge (agencies, units, streets)</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500 uppercase tracking-wider mb-1.5">Approved ({vocab.length})</p>
|
||||
<p className="text-ink-muted uppercase tracking-wider mb-1.5">Approved ({vocab.length})</p>
|
||||
{vocab.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{vocab.map((term) => (
|
||||
<span key={term} className="inline-flex items-center gap-1 bg-gray-800 text-gray-300 px-2 py-0.5 rounded-full">
|
||||
<span key={term} className="inline-flex items-center gap-1 bg-raised text-ink-2 px-2 py-0.5 rounded-full">
|
||||
{term}
|
||||
<button
|
||||
onClick={() => handleRemove(term)}
|
||||
className="text-gray-600 hover:text-red-400 transition-colors leading-none"
|
||||
className="text-ink-muted hover:text-red-400 transition-colors leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -1852,7 +1852,7 @@ function VocabularyPanel({ systemId }: { systemId: string }) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-600 italic">No terms yet — bootstrap or add manually.</p>
|
||||
<p className="text-ink-muted italic">No terms yet — bootstrap or add manually.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1861,12 +1861,12 @@ function VocabularyPanel({ systemId }: { systemId: string }) {
|
||||
value={newTerm}
|
||||
onChange={(e) => setNewTerm(e.target.value)}
|
||||
placeholder="Add term (e.g. 5-baker, YVAC)"
|
||||
className="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-xs font-mono focus:outline-none focus:border-indigo-500"
|
||||
className="flex-1 bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-xs font-mono focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={adding || !newTerm.trim()}
|
||||
className="bg-gray-700 hover:bg-gray-600 disabled:opacity-50 text-gray-200 px-3 py-1.5 rounded-lg transition-colors"
|
||||
className="bg-gray-700 hover:bg-gray-600 disabled:opacity-50 text-ink px-3 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
@@ -1874,17 +1874,17 @@ function VocabularyPanel({ systemId }: { systemId: string }) {
|
||||
|
||||
{pending.length > 0 && (
|
||||
<div>
|
||||
<p className="text-gray-500 uppercase tracking-wider mb-1.5">
|
||||
<p className="text-ink-muted uppercase tracking-wider mb-1.5">
|
||||
Induction suggestions ({pending.length})
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{pending.map((p) => (
|
||||
<div key={p.term} className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-300 flex-1">{p.term}</span>
|
||||
<span className="text-gray-600">{p.source}</span>
|
||||
<span className="text-ink-2 flex-1">{p.term}</span>
|
||||
<span className="text-ink-muted">{p.source}</span>
|
||||
<button onClick={() => handleApprove(p.term)} className="text-green-500 hover:text-green-400 transition-colors px-1">✓</button>
|
||||
<button onClick={() => handleDismiss(p.term)} className="text-gray-600 hover:text-red-400 transition-colors px-1">✕</button>
|
||||
<button onClick={() => handleDismiss(p.term)} className="text-ink-muted hover:text-red-400 transition-colors px-1">✕</button>
|
||||
</div>
|
||||
{p.source_call_ids && p.source_call_ids.length > 0 && (
|
||||
<div className="pl-1 space-y-1">
|
||||
@@ -1970,9 +1970,9 @@ export default function SystemsPage() {
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-gray-600 text-sm font-mono">Loading…</p>
|
||||
<p className="text-ink-muted text-sm font-mono">Loading…</p>
|
||||
) : systems.length === 0 ? (
|
||||
<p className="text-gray-600 text-sm font-mono">No systems defined yet.</p>
|
||||
<p className="text-ink-muted text-sm font-mono">No systems defined yet.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{systems.map((s) => {
|
||||
@@ -1987,13 +1987,13 @@ export default function SystemsPage() {
|
||||
? (s.config.channels as unknown[]).length
|
||||
: null;
|
||||
return (
|
||||
<div key={s.system_id} className="bg-gray-900 border border-gray-800 rounded-lg p-4 font-mono">
|
||||
<div key={s.system_id} className="bg-surface border border-line rounded-lg p-4 font-mono">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-white font-semibold">{s.name}</p>
|
||||
<p className="text-xs text-gray-500">{s.system_id}</p>
|
||||
<p className="text-xs text-ink-muted">{s.system_id}</p>
|
||||
{(s.config.nac || ccCount !== null || chCount !== null || tgCount !== null) && (
|
||||
<p className="text-xs text-gray-600 mt-0.5 space-x-2">
|
||||
<p className="text-xs text-ink-muted mt-0.5 space-x-2">
|
||||
{!!s.config.nac && <span>NAC {String(s.config.nac)}</span>}
|
||||
{ccCount !== null && <span>· {ccCount} CC</span>}
|
||||
{chCount !== null && <span>· {chCount} ch</span>}
|
||||
@@ -2001,7 +2001,7 @@ export default function SystemsPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs bg-gray-800 border border-gray-700 text-gray-400 px-2 py-0.5 rounded">
|
||||
<span className="text-xs bg-raised border border-line-strong text-ink-2 px-2 py-0.5 rounded">
|
||||
{kindLabel(s)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -2014,7 +2014,7 @@ export default function SystemsPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openDuplicate(s)}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 transition-colors"
|
||||
className="text-xs text-ink-muted hover:text-ink transition-colors"
|
||||
>
|
||||
Duplicate
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user