Files
server-26/drb-frontend/components/NodeConfigModal.tsx
T
Logan CusanoandClaude Opus 5.5 94898d1af1 fix(frontend): issues from the 10-25 Chrome sweep
- Incident detail "← Incidents" used router.back(), so arriving from Live
  or a shared link it went to the map / out of the app. Now a Link.
- Resolved incidents showed "0m elapsed" (manual resolve sets no
  resolved_at, updated_at can predate the last call): end is now the
  latest of resolved_at, updated_at and the newest call.
- Admin: the max-width toggle wrapped the tab bar, so it reflowed on every
  tab switch and left the cursor over e.g. "Close calls". Body only now.
- STT Eval sat on "Loading…" forever when a scanned page had zero matches
  but a next_cursor — the refill effect didn't depend on cursor.
- Trip event blocks used dark-only bg-indigo-950/60: unreadable in light
  mode. Theme tokens now.
- Reassign modal said "connected for the first time" on configured nodes.
- Dark-mode placeholders dimmed + italic; they read as filled-in values.

Frontend unverified locally (no Node, #207); CI build typechecks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:12:07 -04:00

165 lines
6.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { c2api } from "@/lib/c2api";
import type { NodeRecord, SystemRecord } from "@/lib/types";
interface Props {
node: NodeRecord;
systems: SystemRecord[];
onClose: () => void;
}
const PRESETS = [
{ value: "rtl-sdr-v3", label: "RTL-SDR v3", hint: "TCXO — LNA:34, tracking off" },
{ value: "nesdr-smart-v4", label: "NESDR Smart v4", hint: "TCXO — LNA:32, tracking off" },
{ value: "other", label: "Other", hint: "LNA:32, tracking on" },
];
export function NodeConfigModal({ node, systems, onClose }: Props) {
const [systemId, setSystemId] = useState(node.assigned_system_id ?? "");
const [preset, setPreset] = useState(node.hardware_preset ?? "rtl-sdr-v3");
const [ppm, setPpm] = useState(node.ppm_override ? String(node.ppm_override) : "0");
const [nodeType, setNodeType] = useState(node.node_type ?? "");
const [enforceTimeout, setEnforceTimeout] = useState(node.enforce_override_timeout ?? true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const ppmVal = parseFloat(ppm);
const ppmOverride = !isNaN(ppmVal) && ppmVal !== 0 ? ppmVal : undefined;
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!systemId) return;
setSaving(true);
setError(null);
try {
await c2api.updateNode(node.node_id, {
node_type: nodeType,
enforce_override_timeout: enforceTimeout,
});
await c2api.assignSystem(node.node_id, systemId, preset, ppmOverride);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to assign system.");
} finally {
setSaving(false);
}
}
const selectedPreset = PRESETS.find((p) => p.value === preset);
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-surface border border-line-strong rounded-xl p-6 w-full max-w-md font-mono max-h-[90vh] overflow-y-auto">
<h2 className="text-white font-semibold mb-1">Configure Node</h2>
<p className="text-ink-2 text-sm mb-5">
{node.configured ? (
<><span className="text-indigo-400">{node.node_id}</span>: choose the radio system it should monitor.</>
) : (
<><span className="text-indigo-400">{node.node_id}</span> connected for the first time.
Assign it a radio system to begin monitoring.</>
)}
</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs text-ink-2 mb-1">Radio System</label>
<select
value={systemId}
onChange={(e) => setSystemId(e.target.value)}
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"
required
>
<option value="">Select a system…</option>
{systems.map((s) => (
<option key={s.system_id} value={s.system_id}>
{s.name} ({s.type})
</option>
))}
</select>
</div>
<div>
<label className="block text-xs text-ink-2 mb-1">SDR Hardware</label>
<select
value={preset}
onChange={(e) => setPreset(e.target.value)}
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"
>
{PRESETS.map((p) => (
<option key={p.value} value={p.value}>{p.label}</option>
))}
</select>
{selectedPreset && (
<p className="text-xs text-ink-muted mt-1">{selectedPreset.hint}</p>
)}
</div>
<div>
<label className="block text-xs text-ink-2 mb-1">
PPM offset <span className="text-ink-muted">(0 = use preset default; calibrate via OP25 web UI)</span>
</label>
<input
type="number"
value={ppm}
onChange={(e) => setPpm(e.target.value)}
step="0.1"
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="0"
/>
</div>
<div>
<label className="block text-xs text-ink-2 mb-1">Node Type *</label>
<select
value={nodeType}
onChange={(e) => setNodeType(e.target.value)}
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"
required
>
<option value="">Select node type...</option>
<option value="fixed">Fixed Node (Standard)</option>
<option value="portable">Portable Node (Handheld)</option>
</select>
</div>
{nodeType === "fixed" && (
<div className="flex items-center gap-2 py-1">
<input
type="checkbox"
id="enforceTimeout"
checked={enforceTimeout}
onChange={(e) => setEnforceTimeout(e.target.checked)}
className="rounded bg-raised border-line-strong text-indigo-600 focus:ring-indigo-500 focus:ring-offset-gray-900"
/>
<label htmlFor="enforceTimeout" className="text-xs text-ink-2 cursor-pointer select-none">
Enforce Local Override Timeout (24 hours)
</label>
</div>
)}
{error && <p className="text-red-400 text-xs">{error}</p>}
<div className="flex gap-3 pt-1">
<button
type="submit"
disabled={saving || !systemId || !nodeType}
className="flex-1 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white rounded-lg py-2 text-sm font-semibold transition-colors"
>
{saving ? "Saving…" : "Assign & Configure"}
</button>
<button
type="button"
onClick={onClose}
className="flex-1 bg-raised hover:bg-gray-700 text-ink-2 rounded-lg py-2 text-sm transition-colors"
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
}