feat: enrich correlation debug with fit_signal and orphan breakdown

_call_fits_incident now returns (bool, signal_str) so each correlation
decision records exactly what evidence fired: unit_overlap, vehicle_overlap,
location_proximity, time_fallback, tactical_default, or the corresponding
false-return variants (unit_loc_conflict, content_divergence, etc.).

- corr_fit_signal and corr_matched_units written to call docs for
  fast/single and fast/disambig paths
- Admin debug endpoint exposes the new fields in calls_detail
- Orphan section adds orphans_by_talkgroup summary (count, no-type count,
  sweep-exhausted count per TGID) and raises orphan limit 100 → 250
- Admin page shows corr_path and fit_signal distribution panels above raw
  JSON; time_fallback highlighted in yellow as a diagnostic marker

No correlation logic changed — diagnostic data only.
This commit is contained in:
Logan
2026-05-25 12:54:34 -04:00
parent 7dd090e8b2
commit 4b7d9dd49a
3 changed files with 128 additions and 24 deletions
+62 -1
View File
@@ -113,7 +113,28 @@ function CorrelationDebugTab() {
}
const json = data ? JSON.stringify(data, null, 2) : null;
const meta = data as { incident_count?: number; orphaned_call_count?: number; generated_at?: string } | null;
const meta = data as {
incident_count?: number;
orphaned_call_count?: number;
generated_at?: string;
incidents?: Array<{ calls_detail?: Array<{ corr_path?: string; corr_fit_signal?: string }> }>;
orphans_by_talkgroup?: Array<{ talkgroup_id?: number; talkgroup_name?: string; count: number; no_type_count: number; sweep_exhausted_count: number }>;
} | null;
// Aggregate corr_path and corr_fit_signal counts across all incident calls.
const pathCounts: Record<string, number> = {};
const signalCounts: Record<string, number> = {};
if (meta?.incidents) {
for (const inc of meta.incidents) {
for (const call of inc.calls_detail ?? []) {
const p = call.corr_path ?? "unknown";
pathCounts[p] = (pathCounts[p] ?? 0) + 1;
if (call.corr_fit_signal) {
signalCounts[call.corr_fit_signal] = (signalCounts[call.corr_fit_signal] ?? 0) + 1;
}
}
}
}
return (
<div className="space-y-4">
@@ -172,6 +193,46 @@ function CorrelationDebugTab() {
</div>
)}
{meta && Object.keys(pathCounts).length > 0 && (
<div className="grid grid-cols-2 gap-3">
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-1">
<p className="text-xs text-gray-400 font-mono font-semibold mb-2">corr_path distribution</p>
{Object.entries(pathCounts).sort((a, b) => b[1] - a[1]).map(([path, n]) => (
<div key={path} className="flex justify-between text-xs font-mono">
<span className="text-gray-300">{path}</span>
<span className="text-indigo-400">{n}</span>
</div>
))}
</div>
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-1">
<p className="text-xs text-gray-400 font-mono font-semibold mb-2">fit_signal distribution</p>
{Object.keys(signalCounts).length === 0
? <p className="text-xs text-gray-600 font-mono">No signal data yet deploy correlator update first</p>
: Object.entries(signalCounts).sort((a, b) => b[1] - a[1]).map(([sig, n]) => (
<div key={sig} className="flex justify-between text-xs font-mono">
<span className={sig === "time_fallback" ? "text-yellow-400" : "text-gray-300"}>{sig}</span>
<span className="text-indigo-400">{n}</span>
</div>
))
}
</div>
</div>
)}
{meta?.orphans_by_talkgroup && meta.orphans_by_talkgroup.length > 0 && (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-1">
<p className="text-xs text-gray-400 font-mono font-semibold mb-2">orphans by talkgroup</p>
{meta.orphans_by_talkgroup.map((tg) => (
<div key={String(tg.talkgroup_id)} className="flex justify-between text-xs font-mono">
<span className="text-gray-300">{tg.talkgroup_name} <span className="text-gray-600">({tg.talkgroup_id})</span></span>
<span className="text-gray-400">
{tg.count} total · <span className="text-gray-500">{tg.no_type_count} no-type</span> · <span className="text-red-400">{tg.sweep_exhausted_count} exhausted</span>
</span>
</div>
))}
</div>
)}
{json && (
<pre
ref={preRef}