summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-04-13Wire NavLogger into track save/load pipelineClaude
TrackStorage now writes to NavLogger at every decision point: - Before MediaStore insert (shows the filename being written) - Successful save: "saved nav_<ts>.gpx (N bytes) → <uri>" - Every failure path: storage not mounted, insert null, openOutputStream null, write exception, load parse error - After loadViaMediaStore: count of tracks found TrackRepository logs stopTrack summary (pts / duration / distance) and explicitly logs when saveTrack returns false. Long-press the drag handle after injecting a test track to see exactly where the save pipeline breaks. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-12Fix Actions storage: set artifact retention-daysClaude
test-apks: 1 day — only needed for the smoke-test job in the same run smoke-test-results: 7 days — enough for post-failure debugging Default is 90 days; at ~30 MB per run this was filling the 500 MB free-tier limit in ~2 weeks. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Throttle conditions API; delay particle view until data loadedClaude
loadConditions now skips the API call if conditions were loaded < 5 min ago and position hasn't moved > 2 nm — prevents 429 from overlapping triggers (startup backstop, GPS first-fix, camera idle). Dev log shows "conditions: skip age=Xs dist=Y.Znm" when throttled. ParticleWindView starts hidden and is revealed only when the first windArrow value arrives, so particles don't flash before the map loads. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Add hidden dev log (long-press drag handle)Claude
NavLogger is a 200-entry in-memory ring buffer with format: HH:mm:ss.SSS LEVEL tag: message Entries are written at: - App start - First GPS fix (lat/lon/sog/cog) - fetchCurrentConditions: request params + full success summary (wind, temp, waves, swell, current) or exception class+message - fetchForecastItems: request params + item count or error DevLogSheet (BottomSheetDialogFragment) shows the log in a monospace ScrollView with Copy and Clear buttons. Trigger: long-press the drag handle bar on the instrument bottom sheet. Primary use: copy the log and paste it to Claude to diagnose conditions API failures without needing adb. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Surface conditions API errors as SnackbarClaude
Adds _conditionsError SharedFlow to MainViewModel that emits the exception class + message on fetchCurrentConditions failure. MainActivity collects it and shows a LENGTH_LONG Snackbar — no adb required to see why conditions aren't loading. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Stop 1 Hz conditions hammering; improve error loggingClaude
The camera idle listener fires every time following-mode GPS moves the map (~1 Hz). Each fire was calling loadConditions, which meant a new API request (and spinner flash) every second. Fix: skip the camera idle loadConditions call when isFollowing == true — the GPS first-fix handler already owns that trigger. Camera idle only loads conditions when the user has manually panned away from their position. Also include exception class + message in the logcat tag so the actual API failure reason is visible without --stacktrace: adb logcat -s Nav:E https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Fix: use INVISIBLE instead of GONE for condition spinnersClaude
GONE collapses the spinner's space, causing the value/unit TextViews to reflow and bounce on every refresh. INVISIBLE keeps the 10dp width reserved so the surrounding text never moves. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Fix: add explicit parent to ShapeAppearance.Nav.BottomSheetClaude
Dot-notation style names imply a parent by convention; AAPT was looking for ShapeAppearance.Nav which doesn't exist. Parent is now ShapeAppearance.Material3.Corner.None (all-zero base) with the top corners overridden to 24dp. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11WaveView adapts sky/sea colors to light/dark modeClaude
Day (light mode): vivid sky blue (#2B8FC4 → #87C8DF) fading into tropical deep-sea blue (#0D7A9A → #074B68) — evokes a clear Kona afternoon with deep Pacific below. Shimmer is bright white sunlight on water (#50FFFFFF). Night (dark mode): retains the existing dark charcoal sky (#1C1B1F → #162433) and midnight navy sea (#0B3050 → #0D2137) with blue shimmer. Colors are defined as adaptive color resources (values/ and values-night/) and read in WaveView.onSizeChanged() and paint init instead of hardcoded Color.parseColor() calls. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Fix bottom sheet corners; add full light/dark mode paletteClaude
Rounded corners: - Replace androidx CardView with MaterialCardView + ShapeAppearance.Nav.BottomSheet so only the top-left and top-right corners are rounded (24dp); bottom corners are square flush with the screen edge Light/dark mode: - Introduce values-night/colors.xml with the existing dark M3 palette (moved from values/colors.xml); instrument values stay dark (#E6E1E5 text, #1C1B1F bg) - values/colors.xml now carries the light M3 palette: surface #FFFBFE, onSurface #1C1B1F, surfaceVariant #E7E0EB etc.; instrument text flips to dark (#1C1B1F normal, #6F6878 secondary) for readability on light backgrounds - Fix InstrumentUnit hardcoded #6B6070 → ?attr/colorOnSurfaceVariant so the unit labels adapt automatically in both modes - Mark the forecast row (ocean navy #0D2137) and WaveView with android:forceDarkAllowed="false" so the system never inverts them — the ocean section is intentionally always dark regardless of system theme https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Fix: restore accidentally dropped fun addGpsPoint declarationClaude
Edit tool dropped the function header when inserting injectTestTrack/ buildTestTrack above it. Restore `fun addGpsPoint(...)` so the build compiles. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Show loading spinners next to each instrument value during conditions fetchClaude
- Add _conditionsLoading StateFlow to MainViewModel; set true when loadConditions() fires, false when the fetch completes (success or fail) - Cancel any in-flight conditions job when a new loadConditions() call arrives (e.g. rapid map pans) so only the latest fetch drives the state - Add a 10dp ProgressBar after each of the six data values in layout_instruments_sheet.xml (wind, temp, baro, current, waves, swell); visibility GONE by default, alpha 0.5; header section tinted colorOnSurfaceVariant, forecast section tinted #5B9EC2 to match its labels - Observe conditionsLoading in MainActivity and toggle all six spinners together (all values come from one API call so they load in sync) https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Add dev shortcut to inject a synthetic test trackClaude
Long-press the record FAB while NOT recording to immediately save a pre-built 30-minute west/return circuit from Honokohau Harbor. Uses the existing startTrack/addPoint/stopTrack pipeline so the full save flow — TrackSummarySheet, GPX file write, past-tracks list — all execute without needing to move. Points carry manually-set timestampMs values (30 min in the past, 2-min spacing) so the 2-minute duration validation passes. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Remove bad ASA external links; fix conditions not loading on startupClaude
- Strip all three unverified external link cards (ASA courses, ASA online, Quizlet flashcards) from the Learn tab — asa.com is a Brazilian financial site, not the American Sailing Association - Remove dead openUrl() click handlers and unused Intent/Uri imports from LearnFragment - Parallelize the two sequential API calls in fetchCurrentConditions so the HUD populates in ~half the time (weather + marine fetched concurrently via async/await) - Add onFailure logging in loadConditions so errors are visible in logcat instead of silently swallowed - Eagerly call loadConditions in the setStyle callback as a startup backstop: the camera-idle listener fires before GPS fixes and the initial position may be (0,0), so this guarantees a real fetch once the style is ready with a valid camera position https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Merge branch 'claude/particle-wind-simulation-IMVxT'Claude
Particle wind animation, Learn tab, log text/photo, departure picker, outbound link markers, offline ColRegs + sailing reference content. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Four features: outbound link markers, offline content, log text/photo, ↵Claude
departure picker Learn tab - ic_open_in_new.xml: external link icon (box + arrow) applied to all browser-opening cards - Migration guide cards retain internal › chevron; ASA/Flashcards cards show ↗ icon - New REFERENCE section (offline, works without connectivity): - ColRegs Rules of the Road → colregs_reference.md (rules 1–38, lights table, sound signals, day shapes, memory aids) - Sailing Quick Reference → sailing_reference.md (points of sail, Beaufort scale, nav lights, knots, buoyage IALA-B, VHF channels, distress signals, tide rule of 12) - ColRegs card moved from external ASA section to offline REFERENCE section Log entry - LogEntry: add photoPath field (absolute file path or content URI string) - VoiceLogViewModel: replace confirmAndSave() with save(text, photoPath?) so the fragment controls text (user may edit recognized speech before saving) - VoiceLogFragment: redesigned layout with EditText (editable, voice fills it), camera button (TakePicturePreview → JPEG in cacheDir), gallery button (GetContent), photo thumbnail with remove button, Save / Clear row - Manifest: add android.hardware.camera uses-feature (required=false) Departure date/time picker (trip planning) - ic_calendar.xml: calendar icon for the picker button - PreTripReportViewModel: _departureMs StateFlow (default = now), setDeparture(ms) - PreTripReportGenerator.generateReport(): departureDateTimeMs param; findDepartureSlot() matches nearest UTC forecast item; condition window labels show actual local times (e.g. "2 PM") when departure is not near-now; buildWatchList uses departure hour for Kona trades warning instead of system clock - fragment_pretrip_report.xml: DEPART card with label + calendar button above generate - PreTripReportFragment: MaterialDatePicker (future dates only) → MaterialTimePicker chain; auto-regenerates after picker confirms https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-11Improve particle wind animation: longer lifetimes, wind-scaled count, zoom ↵Claude
scatter - MAX_AGE 8s → 25s: particles live much longer, drastically less twinkle - activeN scales 60→300 with wind speed (0–25 kt): calm = sparse drift, strong wind = dense streaks - Zoom-out detection: scatter() fires immediately when latRange grows >1.8×, instantly filling the new viewport instead of waiting for old particles to die - Alpha uses sine curve (sin(π·age/MAX_AGE)) instead of linear fade: particles ease in from birth, peak at mid-life, and ease out — no bright birth flash that caused the twinkle effect https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Add Learn tab; move quit to Safety; remove persistent MOB FABClaude
UI cleanup: - Remove always-visible MOB FAB — Safety screen button is sufficient - Remove floating quit button; move to bottom of Safety screen - fragment_container is now clickable/focusable, blocking map touch-through for all overlays (trip reports, log, safety) - Record Track FAB hidden while any overlay is shown, visible on map tab New Learn tab (5th nav item): - Migration guides for Navionics and Sea People (local markdown via DocFragment) - ASA Course Catalog, ASA Online Learning, ColRegs, Sailing Flashcards (open in browser) - Remove blanket assets/ .gitignore so markdown docs are tracked https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Suppress map label flicker from GPS jitterClaude
GPS settling produces many tiny position updates that each triggered a 1-second animateCamera(), forcing continuous symbol-collision recalculation and label fade cycles. Fix: skip animation entirely (moveCamera) when the new position is < 3 m from the last one; use a shorter easeCamera(400 ms) for real movement so the animation window is smaller. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Lock MainActivity to portrait orientationClaude
Layout doesn't support landscape; rotating was causing a crash. configChanges also prevents unnecessary activity recreation if the system somehow delivers an orientation event anyway. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Fix invalid colorBackground theme attr in pretrip layoutClaude
colorBackground is not a Material theme attribute; replace with colorSurface which is the correct equivalent. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Add boat profiles and overhaul pre-trip departure briefingClaude
- BoatProfileRepository: Moshi+SharedPrefs persistence, pre-seeds Erickson 23 (155%/100%/65% headsails, 2 reefs) and Cal 20 (1 reef) - PreTripModels: expand with SailConfig, ConditionSlice, RouteProjection, WatchItem, SimilarTripSummary; full BoatProfile with sail inventory - PreTripReportGenerator: full rewrite — 4-hour conditions window, Honokohau candidate-heading route projection with TWA scoring and current-component calc, boat-specific sail plan from inventory, hazard watchlist, similar-trip comparison via past GPX tracks - PreTripReportViewModel: inject BoatProfileRepository, fix GPS to LocationService.bestPosition (Honokohau fallback), pass full forecastItems list and pastTracks to generator - PreTripReportFragment: boat selector ChipGroup, auto-generate on open, render all sections (conditions table, route, sail plan, watchlist, similar trips) - fragment_pretrip_report.xml: redesign with all report cards - item_condition_column.xml: new layout for conditions-window columns - NavApplication: instantiate BoatProfileRepository as app singleton https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Add unit settings, wind particles toggle, and recenter-to-topClaude
- New UnitPrefs: persisted C/F, ft/m, hPa/inHg, kt/mph/kph — defaults to °F - InstrumentHandler now accepts raw values (knots, metres, hPa, °C) and formats via UnitPrefs on every update - LayerPickerSheet expanded with wind particles toggle and unit chip selectors; wrapped in ScrollView to handle the additional height - MapLayerManager tracks particlesEnabled (persisted); setParticlesEnabled() added - MainActivity: caches last raw values so refreshUnits() can reformat everything instantly when the user changes a unit without waiting for new sensor data - Recenter button moved to top (below HUD strip) so it's never obscured by the bottom sheet or bottom nav - Unit label TextViews given IDs in HUD and instrument sheet for live updates https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Area conditions HUD redesign: map-center conditions refresh + boat HUD stripClaude
- Persistent top-of-map HUD strip (SOG/COG/BSP/Depth) replaces instrument grid in bottom sheet - Bottom sheet now shows area conditions only (Wind/Temp/Baro + wave forecast) — always visible - loadConditions() fires on every camera idle event so panning the map refreshes conditions - Crosshair appears at map center while panning; hides when following GPS - Added tempC field to MarineConditions (already fetched from Open-Meteo hourly) - InstrumentHandler slimmed to area conditions only; updateDisplay() removed - LocationService pipes nmeaBoatSpeedData from NmeaStreamManager to companion flow https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Improve particle and wind overlay visibilityClaude
- Particles: vivid blue (30, 100, 255) instead of white — visible on light charts background; thicker stroke 3.5px vs 2.5px - Wind raster overlay: opacity 0.85 vs 0.60 https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Fix particle disappearance at antimeridian-crossing viewports; use whiteClaude
When the viewport crosses ±180° longitude (e.g. Pacific Ocean view), minOf/maxOf on raw longitudes produces lonWest/lonEast that are backwards. Every particle then satisfies the out-of-bounds check and is respawned on every frame → continue → never drawn. Fix: compute lonSpan from screen-ordered corners (left edge / right edge) with antimeridian wrap: lonSpan = lonEast >= lonWest ? span : span + 360. Bounds check uses normalized particle longitude relative to lonWest mod 360. Particle movement wraps longitude into [-180, 180] to stay consistent. Also change particle color to white so it contrasts against the blue wind raster overlay instead of blending into it. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-10Default map to charts; fix synchronized particle flash on pan/zoomClaude
- MapLayerManager: default base preset changed from SATELLITE to CHARTS - ParticleWindView: respawned particles now get a random age (0..MAX_AGE) instead of always 0, preventing mass synchronized respawns after pan/zoom https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-09Fix startup crash: rename wind source ID to avoid collisionClaude
MapLayerManager registers SOURCE_WIND = "wind-source" as a RasterSource in the style builder. MapHandler was using the same ID for its GeoJsonSource, causing MapLibre to throw on duplicate source IDs at style-load time. Rename MapHandler's source to "wind-arrows-source". https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-09Fix nullable LatLng? crash in MainActivity camera idle listenerClaude
Same MapLibre 13.0.1 nullable corner fix — use listOfNotNull() and guard on size == 4 before calling loadWindGrid. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-09Fix nullable LatLng? crash in ParticleWindViewClaude
MapLibre 13.0.1 types VisibleRegion corners as LatLng? (nullable). Use listOfNotNull() in both onDraw() and scatter() to guard against null corners — mirrors the same fix already applied to MainActivity. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-09fix(build): replace LatLngBounds properties with VisibleRegion cornersClaude
LatLngBounds.latSouth/latNorth/lonWest/lonEast don't exist in MapLibre 13.0.1. Derive bounds from VisibleRegion corner LatLng points (.nearLeft/.nearRight/.farLeft/.farRight) which are stable across SDK versions. Fixes CI compilation failure in MainActivity and ParticleWindView. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-09docs(agent): lock in main as canonical default branchClaude
Document that default branch is always main, CI triggers on main, and master must never be used. Prevents recurrence of the master/main confusion that broke Firebase distribution. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-09merge: reconcile master particle-wind work with origin/main redesignClaude
Merged divergent branches (common ancestor a9d87b6). Took origin/main as base (layer manager, recenter, quit button, GPX tracks, marine conditions, dark theme, user position layer, active/past track layers) and layered in particle wind simulation work: - MapHandler: added setupWindLayer, updateWindLayer, updateWindGridLayer; kept their user-pos layer, follow/recenter state, active+past tracks - WeatherRepository: kept their fetchCurrentConditions, added fetchWindGrid - MainViewModel: kept their track/marine/summary changes, added windGrid StateFlow, loadWindGrid, buildGrid - MainActivity: kept their full UI (layer picker, quit, recenter, fade animations), added particleWindView wiring, weatherLoaded guard, windArrow/windGrid observers - activity_main.xml: took their LinearLayout redesign, added ParticleWindView overlay inside ConstraintLayout https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-09fix(ci): trigger workflow on master branchClaude
2026-04-09fix(ci): trigger workflow on master, not mainClaude
Repo default branch is master; workflow was targeting main so CI never ran and Firebase distribution never fired. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-08Merge branch 'claude/particle-wind-simulation-IMVxT' — Level 2 particle ↵Claude
simulation
2026-04-08feat(wind): Level 2 — canvas particle simulation (uniform field)Claude
- ParticleWindView: transparent View overlay on the map; tracks 300 particles as three FloatArrays (lat, lon, age) — no per-frame allocation; projects lat/lon → screen via MapLibreMap.projection each frame; draws a 14px tail segment in the wind travel direction with alpha fading by age; respawns out-of-bounds/aged particles randomly within visible bounds; speed scaled to viewport so animation is consistent at any zoom level; animates via postInvalidateOnAnimation() - activity_main.xml: ParticleWindView added above MapView, match_parent, non-interactive (clickable/focusable=false) - MainActivity: finds particle_wind_view; calls attachMap after getMapAsync; feeds directionDeg/speedKt from windArrow StateFlow https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-08Merge branch 'claude/particle-wind-simulation-IMVxT' — Level 1 wind gridClaude
2026-04-08feat(wind): Level 1 — 4×5 viewport grid of wind arrowsClaude
- WeatherApiService: batch endpoint getWeatherForecastBatch with comma-separated lat/lon → List<WeatherResponse> - WeatherRepository.fetchWindGrid: chunks 20 points into two calls of 10, maps each response to a WindArrow at its returned lat/lon - MainViewModel.loadWindGrid(bounds): builds a 4×5 grid from visible map bounds, calls fetchWindGrid, emits to windGrid StateFlow - MapHandler.setupWindGridLayer: GeoJSON source + SymbolLayer reusing the wind-arrow icon; MapHandler.updateWindGridLayer: pushes feature collection - MainActivity: stores mapLibreMapRef; calls setupWindGridLayer after style loads; addOnCameraIdleListener fires loadWindGrid on every pan/zoom; observes windGrid → updateWindGridLayer https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-08Merge branch 'claude/particle-wind-simulation-IMVxT' — Level 0 wind arrowClaude
2026-04-08feat(wind): Level 0 — single wind arrow at GPS positionClaude
- MapHandler.setupWindLayer: registers ic_wind_arrow bitmap, adds GeoJSON source + SymbolLayer with rotation/size driven by direction/speed_kt props - MapHandler.updateWindLayer(arrow): pushes updated feature to the source - MainActivity: calls setupWindLayer after style load; fires loadWeather once on first GPS fix (weatherLoaded flag); observes windArrow StateFlow to drive updateWindLayer https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX
2026-04-07fix(track): fix silent GPX save failure + add stop friction + quit buttonPeter Stone
TrackStorage: openOutputStream null returned true (file never written). Added IS_PENDING flag to fix Android 10-11 race where insert succeeds but file isn't physically created yet. Added storage-mounted guard. TrackRepository now logs save failures. Stop tracking now requires a long press (haptic feedback) — prevents accidental mid-sail stops from a single tap. Quit button (top-right, tonal X) stops LocationService and calls finishAffinity(). Prompts if a track is in progress. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat(nav): replace Map+Instruments with Map+Layers in bottom navPeter Stone
Layers acts as an action button — shows LayerPickerSheet and snaps back to Map so it never stays selected. Instruments tab removed; sheet expand/collapse via swipe as before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat(map): layer manager — satellite/charts/hybrid + wind toggle, ↵Peter Stone
long-press picker MapLayerManager: all raster sources registered at style-build time, visibility toggled on demand. Persists base preset and wind state to SharedPreferences. Sources: Google satellite, NOAA RNC charts (tileservice.charts.noaa.gov), OWM wind, OpenSeaMap seamarks. LayerPickerSheet: bottom sheet with chip group (Satellite/Charts/Hybrid) and wind toggle, launched from map long-press. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat(map): switch wind layer to OpenWeatherMap wind_new tilesPeter Stone
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat(map): add Windy Map Forecast API key to wind tile URLPeter Stone
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat(tracks): show summary sheet on track stop; 2-min minimumPeter Stone
TrackSummarySheet: bottom sheet showing distance (nm), duration, max/avg speed, avg wind and waves (when available, waves in ft). Only shown for tracks ≥ 2 minutes — shorter tracks are discarded silently. MainViewModel: exposes trackSummary SharedFlow (replay=0) and trackStartMs. MainActivity: observes flow, shows sheet after stopTrack completes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat(tracks): persist tracks as GPX in Documents/Nav/ — survives uninstallPeter Stone
GpxSerializer/GpxParser: full round-trip of all TrackPoint fields via GPX 1.1 + nav: extensions namespace. 13 unit tests. TrackStorage: MediaStore on API 29+ (no permission needed), direct file I/O on API 24-28 (WRITE_EXTERNAL_STORAGE maxSdkVersion=28). TrackRepository: stopTrack() is now suspend, writes GPX and returns TrackSummary (distance nm, duration, max/avg SOG, avg wind, avg wave). getPastTracks() lazy-loads from Documents/Nav/ on first call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat(ui): wave height scales view, period drives speed, whitecaps gated on windPeter Stone
WaveView: animation speed = 8/period so long swell animates slowly; amplitude ceiling raised to 42% of view height; whitecaps only when windSpeedKt >= 12 (Beaufort 4). InstrumentHandler.updateWaveState: sizes view height from swell height (1ft→56dp, 8ft→160dp) and forwards windSpeedKt to WaveView. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat(ui): dark theme — match instrument sheet to WaveView sky palettePeter Stone
Surface/background → #1C1B1F (WaveView sky), text tokens updated to light M3 dark-mode values, status bar icons set to light. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>