central/tests
malice 3f1fec9846
v0.13.0: sat_orbits adapter (forward-orbit-track per satellite) + antimeridian splitter
## Matt's "each sat's path" framing

After enabling the satellite family in v0.12.1, the `/events` map showed overlapping orange visibility-footprint circles from satpass_predict + a polar-orbit ground track wrapping the wrong way across the antimeridian (the v0.11.2 documented limitation). Matt's ask:

> honestly i just want each sats path.

Interpreted as: one continuous orbital track per satellite, color-coded, no observer-specific clutter, no visibility-footprint overlays. Six tracked sats = six distinguishable lines on the map.

## Family placement — global line counterpart to global points

| Adapter | What it publishes | Geometry | Cadence |
|---|---|---|---|
| satpass_predict (v0.11.1) | Observer-anchored pass alerts | LineString ground-track + Polygon footprint per pass | 1h |
| sat_positions (v0.12.0) | Current sub-sat POINT per sat | Point centroid only | 60s |
| **sat_orbits (this PR)** | Forward-orbit LINE per sat | LineString / MultiLineString, 90min horizon | 5min |

Each answers a different question; they complement.

## Antimeridian splitter — shared sat_common primitive

`split_antimeridian(coords)` lives in `sat_common.py` next to `gmst_rad` / `eci_to_ecef` / `subsatellite_point`. Returns `None` for <2 vertices, a `LineString` dict for the common no-crossing case, or a `MultiLineString` dict when one or more ±180° crossings exist. Each crossing closes the current segment at `sign(prev_lon)*180` with a linearly-interpolated latitude and starts the next at `sign(cur_lon)*180` with the same lat (sub-0.1° error at LEO orbital speeds, well below Leaflet rendering precision).

**Sibling concern fixed:** `satpass_predict._build_pass_geometry` now routes its `ground_track` through `split_antimeridian` too. This was the v0.11.2 documented limitation ("polar-orbit crossings near ±180° will produce a polygon that visually wraps the wrong way"). Sat_orbits and satpass_predict share the helper because the antimeridian problem is identical for both — and **44/44 existing satpass_predict tests still pass** because the splitter returns a LineString identical in shape to the prior inline construction when there's no crossing (which is the case for every CONUS-observer ISS-fixture test).

New test specifically for the splitter inside `_build_pass_geometry`: synthesized polar-orbit `ground_track` produces a `GeometryCollection` whose linear-geometry component is a `MultiLineString` with 2 segments (first ends at +180, second starts at -180).

## GUI per-NORAD-ID color helper

20-line addition to `events_list.html`:

```js
function orbitColorForNoradId(norad) {
    var hue = (norad * 137.508) % 360;  // golden-angle hue distribution
    return "hsl(" + hue.toFixed(1) + ", 70%, 50%)";
}
function getRowColor(adapter, row) {
    if (adapter === "tomtom_flow") return flowColor(row.dataset.severity);
    if (adapter === "sat_orbits") {
        var norad = parseInt((row.dataset.eventId || "").split(":")[0], 10);
        if (!isNaN(norad)) return orbitColorForNoradId(norad);
    }
    return getAdapterColor(adapter);
}
```

`event_id` shape is `<norad_id>:<iso>` (same as sat_positions), so JS reads the first colon-token. **Additive**: tomtom_flow keeps its severity-based color, every other adapter keeps its per-adapter palette color, sat_orbits gets per-satellite distinguishable lines.

## Phase A sanity (per spec)

```
vertices = 91                                      ✓ (90min @ 60s + 1 endpoint)
first vertex = (170.66°, -17.15°, 417.4km)        ✓ matches v0.11.1 ISS pin
last vertex  = (140.52°, -8.60°, 415.9km)         ✓ geographically distinct
antimeridian crossings in 90min track = 1
geometry type = MultiLineString, 2 segments        ✓ splitter integrates
```

## Diff size

**+838 / −9 = +829 net** across 15 files. Spec budget was ≤800 lines. **29 over** — much tighter than v0.12.0 (894) or v0.12.1 (848). Adapter LoC 275 (well under 350 cap). sat_common splitter 51 LoC (~budget).

Test breakdown: 285 (sat_orbits) + 60 (sat_common splitter) + 26 (satpass regression) + 12 (events_feed) + 4 (telemetry-separation) = 387 LoC tests. Production: 275 + 51 + 37 (migration) + 41 (doc) + 16 (partials) + 21 (JS) + 15 (satpass refactor) + 2 (wiring) = 458 LoC.

## Test plan

- [x] `pytest tests/test_sat_orbits.py` — 19 new tests, all pass.
- [x] `pytest tests/test_sat_common.py` — 7 new splitter tests, 16 total pass.
- [x] `pytest tests/test_satpass_predict.py` — **45/45 pass** (44 existing regression-guard + 1 new polar-orbit splitter integration test). The `_build_pass_geometry` rewire is byte-identical for non-crossing tracks.
- [x] `pytest tests/test_events_feed_frontend.py` — 125/125 pass (sat_orbits sample + expected subject extended).
- [x] `pytest tests/test_telemetry_separation.py` — 9/9 pass (`_TELEMETRY` pin extended with `sat_orbits`).
- [x] `pytest tests/test_consumer_doc.py` — 6/6 pass (new `### sat_orbits` subsection accepted).
- [x] Full sweep `pytest tests/` (excluding postgres-dep files): **1274 passed, 1 skipped, 0 failures**.
- [x] Ruff: clean on all new + touched satellite-family code.

## Deploy plan

1. Squash-merge PR #N → tag v0.13.0 at merge SHA → push tag.
2. `ssh central`, `git pull` on `/opt/central`. **No `uv sync`** (no new dep).
3. Apply migration 041 manually via psql (per option C):
   `sudo -u postgres psql central -f /opt/central/sql/migrations/041_add_sat_orbits_adapter.sql`
4. `sudo systemctl restart central-supervisor` (picks up new adapter + STREAM_CATEGORY_DOMAINS extension) + `sudo systemctl restart central-gui` (picks up new partials + ADAPTER_GROUPS extension + JS color helper).
5. **No** `central-archive` restart (CENTRAL_SAT pre-existed; only the category-domain tuple grew, archive already covers `central.sat.>`).
6. Verify: `config.adapters` has `sat_orbits` row with `enabled=false`; supervisor log shows discovery; no polling until Matt flips it.
7. Matt enables via `/adapters/sat_orbits/edit` when ready. First poll happens within 5min; orbit-track LineStrings surface at `/telemetry` filtered by adapter=sat_orbits, color-coded per NORAD ID.

## Halt acknowledgment

Per spec acceptance bar #6: **squash-merge NOT authorized**. Branch + PR open. Halting for line-by-line review.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-09 18:50:47 -06:00
..
fixtures v0.11.0: new celestrak_tle adapter + CENTRAL_SAT satellite-tracking stream (#100) 2026-06-09 00:54:19 -06:00
__init__.py
conftest.py
README.md
test_adapters.py
test_api_key_resolver.py
test_api_keys.py
test_apply_enrichment_coordless.py
test_archive_bbox_filter.py v0.10.2: monitoring-area bbox enforced at supervisor publish (was archive-only) (#PR_NUMBER_PLACEHOLDER) 2026-06-05 20:34:10 -06:00
test_archive_multi_stream.py
test_audit.py
test_auth.py
test_avalanche_org.py v0.10.11: extend avalanche_org adapter — tombstones, geo.bbox, hyphen slugs (#99) 2026-06-08 23:08:22 -06:00
test_backend_settings_schema.py
test_bootstrap_config.py
test_celestrak_tle.py v0.11.1: satpass_predict adapter (server-side pass alerts for fixed observers) (#101) 2026-06-09 01:16:43 -06:00
test_config_source.py
test_config_store.py
test_consumer_doc.py v0.10.3: rip out state_511_atis adapter (superseded by itd_511 v0.10.0; Castle Rock legacy shape EOL per sister-site discovery) (#88) 2026-06-06 14:44:00 -06:00
test_crypto.py
test_csrf_handler.py
test_csrf_race_condition.py
test_dashboard.py
test_dedup_mixin.py
test_enrichment_config_plumbing.py
test_enrichment_framework.py
test_enrichment_locations_coverage.py
test_enrichment_mile_marker.py v0.10.6: extract mile_marker from itd_511 comment field as _enriched.mile_marker (#94) 2026-06-07 21:38:04 -06:00
test_eonet.py v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters 2026-05-27 23:50:30 -06:00
test_events_adapter_column.py
test_events_bbox_guard.py
test_events_feed.py
test_events_feed_frontend.py v0.13.0: sat_orbits adapter (forward-orbit-track per satellite) + antimeridian splitter 2026-06-09 18:50:47 -06:00
test_events_filtering.py
test_events_pagination.py
test_events_retention.py
test_fire_fused.py
test_firms.py v0.10.2: monitoring-area bbox enforced at supervisor publish (was archive-only) (#PR_NUMBER_PLACEHOLDER) 2026-06-05 20:34:10 -06:00
test_form_descriptors.py v0.11.3: fix GUI adapter-edit 500 on list[int] settings fields 2026-06-09 13:26:57 -06:00
test_gdacs.py
test_geocoder_enricher.py
test_gui_adapter_edit.py v0.10.3: rip out state_511_atis adapter (superseded by itd_511 v0.10.0; Castle Rock legacy shape EOL per sister-site discovery) (#88) 2026-06-06 14:44:00 -06:00
test_gui_scaffold.py
test_inciweb.py
test_itd_511.py v0.10.6: extract mile_marker from itd_511 comment field as _enriched.mile_marker (#94) 2026-06-07 21:38:04 -06:00
test_itd_511_cameras.py v0.10.0: ITD 511 official API adapter (events + advisories + cameras) (#85) 2026-06-03 22:36:26 -06:00
test_migrate.py v0.9.18: reconcile schema_migrations drift + add --check drift detection 2026-05-27 06:40:38 +00:00
test_models.py v0.10.8: discriminate Nats-Msg-Id by event.category to prevent incident+perimeter dedup collision (#96) 2026-06-08 01:12:22 -06:00
test_monitoring_area.py v0.10.9: widen monitoring-area default to cover all of Idaho (49.0N) (#97) 2026-06-08 01:42:59 -06:00
test_n2yo_visualpasses.py v0.12.1: n2yo_visualpasses adapter (server-side visible-pass alerts) 2026-06-09 16:00:55 -06:00
test_navi_backend.py
test_nominatim_backend.py
test_nwis.py v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters 2026-05-27 23:50:30 -06:00
test_nwis_enrichment.py
test_nws_normalization.py v0.10.7: fix NWS SAME state-FIPS parse + 5-digit ANSI county form (#95) 2026-06-08 00:30:13 -06:00
test_photon_backend.py
test_preview_hook.py
test_producer_doc.py
test_region_picker.py
test_requires_api_key.py
test_resend.py v0.10.5.2: fix BY_START_TIME feedback loop in Re-send (snapshot last_seq boundary) (#93) 2026-06-06 22:36:04 -06:00
test_sat_common.py v0.13.0: sat_orbits adapter (forward-orbit-track per satellite) + antimeridian splitter 2026-06-09 18:50:47 -06:00
test_sat_orbits.py v0.13.0: sat_orbits adapter (forward-orbit-track per satellite) + antimeridian splitter 2026-06-09 18:50:47 -06:00
test_sat_positions.py v0.12.0: sat_positions adapter (live global satellite positions) + sat_common refactor 2026-06-09 15:23:32 -06:00
test_satpass_predict.py v0.13.0: sat_orbits adapter (forward-orbit-track per satellite) + antimeridian splitter 2026-06-09 18:50:47 -06:00
test_session_auth.py
test_setup_gate.py
test_stream_registry.py
test_streams.py
test_subject_helpers.py v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters 2026-05-27 23:50:30 -06:00
test_supervisor_hotreload.py
test_supervisor_integration.py
test_supervisor_publish_filter.py v0.10.2: monitoring-area bbox enforced at supervisor publish (was archive-only) (#PR_NUMBER_PLACEHOLDER) 2026-06-05 20:34:10 -06:00
test_swpc.py
test_telemetry_separation.py v0.13.0: sat_orbits adapter (forward-orbit-track per satellite) + antimeridian splitter 2026-06-09 18:50:47 -06:00
test_tomtom_flow.py v0.10.2: monitoring-area bbox enforced at supervisor publish (was archive-only) (#PR_NUMBER_PLACEHOLDER) 2026-06-05 20:34:10 -06:00
test_tomtom_flow_passthrough.py
test_tomtom_incidents.py
test_usgs_quake.py v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters 2026-05-27 23:50:30 -06:00
test_wfigs.py v0.10.4: switch wfigs_incidents to non-Current endpoint w/ WF active-only filter (resurrects IMT-managed fires like Blue Ridge) (#89) 2026-06-06 18:10:16 -06:00
test_wizard.py
test_wzdx.py WZDx: poll-time state allowlist with Idaho-region default (v0.9.17) 2026-05-27 05:57:57 +00:00

Central Tests

Test Database

Some tests (notably test_config_store.py) require a real PostgreSQL database. By default, tests connect to:

postgresql://central_test:testpass@localhost/central_test

If your test database uses different credentials, set the CENTRAL_TEST_DB_DSN environment variable:

export CENTRAL_TEST_DB_DSN="postgresql://myuser:mypass@localhost/mydb"
uv run pytest tests/test_config_store.py