| @@ -10,6 +10,7 @@ type compactDecision struct { | |||||
| Reason string `json:"reason,omitempty"` | Reason string `json:"reason,omitempty"` | ||||
| MonitorBias float64 `json:"monitor_bias,omitempty"` | MonitorBias float64 `json:"monitor_bias,omitempty"` | ||||
| MonitorDetail *pipeline.MonitorWindowMatch `json:"monitor_detail,omitempty"` | MonitorDetail *pipeline.MonitorWindowMatch `json:"monitor_detail,omitempty"` | ||||
| MonitorWindow *pipeline.MonitorWindowMatch `json:"monitor_window,omitempty"` | |||||
| RecordWindow *pipeline.MonitorWindowMatch `json:"record_window,omitempty"` | RecordWindow *pipeline.MonitorWindowMatch `json:"record_window,omitempty"` | ||||
| DecodeWindow *pipeline.MonitorWindowMatch `json:"decode_window,omitempty"` | DecodeWindow *pipeline.MonitorWindowMatch `json:"decode_window,omitempty"` | ||||
| RecordAdmission *pipeline.PriorityAdmission `json:"record_admission,omitempty"` | RecordAdmission *pipeline.PriorityAdmission `json:"record_admission,omitempty"` | ||||
| @@ -28,6 +29,7 @@ func compactDecisions(decisions []pipeline.SignalDecision) []compactDecision { | |||||
| Reason: d.Reason, | Reason: d.Reason, | ||||
| MonitorBias: d.MonitorBias, | MonitorBias: d.MonitorBias, | ||||
| MonitorDetail: d.MonitorDetail, | MonitorDetail: d.MonitorDetail, | ||||
| MonitorWindow: d.MonitorWindow, | |||||
| RecordWindow: d.RecordWindow, | RecordWindow: d.RecordWindow, | ||||
| DecodeWindow: d.DecodeWindow, | DecodeWindow: d.DecodeWindow, | ||||
| RecordAdmission: d.RecordAdmission, | RecordAdmission: d.RecordAdmission, | ||||
| @@ -13,6 +13,7 @@ import ( | |||||
| "sdr-wideband-suite/internal/config" | "sdr-wideband-suite/internal/config" | ||||
| "sdr-wideband-suite/internal/detector" | "sdr-wideband-suite/internal/detector" | ||||
| "sdr-wideband-suite/internal/dsp" | "sdr-wideband-suite/internal/dsp" | ||||
| "sdr-wideband-suite/internal/pipeline" | |||||
| "sdr-wideband-suite/internal/recorder" | "sdr-wideband-suite/internal/recorder" | ||||
| ) | ) | ||||
| @@ -110,7 +111,13 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| } | } | ||||
| var debugInfo *SpectrumDebug | var debugInfo *SpectrumDebug | ||||
| plan := state.refinement.Input.Plan | plan := state.refinement.Input.Plan | ||||
| windowStats := buildWindowStats(state.refinement.Input.Windows) | |||||
| windowSummary := buildWindowSummary(plan, state.refinement.Input.Windows, state.surveillance.Candidates) | |||||
| var windowStats *RefinementWindowStats | |||||
| var monitorSummary []pipeline.MonitorWindowStats | |||||
| if windowSummary != nil { | |||||
| windowStats = windowSummary.Refinement | |||||
| monitorSummary = windowSummary.MonitorWindows | |||||
| } | |||||
| hasPlan := plan.TotalCandidates > 0 || plan.Budget > 0 || plan.DroppedBySNR > 0 || plan.DroppedByBudget > 0 | hasPlan := plan.TotalCandidates > 0 || plan.Budget > 0 || plan.DroppedBySNR > 0 || plan.DroppedByBudget > 0 | ||||
| hasWindows := windowStats != nil && windowStats.Count > 0 | hasWindows := windowStats != nil && windowStats.Count > 0 | ||||
| if len(thresholds) > 0 || len(displaySignals) > 0 || noiseFloor != 0 || hasPlan || hasWindows { | if len(thresholds) > 0 || len(displaySignals) > 0 || noiseFloor != 0 || hasPlan || hasWindows { | ||||
| @@ -149,8 +156,11 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| if len(candidateWindows) > 0 { | if len(candidateWindows) > 0 { | ||||
| debugInfo.CandidateWindows = candidateWindows | debugInfo.CandidateWindows = candidateWindows | ||||
| } | } | ||||
| if len(plan.MonitorWindowStats) > 0 { | |||||
| debugInfo.MonitorWindowStats = plan.MonitorWindowStats | |||||
| if len(monitorSummary) > 0 { | |||||
| debugInfo.MonitorWindowStats = monitorSummary | |||||
| } | |||||
| if windowSummary != nil { | |||||
| debugInfo.WindowSummary = windowSummary | |||||
| } | } | ||||
| if hasPlan { | if hasPlan { | ||||
| debugInfo.RefinementPlan = &plan | debugInfo.RefinementPlan = &plan | ||||
| @@ -167,8 +177,11 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||||
| if hasWindows { | if hasWindows { | ||||
| refinementDebug.Windows = windowStats | refinementDebug.Windows = windowStats | ||||
| } | } | ||||
| if len(plan.MonitorWindowStats) > 0 { | |||||
| refinementDebug.MonitorWindowStats = plan.MonitorWindowStats | |||||
| if len(monitorSummary) > 0 { | |||||
| refinementDebug.MonitorWindowStats = monitorSummary | |||||
| } | |||||
| if windowSummary != nil { | |||||
| refinementDebug.WindowSummary = windowSummary | |||||
| } | } | ||||
| refinementDebug.Arbitration = buildArbitrationSnapshot(state.refinement, state.arbitration) | refinementDebug.Arbitration = buildArbitrationSnapshot(state.refinement, state.arbitration) | ||||
| debugInfo.Refinement = refinementDebug | debugInfo.Refinement = refinementDebug | ||||
| @@ -165,7 +165,19 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime | |||||
| mux.HandleFunc("/api/refinement", func(w http.ResponseWriter, r *http.Request) { | mux.HandleFunc("/api/refinement", func(w http.ResponseWriter, r *http.Request) { | ||||
| w.Header().Set("Content-Type", "application/json") | w.Header().Set("Content-Type", "application/json") | ||||
| snap := phaseSnap.Snapshot() | snap := phaseSnap.Snapshot() | ||||
| windowStats := buildWindowStats(snap.refinement.Input.Windows) | |||||
| windowSummary := buildWindowSummary(snap.refinement.Input.Plan, snap.refinement.Input.Windows, snap.surveillance.Candidates) | |||||
| var windowStats *RefinementWindowStats | |||||
| var monitorSummary []pipeline.MonitorWindowStats | |||||
| if windowSummary != nil { | |||||
| windowStats = windowSummary.Refinement | |||||
| monitorSummary = windowSummary.MonitorWindows | |||||
| } | |||||
| if windowStats == nil { | |||||
| windowStats = buildWindowStats(snap.refinement.Input.Windows) | |||||
| } | |||||
| if len(monitorSummary) == 0 && len(snap.refinement.Input.Plan.MonitorWindowStats) > 0 { | |||||
| monitorSummary = snap.refinement.Input.Plan.MonitorWindowStats | |||||
| } | |||||
| arbitration := buildArbitrationSnapshot(snap.refinement, snap.arbitration) | arbitration := buildArbitrationSnapshot(snap.refinement, snap.arbitration) | ||||
| levelSet := snap.surveillance.LevelSet | levelSet := snap.surveillance.LevelSet | ||||
| spectraBins := map[string]int{} | spectraBins := map[string]int{} | ||||
| @@ -184,6 +196,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime | |||||
| "plan": snap.refinement.Input.Plan, | "plan": snap.refinement.Input.Plan, | ||||
| "windows": snap.refinement.Input.Windows, | "windows": snap.refinement.Input.Windows, | ||||
| "window_stats": windowStats, | "window_stats": windowStats, | ||||
| "window_summary": windowSummary, | |||||
| "request": snap.refinement.Input.Request, | "request": snap.refinement.Input.Request, | ||||
| "context": snap.refinement.Input.Context, | "context": snap.refinement.Input.Context, | ||||
| "detail_level": snap.refinement.Input.Detail, | "detail_level": snap.refinement.Input.Detail, | ||||
| @@ -220,7 +233,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime | |||||
| "candidate_evidence_states": candidateEvidenceStates, | "candidate_evidence_states": candidateEvidenceStates, | ||||
| "candidate_windows": candidateWindows, | "candidate_windows": candidateWindows, | ||||
| "monitor_windows": snap.refinement.Input.Plan.MonitorWindows, | "monitor_windows": snap.refinement.Input.Plan.MonitorWindows, | ||||
| "monitor_window_stats": snap.refinement.Input.Plan.MonitorWindowStats, | |||||
| "monitor_window_stats": monitorSummary, | |||||
| "display_level": snap.surveillance.DisplayLevel, | "display_level": snap.surveillance.DisplayLevel, | ||||
| "refinement_level": snap.refinement.Input.Level, | "refinement_level": snap.refinement.Input.Level, | ||||
| "presentation_level": snap.presentation, | "presentation_level": snap.presentation, | ||||
| @@ -22,6 +22,7 @@ type SpectrumDebug struct { | |||||
| CandidateEvidenceStates *CandidateEvidenceStateSummary `json:"candidate_evidence_states,omitempty"` | CandidateEvidenceStates *CandidateEvidenceStateSummary `json:"candidate_evidence_states,omitempty"` | ||||
| CandidateWindows []CandidateWindowSummary `json:"candidate_windows,omitempty"` | CandidateWindows []CandidateWindowSummary `json:"candidate_windows,omitempty"` | ||||
| MonitorWindowStats []pipeline.MonitorWindowStats `json:"monitor_window_stats,omitempty"` | MonitorWindowStats []pipeline.MonitorWindowStats `json:"monitor_window_stats,omitempty"` | ||||
| WindowSummary *WindowSummary `json:"window_summary,omitempty"` | |||||
| RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"` | RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"` | ||||
| Windows *RefinementWindowStats `json:"refinement_windows,omitempty"` | Windows *RefinementWindowStats `json:"refinement_windows,omitempty"` | ||||
| Refinement *RefinementDebug `json:"refinement,omitempty"` | Refinement *RefinementDebug `json:"refinement,omitempty"` | ||||
| @@ -42,6 +43,7 @@ type RefinementDebug struct { | |||||
| WorkItems []pipeline.RefinementWorkItem `json:"work_items,omitempty"` | WorkItems []pipeline.RefinementWorkItem `json:"work_items,omitempty"` | ||||
| Windows *RefinementWindowStats `json:"windows,omitempty"` | Windows *RefinementWindowStats `json:"windows,omitempty"` | ||||
| MonitorWindowStats []pipeline.MonitorWindowStats `json:"monitor_window_stats,omitempty"` | MonitorWindowStats []pipeline.MonitorWindowStats `json:"monitor_window_stats,omitempty"` | ||||
| WindowSummary *WindowSummary `json:"window_summary,omitempty"` | |||||
| Arbitration *ArbitrationSnapshot `json:"arbitration,omitempty"` | Arbitration *ArbitrationSnapshot `json:"arbitration,omitempty"` | ||||
| } | } | ||||
| @@ -0,0 +1,113 @@ | |||||
| package main | |||||
| import ( | |||||
| "sort" | |||||
| "sdr-wideband-suite/internal/pipeline" | |||||
| ) | |||||
| type WindowSummary struct { | |||||
| Refinement *RefinementWindowStats `json:"refinement,omitempty"` | |||||
| MonitorWindows []pipeline.MonitorWindowStats `json:"monitor_windows,omitempty"` | |||||
| } | |||||
| func buildWindowSummary(plan pipeline.RefinementPlan, refinementWindows []pipeline.RefinementWindow, candidates []pipeline.Candidate) *WindowSummary { | |||||
| refinementStats := buildWindowStats(refinementWindows) | |||||
| monitorSummary := buildMonitorWindowSummary(plan.MonitorWindows, plan.MonitorWindowStats, candidates) | |||||
| if refinementStats == nil && len(monitorSummary) == 0 { | |||||
| return nil | |||||
| } | |||||
| return &WindowSummary{ | |||||
| Refinement: refinementStats, | |||||
| MonitorWindows: monitorSummary, | |||||
| } | |||||
| } | |||||
| func buildMonitorWindowSummary(windows []pipeline.MonitorWindow, stats []pipeline.MonitorWindowStats, candidates []pipeline.Candidate) []pipeline.MonitorWindowStats { | |||||
| var summary []pipeline.MonitorWindowStats | |||||
| switch { | |||||
| case len(stats) > 0: | |||||
| summary = append([]pipeline.MonitorWindowStats(nil), stats...) | |||||
| case len(windows) > 0: | |||||
| summary = make([]pipeline.MonitorWindowStats, 0, len(windows)) | |||||
| for _, win := range windows { | |||||
| summary = append(summary, pipeline.MonitorWindowStats{ | |||||
| Index: win.Index, | |||||
| Label: win.Label, | |||||
| Zone: win.Zone, | |||||
| Source: win.Source, | |||||
| StartHz: win.StartHz, | |||||
| EndHz: win.EndHz, | |||||
| CenterHz: win.CenterHz, | |||||
| SpanHz: win.SpanHz, | |||||
| Priority: win.Priority, | |||||
| PriorityBias: win.PriorityBias, | |||||
| RecordBias: win.RecordBias, | |||||
| DecodeBias: win.DecodeBias, | |||||
| AutoRecord: win.AutoRecord, | |||||
| AutoDecode: win.AutoDecode, | |||||
| }) | |||||
| } | |||||
| default: | |||||
| return nil | |||||
| } | |||||
| if len(candidates) > 0 && len(summary) > 0 { | |||||
| windowsForMatch := windows | |||||
| if len(windowsForMatch) == 0 { | |||||
| windowsForMatch = monitorWindowsFromStats(summary) | |||||
| } | |||||
| if len(windowsForMatch) > 0 { | |||||
| counts := map[int]int{} | |||||
| total := 0 | |||||
| for _, cand := range candidates { | |||||
| matches := cand.MonitorMatches | |||||
| if len(matches) == 0 { | |||||
| matches = pipeline.MonitorWindowMatchesForCandidate(windowsForMatch, cand) | |||||
| } | |||||
| for _, match := range matches { | |||||
| counts[match.Index]++ | |||||
| total++ | |||||
| } | |||||
| } | |||||
| if total > 0 { | |||||
| for i := range summary { | |||||
| if summary[i].Candidates == 0 { | |||||
| summary[i].Candidates = counts[summary[i].Index] | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| sort.Slice(summary, func(i, j int) bool { | |||||
| return summary[i].Index < summary[j].Index | |||||
| }) | |||||
| return summary | |||||
| } | |||||
| func monitorWindowsFromStats(stats []pipeline.MonitorWindowStats) []pipeline.MonitorWindow { | |||||
| if len(stats) == 0 { | |||||
| return nil | |||||
| } | |||||
| windows := make([]pipeline.MonitorWindow, 0, len(stats)) | |||||
| for _, stat := range stats { | |||||
| windows = append(windows, pipeline.MonitorWindow{ | |||||
| Index: stat.Index, | |||||
| Label: stat.Label, | |||||
| Zone: stat.Zone, | |||||
| Source: stat.Source, | |||||
| StartHz: stat.StartHz, | |||||
| EndHz: stat.EndHz, | |||||
| CenterHz: stat.CenterHz, | |||||
| SpanHz: stat.SpanHz, | |||||
| Priority: stat.Priority, | |||||
| PriorityBias: stat.PriorityBias, | |||||
| RecordBias: stat.RecordBias, | |||||
| DecodeBias: stat.DecodeBias, | |||||
| AutoRecord: stat.AutoRecord, | |||||
| AutoDecode: stat.AutoDecode, | |||||
| }) | |||||
| } | |||||
| return windows | |||||
| } | |||||
| @@ -0,0 +1,47 @@ | |||||
| package main | |||||
| import ( | |||||
| "testing" | |||||
| "sdr-wideband-suite/internal/pipeline" | |||||
| ) | |||||
| func TestBuildMonitorWindowSummaryCountsCandidates(t *testing.T) { | |||||
| windows := []pipeline.MonitorWindow{ | |||||
| {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100}, | |||||
| {Index: 1, Label: "secondary", StartHz: 300, EndHz: 400, CenterHz: 350, SpanHz: 100}, | |||||
| } | |||||
| candidates := []pipeline.Candidate{ | |||||
| {ID: 1, CenterHz: 150, BandwidthHz: 20}, | |||||
| {ID: 2, CenterHz: 320, BandwidthHz: 10}, | |||||
| } | |||||
| summary := buildMonitorWindowSummary(windows, nil, candidates) | |||||
| if len(summary) != 2 { | |||||
| t.Fatalf("expected 2 window summaries, got %d", len(summary)) | |||||
| } | |||||
| if summary[0].Candidates != 1 || summary[1].Candidates != 1 { | |||||
| t.Fatalf("unexpected candidate counts: %+v", summary) | |||||
| } | |||||
| } | |||||
| func TestBuildMonitorWindowSummaryPreservesStatsCounts(t *testing.T) { | |||||
| stats := []pipeline.MonitorWindowStats{ | |||||
| {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100, Candidates: 2, Planned: 1}, | |||||
| } | |||||
| windows := []pipeline.MonitorWindow{ | |||||
| {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100}, | |||||
| } | |||||
| candidates := []pipeline.Candidate{ | |||||
| {ID: 1, CenterHz: 150, BandwidthHz: 20}, | |||||
| } | |||||
| summary := buildMonitorWindowSummary(windows, stats, candidates) | |||||
| if len(summary) != 1 { | |||||
| t.Fatalf("expected 1 window summary, got %d", len(summary)) | |||||
| } | |||||
| if summary[0].Candidates != 2 { | |||||
| t.Fatalf("expected candidates to stay at 2, got %d", summary[0].Candidates) | |||||
| } | |||||
| if summary[0].Planned != 1 { | |||||
| t.Fatalf("expected planned to stay at 1, got %d", summary[0].Planned) | |||||
| } | |||||
| } | |||||
| @@ -16,6 +16,7 @@ type SignalDecision struct { | |||||
| RecordBias float64 `json:"record_bias,omitempty"` | RecordBias float64 `json:"record_bias,omitempty"` | ||||
| DecodeBias float64 `json:"decode_bias,omitempty"` | DecodeBias float64 `json:"decode_bias,omitempty"` | ||||
| MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"` | MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"` | ||||
| MonitorWindow *MonitorWindowMatch `json:"monitor_window,omitempty"` | |||||
| RecordWindow *MonitorWindowMatch `json:"record_window,omitempty"` | RecordWindow *MonitorWindowMatch `json:"record_window,omitempty"` | ||||
| DecodeWindow *MonitorWindowMatch `json:"decode_window,omitempty"` | DecodeWindow *MonitorWindowMatch `json:"decode_window,omitempty"` | ||||
| RecordAdmission *PriorityAdmission `json:"record_admission,omitempty"` | RecordAdmission *PriorityAdmission `json:"record_admission,omitempty"` | ||||
| @@ -89,6 +90,7 @@ func DecideSignalAction(policy Policy, candidate Candidate, cls *classifier.Clas | |||||
| } | } | ||||
| if monitorDetail != nil { | if monitorDetail != nil { | ||||
| decision.MonitorDetail = monitorDetail | decision.MonitorDetail = monitorDetail | ||||
| decision.MonitorWindow = monitorDetail | |||||
| } | } | ||||
| return decision | return decision | ||||
| } | } | ||||
| @@ -30,6 +30,7 @@ type RefinementScoreDetails struct { | |||||
| PolicyBoost float64 `json:"policy_boost"` | PolicyBoost float64 `json:"policy_boost"` | ||||
| MonitorBias float64 `json:"monitor_bias,omitempty"` | MonitorBias float64 `json:"monitor_bias,omitempty"` | ||||
| MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"` | MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"` | ||||
| MonitorWindow *MonitorWindowMatch `json:"monitor_window,omitempty"` | |||||
| EvidenceScore float64 `json:"evidence_score"` | EvidenceScore float64 `json:"evidence_score"` | ||||
| EvidenceDetail *EvidenceScoreDetails `json:"evidence_detail,omitempty"` | EvidenceDetail *EvidenceScoreDetails `json:"evidence_detail,omitempty"` | ||||
| } | } | ||||
| @@ -201,6 +202,7 @@ func BuildRefinementPlanWithBudget(candidates []Candidate, policy Policy, budget | |||||
| PolicyBoost: policyBoost, | PolicyBoost: policyBoost, | ||||
| MonitorBias: monitorBias, | MonitorBias: monitorBias, | ||||
| MonitorDetail: monitorDetail, | MonitorDetail: monitorDetail, | ||||
| MonitorWindow: monitorDetail, | |||||
| EvidenceScore: evidenceScore, | EvidenceScore: evidenceScore, | ||||
| EvidenceDetail: &evidenceDetail, | EvidenceDetail: &evidenceDetail, | ||||
| }, | }, | ||||
| @@ -849,7 +849,8 @@ function updateHeroMetrics() { | |||||
| ? `CFAR ${showDebugOverlay ? 'on' : 'hidden'} · noise ${(Number.isFinite(debug.noise_floor) ? debug.noise_floor.toFixed(1) : 'n/a')} dB` | ? `CFAR ${showDebugOverlay ? 'on' : 'hidden'} · noise ${(Number.isFinite(debug.noise_floor) ? debug.noise_floor.toFixed(1) : 'n/a')} dB` | ||||
| : `CFAR off · noise ${(Number.isFinite(debug.noise_floor) ? debug.noise_floor.toFixed(1) : 'n/a')} dB`; | : `CFAR off · noise ${(Number.isFinite(debug.noise_floor) ? debug.noise_floor.toFixed(1) : 'n/a')} dB`; | ||||
| const plan = debug.refinement_plan || null; | const plan = debug.refinement_plan || null; | ||||
| const windows = debug.refinement_windows || null; | |||||
| const windowSummary = debug.window_summary || null; | |||||
| const windows = (windowSummary && windowSummary.refinement) || debug.refinement_windows || null; | |||||
| const refineInfo = plan && showDebugOverlay | const refineInfo = plan && showDebugOverlay | ||||
| ? `refine ${plan.selected?.length || 0}/${plan.budget || 0} drop ${plan.dropped_by_snr || 0}/${plan.dropped_by_budget || 0}` | ? `refine ${plan.selected?.length || 0}/${plan.budget || 0} drop ${plan.dropped_by_snr || 0}/${plan.dropped_by_budget || 0}` | ||||
| : ''; | : ''; | ||||
| @@ -881,7 +882,7 @@ function updateHeroMetrics() { | |||||
| healthRefinePlan.textContent = `${plan.selected?.length || 0}/${plan.budget || 0} · drop ${plan.dropped_by_snr || 0}/${plan.dropped_by_budget || 0} · rec ${recOn} dec ${decOn} ${queueText} ${reasonText}`; | healthRefinePlan.textContent = `${plan.selected?.length || 0}/${plan.budget || 0} · drop ${plan.dropped_by_snr || 0}/${plan.dropped_by_budget || 0} · rec ${recOn} dec ${decOn} ${queueText} ${reasonText}`; | ||||
| } | } | ||||
| if (healthRefineWindows) { | if (healthRefineWindows) { | ||||
| const stats = refinementInfo.window_stats || null; | |||||
| const stats = refinementInfo.window_summary?.refinement || refinementInfo.window_stats || null; | |||||
| if (stats && stats.count) { | if (stats && stats.count) { | ||||
| const levelSet = refinementInfo.surveillance_level_set || {}; | const levelSet = refinementInfo.surveillance_level_set || {}; | ||||
| const primary = levelSet.primary || refinementInfo.surveillance_level; | const primary = levelSet.primary || refinementInfo.surveillance_level; | ||||