Let C2 name a talkgroup it already knows
84 of the 100 incidents in the 2026-08-23 dump were titled "Ems — TGID 9048"
or "Other — TGID 9600" -- the fallback, not a description. The title is the
incident's name everywhere it appears: list rows, map pins, Discord alerts.
_create_incident builds it from a content tag and a talkgroup label, and the
label was collapsing to "TGID {id}" because talkgroup_name arrived as None.
It is a plain form field on /upload, forwarded untouched into correlation, and
the node only sends it when OP25 had the name in its loaded tags file -- which
is exactly the case C2 can cover from its own systems collection, where all 125
talkgroup definitions live.
The lookup already existed, on the other path: mqtt_handler resolved it from
the system config on call_start. So the call document held the right name while
the pipeline that titles the incident ignored it. That asymmetry is the bug.
internal/talkgroups.py is now the one implementation -- caller's hint, then the
call document, then the system config -- and both paths use it.
_run_intelligence_pipeline resolves once at the funnel /upload and
/calls/{id}/reprocess share, so the dispatch-channel test, scene extraction and
the title all see a real name. When the call document was the thing missing it,
the resolved name is written back, so the archive and the orphan panel stop
showing a bare TGID too.
Also gives fast/thin a corr_fit_signal. It is 63% of all links and was the only
path writing none, so corr_fit_signal was absent on 295 of 309 calls and the
admin debug view's distribution panel read empty -- looking broken when it was
faithfully reporting that the dominant path records nothing. It now says
thin_recency, which is what actually decided it.
Closes server-26#34. Refs server-26#35 -- the tier's 3.5% invocation rate is a
cost/benefit question, not a bug, and stays open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a278e2215a
commit
039a06dc72
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Talkgroup name resolution.
|
||||
|
||||
C2 owns the `systems` collection, and a system's config carries the full
|
||||
talkgroup table — id and human name for every channel the node scans. The edge
|
||||
node only knows the name when OP25 happened to have it in the loaded tags file,
|
||||
so `tgid_name` on a call_start, and the `talkgroup_name` form field on /upload,
|
||||
are both frequently empty for a talkgroup C2 can name perfectly well.
|
||||
|
||||
This resolver is the single place that closes that gap. `mqtt_handler` had its
|
||||
own copy of the lookup on the call_start path, so calls got a name written to
|
||||
their document while the /upload path — the one that drives transcription,
|
||||
correlation and, critically, the incident *title* — kept whatever empty string
|
||||
the node sent. The result was 84 of 100 incidents named "Ems — TGID 9048"
|
||||
instead of "Ems — Ossining Police Dispatch" (server-26#34).
|
||||
|
||||
Order of preference: whatever the caller was given, then the call document
|
||||
(written at call_start), then the system config. Returns None when nothing
|
||||
knows the name, so callers keep their existing "TGID {id}" fallback.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from app.internal import firestore as fstore
|
||||
from app.internal.logger import logger
|
||||
|
||||
|
||||
async def name_from_system(system_id: Optional[str], talkgroup_id: Optional[int]) -> Optional[str]:
|
||||
"""Look a talkgroup's name up in its system's config. None if unknown."""
|
||||
if not system_id or talkgroup_id is None:
|
||||
return None
|
||||
try:
|
||||
tgid_int = int(talkgroup_id)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
system_doc = await fstore.doc_get_cached("systems", system_id)
|
||||
if not system_doc:
|
||||
return None
|
||||
|
||||
for tg in system_doc.get("config", {}).get("talkgroups", []):
|
||||
try:
|
||||
if int(tg.get("id", -1)) == tgid_int:
|
||||
return tg.get("name") or None
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
async def resolve(
|
||||
system_id: Optional[str],
|
||||
talkgroup_id: Optional[int],
|
||||
hint: Optional[str] = None,
|
||||
call_doc: Optional[dict] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Best available human name for a talkgroup.
|
||||
|
||||
`hint` is whatever the caller already had (OP25 metadata, a form field).
|
||||
`call_doc` is an already-fetched call document, if the caller has one —
|
||||
passing it avoids a second read.
|
||||
"""
|
||||
if hint:
|
||||
return hint
|
||||
|
||||
if call_doc:
|
||||
from_doc = call_doc.get("talkgroup_name")
|
||||
if from_doc:
|
||||
return from_doc
|
||||
|
||||
resolved = await name_from_system(system_id, talkgroup_id)
|
||||
if resolved:
|
||||
logger.info(
|
||||
f"Resolved talkgroup name from system config: "
|
||||
f"TGID {talkgroup_id} → {resolved!r}"
|
||||
)
|
||||
return resolved
|
||||
Reference in New Issue
Block a user