feat(3-1): stale state detection and dimmed display

When fetch times out or returns empty after prior data, retain last aircraft
with is_stale=True and render them as black outlines so the display stays
informative rather than blank or crashing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Edholm
2026-04-22 23:47:04 -04:00
parent 316e7aa9a8
commit 833a7f0917
6 changed files with 131 additions and 28 deletions
@@ -1,6 +1,6 @@
# Story 3.1: Stale State Detection & Dimmed Display
Status: ready-for-dev
Status: review
## Story
@@ -20,27 +20,27 @@ AC4: **Given** a stale render cycle **When** the render loop timing is measured
## Tasks / Subtasks
- [ ] Task 1: Update `_run_one_cycle()` and `main()` in `src/planemapper/main.py` (AC: #1, #2, #4)
- [ ] 1.1 Add `last_aircraft: list[Aircraft]` parameter to `_run_one_cycle`; change return type to `list[Aircraft]`
- [ ] 1.2 Wrap `fetcher.fetch()` in `try/except requests.Timeout`
- [ ] 1.3 Implement stale logic: use `last_aircraft` with `is_stale=True` when timeout or empty+had-previous
- [ ] 1.4 Update `main()` to initialise `last: list[Aircraft] = []` and track return value of `_run_one_cycle`
- [ ] 1.5 Add `import dataclasses` and `import requests` to `main.py`
- [x] Task 1: Update `_run_one_cycle()` and `main()` in `src/planemapper/main.py` (AC: #1, #2, #4)
- [x] 1.1 Add `last_aircraft: list[Aircraft]` parameter to `_run_one_cycle`; change return type to `list[Aircraft]`
- [x] 1.2 Wrap `fetcher.fetch()` in `try/except requests.Timeout`
- [x] 1.3 Implement stale logic: use `last_aircraft` with `is_stale=True` when timeout or empty+had-previous
- [x] 1.4 Update `main()` to initialise `last: list[Aircraft] = []` and track return value of `_run_one_cycle`
- [x] 1.5 Add `import dataclasses` and `import requests` to `main.py`
- [ ] Task 2: Update `draw_aircraft()` in `src/planemapper/renderer/aircraft.py` (AC: #3)
- [ ] 2.1 Import `COLOUR_STALE_OUTLINE` from `planemapper.constants`
- [ ] 2.2 Check `aircraft.is_stale` before calling `_draw_arrow`: if stale, use `COLOUR_STALE_OUTLINE` with forced outline mode
- [x] Task 2: Update `draw_aircraft()` in `src/planemapper/renderer/aircraft.py` (AC: #3)
- [x] 2.1 Import `COLOUR_STALE_OUTLINE` from `planemapper.constants`
- [x] 2.2 Check `aircraft.is_stale` before calling `_draw_arrow`: if stale, use `COLOUR_STALE_OUTLINE` with forced outline mode
- [ ] Task 3: Write tests in `tests/test_stale.py` (AC: #1, #2, #3, #4)
- [ ] 3.1 Test AC1: mock `fetcher.fetch()` to raise `requests.Timeout`; call `_run_one_cycle` with `last_aircraft=[some_aircraft]`; assert returned list has `is_stale=True`
- [ ] 3.2 Test AC2: mock `fetcher.fetch()` to return `[]`; call with non-empty `last_aircraft`; assert returned list has `is_stale=True`
- [ ] 3.3 Test AC3: render stale aircraft; assert pixel at aircraft position is not the altitude colour (stale colour is `COLOUR_STALE_OUTLINE` = black)
- [ ] 3.4 Test AC4: full stale cycle with renderer completes without exception
- [x] Task 3: Write tests in `tests/test_stale.py` (AC: #1, #2, #3, #4)
- [x] 3.1 Test AC1: mock `fetcher.fetch()` to raise `requests.Timeout`; call `_run_one_cycle` with `last_aircraft=[some_aircraft]`; assert returned list has `is_stale=True`
- [x] 3.2 Test AC2: mock `fetcher.fetch()` to return `[]`; call with non-empty `last_aircraft`; assert returned list has `is_stale=True`
- [x] 3.3 Test AC3: render stale aircraft; assert pixel at aircraft position is not the altitude colour (stale colour is `COLOUR_STALE_OUTLINE` = black)
- [x] 3.4 Test AC4: full stale cycle with renderer completes without exception
- [ ] Task 4: Run quality gates
- [ ] 4.1 `python -m pytest tests/` — all tests pass
- [ ] 4.2 `python -m ruff check .` — zero violations
- [ ] 4.3 `python -m ruff format --check .` — no formatting issues
- [x] Task 4: Run quality gates
- [x] 4.1 `python -m pytest tests/` — all tests pass
- [x] 4.2 `python -m ruff check .` — zero violations
- [x] 4.3 `python -m ruff format --check .` — no formatting issues
## Implementation Notes
@@ -35,7 +35,7 @@
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
generated: 2026-04-22
last_updated: 2026-04-22 # 2-1 done, 2-2 done, 2-3 done, 2-4 done, 2-5 done, 2-6 done, 2-7 done, epic-2 done, epic-3 in-progress, 3-1 ready-for-dev
last_updated: 2026-04-22 # 2-1 done, 2-2 done, 2-3 done, 2-4 done, 2-5 done, 2-6 done, 2-7 done, epic-2 done, epic-3 in-progress, 3-1 review
project: planeMapper
project_key: NOKEY
tracking_system: file-system
@@ -64,7 +64,7 @@ development_status:
# Epic 3: Stale Data Resilience
epic-3: in-progress
3-1-stale-state-detection-and-dimmed-display: ready-for-dev
3-1-stale-state-detection-and-dimmed-display: review
3-2-automatic-recovery-on-fresh-decode: backlog
epic-3-retrospective: optional