central/sql/migrations/041_add_sat_orbits_adapter.sql

37 lines
1.4 KiB
MySQL
Raw Normal View History

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
-- Migration 041: register sat_orbits adapter (v0.13.0)
--
-- Forward-orbit-track publisher. One LineString telemetry event per tracked
-- satellite per poll, projecting the next 90 minutes of sub-satellite
-- ground track. Line counterpart to sat_positions (which publishes the
-- current sub-sat POINT per minute). Drives the "each sat's path" map
-- view Matt asked for after enabling the satellite family.
--
-- Subject: central.sat.orbit.<norad_id>. Stream: existing CENTRAL_SAT.
-- The supervisor's STREAM_CATEGORY_DOMAINS["CENTRAL_SAT"] extends from
-- ("tle", "pass", "position") to ("tle", "pass", "position", "orbit")
-- in code (not migration) so the retention sweep covers orbit events.
--
-- No max_bytes bump needed on CENTRAL_SAT. Volume estimate: 6 sats x 12
-- polls/hour (300s cadence) x 24 hours = 1728 events/day at ~5 KB each
-- (~90 vertices) = ~8.5 MB/day. Negligible against the 5 GiB cap from
-- v0.12.0.
--
-- Ships disabled (enabled=false). celestrak_tle must be enabled and
-- polling before sat_orbits has TLE data to propagate; missing-TLE path
-- is graceful (INFO log + zero events).
--
-- Idempotent: ON CONFLICT preserves operator-tuned state.
INSERT INTO config.adapters (name, enabled, cadence_s, settings)
VALUES (
'sat_orbits',
false,
300,
'{
"track_only_norad_ids": [],
"forward_minutes": 90,
"sample_seconds": 60,
"max_tle_age_days": 14
}'::jsonb
)
ON CONFLICT (name) DO NOTHING;