Skip to content

refactor(contacts): deepen the contact-sighting module - #12

Merged
clorth0 merged 1 commit into
mainfrom
refactor/contact-sighting
Oct 18, 2026
Merged

clorth0 merged 1 commit into
mainfrom
refactor/contact-sighting

Conversation

@clorth0

@clorth0 clorth0 commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Candidate #4 from the architecture review: deepen the contact-sighting module. Identity + dedup only — destination unchanged.

Inventory (pre-refactor, origin/main a673e07)

Source Identity shape Dedup key Window / TTL Consumer today
Live ISM (rtl_433 via _emit_event, app.py:247-252) ident = str(event.get("id", event.get("channel", ""))); info picks channel/battery_ok/temperature_C/humidity/freq/rssi ism:{model}:{ident} Per-key 2.0s write throttle (_CONTACT_WRITE_DT, anchored at last write; suppressed attempts don't refresh); 3.0s global contacts emit throttle store.record_contact → sqlite contacts PK → contacts socket event. decoded emitted every event
Live ADS-B (_emit_adsb, app.py:372-386) hex (a["hex"], skipped if falsy); label = flight.strip() or registration or hex.upper(); info = registration/country/alt/gs (non-None) adsb:{hex} Same 2.0s per-key throttle Same store + adsb socket event every update
Scan rtl_433 (_phase_rtl433, scan.py:252) {model or '?'}/{id|hex|channel or '?'} (id > hex > channel) same string, _key on device dicts Whole phase (rtl433_seconds, default 20s), no TTL; first event wins, count++, last_time updates phase.findings["devices"] → phase_completed; scan_decoded live-emitted for every event. Never the store
Scan ADS-B (_phase_adsb, scan.py:295-298) hex alone hex Whole phase (adsb_seconds), no TTL; latest aircraft per hex wins phase.findings["aircraft"] → phase_completed. Never the store

No other protocols construct sighting identity: Wi-Fi/BLE bands appear only as sweep band labels (scan.py BAND_MAP), with no sighting/dedup code. Frontend keys ADS-B markers by raw hex for map rendering only (waterfall.js:1489) — unchanged, not a sighting store.

Design

One deep module, backend/contacts.py (named in docs/architecture.md; architecture-names check passes):

  • ContactSighting + ism_sighting(event, lat, lon) / adsb_sighting(aircraft, registration, country) — derivation of key/kind/label/ident/info/position in one place. Enrichment (readsb r, icao_to_registration/icao_country) stays in app.py; the module owns how it joins the sighting.
  • Two keys named explicitly, not forced into one: durable inventory_key (ism:{model}:{ident}, adsb:{hex}) vs ephemeral scan_key ({model}/{id|hex|channel}, hex alone for ADS-B). Key-byte parity: both are byte-identical to the old strings — the inventory key is the sqlite contacts PRIMARY KEY and crosses to the frontend, so renaming would orphan stored history. No key changed; none flagged for change.
  • Dedup: SightingThrottle(window_s) (register/seen; window anchored at last passed sighting) for live writes, ScanSightings (phase-lifetime, no TTL; first-wins+count for ISM, latest-wins for ADS-B) for scan findings. app.py and scan.py both consume these; no key string is built outside the module anymore (grep ism: in app/scan: none).

Parked question — untouched

"Should Auto-Scan sightings enter the persistent inventory or stay scan-only?" is NOT answered. Scan sightings go exactly where they went today (phase findings only); scan.py has no store reference. Inventory contents are unchanged: same keys, same rows, same throttle. This PR only gives sighting identity/dedup one home.

Parity evidence

  • Characterization tests pin, per source: first sighting passes, repeat within window suppressed, at/after window passes, different model/ident not suppressed, suppressed attempt does not refresh the window, scan hex-alone collision (same hex, different flight → latest wins, one entry).
  • App consumer tests: _emit_event records exactly ("ism:M:7", "ism", "M", "7", {...}) once for two events while decoded is emitted twice; _emit_adsb records adsb:abc123/UAL1 with position, no-hex aircraft skipped.
  • Scan phase tests (fake decoder/receiver driving the real _phase_rtl433/_phase_adsb): devices ["M/7", "Other/7"] with _count=2, findings hold the deduped lists, scan_decoded emitted for every event.

Tests + mutant

  • New tests/test_contact_sighting.py: 11/11 (written failing-first — ModuleNotFoundError: backend.contacts — then green).
  • Mutant: dropping the model prefix from both ISM key builders in backend/contacts.py only → 6/11, reddening dedup across both consumers (test_app_emit_event_records_through_module, test_scan_ism_dedup, test_scan_phases_consume_module_and_stay_scan_only, + key-byte tests). Restored → 11/11.

CI snapshot (local, CI-way)

All tests/test_*.py script-style suites pass (24 files incl. new one), check_html_templates.py pass, check_architecture_names.py pass, node --check on all frontend JS pass. GitHub test check: pass (run 39144786324, 22s).

Candidates #5–#6, auth, and other parked questions untouched. Do not merge without review.

@clorth0
clorth0 merged commit d1ee931 into main Oct 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant