Secondary SDR panel: never present an unreported SDR count or run state as fact

QA (drb-qa-review) blockers: sdr_count defaulted to 1 for nodes that
never sent it, so the panel claimed 'reports 1 SDR (0 spare)'. It is now
None until reported, and the count is only quoted alongside a real
secondary_sdr_running report. Rows read 'Not reported' instead of
'Waiting for SDR' when the node hasn't said what's running (closes #187).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-09-27 14:14:42 -04:00
co-authored by Claude Opus 5.5
parent 3cbb0828ae
commit 012cca402a
4 changed files with 22 additions and 7 deletions
+1 -1
View File
@@ -114,7 +114,7 @@ class MQTTHandler:
"secondary_sdr_mode": payload.get("secondary_sdr_mode", "none"), "secondary_sdr_mode": payload.get("secondary_sdr_mode", "none"),
"secondary_sdr_priority": payload.get("secondary_sdr_priority", []), "secondary_sdr_priority": payload.get("secondary_sdr_priority", []),
"secondary_sdr_running": payload.get("secondary_sdr_running"), "secondary_sdr_running": payload.get("secondary_sdr_running"),
"sdr_count": payload.get("sdr_count", 1), "sdr_count": payload.get("sdr_count"), # None until reported, never a guessed 1
"enforce_override_timeout": payload.get("enforce_override_timeout", True), "enforce_override_timeout": payload.get("enforce_override_timeout", True),
"is_overridden": False, "is_overridden": False,
"override_system_id": None, "override_system_id": None,
+1 -1
View File
@@ -68,7 +68,7 @@ class NodeRecord(BaseModel):
# checkin, which is the source of truth; set via PATCH /nodes/{id}. # checkin, which is the source of truth; set via PATCH /nodes/{id}.
secondary_sdr_priority: List[str] = [] secondary_sdr_priority: List[str] = []
secondary_sdr_running: Optional[List[str]] = None # what the node reports actually running secondary_sdr_running: Optional[List[str]] = None # what the node reports actually running
sdr_count: int = 1 # self-reported by the node's checkin, best-effort sdr_count: Optional[int] = None # self-reported by the node's checkin; None = never reported
enforce_override_timeout: bool = True enforce_override_timeout: bool = True
is_overridden: bool = False is_overridden: bool = False
override_system_id: Optional[str] = None override_system_id: Optional[str] = None
@@ -22,6 +22,9 @@ function rowsFrom(priority: string[]): Row[] {
export function SecondarySdrPriority({ node, canEdit }: { node: NodeRecord; canEdit: boolean }) { export function SecondarySdrPriority({ node, canEdit }: { node: NodeRecord; canEdit: boolean }) {
const priority = node.secondary_sdr_priority ?? []; const priority = node.secondary_sdr_priority ?? [];
// null/absent = the node has never reported (older firmware, container down
// at checkin): unknown, not "nothing running" (server-26#187).
const reported = node.secondary_sdr_running != null;
const running = node.secondary_sdr_running ?? []; const running = node.secondary_sdr_running ?? [];
const [rows, setRows] = useState<Row[]>(() => rowsFrom(priority)); const [rows, setRows] = useState<Row[]>(() => rowsFrom(priority));
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
@@ -62,8 +65,9 @@ export function SecondarySdrPriority({ node, canEdit }: { node: NodeRecord; canE
} }
} }
const sdrCount = node.sdr_count ?? 1; // Only quote a count the node actually sent alongside its running list; a
const spare = Math.max(sdrCount - 1, 0); // bare sdr_count may be the Firestore default, not a report.
const sdrCount = reported ? node.sdr_count : undefined;
let rank = 0; let rank = 0;
return ( return (
@@ -71,13 +75,24 @@ export function SecondarySdrPriority({ node, canEdit }: { node: NodeRecord; canE
<h2 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-1">Secondary SDRs</h2> <h2 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-1">Secondary SDRs</h2>
<p className="text-xs text-gray-500 font-mono mb-3"> <p className="text-xs text-gray-500 font-mono mb-3">
OP25 always keeps its own SDR. Every other SDR runs the next enabled item, top first. OP25 always keeps its own SDR. Every other SDR runs the next enabled item, top first.
{" "}This node reports {sdrCount} SDR{sdrCount === 1 ? "" : "s"} ({spare} spare). {" "}
{sdrCount != null
? `This node reports ${sdrCount} SDR${sdrCount === 1 ? "" : "s"} (${Math.max(sdrCount - 1, 0)} spare).`
: "This node hasn't reported its SDRs yet."}
</p> </p>
<div className="bg-gray-900 border border-gray-800 rounded-lg divide-y divide-gray-800 font-mono text-sm"> <div className="bg-gray-900 border border-gray-800 rounded-lg divide-y divide-gray-800 font-mono text-sm">
{rows.map((row, i) => { {rows.map((row, i) => {
const meta = MODES.find((x) => x.mode === row.mode)!; const meta = MODES.find((x) => x.mode === row.mode)!;
const isRunning = running.includes(row.mode); const isRunning = running.includes(row.mode);
const state = !row.enabled ? "Off" : dirty ? "Unsaved" : isRunning ? "Running" : "Waiting for SDR"; const state = !row.enabled
? "Off"
: dirty
? "Unsaved"
: !reported
? "Not reported"
: isRunning
? "Running"
: "Waiting for SDR";
return ( return (
<div key={row.mode} className="flex items-center gap-3 px-4 py-2.5"> <div key={row.mode} className="flex items-center gap-3 px-4 py-2.5">
<span className="w-4 text-right text-gray-600 text-xs">{row.enabled ? ++rank : ""}</span> <span className="w-4 text-right text-gray-600 text-xs">{row.enabled ? ++rank : ""}</span>
+1 -1
View File
@@ -58,7 +58,7 @@ export interface NodeRecord {
secondary_sdr_priority?: string[]; secondary_sdr_priority?: string[];
/** What the node's last checkin reported actually running. */ /** What the node's last checkin reported actually running. */
secondary_sdr_running?: string[] | null; secondary_sdr_running?: string[] | null;
sdr_count?: number; sdr_count?: number | null; // null = never reported
enforce_override_timeout?: boolean; enforce_override_timeout?: boolean;
is_overridden?: boolean; is_overridden?: boolean;
override_system_id?: string | null; override_system_id?: string | null;