# MapObjects Post-Flight Push + Durable Queue

**Task**: AZ-647_mission_client_mapobjects_push
**Name**: Post-flight MapObjects push with durable queue and crash-recovery push
**Description**: On `mission_executor` terminal state, drain `mapobjects_store`'s pending diff and POST to `/missions/{id}/mapobjects` + `/missions/{id}/mapobjects/ignored`. Independent retry per endpoint. Persist pending diff on disk for 24 h durable retry. At startup, replay any non-empty pending diff from a previously terminated mission BEFORE BIT for any new mission begins.
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-644_mission_client_pull_and_schema, AZ-646_mission_client_mapobjects_pull
**Component**: mission_client
**Tracker**: AZ-647
**Epic**: AZ-638

## Problem

The full pass diff (NEW / MOVED / EXISTING / REMOVED-candidate observations + IgnoredItem appends) must reach the central API after the mission ends. In-flight central writes are forbidden (Frozen choice 6 — `architecture.md §7.3`). The post-flight push must survive transient failure (independent retry per endpoint), persistent failure (operator-visible warning + manual replay), and crash mid-mission (next-boot push of pending diff). The durable queue is the disk-backed safety net.

## Outcome

- `MissionClient::push_mapobjects_diff(mission_id, diff) -> PushReport` posts the observations and ignored-items independently; partial success does not roll back the successful endpoint.
- The pending diff is persisted on disk at `${state_dir}/mapobjects_push/<mission_id>.json` BEFORE the push starts (write-ahead).
- Per-endpoint bounded exponential backoff (24 h durable retry window; configurable).
- Persistent failure: `sync_state = degraded`; operator-visible warning; entry stays on disk for manual replay.
- At startup, if `${state_dir}/mapobjects_push/` has any non-empty file, run the push for those missions BEFORE BIT for any new mission begins (crash-recovery path).

## Scope

### Included
- Two POST endpoints, called independently with separate retry/backoff state.
- Write-ahead persistence of the pending diff before the network call.
- Crash-recovery sweep at startup.
- `PushReport { observations: PerEndpointStatus, ignored: PerEndpointStatus }`.
- Health surface: `mapobjects_push_pending`, `last_push_ts`, per-endpoint last error.

### Excluded
- Building the pending diff (`mapobjects_store` — task 28 owns `pending_observations` + `pending_ignored`).
- Choosing what's a terminal state (`mission_executor`).
- Operator UI for the manual-replay warning (`operator_bridge` / Ground Station).

## Acceptance Criteria

**AC-1: Happy path push**
Given the mission ended with N observations and M ignored items
When `push_mapobjects_diff("M1", diff)` is called and both endpoints return 200
Then both succeed, the disk file is cleared, and `sync_state = synced`.

**AC-2: Partial success — independent retry**
Given `/mapobjects` returns 200 and `/mapobjects/ignored` returns 503
When the push runs
Then the observations endpoint is reported success, the ignored endpoint is queued for retry, and the disk file retains ONLY the ignored portion.

**AC-3: Persistent failure persists for manual replay**
Given both endpoints return 503 for all 24 h of bounded retry
When the retry window closes
Then `sync_state = degraded`, the disk file remains intact, and a manual-replay warning is observable in `health()`.

**AC-4: Crash-recovery push at startup**
Given a previous run terminated with a non-empty disk file at `${state_dir}/mapobjects_push/M0.json`
When the process starts a new run for mission `M1`
Then the push for `M0` is attempted before BIT begins for `M1`; the order is observable via logs.

**AC-5: 60-min mission push within budget**
Given a fixture pass diff sized for a 60-min mission
When the push is performed on a 100 Mbps loopback link
Then both endpoints complete in ≤2 min.

## Non-Functional Requirements

**Performance**
- ≤2 min for a 60-min mission's pass diff (per `description.md §8`).

**Reliability**
- 24 h durable retry window.
- Crash-mid-mission: nothing is lost on disk.

## Contract

- MapObjects POST schemas: `shared/contracts/mapobjects-observations.json` and `shared/contracts/mapobjects-ignored.json`. Owner: `../_docs/02_missions.md` §7.13 extension.
- Canonical typed model: `data_model.md §MapObjectObservation`, `§IgnoredItem`.

## Runtime Completeness

- **Named capability**: durable on-disk queue + post-flight push to the central `missions` API.
- **Production code that must exist**: real disk write-ahead (atomic rename); real HTTPS POST; real backoff state machine; real crash-recovery sweep.
- **Allowed external stubs**: `wiremock`/`mockito` for tests; `tempfile` for the disk-queue tests.
- **Unacceptable substitutes**: an in-memory-only queue is not acceptable (crash recovery requires disk).
