area_context v2 + Maps place verification (server-26#36, #37)
#36 — the correction pass shipped in 58efdbd was right, its reference-data
shape was not. One shape now, at both scopes, every field nullable:
area_context: { municipality?, county?, state?,
center?, radius_km?, resolved_from?, resolved_at?,
local_knowledge?: [{term, meaning}] }
`state` closes the ambiguity that made "Ossining" a national guess.
`local_knowledge` replaces roads[]/landmarks[], which could not hold
intersections, schools or nicknames and carried no meanings — `11-X-ray` is
useless alone, `11-X-ray — MTA PD patrol unit` is what a corrector can act on.
Pre-#36 roads[]/landmarks[] are read forward as bare terms so nothing an
operator already entered is lost.
Nullability is the mechanism: which scope gets filled is the operator's
declaration of how homogeneous the system is. One town — fill it once at system
level. Statewide — leave it blank and fill each talkgroup.
The backend owns the derived anchor. PUT /systems/{id} merges config.talkgroups[]
against what is stored instead of writing the client's blob verbatim, which
would have erased the anchor and the pending queue — the same defect as the
ten_codes wipe.
#37 — Maps as a verifier, not as prompt stuffing. The corrector emits its
location nouns; each is geocoded against the talkgroup's anchor, and on a miss
we look for a sound-alike that does resolve there, correct to it, and propose
{term, meaning} to that talkgroup. Cost scales with location nouns, not calls.
No anchor means SKIP. An area too wide to discriminate stores no anchor at all,
because a statewide radius would confirm anything inside it — verification that
passes everything is worse than none, since it reads as a check in the data.
Also re-anchors _geocode_location, which rejected results >40km from the NODE
(server-26#6). An antenna is not a jurisdiction; distance-from-node was always
a stand-in for the anchor and is now only the fallback.
The induction loop proposes at talkgroup level and never promotes. Blast
radius: a wrong term on a channel misleads that channel, the same term
system-wide misleads one 400km away on a statewide system.
38 new tests; 240 pass. Frontend typechecks clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
58efdbd6eb
commit
964343c819
@@ -1,5 +1,5 @@
|
||||
import { auth } from "@/lib/firebase";
|
||||
import type { AreaContext } from "@/lib/types";
|
||||
import type { AreaContext, TalkgroupPending } from "@/lib/types";
|
||||
|
||||
const BASE = process.env.NEXT_PUBLIC_C2_URL ?? "http://localhost:8000";
|
||||
|
||||
@@ -151,12 +151,38 @@ export const c2api = {
|
||||
// {name, type, config} and would otherwise wipe them on every save.
|
||||
getAreaContext: (systemId: string) =>
|
||||
request<{ area_context: AreaContext }>(`/systems/${systemId}/area-context`),
|
||||
// Only the operator-set fields go up. center/radius_km/resolved_* are the
|
||||
// backend's — it geocodes them from the place and merges them back, and the
|
||||
// response carries the anchor its edit produced.
|
||||
updateAreaContext: (systemId: string, area: AreaContext) =>
|
||||
request<{ ok: boolean; area_context: AreaContext }>(
|
||||
`/systems/${systemId}/area-context`,
|
||||
{ method: "PUT", body: JSON.stringify(area) },
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
municipality: area.municipality ?? null,
|
||||
county: area.county ?? null,
|
||||
state: area.state ?? null,
|
||||
local_knowledge: area.local_knowledge ?? [],
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
||||
// Talkgroup-level pending local knowledge (server-26#37). Proposals land on
|
||||
// the talkgroup and are never promoted to the system automatically.
|
||||
getTalkgroupPending: (systemId: string) =>
|
||||
request<{ talkgroups: TalkgroupPending[] }>(`/systems/${systemId}/talkgroup-pending`),
|
||||
approveTalkgroupTerm: (systemId: string, talkgroupId: number, term: string) =>
|
||||
request(`/systems/${systemId}/talkgroup-pending/approve`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ talkgroup_id: talkgroupId, term }),
|
||||
}),
|
||||
dismissTalkgroupTerm: (systemId: string, talkgroupId: number, term: string) =>
|
||||
request(`/systems/${systemId}/talkgroup-pending/dismiss`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ talkgroup_id: talkgroupId, term }),
|
||||
}),
|
||||
|
||||
// Vocabulary
|
||||
getVocabulary: (systemId: string) =>
|
||||
request<{ vocabulary: string[]; vocabulary_pending: { term: string; source: "induction" | "correction"; added_at: string }[]; vocabulary_bootstrapped: boolean }>(
|
||||
|
||||
@@ -237,9 +237,40 @@ export interface AlertEvent {
|
||||
* a multi-county system can be specific per channel without its wider list
|
||||
* burying the detail.
|
||||
*/
|
||||
export interface LocalKnowledgeEntry {
|
||||
term: string;
|
||||
meaning?: string | null;
|
||||
}
|
||||
|
||||
export interface AreaContext {
|
||||
// Set by an operator. Every field nullable on purpose: which SCOPE gets
|
||||
// filled is the declaration of how homogeneous the system is. A one-town
|
||||
// system is described once here and inherited by every talkgroup; a
|
||||
// statewide one is left empty here and described per talkgroup.
|
||||
municipality?: string;
|
||||
county?: string;
|
||||
roads?: string[];
|
||||
landmarks?: string[];
|
||||
state?: string;
|
||||
local_knowledge?: LocalKnowledgeEntry[];
|
||||
// Written by the backend, read-only here. Absent means the place is unset or
|
||||
// too wide to discriminate, and location verification skips entirely
|
||||
// (server-26#37) — never send these back.
|
||||
center?: { lat: number; lng: number };
|
||||
radius_km?: number;
|
||||
resolved_from?: string;
|
||||
resolved_at?: string;
|
||||
}
|
||||
|
||||
/** A term the verifier or the induction loop proposed for one talkgroup. */
|
||||
export interface PendingLocalTerm {
|
||||
term: string;
|
||||
meaning?: string | null;
|
||||
source: string;
|
||||
added_at: string;
|
||||
source_call_ids?: string[];
|
||||
}
|
||||
|
||||
export interface TalkgroupPending {
|
||||
talkgroup_id: number;
|
||||
talkgroup_name?: string;
|
||||
pending: PendingLocalTerm[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user