feat: Add local system override with 24h timeout support
This commit is contained in:
@@ -192,6 +192,54 @@ export default function NodeDetailPage() {
|
||||
<StatusBadge status={node.status} />
|
||||
</div>
|
||||
|
||||
{/* Override Warning */}
|
||||
{node.is_overridden && (
|
||||
<div className="bg-yellow-950/40 border border-yellow-800/60 rounded-lg p-4 font-mono text-sm space-y-3">
|
||||
<div className="flex items-center gap-2 text-yellow-400 font-semibold">
|
||||
<span className="text-base">⚠</span>
|
||||
<span>Local System Override Active</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-xs leading-relaxed">
|
||||
This node is operating on a local system override.
|
||||
{node.override_timeout_at ? (
|
||||
<> Resets automatically on: <span className="text-white font-bold">{new Date(node.override_timeout_at).toLocaleString()}</span>.</>
|
||||
) : (
|
||||
<> No timeout is currently enforced (permanent override).</>
|
||||
)}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
{node.override_timeout_at && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await c2api.ackOverride(id, 1440);
|
||||
} catch (e) {
|
||||
alert("Failed to extend timer.");
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1 bg-yellow-800 hover:bg-yellow-700 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
Ack (Reset 24h Timer)
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (confirm("Force this node to revert back to its assigned system config?")) {
|
||||
try {
|
||||
await c2api.resetOverride(id);
|
||||
} catch (e) {
|
||||
alert("Failed to reset override.");
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1 bg-red-900 hover:bg-red-800 text-red-200 rounded text-xs transition-colors"
|
||||
>
|
||||
Force Revert Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info */}
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-lg divide-y divide-gray-800 font-mono text-sm">
|
||||
{[
|
||||
@@ -199,6 +247,8 @@ export default function NodeDetailPage() {
|
||||
["Location", `${node.lat}, ${node.lon}`],
|
||||
["Last Seen", node.last_seen ? new Date(node.last_seen).toLocaleString() : "never"],
|
||||
["Configured", node.configured ? "Yes" : "No"],
|
||||
["Node Type", node.node_type ?? "fixed"],
|
||||
...(node.node_type !== "portable" ? [["Enforce Timeout", node.enforce_override_timeout ? "Yes" : "No"]] : []),
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="flex justify-between px-4 py-2.5">
|
||||
<span className="text-gray-500">{label}</span>
|
||||
|
||||
@@ -45,6 +45,18 @@ export function NodeCard({ node, system }: Props) {
|
||||
⚠ Needs configuration
|
||||
</div>
|
||||
)}
|
||||
{node.is_overridden && (
|
||||
<div className="mt-3 text-xs text-yellow-500 font-mono border-t border-gray-800 pt-2 flex justify-between">
|
||||
<span>⚠ Local Override</span>
|
||||
{node.override_timeout_at ? (
|
||||
<span className="text-gray-500">
|
||||
Resets: {new Date(node.override_timeout_at).toLocaleTimeString()}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-500">Permanent</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -17,9 +17,11 @@ const PRESETS = [
|
||||
];
|
||||
|
||||
export function NodeConfigModal({ node, systems, onClose }: Props) {
|
||||
const [systemId, setSystemId] = useState("");
|
||||
const [preset, setPreset] = useState("rtl-sdr-v3");
|
||||
const [ppm, setPpm] = useState("0");
|
||||
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 ?? "fixed");
|
||||
const [enforceTimeout, setEnforceTimeout] = useState(node.enforce_override_timeout ?? true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -32,6 +34,10 @@ export function NodeConfigModal({ node, systems, onClose }: Props) {
|
||||
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) {
|
||||
@@ -100,6 +106,33 @@ export function NodeConfigModal({ node, systems, onClose }: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Node Type</label>
|
||||
<select
|
||||
value={nodeType}
|
||||
onChange={(e) => setNodeType(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"
|
||||
>
|
||||
<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-gray-800 border-gray-700 text-indigo-600 focus:ring-indigo-500 focus:ring-offset-gray-900"
|
||||
/>
|
||||
<label htmlFor="enforceTimeout" className="text-xs text-gray-400 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">
|
||||
|
||||
@@ -30,6 +30,12 @@ export const c2api = {
|
||||
if (ppmOverride !== undefined) params.set("ppm_override", String(ppmOverride));
|
||||
return request(`/nodes/${nodeId}/config/${systemId}?${params}`, { method: "POST" });
|
||||
},
|
||||
ackOverride: (nodeId: string, timeoutMinutes: number = 1440) =>
|
||||
request(`/nodes/${nodeId}/override/ack`, { method: "POST", body: JSON.stringify({ timeout_minutes: timeoutMinutes }) }),
|
||||
resetOverride: (nodeId: string) =>
|
||||
request(`/nodes/${nodeId}/override/reset`, { method: "POST" }),
|
||||
updateNode: (id: string, body: { node_type?: string; enforce_override_timeout?: boolean }) =>
|
||||
request(`/nodes/${id}`, { method: "PATCH", body: JSON.stringify(body) }),
|
||||
|
||||
// Systems
|
||||
getSystems: () => request<unknown[]>("/systems"),
|
||||
|
||||
@@ -52,6 +52,11 @@ export interface NodeRecord {
|
||||
approval_status: ApprovalStatus | null;
|
||||
hardware_preset?: string;
|
||||
ppm_override?: number | null;
|
||||
node_type?: string;
|
||||
enforce_override_timeout?: boolean;
|
||||
is_overridden?: boolean;
|
||||
override_system_id?: string | null;
|
||||
override_timeout_at?: string | null;
|
||||
}
|
||||
|
||||
export interface VocabularyPendingTerm {
|
||||
|
||||
Reference in New Issue
Block a user