diff options
| author | Claude <noreply@anthropic.com> | 2026-04-23 23:10:11 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-04-23 23:10:11 +0000 |
| commit | e1db9200e9d44c10450361cc8984a45b2eda87b7 (patch) | |
| tree | 21bdfca02491e6ff4cde7a22673c821c0a03e108 /scripts/.claude | |
| parent | 55509f579b4d074f01237dd90791b6d25aaec3cd (diff) | |
| parent | eb78d317c722234a7ef2c501c68c9aa730ec2758 (diff) | |
Merge origin/main — unite all work onto one branch
Brings in dev log (NavLogger), UnitPrefs, MapLayerManager, HUD views,
conditions throttling, track save/load pipeline, improved ParticleWindView
(antimeridian-aware, dynamic particle count), Snackbar error surfacing,
and all other main-branch work from the prior session.
Combined with master's hardware source flags, vessel registry, crew
management, thermal alarm, CPA collision alerts, and track stats.
Also documents primary branch policy in CLAUDE.md and .agent/config.md:
all work merges to main, never master.
https://claude.ai/code/session_011h2dXbgXg3PesQMmQUNTCW
Diffstat (limited to 'scripts/.claude')
| -rw-r--r-- | scripts/.claude/ct-fix-weather-fallback.yaml | 64 | ||||
| -rw-r--r-- | scripts/.claude/ct-remove-mock-tidal.yaml | 83 | ||||
| -rw-r--r-- | scripts/.claude/ct-show-swell-direction.yaml | 75 | ||||
| -rw-r--r-- | scripts/.claude/ct-show-wind-direction.yaml | 92 | ||||
| -rw-r--r-- | scripts/.claude/ct-wind-wiring.txt | 156 | ||||
| -rw-r--r-- | scripts/.claude/ct-wire-anchor-to-map.yaml | 82 | ||||
| -rw-r--r-- | scripts/.claude/ct-wire-tidal-to-map.yaml | 75 |
7 files changed, 627 insertions, 0 deletions
diff --git a/scripts/.claude/ct-fix-weather-fallback.yaml b/scripts/.claude/ct-fix-weather-fallback.yaml new file mode 100644 index 0000000..97823c7 --- /dev/null +++ b/scripts/.claude/ct-fix-weather-fallback.yaml @@ -0,0 +1,64 @@ +name: "Fix WeatherActivity silent fallback to San Francisco" +description: "WeatherActivity silently uses lat=37.8, lon=-122.4 (San Francisco Bay) when GPS is unavailable, showing real API weather data for the wrong location with no indication to the user." +agent: + model: "sonnet" + working_dir: "/workspace/nav" + instructions: | + Context + ------- + WeatherActivity has: + (android-app/app/src/main/kotlin/org/terst/nav/ui/WeatherActivity.kt, lines ~23-24) + + private val defaultLat = 37.8 + private val defaultLon = -122.4 + + When GPS permission is denied or location unavailable, loadWeatherAtDefault() calls + viewModel.loadWeather(defaultLat, defaultLon) and displays the result as if it were + the user's actual location weather — with no disclaimer or error state shown. + + Goal + ---- + Instead of silently fetching weather for San Francisco, show an explicit error/unavailable + state when the device location cannot be obtained. Do not fetch any weather data in that path. + + Remove defaultLat/defaultLon and loadWeatherAtDefault() entirely. + When location is unavailable, set UiState.Error("Location unavailable") so the existing + error UI in the layout is shown instead. + + Step 1 — Read the current WeatherActivity + ----------------------------------------- + Read android-app/app/src/main/kotlin/org/terst/nav/ui/WeatherActivity.kt in full + to understand the current permission and location flow before making changes. + + Step 2 — Remove the fallback + ---------------------------- + - Delete the defaultLat and defaultLon fields + - Delete the loadWeatherAtDefault() method (or equivalent fallback call) + - Where loadWeatherAtDefault() was called (on permission denied or location null), + replace with: + viewModel.setError("Location unavailable — enable GPS to load weather") + OR if MainViewModel does not have setError(), use: + // Just leave UiState.Loading — do not fetch for a fake position + + The simplest correct fix: do nothing when location is unavailable. + The existing "Loading" spinner is less harmful than wrong-location data. + + Step 3 — Build + -------------- + cd android-app && ANDROID_HOME=/opt/android-sdk ./gradlew assembleDebug 2>&1 | tail -5 + Confirm BUILD SUCCESSFUL. + + Step 4 — Commit and push + ------------------------ + git add android-app/app/src/main/kotlin/org/terst/nav/ui/WeatherActivity.kt + git commit -m "fix(weather): remove silent fallback to San Francisco coordinates + + When GPS was unavailable, WeatherActivity fetched and displayed real weather + data for lat=37.8 lon=-122.4 (San Francisco Bay) with no indication to the + user. Remove the fallback; leave the loading state when location is unavailable." + git push github main && git push local main +timeout: "15m" +tags: + - "weather" + - "bug" + - "ux" diff --git a/scripts/.claude/ct-remove-mock-tidal.yaml b/scripts/.claude/ct-remove-mock-tidal.yaml new file mode 100644 index 0000000..86d4e76 --- /dev/null +++ b/scripts/.claude/ct-remove-mock-tidal.yaml @@ -0,0 +1,83 @@ +name: "Remove MockTidalCurrentGenerator" +description: "LocationService generates random fake tidal current arrows near a hardcoded Solent coordinate (50.8, -1.3) every 60 seconds regardless of boat position. Remove the mock generator; leave the layer empty until a real tidal API is integrated." +agent: + model: "sonnet" + working_dir: "/workspace/nav" + instructions: | + Context + ------- + In LocationService.onCreate() there is a coroutine loop: + (android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt, lines ~113-119) + + serviceScope.launch { + while (true) { + val currents = MockTidalCurrentGenerator.generateMockCurrents() + _tidalCurrentState.update { it.copy(currents = currents) } + kotlinx.coroutines.delay(60000) + } + } + + MockTidalCurrentGenerator.generateMockCurrents() produces 10 TidalCurrent objects with: + - Random speedKnots (0–5 kn) + - Random directionDegrees (0–360°) + - Positions scattered within 0.05° of lat=50.8, lon=-1.3 (Solent, UK) + regardless of actual boat position. + (android-app/app/src/main/kotlin/org/terst/nav/MockTidalCurrentGenerator.kt) + + TidalCurrentState has an isVisible flag (defaults false) and a currents list. + MapHandler.updateTidalCurrents() respects isVisible — when false it clears the layer. + + Goal + ---- + 1. Remove the mock generator loop from LocationService.onCreate() + 2. Delete MockTidalCurrentGenerator.kt + 3. Ensure _tidalCurrentState starts with isVisible=false and empty currents (the default) + 4. Remove the import of MockTidalCurrentGenerator from LocationService + + Do NOT remove TidalCurrentState, _tidalCurrentState, or the tidalCurrentState public flow — + those are needed when a real tidal API is added later. + Do NOT remove ACTION_TOGGLE_TIDAL_VISIBILITY handling in onStartCommand — keep the + visibility toggle working for future use. + + Step 1 — Remove the mock loop from LocationService + --------------------------------------------------- + In android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt: + + Delete the entire block (including the comment): + // Mock tidal current data generator + serviceScope.launch { + while (true) { + val currents = MockTidalCurrentGenerator.generateMockCurrents() + _tidalCurrentState.update { it.copy(currents = currents) } + kotlinx.coroutines.delay(60000) // Update every minute + } + } + + Remove the import: + import org.terst.nav.MockTidalCurrentGenerator + + Step 2 — Delete MockTidalCurrentGenerator.kt + --------------------------------------------- + Delete android-app/app/src/main/kotlin/org/terst/nav/MockTidalCurrentGenerator.kt + + Step 3 — Build + -------------- + cd android-app && ANDROID_HOME=/opt/android-sdk ./gradlew assembleDebug 2>&1 | tail -5 + Confirm BUILD SUCCESSFUL with no references to MockTidalCurrentGenerator remaining. + + Step 4 — Commit and push + ------------------------ + git add -A + git commit -m "chore: remove MockTidalCurrentGenerator + + The mock generator was producing 10 random tidal current arrows near a + hardcoded Solent coordinate (50.8, -1.3) every 60 seconds regardless of + boat position. Remove it entirely. The tidal overlay infrastructure + (TidalCurrentState, mapHandler.updateTidalCurrents) is retained for when + a real tidal current API is integrated." + git push github main && git push local main +timeout: "15m" +tags: + - "cleanup" + - "tidal" + - "mock-data" diff --git a/scripts/.claude/ct-show-swell-direction.yaml b/scripts/.claude/ct-show-swell-direction.yaml new file mode 100644 index 0000000..6021bf0 --- /dev/null +++ b/scripts/.claude/ct-show-swell-direction.yaml @@ -0,0 +1,75 @@ +name: "Display swell direction in instrument sheet" +description: "MarineConditions.swellDirDeg is fetched from Open-Meteo and stored but never shown. The Swell cell shows height and period but drops the direction." +agent: + model: "sonnet" + working_dir: "/workspace/nav" + instructions: | + Context + ------- + fetchCurrentConditions() populates MarineConditions.swellDirDeg from + swell_wave_direction (degrees true from Open-Meteo). + (android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt, line 13) + + The instrument sheet expanded section has a Swell column: + (android-app/app/src/main/res/layout/layout_instruments_sheet.xml) + <LinearLayout vertical gravity=center> + <TextView style="InstrumentLabel" text="Swell" /> + <TextView id="value_swell_ht" style="InstrumentPrimaryValue" /> <!-- e.g. "0.8 m" --> + <TextView id="value_swell_per" style="InstrumentLabel" /> <!-- e.g. "8 s" --> + </LinearLayout> + + MainActivity's marineConditions collector sets swellHt and swellPer but ignores swellDirDeg: + (android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt, marineConditions collector) + + InstrumentHandler.updateConditions() has swellPer but no swellDir parameter. + (android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt) + + Goal + ---- + Show swell direction by updating value_swell_per to contain both direction and period + (e.g. "045° / 8 s"), avoiding the need for a fourth TextView in an already-tight column. + Alternatively add a third sub-label TextView — use whichever fits better visually. + + The simplest approach: combine direction and period into value_swell_per as "DIR° / Xs". + + Step 1 — Update updateConditions() in InstrumentHandler + -------------------------------------------------------- + In android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt, + the swellPer parameter already exists. No layout change needed if we combine + direction + period into that field. + + Step 2 — Update the marineConditions collector in MainActivity + -------------------------------------------------------------- + In android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt, + in observeDataSources(), update the swellPer line in updateConditions() to combine + direction and period: + + swellPer = when { + c.swellDirDeg != null && c.swellPeriodS != null -> + "%.0f° / %.0f s".format(Locale.getDefault(), c.swellDirDeg, c.swellPeriodS) + c.swellPeriodS != null -> + "%.0f s".format(Locale.getDefault(), c.swellPeriodS) + c.swellDirDeg != null -> + "%.0f°".format(Locale.getDefault(), c.swellDirDeg) + else -> "—" + } + + Step 3 — Build + -------------- + cd android-app && ANDROID_HOME=/opt/android-sdk ./gradlew assembleDebug 2>&1 | tail -5 + Confirm BUILD SUCCESSFUL. + + Step 4 — Commit and push + ------------------------ + git add android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt + git commit -m "feat(instruments): show swell direction alongside swell period + + swellDirDeg was fetched from Open-Meteo and stored in MarineConditions + but never displayed. Combines direction and period in the swell sub-label + as e.g. '045° / 8 s'." + git push github main && git push local main +timeout: "15m" +tags: + - "instruments" + - "swell" + - "forecast" diff --git a/scripts/.claude/ct-show-wind-direction.yaml b/scripts/.claude/ct-show-wind-direction.yaml new file mode 100644 index 0000000..3ac9276 --- /dev/null +++ b/scripts/.claude/ct-show-wind-direction.yaml @@ -0,0 +1,92 @@ +name: "Display forecast wind direction (TWD) in instrument sheet" +description: "MarineConditions.windDirDeg is fetched from Open-Meteo and stored in the model but never displayed anywhere. Show it as a sub-label under the TWS cell." +agent: + model: "sonnet" + working_dir: "/workspace/nav" + instructions: | + Context + ------- + fetchCurrentConditions() in WeatherRepository populates MarineConditions.windDirDeg + from the Open-Meteo hourly winddirection_10m field (in degrees true). + (android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt, line ~63) + (android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt, line 9) + + In MainActivity.observeDataSources(), the marineConditions collector reads windSpeedKt + (displays as TWS) but never reads windDirDeg: + (android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt, marineConditions collector) + + The instrument sheet layout has a TWS cell: + (android-app/app/src/main/res/layout/layout_instruments_sheet.xml) + <TextView android:id="@+id/value_tws" style="@style/InstrumentPrimaryValue" /> + + Goal + ---- + Show wind direction as a secondary line below the TWS value (e.g. "045°") using the + same pattern used for curr_dir below curr_spd in the conditions section. + + Step 1 — Add value_twd TextView to layout + ------------------------------------------ + In android-app/app/src/main/res/layout/layout_instruments_sheet.xml, + find the TWS LinearLayout: + + <LinearLayout ... android:gravity="center" android:padding="8dp"> + <TextView style="@style/InstrumentLabel" android:text="TWS" /> + <TextView android:id="@+id/value_tws" style="@style/InstrumentPrimaryValue" tools:text="—" /> + </LinearLayout> + + Add a third child TextView for the direction: + + <LinearLayout ... android:gravity="center" android:padding="8dp"> + <TextView style="@style/InstrumentLabel" android:text="TWS" /> + <TextView android:id="@+id/value_tws" style="@style/InstrumentPrimaryValue" tools:text="—" /> + <TextView android:id="@+id/value_twd" style="@style/InstrumentLabel" tools:text="—" /> + </LinearLayout> + + Step 2 — Add valueTwd field to InstrumentHandler + ------------------------------------------------- + In android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt: + + Add constructor parameter: + private val valueTwd: TextView + + Add twd parameter to updateDisplay(): + twd: String? = null + + Add in the updateDisplay body: + twd?.let { valueTwd.text = it } + + Step 3 — Wire in MainActivity + ------------------------------ + In android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt, + in setupHandlers(), add to InstrumentHandler constructor: + valueTwd = findViewById(R.id.value_twd) + + In setupHandlers(), add "—" initial value: + instrumentHandler?.updateDisplay(..., twd = "—") + + In observeDataSources(), in the marineConditions collector, add: + instrumentHandler?.updateDisplay( + tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) } ?: "—", + twd = c.windDirDeg?.let { "%.0f°".format(Locale.getDefault(), it) } ?: "—" + ) + + Step 4 — Build + -------------- + cd android-app && ANDROID_HOME=/opt/android-sdk ./gradlew assembleDebug 2>&1 | tail -5 + Confirm BUILD SUCCESSFUL. + + Step 5 — Commit and push + ------------------------ + git add android-app/app/src/main/res/layout/layout_instruments_sheet.xml \ + android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt \ + android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt + git commit -m "feat(instruments): display forecast wind direction (TWD) under TWS cell + + windDirDeg was fetched from Open-Meteo and stored in MarineConditions + but never shown. Adds value_twd sub-label below the TWS primary value." + git push github main && git push local main +timeout: "15m" +tags: + - "instruments" + - "wind" + - "forecast" diff --git a/scripts/.claude/ct-wind-wiring.txt b/scripts/.claude/ct-wind-wiring.txt new file mode 100644 index 0000000..09768ab --- /dev/null +++ b/scripts/.claude/ct-wind-wiring.txt @@ -0,0 +1,156 @@ +Context +------- +MainViewModel.addGpsPoint() (android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt, +line 67) builds TrackPoint with hardcoded wind zeroes: + + windSpeedKnots = 0.0, windAngleDeg = 0.0, isTrueWind = false + +LocationService already exposes live NMEA wind data: + LocationService.nmeaWindDataFlow: SharedFlow<WindData> (companion object, line 362) + +WindData (android-app/app/src/main/kotlin/org/terst/nav/sensors/WindData.kt): + windAngle: Double // degrees, relative or true + windSpeed: Double // knots + isTrueWind: Boolean + timestampMs: Long + +TrackPoint (android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt) already has +windSpeedKnots, windAngleDeg, isTrueWind fields — they just aren't populated. + +Goal +---- +Cache the latest WindData in MainViewModel and use it when building TrackPoints. +Default to zero wind if no NMEA wind sentence has arrived yet. + +Step 1 — Red: write failing tests +---------------------------------- +Create android-app/app/src/test/kotlin/org/terst/nav/ui/MainViewModelWindTest.kt + +Setup boilerplate needed (Dispatchers.setMain / resetMain with UnconfinedTestDispatcher): + + import androidx.arch.core.executor.testing.InstantTaskExecutorRule + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.ExperimentalCoroutinesApi + import kotlinx.coroutines.test.UnconfinedTestDispatcher + import kotlinx.coroutines.test.resetMain + import kotlinx.coroutines.test.runTest + import kotlinx.coroutines.test.setMain + import org.junit.After + import org.junit.Assert.assertEquals + import org.junit.Before + import org.junit.Rule + import org.junit.Test + import org.terst.nav.sensors.WindData + import org.terst.nav.ui.MainViewModel + + @OptIn(ExperimentalCoroutinesApi::class) + class MainViewModelWindTest { + + @get:Rule val instantTask = InstantTaskExecutorRule() + private val dispatcher = UnconfinedTestDispatcher() + + @Before fun setUp() { Dispatchers.setMain(dispatcher) } + @After fun tearDown() { Dispatchers.resetMain() } + + @Test fun `addGpsPoint uses zero wind when no wind update received`() = runTest { + val vm = MainViewModel() + vm.startTrack() + vm.addGpsPoint(37.0, -122.0, 5.0, 90.0) + val pt = vm.trackPoints.value.first() + assertEquals(0.0, pt.windSpeedKnots, 0.001) + assertEquals(0.0, pt.windAngleDeg, 0.001) + assertEquals(false, pt.isTrueWind) + } + + @Test fun `addGpsPoint uses latest wind data after updateWind`() = runTest { + val vm = MainViewModel() + vm.updateWind(WindData(windAngle = 45.0, windSpeed = 15.5, isTrueWind = true, timestampMs = 1000L)) + vm.startTrack() + vm.addGpsPoint(37.0, -122.0, 5.0, 90.0) + val pt = vm.trackPoints.value.first() + assertEquals(15.5, pt.windSpeedKnots, 0.001) + assertEquals(45.0, pt.windAngleDeg, 0.001) + assertEquals(true, pt.isTrueWind) + } + + @Test fun `addGpsPoint reflects wind update between fixes`() = runTest { + val vm = MainViewModel() + vm.startTrack() + vm.addGpsPoint(37.0, -122.0, 5.0, 90.0) + vm.updateWind(WindData(windAngle = 180.0, windSpeed = 8.0, isTrueWind = false, timestampMs = 2000L)) + vm.addGpsPoint(37.1, -122.0, 5.0, 90.0) + val pts = vm.trackPoints.value + assertEquals(0.0, pts[0].windSpeedKnots, 0.001) + assertEquals(8.0, pts[1].windSpeedKnots, 0.001) + } + } + +Run and confirm RED: + cd android-app && ./gradlew :app:testDebugUnitTest --tests "*MainViewModelWind*" + +Step 2 — Green: implement +-------------------------- +In android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt: + +1. Add import: + import org.terst.nav.sensors.WindData + +2. Add field after trackRepository: + private var latestWind: WindData? = null + +3. Add method: + fun updateWind(wind: WindData) { + latestWind = wind + } + +4. Update addGpsPoint() to use latestWind: + fun addGpsPoint(lat: Double, lon: Double, sogKnots: Double, cogDeg: Double) { + val wind = latestWind + val point = TrackPoint( + lat = lat, lon = lon, + sogKnots = sogKnots, cogDeg = cogDeg, + windSpeedKnots = wind?.windSpeed ?: 0.0, + windAngleDeg = wind?.windAngle ?: 0.0, + isTrueWind = wind?.isTrueWind ?: false, + timestampMs = System.currentTimeMillis() + ) + if (trackRepository.addPoint(point)) { + _trackPoints.value = trackRepository.getPoints() + } + } + +Run and confirm GREEN: + cd android-app && ./gradlew :app:testDebugUnitTest --tests "*MainViewModelWind*" + +Step 3 — Wire: feed wind updates from LocationService +------------------------------------------------------ +In android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt, +inside observeDataSources(), add after the anchorWatchState collector: + + lifecycleScope.launch { + LocationService.nmeaWindDataFlow.collect { wind -> + viewModel.updateWind(wind) + } + } + +No additional imports needed (LocationService is already imported). + +Step 4 — Verify all tests pass +------------------------------- +cd android-app && ./gradlew :app:testDebugUnitTest + +Confirm all existing tests still pass alongside the 3 new wind tests. + +Step 5 — Commit and push +------------------------- +git add -A && git commit -m "feat(track): wire NMEA wind data into GPS track points + +MainViewModel caches the latest WindData from LocationService.nmeaWindDataFlow +via updateWind(). addGpsPoint() populates TrackPoint wind fields from the cache, +defaulting to zero if no NMEA wind sentence has arrived yet. + +MainActivity.observeDataSources() feeds LocationService.nmeaWindDataFlow +into viewModel.updateWind() alongside the existing GPS and anchor observers. + +3 new unit tests in MainViewModelWindTest verify zero-default, wind capture, +and mid-track wind update behaviour." && git push origin main diff --git a/scripts/.claude/ct-wire-anchor-to-map.yaml b/scripts/.claude/ct-wire-anchor-to-map.yaml new file mode 100644 index 0000000..9ef8513 --- /dev/null +++ b/scripts/.claude/ct-wire-anchor-to-map.yaml @@ -0,0 +1,82 @@ +name: "Wire anchor watch state to map overlay" +description: "LocationService.anchorWatchState is collected in MainActivity but only updates SafetyFragment text — MapHandler.updateAnchorWatch() is never called, so the anchor point and radius circle never appear on the map" +agent: + model: "sonnet" + working_dir: "/workspace/nav" + instructions: | + Context + ------- + LocationService.anchorWatchState: StateFlow<AnchorWatchState> + (android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt, companion, line ~353) + + MapHandler.updateAnchorWatch(state: AnchorWatchState) is fully implemented — it shows + an anchor icon and radius circle on the map, or clears them when inactive: + (android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt, line ~139) + + In MainActivity.observeDataSources(), anchorWatchState IS collected, but only + to update a text field in SafetyFragment: + LocationService.anchorWatchState.collect { state -> + safetyFragment.updateAnchorStatus(...) + } + There is no call to mapHandler?.updateAnchorWatch(state) anywhere. + + AnchorWatchState: + (android-app/app/src/main/kotlin/org/terst/nav/AnchorWatchData.kt) + data class AnchorWatchState( + val isActive: Boolean, + val anchorLocation: Location?, + val watchCircleRadiusMeters: Double, + val setTimeMillis: Long + ) + + The map style must be loaded before calling updateAnchorWatch — gate on loadedStyleFlow. + + Goal + ---- + In the existing anchorWatchState collector in observeDataSources(), add a call to + mapHandler?.updateAnchorWatch(state) alongside the existing safetyFragment update. + Gate the map call on the style being loaded. + + Step 1 — Update the existing collector + --------------------------------------- + In android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt, + find the anchorWatchState collector in observeDataSources(): + + lifecycleScope.launch { + LocationService.anchorWatchState.collect { state -> + safetyFragment.updateAnchorStatus(if (state.isActive) "Active: ${state.watchCircleRadiusMeters}m" else "Inactive") + } + } + + Replace with: + + lifecycleScope.launch { + LocationService.anchorWatchState.collect { state -> + safetyFragment.updateAnchorStatus(if (state.isActive) "Active: ${state.watchCircleRadiusMeters}m" else "Inactive") + if (loadedStyleFlow.value != null) { + mapHandler?.updateAnchorWatch(state) + } + } + } + + No import changes needed. + + Step 2 — Build + -------------- + cd android-app && ANDROID_HOME=/opt/android-sdk ./gradlew assembleDebug 2>&1 | tail -5 + Confirm BUILD SUCCESSFUL. + + Step 3 — Commit and push + ------------------------ + git add android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt + git commit -m "fix(map): wire anchor watch state to map overlay + + The anchorWatchState collector in observeDataSources() was only updating + SafetyFragment text. Now also calls mapHandler.updateAnchorWatch(state) + when the map style is loaded, showing the anchor icon and radius circle." + git push github main && git push local main +timeout: "15m" +tags: + - "map" + - "anchor" + - "wiring" diff --git a/scripts/.claude/ct-wire-tidal-to-map.yaml b/scripts/.claude/ct-wire-tidal-to-map.yaml new file mode 100644 index 0000000..b5a1923 --- /dev/null +++ b/scripts/.claude/ct-wire-tidal-to-map.yaml @@ -0,0 +1,75 @@ +name: "Wire tidal current state to map overlay" +description: "LocationService.tidalCurrentState is emitted but never collected in MainActivity — MapHandler.updateTidalCurrents() is never called, so the tidal arrow layer is always empty" +agent: + model: "sonnet" + working_dir: "/workspace/nav" + instructions: | + Context + ------- + LocationService exposes a public StateFlow: + LocationService.tidalCurrentState: StateFlow<TidalCurrentState> + (android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt, companion object, line ~354) + + MapHandler has a fully-implemented method: + fun updateTidalCurrents(state: TidalCurrentState) + (android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt, line ~155) + + MainActivity.observeDataSources() + (android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt) + never subscribes to tidalCurrentState, so updateTidalCurrents() is never called. + The tidal arrow overlay on the map is always empty regardless of what data arrives. + + The map style must be loaded before calling updateTidalCurrents — use loadedStyleFlow + as a gate, the same way updateTrackLayer is gated (see the combine() pattern already + in observeDataSources for track points). + + Goal + ---- + Subscribe to LocationService.tidalCurrentState in MainActivity.observeDataSources() + and forward each emission to mapHandler?.updateTidalCurrents(state). + + The subscription should be gated on loadedStyleFlow being non-null (style loaded), + matching the pattern used for track points. + + Step 1 — Add the observer + ------------------------- + In android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt, + inside observeDataSources(), add after the existing collectors: + + lifecycleScope.launch { + loadedStyleFlow.filterNotNull().collect { + LocationService.tidalCurrentState.collect { state -> + mapHandler?.updateTidalCurrents(state) + } + } + } + + Or equivalently using combine(), consistent with the track pattern: + + lifecycleScope.launch { + loadedStyleFlow.filterNotNull() + .combine(LocationService.tidalCurrentState) { _, state -> state } + .collect { state -> mapHandler?.updateTidalCurrents(state) } + } + + No import changes needed — LocationService is already imported. + + Step 2 — Build + -------------- + cd android-app && ANDROID_HOME=/opt/android-sdk ./gradlew assembleDebug 2>&1 | tail -5 + Confirm BUILD SUCCESSFUL. + + Step 3 — Commit and push + ------------------------ + git add android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt + git commit -m "fix(map): wire tidal current state to map overlay in MainActivity + + LocationService.tidalCurrentState was emitted but never collected. + Subscribe in observeDataSources() gated on style load, forwarding + each TidalCurrentState to mapHandler.updateTidalCurrents()." + git push github main && git push local main +timeout: "15m" +tags: + - "map" + - "tidal" + - "wiring" |
