# SatelliteProviderRouteClient + seed_route.py CLI

**Task**: AZ-838_satellite_provider_route_client
**Name**: SatelliteProviderRouteClient + seed_route.py CLI: POST tlog-derived route to satellite-provider (AZ-835 C2)
**Description**: Second building block of Epic AZ-835. Consumer-side HTTP client + CLI wrapper that takes a `RouteSpec` (from AZ-836 / C1) and registers it with satellite-provider's `POST /api/satellite/route` endpoint, polls until `mapsReady=true`, and returns the inventory size for downstream consumption.
**Complexity**: 3 SP
**Dependencies**: AZ-836 (C1, RouteSpec dataclass + extractor — hard code dep); AZ-777 Phase 1 (existing satellite-provider HTTP plumbing patterns + JWT handling — done); AZ-809 (Route API validation — SOFT prereq, client pre-emptively validates so it's correct without it); AZ-835 (parent Epic)
**Component**: new `src/gps_denied_onboard/satellite_provider/route_client.py` + new CLI `tests/fixtures/derkachi_c6/seed_route.py`
**Tracker**: AZ-838 (https://denyspopov.atlassian.net/browse/AZ-838)
**Parent Epic**: AZ-835

Jira AZ-838 is the authoritative spec; this file is the in-workspace mirror.

## Public surface

```python
import uuid
from dataclasses import dataclass
from gps_denied_onboard.replay_input.tlog_route import RouteSpec  # AZ-836

@dataclass(frozen=True, slots=True)
class RouteSeedResult:
    route_id: uuid.UUID
    terminal_status: str
    maps_ready: bool
    tile_count: int
    elapsed_ms: int
    submitted_payload_sha256: str

class SatelliteProviderRouteError(Exception): ...
class RouteValidationError(SatelliteProviderRouteError): ...      # 4xx + ProblemDetails
class RouteTransientError(SatelliteProviderRouteError): ...       # 5xx / network / timeout
class RouteTerminalFailureError(SatelliteProviderRouteError): ... # mapsReady never reached

class SatelliteProviderRouteClient:
    def __init__(self, base_url: str, jwt: str, *, tls_insecure: bool = False,
                 request_timeout_s: float = 30.0, poll_interval_s: float = 5.0,
                 poll_max_attempts: int = 60): ...
    def seed_route(self, spec: RouteSpec, *, name: str | None = None) -> RouteSeedResult: ...
```

## Wire shape

No formal Route API contract doc exists in `../satellite-provider/_docs/02_document/contracts/api/` as of 2026-05-22. DTOs are the source of truth:

- `../satellite-provider/SatelliteProvider.Common/DTO/CreateRouteRequest.cs` (top-level)
- `../satellite-provider/SatelliteProvider.Common/DTO/RoutePoint.cs` (`[JsonPropertyName("lat")] Latitude`, `[JsonPropertyName("lon")] Longitude` — input/output naming asymmetry flagged in AZ-809 AC-10; consume `lat`/`lon` in the JSON)
- `../satellite-provider/SatelliteProvider.Common/DTO/GeoPoint.cs` (nested geofence point)

Probe 2026-05-22: 2-point route + `requestMaps=true` completes end-to-end in ~15 s.

## Behaviour

- **Pre-emptive validation** against AZ-809 rules — surface as `RouteValidationError` BEFORE HTTP POST:
  - `points` non-empty AND `len(points) <= 100`
  - `id` non-zero Guid
  - `regionSizeMeters > 0` AND `<= 10000`
  - `zoomLevel` in 15..18 (per AZ-777 Phase 2 bbox config)
  - Each point's `lat` in -90..90, `lon` in -180..180
- **Submit** `POST /api/satellite/route` with `requestMaps=true`, `createTilesZip=false`.
- **Poll** `GET /api/satellite/route/{id}` every `poll_interval_s` up to `poll_max_attempts` until `mapsReady=true` OR terminal failure. Log cadence at INFO.
- **Return** `RouteSeedResult`; `tile_count` from a final `POST /api/satellite/tiles/inventory` enumerating the route's tile coverage (computed locally from waypoints + `regionSizeMeters`).
- **Raise** `RouteTerminalFailureError` on terminal failure (`.detail` = SP response JSON).
- **Raise** `RouteTransientError` on 5xx / network / timeout (`__cause__` = underlying `httpx` exception).
- **Raise** `RouteValidationError` on 4xx; parse RFC 7807 `errors` dict into `field_errors`.

## CLI (`tests/fixtures/derkachi_c6/seed_route.py`)

Mirrors `seed_region.py` (AZ-777 Phase 2):

- Env: `SATELLITE_PROVIDER_URL`, `SATELLITE_PROVIDER_API_KEY`, `SATELLITE_PROVIDER_TLS_INSECURE`, optional `--auto-mint-jwt` (uses `scripts/mint_dev_jwt.py`)
- Required: `--tlog <path>` (delegates to AZ-836's `extract_route_from_tlog`)
- Optional: `--max-waypoints` (10), `--region-size-meters` (500), `--name`, `--output-summary <path>`, `--dry-run`
- Exit codes: 0 success, 71 config malformed, 72 missing env, 73 SP unreachable, 74 4xx, 75 5xx / terminal failure, 76 inventory verification mismatch

## Acceptance criteria

| # | Criterion |
|---|-----------|
| AC-1 | POSTs wire shape exactly per `CreateRouteRequest.cs` + `RoutePoint.cs` + `GeoPoint.cs` |
| AC-2 | Polls `GET /api/satellite/route/{id}` until `mapsReady=true` OR terminal failure; respects `poll_max_attempts` + `poll_interval_s` |
| AC-3 | 4xx + RFC 7807 ProblemDetails → `RouteValidationError`; `field_errors` populated from `errors` dict |
| AC-4 | 5xx / network / timeout → `RouteTransientError`; `__cause__` = underlying `httpx` exc |
| AC-5 | Terminal failure → `RouteTerminalFailureError`; `.detail` = SP response JSON |
| AC-6 | Pre-emptive validation rejects (BEFORE HTTP POST): empty `points`, >100 `points`, missing/zero `id`, missing/zero `regionSizeMeters`, OOR `zoomLevel`, OOR lat/lon |
| AC-7 | `seed_route.py --dry-run --tlog <derkachi.tlog>`: extracts route, prints planned payload + sha256, exit 0, no HTTP |
| AC-8 | `seed_route.py --tlog <derkachi.tlog>` against Jetson SP: exit 0, prints `RouteSeedResult`, optional summary JSON |
| AC-9 | Unit tests (mocked HTTPX): happy path, 400+ProblemDetails, 500 transient, terminal failure, timeout, dry-run, missing env, all pre-emptive validation cases |
| AC-10 | Integration test gated by `RUN_E2E=1` + `SATELLITE_PROVIDER_URL`: Derkachi route seeded, `tile_count > 0`, `maps_ready=True` |

## Out of scope

- FAISS index from seeded tiles (AZ-835 C3 / C5)
- C6 cache population (AZ-835 C3 — new `operator_pre_flight_setup` fixture)
- Modifying satellite-provider source (Route API consumed as-is)
- Multi-route batching (one RouteSpec → one POST)
- Authentication beyond existing JWT pattern (AZ-494)

## References

- Parent Epic: AZ-835 — https://denyspopov.atlassian.net/browse/AZ-835
- Sibling: AZ-836 (C1) — RouteSpec source
- Mirror CLI: `tests/fixtures/derkachi_c6/seed_region.py` (AZ-777 Phase 2)
- HTTP patterns: `src/gps_denied_onboard/components/c11_tile_manager/tile_downloader.py` (AZ-316/777 Phase 1)
- DTOs (in `../satellite-provider/`): `SatelliteProvider.Common/DTO/{CreateRouteRequest,RoutePoint,GeoPoint}.cs`
- Soft prereq: AZ-809 (Route API validation in satellite-provider)
