Files
planeMapper/_bmad-output/implementation-artifacts/deferred-work.md
T
Matt Edholm be32469284 review(2-4): pass story 2-4 — altitude colour bands & aircraft type icons
All 7 ACs verified. No code fixes required: boundary logic (<=), category
map, callsign prefix guard, altitude fallback thresholds, empty-category
guard, and type annotations are all correct. 89/89 tests pass, ruff clean.

Added two deferred items: hardcoded _AIRLINE_PREFIXES subset and A-category
military aircraft misclassification. Story and sprint-status marked done.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:23:37 -04:00

9.3 KiB
Raw Blame History

Deferred Work Manifest

Tracks blocked, deferred, and tech-debt items across sprints.


Infrastructure / environment setup

[1-1] systemd unit installation

Story: 1-1-project-scaffold-and-verified-entry-points Task: 7.1, 7.2 Description: Unit files created at systemd/. Must be symlinked or copied to /etc/systemd/system/ on the Pi and systemctl daemon-reload run before they take effect. Cannot be automated without root access to target device.

[1-1] Pi Zero 2W runtime verification

Story: 1-1-project-scaffold-and-verified-entry-points Task: 9.1, 9.2 Description: Entry points verified on host (Pi 5, Linux). Full AC1 verification on Pi Zero 2W hardware requires physical deployment.


Story 1.2 review — no new deferred items

Story 1-2-configuration-read-write-wipe reviewed 2026-04-22. All 4 ACs pass, all 7 tests pass, ruff check and format clean. No deferred items required: config.write() already handles directory creation via mkdir(parents=True, exist_ok=True), so deployment to a fresh device with no /etc/planemapper/ directory is covered at runtime.


Story 1.3: WiFi Hotspot & Captive Portal Form

[1-3] hostapd and dnsmasq system packages

Story: 1-3-wifi-hotspot-and-captive-portal-form Category: Infrastructure/environment Description: hostapd and dnsmasq require system packages installed on the Pi; AP mode requires wlan0 in AP-capable state. Cannot be verified without hardware.

[1-3] Captive portal device testing

Story: 1-3-wifi-hotspot-and-captive-portal-form Category: Runtime verification Description: Actual captive portal detection behaviour (iOS/Android/Windows triggering) requires physical device testing. Automated tests confirm redirect routes are correct but cannot simulate OS-level captive portal probe behaviour.

[1-3] Provisioning loop placeholder

Story: 1-3-wifi-hotspot-and-captive-portal-form Category: Infrastructure/environment Description: provision.py provisioning loop currently exits after one iteration (placeholder provisioned = True) — full sequence wired in Story 1.5.


Story 1.4: Location Resolution (ICAO & Address)

[1-4] Nominatim geocoding runtime verification

Story: 1-4-location-resolution-icao-and-address Category: Runtime verification Description: Nominatim geocoding verified in tests with mocks only; real geocoding requires internet access and can only be confirmed on device at provisioning time. No automated test covers live HTTP to Nominatim.

[1-4] ICAO heuristic false-positive risk

Story: 1-4-location-resolution-icao-and-address Category: Technical debt Description: ICAO heuristic (len(query) == 4 and query.isalpha()) may misclassify 4-letter words (e.g. "BATH", "YORK") as ICAO codes, causing them to be looked up in airports.csv before falling back to Nominatim. Acceptable for MVP given the provisioning context, but noted for future hardening (e.g. validate against a known ICAO prefix list).


Story 1.5: Provisioning Execution — Tile Download, Cache Validation & WiFi Kill

[1-5] nmcli / NetworkManager dependency

Story: 1-5-provisioning-execution-tile-download-cache-validation-and-wifi-kill Category: Infrastructure/environment Description: nmcli requires NetworkManager to be installed and running on the Pi; the wlan0 interface must support managed mode. Raspberry Pi OS Lite uses dhcpcd by default — NetworkManager must be installed and enabled before join_home_wifi() will work.

[1-5] rfkill permission requirement

Story: 1-5-provisioning-execution-tile-download-cache-validation-and-wifi-kill Category: Infrastructure/environment Description: rfkill block wifi requires the process to have permission to block the WiFi interface. The user running the provisioning service must be root or have the CAP_NET_ADMIN capability. The systemd unit must be configured accordingly.

[1-5] OSM tile download and OpenAIP API runtime verification

Story: 1-5-provisioning-execution-tile-download-cache-validation-and-wifi-kill Category: Runtime verification Description: OSM tile download, OpenAIP API call, and the full provisioning sequence (WiFi join → tile download → airspace download → validate → write config → rfkill) can only be end-to-end verified on device with real network access. All tests use mocks only.

[1-5] provision.py port 80 requires root

Story: 1-5-provisioning-execution-tile-download-cache-validation-and-wifi-kill Category: Infrastructure/environment Description: provision.py calls app.run(port=80) which requires root privileges or the CAP_NET_BIND_SERVICE capability to bind to a port below 1024. The systemd unit for the provisioning service must run as root or be granted the appropriate capability.

[1-5] Synchronous POST /submit — browser waits during provisioning

Story: 1-5-provisioning-execution-tile-download-cache-validation-and-wifi-kill Category: Technical debt Description: The POST /submit handler is fully synchronous — the browser connection stays open while tile download, airspace download, and cache validation complete (potentially 25 minutes). This is acceptable for MVP but a streaming response (using flask.stream_with_context or a background thread with server-sent events) would improve UX by allowing the browser to render progress feedback without holding an open connection.


Story 2.1: Aircraft Data Model & Fetcher

[2-1] HttpFetcher live dump1090 runtime verification

Story: 2-1-aircraft-data-model-and-fetcher Category: Runtime verification Description: HttpFetcher is tested with mocks only. Live feed at http://localhost:8080/data/aircraft.json can only be verified on device with an RTL-SDR dongle connected and dump1090 running. No automated test covers the real HTTP path to dump1090.


Story 2.2: Coordinate Projection & Base Map Loading

[2-2] Equirectangular projection distortion at high latitudes or large radius

Story: 2-2-coordinate-projection-and-base-map-loading Category: Technical debt Description: The equirectangular projection corrects only for longitude convergence at the home latitude (cos(home_lat)). For large radius values (e.g. >150nm) or locations above ~60°N, distortion accumulates toward the display edges. Aircraft positions at the map boundary can be displaced by several pixels from their true screen location. Acceptable for a ~100nm display centred on a UK airfield, but worth revisiting if radius or latitude range is extended.

[2-2] basemap.load() does not verify image dimensions

Story: 2-2-coordinate-projection-and-base-map-loading Category: Technical debt Description: basemap.load() opens and returns whatever image is at BACKGROUND_PATH without asserting it is 800×480. A mismatched tile composite (e.g. from a re-provisioning at a different zoom level) will be silently accepted and the rendered output will be corrupted. Future hardening: add a dimension assertion and raise ValueError if the image does not match DISPLAY_WIDTH × DISPLAY_HEIGHT.


Story 2.3: Home Marker & Airspace Outlines

[2-3] draw_airspace() silently skips non-Polygon geometry types

Story: 2-3-home-marker-and-airspace-outlines Category: Technical debt Description: draw_airspace() skips any GeoJSON feature whose geometry.type is not "Polygon" (e.g. Point, LineString, MultiPolygon). This is intentional for MVP per spec, but MultiPolygon airspace features from OpenAIP will be silently ignored. Future hardening: add support for MultiPolygon by iterating each ring, and log a debug message when an unsupported geometry type is encountered.

[2-3] draw_airspace() does not handle null geometry features

Story: 2-3-home-marker-and-airspace-outlines Category: Technical debt Description: If a GeoJSON feature has "geometry": null (valid per GeoJSON spec for featureless features), feature.get("geometry", {}) returns None rather than {}, and the subsequent .get("type") call raises AttributeError. Acceptable for MVP given controlled OpenAIP input, but real-world GeoJSON files can contain null geometry. Future hardening: guard with if not geom or not isinstance(geom, dict): continue.


Story 2.4: Altitude Colour Bands & Aircraft Type Icons

[2-4] _AIRLINE_PREFIXES is a hardcoded subset of ICAO airline codes

Story: 2-4-altitude-colour-bands-and-aircraft-type-icons Category: Technical debt Description: _AIRLINE_PREFIXES contains 23 hand-picked ICAO 3-letter designators. Any airline callsign not in this set (e.g. "SXS", "WJA", "FDX") will fall through to the altitude fallback and may be misclassified as GA_LIGHT, PRIVATE_JET, or AIRLINER depending on altitude rather than as COMMERCIAL. Acceptable for MVP display purposes, but notable for any use case that relies on accurate airline/GA distinction. Future hardening: source the full ICAO airline prefix list from a static CSV bundled with the package.

[2-4] Military aircraft with A-category ADS-B codes are misclassified

Story: 2-4-altitude-colour-bands-and-aircraft-type-icons Category: Technical debt Description: _CATEGORY_MAP maps only ADS-B B-categories (B1B4) to AircraftType.MILITARY. Military aircraft that transmit A-category ADS-B codes (e.g. training jets advertising as A3) or no category at all will fall through to the callsign/altitude fallback and be misclassified. A military callsign prefix list (e.g. "RRR", "GAF", "USAF") would improve detection but is not required by any story AC.