# LocalSky Radar

macOS WidgetKit app that displays a live ADS-B radar scope with nearby aircraft. The app's primary surface **is its widgets** — no traditional main window.

## Status

Swift/WidgetKit implementation exists under `LocalSkyRadar/`. The original design handoff remains useful, but the source of truth for behavior is now the Xcode project plus focused render/runtime checks.

## Spec & Design Assets

- **Primary spec:** `docs/design/design_handoff_localsky_radar/README.md` — 562-line implementation guide covering architecture, layouts, data models, design tokens, interactions, and an implementation checklist
- **Interactive prototype:** `docs/design/LocalSky Radar Widgets.dc.html` — open in browser at 1280px+ width; toggles all settings live, demonstrates tap interactions
- **Component prototypes:** `Widget.dc.html` (layout logic), `RadarFace.dc.html` (SVG scope drawing), `radar-data.js` (themes, projection math, sample data)
- **Mockups:** `docs/design/design_handoff_localsky_radar/mockups/` — PNG renders of each widget variant
- **PDF spec:** `docs/LocalSky_Radar_Specification.pdf` (design tool export; HTML prototypes are the authoritative reference)

## Implementation Plan (from spec)

- **Platform:** macOS 14 Sonoma+ (interactive widgets via AppIntent), iOS 17+ if ported
- **Framework:** WidgetKit, SwiftUI, CoreLocation, App Intents
- **Data:** `airplanes.live` API (`GET https://api.airplanes.live/v2/point/{lat}/{lon}/100`), polled every 60s
- **Architecture:** Main app target (location, data fetch, widget reload, App Intents) + Widget Extension target (bundle, ExtraLargeWidget)
- **Shared state:** App Group UserDefaults for selected aircraft ID between app and widgets
- **Widget sizes:** Extra Large only (720×382). Medium/Large remain in the design prototypes for reference but are not registered in the active WidgetKit bundle.
- **Configuration:** 4 widget settings — scope shape (circle/square), range (adaptive/50/75/100 NM), location (GPS/airport ICAO), theme (muted/green/gray)

## Before You Start Coding

1. Read the full spec README at `docs/design/design_handoff_localsky_radar/README.md`
2. Open `docs/design/LocalSky Radar Widgets.dc.html` in a browser to understand the interactive behavior
3. Review `second-brain/wiki/concepts/WidgetKit Engineering Pitfalls.md` and `second-brain/wiki/concepts/Apple Widget Rendering Modes.md` (monorepo-wide WidgetKit guidance)
4. For active Swift changes, edit files under `LocalSkyRadar/LocalSkyRadar/`. The similarly named `LocalSkyRadar/LocalSkyRadarWidgetExtension/` directory contains extension metadata and older/unused source copies; confirm target membership before changing widget logic.

## Key Gotchas

- WidgetKit widgets run in a separate process; shared state requires an App Group container
- Interactive widgets (tap-to-select) require macOS Sonoma+ / iOS 17+ and `AppIntent`
- Timeline entries refresh on a fixed interval (60s); immediate reload requires `WidgetCenter.shared.reloadAllTimelines()` from the main app
- Adaptive range is intentionally local: default/max 25 NM, zooming down in 5 NM steps only when at least six aircraft fit closer. Keep this tied to table density so the widget shows useful nearby traffic without long-range clutter.
- The spec uses design tokens (CSS variable names like `acHi`, `ringStrong`) — map these to SwiftUI `Color` assets when implementing
- Radar marker placement depends on two separate transforms: `CoordinateMath.projectToScope` must scale by `dist / rangeNM`, and SwiftUI marker drawing must use local coordinates before applying `.position`. A render test should sample marker-colored pixels near the projected point when this changes.
- Desktop widgets must handle `widgetRenderingMode != .fullColor`: broad custom backgrounds should become clear or mode-specific, otherwise macOS can flatten them into large pale blocks that cover the widget.
- Edit Widget field behavior comes from the compiled AppIntents metadata in the installed appex, not just the Swift source. For `airportCode`, keep the parameter as `String` with explicit `String.IntentInputOptions`.

## Local Tools

- Run tests: `xcodebuild test -project LocalSkyRadar/LocalSkyRadar.xcodeproj -scheme LocalSkyRadar -destination 'platform=macOS'`
- Build app: `xcodebuild build -project LocalSkyRadar/LocalSkyRadar.xcodeproj -scheme LocalSkyRadar -destination 'platform=macOS'`
- Install verified local build: `ditto ~/Library/Developer/Xcode/DerivedData/LocalSkyRadar-beoqlyvaelyeondsibqvhfzjkwvk/Build/Products/Debug/LocalSkyRadar.app /Applications/LocalSkyRadar.app`, then register with `pluginkit -a /Applications/LocalSkyRadar.app/Contents/PlugIns/LocalSkyRadarWidgetExtension.appex`
- Widget runtime verification needs the built app installed, the extension registered, chronod descriptor/AppIntents caches refreshed, and a desktop screenshot inspected. Do this after changes to widget views, AppIntents, target membership, or rendering mode behavior. First compare the installed appex binary, not just DerivedData: `strings /Applications/LocalSkyRadar.app/Contents/PlugIns/LocalSkyRadarWidgetExtension.appex/Contents/MacOS/LocalSkyRadarWidgetExtension.debug.dylib | rg "ExtraLargeWidget|MediumWidget|LargeWidget|ADAPTIVE"`

## Widget Completion Gate

For widget work, tests, render artifacts, and compiled metadata are necessary but not sufficient. Do not mark a widget UI, rendering-mode, AppIntent interaction, target-membership, or descriptor/configuration change complete until the actual installed desktop widget has been visually verified.

Required completion evidence for widget surface changes:

- rebuild the app and extension,
- install the built app into `/Applications`,
- register the installed appex,
- refresh relevant WidgetKit/`chronod`/host cache state and let it settle,
- capture and inspect a desktop screenshot of the placed widget.

Focused `ImageRenderer` artifacts are useful during iteration, but they are supplemental only. Record the actual desktop screenshot path or evidence in the final response.
