From afe94c5a2ce33c7f98d85b287ebbe07488dc181f Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Mon, 16 Mar 2026 00:06:33 +0000 Subject: feat: offline GRIB staleness checker, ViewModel integration, and UI badge - Add GribRegion, GribFile data models and GribFileManager interface - Add InMemoryGribFileManager for testing and default use - Add GribStalenessChecker with FreshnessResult sealed class (Fresh/Stale/NoData) - Integrate weatherStaleness StateFlow into MainViewModel (checked after loadWeather) - Add yellow staleness banner TextView to fragment_map.xml - Wire staleness banner in MapFragment (shown on Stale, hidden on Fresh/NoData) - Add GribStalenessCheckerTest (4 TDD tests) Co-Authored-By: Claude Sonnet 4.6 --- android-app/app/src/main/res/layout/fragment_map.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/res/layout/fragment_map.xml b/android-app/app/src/main/res/layout/fragment_map.xml index e5b86b7..2b9b40d 100644 --- a/android-app/app/src/main/res/layout/fragment_map.xml +++ b/android-app/app/src/main/res/layout/fragment_map.xml @@ -27,4 +27,18 @@ android:textSize="14sp" android:visibility="gone" /> + + + -- cgit v1.2.3 From e5cd0ce6bf65fff1bbbb5d8e12c4076da088ebe1 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 24 Mar 2026 23:02:14 +0000 Subject: feat: add AnchorWatchHandler UI with Depth/Rode Out inputs and suggested radius - Add AnchorWatchState with calculateRecommendedWatchCircleRadius, which uses ScopeCalculator.watchCircleRadius (Pythagorean scope formula) and falls back to rode length when geometry is invalid - Add AnchorWatchHandler Fragment with EditText inputs for Depth (m) and Rode Out (m); updates suggested watch circle radius live via TextWatcher - Add fragment_anchor_watch.xml layout - Wire AnchorWatchHandler into bottom navigation (MainActivity + menu) - Add AnchorWatchStateTest covering valid geometry, short-rode fallback Co-Authored-By: Claude Sonnet 4.6 --- .../example/androidapp/safety/AnchorWatchState.kt | 23 +++++++ .../ui/anchorwatch/AnchorWatchHandler.kt | 58 ++++++++++++++++ .../src/main/res/layout/fragment_anchor_watch.xml | 79 ++++++++++++++++++++++ android-app/app/src/main/res/values/strings.xml | 10 +++ .../androidapp/safety/AnchorWatchStateTest.kt | 32 +++++++++ 5 files changed, 202 insertions(+) create mode 100644 android-app/app/src/main/kotlin/com/example/androidapp/safety/AnchorWatchState.kt create mode 100644 android-app/app/src/main/kotlin/com/example/androidapp/ui/anchorwatch/AnchorWatchHandler.kt create mode 100644 android-app/app/src/main/res/layout/fragment_anchor_watch.xml create mode 100644 android-app/app/src/test/kotlin/com/example/androidapp/safety/AnchorWatchStateTest.kt (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/safety/AnchorWatchState.kt b/android-app/app/src/main/kotlin/com/example/androidapp/safety/AnchorWatchState.kt new file mode 100644 index 0000000..507736e --- /dev/null +++ b/android-app/app/src/main/kotlin/com/example/androidapp/safety/AnchorWatchState.kt @@ -0,0 +1,23 @@ +package com.example.androidapp.safety + +/** + * Holds UI-facing state for the anchor watch setup screen and provides + * the suggested watch-circle radius derived from depth and rode out. + */ +class AnchorWatchState { + + /** + * Returns the recommended watch-circle radius (metres) for the given depth + * and amount of rode deployed. + * + * Uses the Pythagorean formula via [ScopeCalculator.watchCircleRadius] when + * the geometry is valid (rode > depth + freeboard). Falls back to [rodeOutM] + * itself as the maximum possible swing radius when the rode is too short to + * form a catenary angle. + */ + fun calculateRecommendedWatchCircleRadius(depthM: Double, rodeOutM: Double): Double { + val vertical = depthM + 2.0 // 2 m default freeboard + return if (rodeOutM > vertical) ScopeCalculator.watchCircleRadius(rodeOutM, depthM) + else rodeOutM + } +} diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/ui/anchorwatch/AnchorWatchHandler.kt b/android-app/app/src/main/kotlin/com/example/androidapp/ui/anchorwatch/AnchorWatchHandler.kt new file mode 100644 index 0000000..bc82795 --- /dev/null +++ b/android-app/app/src/main/kotlin/com/example/androidapp/ui/anchorwatch/AnchorWatchHandler.kt @@ -0,0 +1,58 @@ +package com.example.androidapp.ui.anchorwatch + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.example.androidapp.R +import com.example.androidapp.databinding.FragmentAnchorWatchBinding +import com.example.androidapp.safety.AnchorWatchState + +class AnchorWatchHandler : Fragment() { + + private var _binding: FragmentAnchorWatchBinding? = null + private val binding get() = _binding!! + + private val anchorWatchState = AnchorWatchState() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAnchorWatchBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val watcher = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + override fun afterTextChanged(s: Editable?) = updateSuggestedRadius() + } + binding.etDepth.addTextChangedListener(watcher) + binding.etRodeOut.addTextChangedListener(watcher) + } + + private fun updateSuggestedRadius() { + val depth = binding.etDepth.text.toString().toDoubleOrNull() + val rode = binding.etRodeOut.text.toString().toDoubleOrNull() + + if (depth != null && rode != null && depth >= 0.0 && rode > 0.0) { + val radius = anchorWatchState.calculateRecommendedWatchCircleRadius(depth, rode) + binding.tvSuggestedRadius.text = + getString(R.string.anchor_suggested_radius_fmt, radius) + } else { + binding.tvSuggestedRadius.text = getString(R.string.anchor_suggested_radius_empty) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/android-app/app/src/main/res/layout/fragment_anchor_watch.xml b/android-app/app/src/main/res/layout/fragment_anchor_watch.xml new file mode 100644 index 0000000..96b9969 --- /dev/null +++ b/android-app/app/src/main/res/layout/fragment_anchor_watch.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml index 499ba8d..756f5e3 100755 --- a/android-app/app/src/main/res/values/strings.xml +++ b/android-app/app/src/main/res/values/strings.xml @@ -58,4 +58,14 @@ %.0f °C %d%% Location is needed to show weather for your current position. + Anchor + Anchor Watch + Depth (m) + e.g. 5.0 + Rode Out (m) + e.g. 30.0 + Suggested Watch Radius + + %.1f m + Calculated from rode and depth using Pythagorean scope formula (2 m freeboard assumed) diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/safety/AnchorWatchStateTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/safety/AnchorWatchStateTest.kt new file mode 100644 index 0000000..40f7df0 --- /dev/null +++ b/android-app/app/src/test/kotlin/com/example/androidapp/safety/AnchorWatchStateTest.kt @@ -0,0 +1,32 @@ +package com.example.androidapp.safety + +import org.junit.Assert.* +import org.junit.Test +import kotlin.math.sqrt + +class AnchorWatchStateTest { + + private val state = AnchorWatchState() + + @Test + fun calculateRecommendedWatchCircleRadius_validGeometry() { + // depth=6m, rode=50m → vertical=8m, radius=sqrt(50²-8²)=sqrt(2436) + val expected = sqrt(2436.0) + val actual = state.calculateRecommendedWatchCircleRadius(depthM = 6.0, rodeOutM = 50.0) + assertEquals(expected, actual, 0.001) + } + + @Test + fun calculateRecommendedWatchCircleRadius_rodeShorterThanVertical_fallsBackToRode() { + // depth=10m, rode=5m → vertical=12m > rode, fallback returns rode + val actual = state.calculateRecommendedWatchCircleRadius(depthM = 10.0, rodeOutM = 5.0) + assertEquals(5.0, actual, 0.001) + } + + @Test + fun calculateRecommendedWatchCircleRadius_rodeEqualsVertical_fallsBackToRode() { + // depth=8m, rode=10m → vertical=10m == rode, fallback returns rode + val actual = state.calculateRecommendedWatchCircleRadius(depthM = 8.0, rodeOutM = 10.0) + assertEquals(10.0, actual, 0.001) + } +} -- cgit v1.2.3 From ca57e40adc0b89e7dc5409475f7510c0c188d715 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 25 Mar 2026 18:09:53 +0000 Subject: feat(track): implement GPS track recording with map overlay - TrackRepository + TrackPoint wired into MainViewModel: isRecording/trackPoints StateFlows, startTrack/stopTrack/addGpsPoint - MapHandler.updateTrackLayer(): lazily initialises a red LineLayer and updates GeoJSON polyline from List - fab_record_track FAB in activity_main.xml (top|end of bottom nav); icon toggles between ic_track_record and ic_close while recording - MainActivity feeds every GPS fix into viewModel.addGpsPoint() and observes trackPoints to redraw the polyline in real time - ic_track_record.xml vector drawable (red record dot) - 8 TrackRepositoryTest tests all GREEN Co-Authored-By: Claude Sonnet 4.6 --- .agent/worklog.md | 19 +++++- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 29 +++++++- .../main/kotlin/org/terst/nav/track/TrackPoint.kt | 12 ++++ .../kotlin/org/terst/nav/track/TrackRepository.kt | 24 +++++++ .../main/kotlin/org/terst/nav/ui/MainViewModel.kt | 32 +++++++++ .../src/main/kotlin/org/terst/nav/ui/MapHandler.kt | 29 ++++++++ .../app/src/main/res/drawable/ic_track_record.xml | 17 +++++ .../app/src/main/res/layout/activity_main.xml | 13 ++++ .../main/kotlin/org/terst/nav/track/TrackPoint.kt | 12 ++++ .../kotlin/org/terst/nav/track/TrackRepository.kt | 24 +++++++ .../org/terst/nav/track/TrackRepositoryTest.kt | 79 ++++++++++++++++++++++ 11 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt create mode 100644 android-app/app/src/main/res/drawable/ic_track_record.xml create mode 100644 test-runner/src/main/kotlin/org/terst/nav/track/TrackPoint.kt create mode 100644 test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt create mode 100644 test-runner/src/test/kotlin/org/terst/nav/track/TrackRepositoryTest.kt (limited to 'android-app/app/src/main/res/layout') diff --git a/.agent/worklog.md b/.agent/worklog.md index e17781b..7a4467f 100644 --- a/.agent/worklog.md +++ b/.agent/worklog.md @@ -127,10 +127,23 @@ Section 7.3 AIS display — COMPLETE (2026-03-15): AIS integrated into ViewModel - `MainViewModelTest` — 3 new tests: valid type-1 adds vessel, same MMSI deduped, non-AIS stays empty - JVM test harness: `/tmp/ais-vm-test-runner/` (3 tests — all GREEN) +### [APPROVED] TrackRepository (2026-03-25) +- `android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt` +- `test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt` +- `isRecording` flag; `startTrack()` clears + starts; `stopTrack()`; `addPoint()` guards on isRecording; `getPoints()` returns snapshot +- 8 tests — all GREEN (`TrackRepositoryTest`) + +### [APPROVED] Track ViewModel + Map overlay + Record FAB (2026-03-25) +- `MainViewModel`: TrackRepository wired in; exposes `isRecording: StateFlow`, `trackPoints: StateFlow>`; `startTrack()`, `stopTrack()`, `addGpsPoint(lat, lon, sogKnots, cogDeg)` +- `MapHandler.updateTrackLayer(style, points)`: lazy LineLayer init; red (#E53935) 3dp polyline from List +- `MainActivity`: stores `loadedStyle`; GPS flow feeds `viewModel.addGpsPoint()` (m/s→knots); observes `trackPoints` → `mapHandler.updateTrackLayer()`; observes `isRecording` → FAB icon toggle (ic_track_record / ic_close) +- `activity_main.xml`: `fab_record_track` FAB anchored top|end of bottom nav +- `drawable/ic_track_record.xml`: red dot record icon + ## Next 3 Specific Steps -1. **CPA/TCPA alarms** — use CpaCalculator in ViewModel to emit alarm when CPA < threshold; add UI indicator in MapFragment -2. **AISHub periodic polling** — call refreshAisFromInternet() on a timer (e.g. every 60s) when GPS position is known -3. **AIS TCP full implementation** — replace stub socket reader with NmeaStreamManager integration +1. **Persist track to GPX/Room** — export recorded track as GPX file or store in Room DB +2. **Track stats in Log tab** — show elapsed time, distance, avg SOG while recording +3. **AnchorWatchHandler UI** — wire `AnchorWatchHandler` fully into SafetyFragment (currently stub) ## Scripts Added - `test-runner/` — standalone Kotlin/JVM Gradle project; runs all 22 GPS/NMEA tests without Android SDK diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 61d8b9b..ecaddc0 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -45,9 +45,11 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private var instrumentHandler: InstrumentHandler? = null private var mapHandler: MapHandler? = null private var anchorWatchHandler: AnchorWatchHandler? = null - + private var loadedStyle: Style? = null + private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var fragmentContainer: FrameLayout + private lateinit var fabRecordTrack: FloatingActionButton private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) } private val viewModel: MainViewModel by viewModels() @@ -81,6 +83,11 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { findViewById(R.id.fab_mob).setOnClickListener { onActivateMob() } + + fabRecordTrack = findViewById(R.id.fab_record_track) + fabRecordTrack.setOnClickListener { + if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack() + } } private fun setupBottomSheet() { @@ -227,10 +234,11 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { }, 256)) .withLayer(RasterLayer("openseamap-layer", "openseamap-source")) - maplibreMap.setStyle(style) { loadedStyle -> + maplibreMap.setStyle(style) { style -> + loadedStyle = style val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor) val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow) - mapHandler?.setupLayers(loadedStyle, anchorBitmap, arrowBitmap) + mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap) } } } @@ -239,6 +247,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { lifecycleScope.launch { LocationService.locationFlow.collect { gpsData -> mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude) + val sogKnots = gpsData.speedOverGround * 1.94384 + viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, sogKnots, gpsData.courseOverGround.toDouble()) } } lifecycleScope.launch { @@ -246,6 +256,19 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { safetyFragment.updateAnchorStatus(if (state.isActive) "Active: ${state.watchCircleRadiusMeters}m" else "Inactive") } } + lifecycleScope.launch { + viewModel.trackPoints.collect { points -> + val style = loadedStyle ?: return@collect + mapHandler?.updateTrackLayer(style, points) + } + } + lifecycleScope.launch { + viewModel.isRecording.collect { recording -> + val icon = if (recording) R.drawable.ic_close else R.drawable.ic_track_record + fabRecordTrack.setImageResource(icon) + fabRecordTrack.contentDescription = if (recording) "Stop Recording" else "Record Track" + } + } } private fun startInstrumentSimulation(polarTable: PolarTable) { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt new file mode 100644 index 0000000..d803c8c --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt @@ -0,0 +1,12 @@ +package org.terst.nav.track + +data class TrackPoint( + val lat: Double, + val lon: Double, + val sogKnots: Double, + val cogDeg: Double, + val windSpeedKnots: Double, + val windAngleDeg: Double, + val isTrueWind: Boolean, + val timestampMs: Long +) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt new file mode 100644 index 0000000..c90adb9 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -0,0 +1,24 @@ +package org.terst.nav.track + +class TrackRepository { + + var isRecording: Boolean = false + private set + + private val points = mutableListOf() + + fun startTrack() { + points.clear() + isRecording = true + } + + fun stopTrack() { + isRecording = false + } + + fun addPoint(point: TrackPoint) { + if (isRecording) points.add(point) + } + + fun getPoints(): List = points.toList() +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt index 8e84e1e..33decbe 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt @@ -6,6 +6,8 @@ import org.terst.nav.ais.AisHubSource import org.terst.nav.ais.AisRepository import org.terst.nav.ais.AisVessel import org.terst.nav.data.api.AisHubApiService +import org.terst.nav.track.TrackPoint +import org.terst.nav.track.TrackRepository import org.terst.nav.data.model.ForecastItem import org.terst.nav.data.model.WindArrow import org.terst.nav.data.repository.WeatherRepository @@ -43,6 +45,36 @@ class MainViewModel( private val aisRepository = AisRepository() + private val trackRepository = TrackRepository() + + private val _isRecording = MutableStateFlow(false) + val isRecording: StateFlow = _isRecording.asStateFlow() + + private val _trackPoints = MutableStateFlow>(emptyList()) + val trackPoints: StateFlow> = _trackPoints.asStateFlow() + + fun startTrack() { + trackRepository.startTrack() + _trackPoints.value = emptyList() + _isRecording.value = true + } + + fun stopTrack() { + trackRepository.stopTrack() + _isRecording.value = false + } + + fun addGpsPoint(lat: Double, lon: Double, sogKnots: Double, cogDeg: Double) { + val point = TrackPoint( + lat = lat, lon = lon, + sogKnots = sogKnots, cogDeg = cogDeg, + windSpeedKnots = 0.0, windAngleDeg = 0.0, isTrueWind = false, + timestampMs = System.currentTimeMillis() + ) + trackRepository.addPoint(point) + _trackPoints.value = trackRepository.getPoints() + } + private val aisHubApi: AisHubApiService by lazy { Retrofit.Builder() .baseUrl("https://data.aishub.net") diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt index 91569cf..cbc2e90 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt @@ -7,6 +7,7 @@ import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.Style import org.maplibre.android.style.layers.CircleLayer +import org.maplibre.android.style.layers.LineLayer import org.maplibre.android.style.layers.PropertyFactory import org.maplibre.android.style.layers.RasterLayer import org.maplibre.android.style.layers.SymbolLayer @@ -15,10 +16,12 @@ import org.maplibre.android.style.sources.RasterSource import org.maplibre.android.style.sources.TileSet import org.maplibre.geojson.Feature import org.maplibre.geojson.FeatureCollection +import org.maplibre.geojson.LineString import org.maplibre.geojson.Point import org.maplibre.geojson.Polygon import org.terst.nav.AnchorWatchState import org.terst.nav.TidalCurrentState +import org.terst.nav.track.TrackPoint import kotlin.math.cos import kotlin.math.sin @@ -37,9 +40,13 @@ class MapHandler(private val maplibreMap: MapLibreMap) { private val TIDAL_CURRENT_LAYER_ID = "tidal-current-layer" private val TIDAL_ARROW_ICON_ID = "tidal-arrow-icon" + private val TRACK_SOURCE_ID = "track-source" + private val TRACK_LAYER_ID = "track-line" + private var anchorPointSource: GeoJsonSource? = null private var anchorCircleSource: GeoJsonSource? = null private var tidalCurrentSource: GeoJsonSource? = null + private var trackSource: GeoJsonSource? = null /** * Initializes map layers for anchor watch and tidal currents. @@ -127,6 +134,28 @@ class MapHandler(private val maplibreMap: MapLibreMap) { } } + /** + * Updates the GPS track polyline on the map. Lazily initialises the layer on first call. + */ + fun updateTrackLayer(style: Style, points: List) { + if (trackSource == null) { + trackSource = GeoJsonSource(TRACK_SOURCE_ID) + style.addSource(trackSource!!) + style.addLayer(LineLayer(TRACK_LAYER_ID, TRACK_SOURCE_ID).apply { + setProperties( + PropertyFactory.lineColor("#E53935"), + PropertyFactory.lineWidth(3f) + ) + }) + } + if (points.size >= 2) { + val coords = points.map { Point.fromLngLat(it.lon, it.lat) } + trackSource?.setGeoJson(Feature.fromGeometry(LineString.fromLngLats(coords))) + } else { + trackSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList())) + } + } + private fun createCirclePolygon(lat: Double, lon: Double, radiusMeters: Double): Polygon { val points = mutableListOf() val degreesBetweenPoints = 8 diff --git a/android-app/app/src/main/res/drawable/ic_track_record.xml b/android-app/app/src/main/res/drawable/ic_track_record.xml new file mode 100644 index 0000000..9016369 --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_track_record.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index 66d1abe..552bf99 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -68,4 +68,17 @@ app:layout_anchor="@id/bottom_navigation" app:layout_anchorGravity="top|start" /> + + + diff --git a/test-runner/src/main/kotlin/org/terst/nav/track/TrackPoint.kt b/test-runner/src/main/kotlin/org/terst/nav/track/TrackPoint.kt new file mode 100644 index 0000000..d803c8c --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/track/TrackPoint.kt @@ -0,0 +1,12 @@ +package org.terst.nav.track + +data class TrackPoint( + val lat: Double, + val lon: Double, + val sogKnots: Double, + val cogDeg: Double, + val windSpeedKnots: Double, + val windAngleDeg: Double, + val isTrueWind: Boolean, + val timestampMs: Long +) diff --git a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt new file mode 100644 index 0000000..c90adb9 --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -0,0 +1,24 @@ +package org.terst.nav.track + +class TrackRepository { + + var isRecording: Boolean = false + private set + + private val points = mutableListOf() + + fun startTrack() { + points.clear() + isRecording = true + } + + fun stopTrack() { + isRecording = false + } + + fun addPoint(point: TrackPoint) { + if (isRecording) points.add(point) + } + + fun getPoints(): List = points.toList() +} diff --git a/test-runner/src/test/kotlin/org/terst/nav/track/TrackRepositoryTest.kt b/test-runner/src/test/kotlin/org/terst/nav/track/TrackRepositoryTest.kt new file mode 100644 index 0000000..dea19c6 --- /dev/null +++ b/test-runner/src/test/kotlin/org/terst/nav/track/TrackRepositoryTest.kt @@ -0,0 +1,79 @@ +package org.terst.nav.track + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +private fun makePoint( + lat: Double = 37.0, + lon: Double = -122.0, + sog: Double = 5.0, + cog: Double = 90.0, + windSpeed: Double = 10.0, + windAngle: Double = 45.0, + isTrueWind: Boolean = false, + ts: Long = 1_000L +) = TrackPoint(lat, lon, sog, cog, windSpeed, windAngle, isTrueWind, ts) + +class TrackRepositoryTest { + + private lateinit var repo: TrackRepository + + @Before fun setUp() { + repo = TrackRepository() + } + + @Test fun `initial state is not recording`() { + assertFalse(repo.isRecording) + } + + @Test fun `startTrack sets isRecording to true`() { + repo.startTrack() + assertTrue(repo.isRecording) + } + + @Test fun `stopTrack sets isRecording to false`() { + repo.startTrack() + repo.stopTrack() + assertFalse(repo.isRecording) + } + + @Test fun `addPoint appends point when recording`() { + repo.startTrack() + repo.addPoint(makePoint(lat = 37.1)) + assertEquals(1, repo.getPoints().size) + assertEquals(37.1, repo.getPoints()[0].lat, 0.0001) + } + + @Test fun `addPoint is ignored when not recording`() { + repo.addPoint(makePoint()) + assertEquals(0, repo.getPoints().size) + } + + @Test fun `startTrack clears previous points`() { + repo.startTrack() + repo.addPoint(makePoint()) + repo.addPoint(makePoint()) + repo.stopTrack() + repo.startTrack() + assertEquals(0, repo.getPoints().size) + } + + @Test fun `multiple points accumulate in order`() { + repo.startTrack() + repo.addPoint(makePoint(lat = 37.0, ts = 1000L)) + repo.addPoint(makePoint(lat = 37.1, ts = 2000L)) + repo.addPoint(makePoint(lat = 37.2, ts = 3000L)) + val pts = repo.getPoints() + assertEquals(3, pts.size) + assertEquals(37.0, pts[0].lat, 0.0001) + assertEquals(37.2, pts[2].lat, 0.0001) + } + + @Test fun `getPoints returns snapshot not live list`() { + repo.startTrack() + val snapshot = repo.getPoints() + repo.addPoint(makePoint()) + assertEquals(0, snapshot.size) // snapshot taken before addPoint + } +} -- cgit v1.2.3 From 2e0c420020526873906fedd024b50baef56011fa Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 2 Apr 2026 21:53:08 +0000 Subject: fix(ui): anchor both FABs to instrument sheet (squash merge recovery) Both fab_mob and fab_record_track now anchor to instrument_bottom_sheet so they sit above the sheet peek zone rather than being occluded by the sheet's 16dp elevation. Resolves stash conflict in settings.local.json. Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 4 +++- android-app/app/src/main/res/layout/activity_main.xml | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bcb3577..4fe9a49 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -57,7 +57,9 @@ "Bash(screen -list)", "Bash(adb logcat:*)", "Bash(grep -v \"^--$\")", - "Bash(./gradlew assembleDebug)" + "Bash(./gradlew assembleDebug)", + "Skill(commit-commands:commit-push-pr)", + "Skill(code-review:code-review)" ] } } diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index 552bf99..68abc60 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -65,7 +65,7 @@ android:contentDescription="Man Overboard" app:srcCompat="@android:drawable/ic_dialog_alert" app:backgroundTint="@color/mob_button_background" - app:layout_anchor="@id/bottom_navigation" + app:layout_anchor="@id/instrument_bottom_sheet" app:layout_anchorGravity="top|start" /> @@ -78,7 +78,7 @@ android:focusable="true" android:contentDescription="Record Track" app:srcCompat="@drawable/ic_track_record" - app:layout_anchor="@id/bottom_navigation" + app:layout_anchor="@id/instrument_bottom_sheet" app:layout_anchorGravity="top|end" /> -- cgit v1.2.3 From 04d990f8066312e79532cad9f70f7b4c92a3d9c8 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 3 Apr 2026 02:44:25 +0000 Subject: fix(ui): raise both FABs fully above instrument sheet top edge anchorGravity=top centers the FAB on the sheet edge, leaving half the button occluded. Increase marginBottom to 44dp (28dp to clear the FAB radius + 16dp gap) so both buttons sit fully above the sheet. Co-Authored-By: Claude Sonnet 4.6 --- android-app/app/src/main/res/layout/activity_main.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index 68abc60..edac466 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -59,7 +59,8 @@ android:id="@+id/fab_mob" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="16dp" + android:layout_marginStart="16dp" + android:layout_marginBottom="44dp" android:clickable="true" android:focusable="true" android:contentDescription="Man Overboard" @@ -73,7 +74,8 @@ android:id="@+id/fab_record_track" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="44dp" android:clickable="true" android:focusable="true" android:contentDescription="Record Track" -- cgit v1.2.3 From e9df7afa1d96fde80c482e497d7c17617c2d95c3 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 3 Apr 2026 02:58:19 +0000 Subject: fix(ui): raise FAB elevation above CardView sheet to fix z-order The instrument sheet CardView has cardElevation=16dp which was rendering on top of the FABs (default ~6dp elevation). Set app:elevation=20dp on both FABs so they always appear above the sheet. Also reverts oversized marginBottom back to standard 16dp all-round now that elevation stacking is correct. Co-Authored-By: Claude Sonnet 4.6 --- android-app/app/src/main/res/layout/activity_main.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index edac466..b4221ed 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -59,13 +59,13 @@ android:id="@+id/fab_mob" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginBottom="44dp" + android:layout_margin="16dp" android:clickable="true" android:focusable="true" android:contentDescription="Man Overboard" app:srcCompat="@android:drawable/ic_dialog_alert" app:backgroundTint="@color/mob_button_background" + app:elevation="20dp" app:layout_anchor="@id/instrument_bottom_sheet" app:layout_anchorGravity="top|start" /> @@ -74,12 +74,12 @@ android:id="@+id/fab_record_track" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="16dp" - android:layout_marginBottom="44dp" + android:layout_margin="16dp" android:clickable="true" android:focusable="true" android:contentDescription="Record Track" app:srcCompat="@drawable/ic_track_record" + app:elevation="20dp" app:layout_anchor="@id/instrument_bottom_sheet" app:layout_anchorGravity="top|end" /> -- cgit v1.2.3 From bf2223827c53fbc0e77d1af2a7d4654a7c248ee0 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 2 Apr 2026 21:08:04 -1000 Subject: feat(map): interactive map with auto-follow, recenter button, and UI immersive mode (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add map interaction design spec Co-Authored-By: Claude Sonnet 4.6 * docs: add map interaction implementation plan Co-Authored-By: Claude Sonnet 4.6 * feat(map): add isFollowing state and gesture-driven manual mode to MapHandler * feat(ui): add fab_recenter pill button to map layout * feat(map): wire UI fade-out and recenter button to MapHandler.isFollowing * fix(map): prevent fadeIn flash on cold start; consolidate fab_mob listener * fix(map): preserve user zoom level on recenter Capture the current camera zoom when the user gestures (entering manual mode) and pass it back to centerOnLocation in recenter(), so tapping Recenter returns to the user's chosen zoom rather than always snapping to the default 14. Co-Authored-By: Claude Sonnet 4.6 * fix(map): capture lastZoom on camera idle, not gesture start OnCameraMoveStartedListener fires before the gesture completes, so it captured the pre-gesture zoom. OnCameraIdleListener fires after the camera settles, giving the user's final intended zoom level. Only update lastZoom while in manual mode (isFollowing=false). Co-Authored-By: Claude Sonnet 4.6 * fix(map): guard recenter against null island and add KDoc - Skip recenter() if no GPS fix received (lastLat/lastLon still 0.0) to avoid animating to 0°N, 0°E - Add KDoc comment to recenter() consistent with other public methods Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 48 +++- .../src/main/kotlin/org/terst/nav/ui/MapHandler.kt | 36 +++ .../app/src/main/res/layout/activity_main.xml | 16 ++ .../plans/2026-04-03-map-interaction.md | 256 +++++++++++++++++++++ .../specs/2026-04-03-map-interaction-design.md | 59 +++++ 5 files changed, 411 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-03-map-interaction.md create mode 100644 docs/superpowers/specs/2026-04-03-map-interaction-design.md (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index f9d4dbd..252761e 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -15,9 +15,11 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.button.MaterialButton import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.floatingactionbutton.FloatingActionButton import kotlinx.coroutines.Dispatchers @@ -53,6 +55,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var fragmentContainer: FrameLayout private lateinit var fabRecordTrack: FloatingActionButton + private lateinit var fabMob: FloatingActionButton + private lateinit var fabRecenter: MaterialButton + private lateinit var bottomSheet: CardView + private lateinit var bottomNav: BottomNavigationView private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) } private val viewModel: MainViewModel by viewModels() @@ -83,14 +89,20 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { setupBottomNavigation() setupHandlers() - findViewById(R.id.fab_mob).setOnClickListener { - onActivateMob() - } - fabRecordTrack = findViewById(R.id.fab_record_track) fabRecordTrack.setOnClickListener { if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack() } + + fabMob = findViewById(R.id.fab_mob) + fabMob.setOnClickListener { onActivateMob() } + fabRecenter = findViewById(R.id.fab_recenter) + bottomSheet = findViewById(R.id.instrument_bottom_sheet) + bottomNav = findViewById(R.id.bottom_navigation) + + fabRecenter.setOnClickListener { + mapHandler?.recenter() + } // Observe immediately — pure UI state, not gated on GPS permission lifecycleScope.launch { viewModel.isRecording.collect { recording -> @@ -232,6 +244,17 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { mapView?.onCreate(null) mapView?.getMapAsync { maplibreMap -> mapHandler = MapHandler(maplibreMap) + lifecycleScope.launch { + mapHandler!!.isFollowing.collect { following -> + if (following) { + fadeOut(fabRecenter, gone = true) + fadeIn(bottomSheet, bottomNav, fabMob, fabRecordTrack) + } else { + fadeOut(bottomSheet, bottomNav, fabMob, fabRecordTrack, gone = true) + fadeIn(fabRecenter) + } + } + } val style = Style.Builder() .fromUri("https://tiles.openfreemap.org/styles/liberty") .withSource(RasterSource("openseamap-source", @@ -309,6 +332,23 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { return PolarTable(curves) } + private fun fadeOut(vararg views: View, gone: Boolean = false) { + views.forEach { v -> + v.animate().alpha(0f).setDuration(150).withEndAction { + v.visibility = if (gone) View.GONE else View.INVISIBLE + }.start() + } + } + + private fun fadeIn(vararg views: View) { + views.forEach { v -> + if (v.visibility == View.VISIBLE && v.alpha == 1f) return@forEach + v.alpha = 0f + v.visibility = View.VISIBLE + v.animate().alpha(1f).setDuration(150).start() + } + } + override fun onStart() { super.onStart(); mapView?.onStart() } override fun onPause() { super.onPause(); mapView?.onPause() } override fun onStop() { super.onStop(); mapView?.onStop() } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt index cbc2e90..7c82808 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt @@ -24,12 +24,35 @@ import org.terst.nav.TidalCurrentState import org.terst.nav.track.TrackPoint import kotlin.math.cos import kotlin.math.sin +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow /** * Handles MapLibre initialization, layers, and updates. */ class MapHandler(private val maplibreMap: MapLibreMap) { + init { + maplibreMap.addOnCameraMoveStartedListener { reason -> + if (reason == MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE) { + _isFollowing.value = false + } + } + maplibreMap.addOnCameraIdleListener { + if (!_isFollowing.value) { + lastZoom = maplibreMap.cameraPosition.zoom + } + } + } + + private val _isFollowing = MutableStateFlow(true) + val isFollowing: StateFlow = _isFollowing.asStateFlow() + + private var lastLat: Double = 0.0 + private var lastLon: Double = 0.0 + private var lastZoom: Double = 14.0 + private val ANCHOR_POINT_SOURCE_ID = "anchor-point-source" private val ANCHOR_CIRCLE_SOURCE_ID = "anchor-circle-source" private val ANCHOR_POINT_LAYER_ID = "anchor-point-layer" @@ -95,6 +118,9 @@ class MapHandler(private val maplibreMap: MapLibreMap) { * Centers the map on the specified location. */ fun centerOnLocation(lat: Double, lon: Double, zoom: Double = 14.0) { + lastLat = lat + lastLon = lon + if (!_isFollowing.value) return val position = CameraPosition.Builder() .target(LatLng(lat, lon)) .zoom(zoom) @@ -102,6 +128,16 @@ class MapHandler(private val maplibreMap: MapLibreMap) { maplibreMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000) } + /** + * Restores auto-follow mode and animates the camera back to the last known GPS position. + * No-op if no GPS fix has been received yet. + */ + fun recenter() { + if (lastLat == 0.0 && lastLon == 0.0) return + _isFollowing.value = true + centerOnLocation(lastLat, lastLon, lastZoom) + } + /** * Updates the anchor watch visualization on the map. */ diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index b4221ed..a3d347f 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -25,6 +25,22 @@ android:visibility="gone" android:background="?attr/colorSurface" /> + + diff --git a/docs/superpowers/plans/2026-04-03-map-interaction.md b/docs/superpowers/plans/2026-04-03-map-interaction.md new file mode 100644 index 0000000..9f8fa13 --- /dev/null +++ b/docs/superpowers/plans/2026-04-03-map-interaction.md @@ -0,0 +1,256 @@ +# Map Interaction Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add manual pan/zoom with auto-follow GPS centering, a Recenter button that appears when the user pans away, and full UI fade-out in manual mode. + +**Architecture:** `MapHandler` owns an `isFollowing: StateFlow` and registers a `MapLibreMap.OnCameraMoveStartedListener` to detect gesture-driven camera moves. `MainActivity` collects the flow and animates the bottom sheet, nav bar, FABs, and recenter button in/out. + +**Tech Stack:** MapLibre Android SDK (`MapLibreMap.OnCameraMoveStartedListener`, `REASON_API_GESTURE`), Kotlin `StateFlow`, Android `View.animate()`. + +--- + +## File Map + +| File | What changes | +|------|-------------| +| `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt` | Add `isFollowing` StateFlow, `lastLat`/`lastLon`, gesture listener, `recenter()`, guard in `centerOnLocation()` | +| `android-app/app/src/main/res/layout/activity_main.xml` | Add `fab_recenter` pill button inside the map ConstraintLayout | +| `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt` | Observe `mapHandler.isFollowing`, animate UI in/out, wire `fab_recenter` click | + +--- + +### Task 1: Extend MapHandler with follow state and gesture detection + +**Files:** +- Modify: `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt` + +- [ ] **Step 1: Add imports and new fields** + +Open `MapHandler.kt`. Add these imports at the top (after existing imports): + +```kotlin +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.maplibre.android.maps.MapLibreMap +``` + +Add these fields inside the `MapHandler` class, before `setupLayers`: + +```kotlin +private val _isFollowing = MutableStateFlow(true) +val isFollowing: StateFlow = _isFollowing.asStateFlow() + +private var lastLat: Double = 0.0 +private var lastLon: Double = 0.0 +``` + +- [ ] **Step 2: Register the gesture listener in the constructor** + +Replace the class declaration line: +```kotlin +class MapHandler(private val maplibreMap: MapLibreMap) { +``` +with: +```kotlin +class MapHandler(private val maplibreMap: MapLibreMap) { + + init { + maplibreMap.addOnCameraMoveStartedListener { reason -> + if (reason == MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE) { + _isFollowing.value = false + } + } + } +``` + +- [ ] **Step 3: Guard centerOnLocation and store last position** + +Replace the existing `centerOnLocation` method: +```kotlin +fun centerOnLocation(lat: Double, lon: Double, zoom: Double = 14.0) { + lastLat = lat + lastLon = lon + if (!_isFollowing.value) return + val position = CameraPosition.Builder() + .target(LatLng(lat, lon)) + .zoom(zoom) + .build() + maplibreMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000) +} +``` + +- [ ] **Step 4: Add recenter()** + +Add this method after `centerOnLocation`: +```kotlin +fun recenter() { + _isFollowing.value = true + centerOnLocation(lastLat, lastLon) +} +``` + +- [ ] **Step 5: Commit** + +```bash +git add android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt +git commit -m "feat(map): add isFollowing state and gesture-driven manual mode to MapHandler" +``` + +--- + +### Task 2: Add the Recenter button to the layout + +**Files:** +- Modify: `android-app/app/src/main/res/layout/activity_main.xml` + +- [ ] **Step 1: Add fab_recenter inside the ConstraintLayout** + +In `activity_main.xml`, find the ConstraintLayout that wraps `mapView` (around line 10). It currently contains `mapView` and `fragment_container`. Add the recenter button as a third child, before the closing `` tag: + +```xml + +``` + +- [ ] **Step 2: Commit** + +```bash +git add android-app/app/src/main/res/layout/activity_main.xml +git commit -m "feat(ui): add fab_recenter pill button to map layout" +``` + +--- + +### Task 3: Wire MainActivity to animate UI on follow state changes + +**Files:** +- Modify: `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt` + +- [ ] **Step 1: Add imports** + +Add these imports to `MainActivity.kt` (with existing imports): +```kotlin +import androidx.cardview.widget.CardView +import com.google.android.material.button.MaterialButton +``` + +- [ ] **Step 2: Add view fields** + +In the `MainActivity` class body, alongside the existing `fabRecordTrack` declaration, add: +```kotlin +private lateinit var fabMob: FloatingActionButton +private lateinit var fabRecenter: MaterialButton +private lateinit var bottomSheet: CardView +private lateinit var bottomNav: BottomNavigationView +``` + +- [ ] **Step 3: Add the fade helpers** + +Add these two private methods to `MainActivity` (before `onStart`): + +```kotlin +private fun fadeOut(vararg views: View, gone: Boolean = false) { + views.forEach { v -> + v.animate().alpha(0f).setDuration(150).withEndAction { + v.visibility = if (gone) View.GONE else View.INVISIBLE + }.start() + } +} + +private fun fadeIn(vararg views: View) { + views.forEach { v -> + v.alpha = 0f + v.visibility = View.VISIBLE + v.animate().alpha(1f).setDuration(150).start() + } +} +``` + +- [ ] **Step 4: Wire up views and observe isFollowing in initializeUI** + +In `initializeUI()`, after the existing `fabRecordTrack` setup, add: + +```kotlin +fabMob = findViewById(R.id.fab_mob) +fabRecenter = findViewById(R.id.fab_recenter) +bottomSheet = findViewById(R.id.instrument_bottom_sheet) +bottomNav = findViewById(R.id.bottom_navigation) + +fabRecenter.setOnClickListener { + mapHandler?.recenter() +} +``` + +- [ ] **Step 5: Observe isFollowing after mapHandler is created** + +In `setupMap()`, inside the `getMapAsync` lambda, after `mapHandler = MapHandler(maplibreMap)`, add: + +```kotlin +lifecycleScope.launch { + mapHandler!!.isFollowing.collect { following -> + if (following) { + fadeOut(fabRecenter, gone = true) + fadeIn(bottomSheet, bottomNav, fabMob, fabRecordTrack) + } else { + fadeOut(bottomSheet, bottomNav, fabMob, fabRecordTrack, gone = true) + fadeIn(fabRecenter) + } + } +} +``` + +- [ ] **Step 6: Manual smoke test** + +Build and install. Verify: +1. App opens — bottom sheet, nav, FABs visible; no Recenter button +2. Pan the map — all UI fades out, Recenter button fades in +3. Tap Recenter — UI fades back in, map animates to GPS position, Recenter gone +4. GPS updates while in manual mode — map does NOT jump back to GPS position + +- [ ] **Step 7: Commit** + +```bash +git add android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +git commit -m "feat(map): wire UI fade-out and recenter button to MapHandler.isFollowing" +``` + +--- + +### Task 4: Request Gemini review, fix issues, loop until clean, merge + +- [ ] **Step 1: Push branch and open PR** +```bash +git push local main +git push github main +``` +Then open a PR (or request review inline if working on main). + +- [ ] **Step 2: Request code review using code-review:code-review skill** + +Invoke `code-review:code-review` skill against the PR. Address all issues with confidence ≥ 80. + +- [ ] **Step 3: Loop until review returns clean** + +Re-request review after each fix. Stop when no issues ≥ 80 are found. + +- [ ] **Step 4: Merge and push to both remotes** +```bash +gh pr merge --repo thepeterstone/nav --squash --delete-branch +git checkout main && git pull github main +git push local main +``` diff --git a/docs/superpowers/specs/2026-04-03-map-interaction-design.md b/docs/superpowers/specs/2026-04-03-map-interaction-design.md new file mode 100644 index 0000000..02e38e1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-map-interaction-design.md @@ -0,0 +1,59 @@ +# Map Interaction Design +_2026-04-03_ + +## Overview + +Enable free map pan/zoom while keeping GPS auto-follow as the default. When the user gestures on the map, the UI hides and a Recenter button appears. Tapping Recenter restores auto-follow and the full UI. + +## State + +A single `isFollowing: Boolean` flag owned by `MapHandler`. Starts `true`. This is the only piece of new state. + +## Entering Manual Mode + +`MapLibreMap.addOnCameraMoveStartedListener` fires with `REASON_API_GESTURE` when the user initiates a pan, pinch-zoom, or rotate. On that event: + +- `MapHandler.isFollowing` → `false` +- `MapHandler` emits via a new `StateFlow isFollowing` exposed to MainActivity +- `MapHandler.centerOnLocation()` becomes a no-op while `!isFollowing` — GPS updates still arrive, last position is stored + +## Returning to Auto-Follow + +`fab_recenter` click handler: + +- Calls `MapHandler.recenter()` — sets `isFollowing = true`, calls `centerOnLocation(lastLat, lastLon)` +- `isFollowing` StateFlow emits `true` → MainActivity restores UI + +## UI Changes + +### New element: `fab_recenter` + +- Pill-shaped button (`wrap_content` width, 40dp height, 20dp corner radius) +- Label: "⊙ Recenter" +- Position: centered horizontally, `24dp` above the bottom of the map `ConstraintLayout` +- `android:visibility="gone"` by default +- Elevation: 20dp (above the instrument sheet) + +### Visibility toggling (MainActivity) + +When `isFollowing` → `false`: +- Fade out (alpha 0, 150ms): `instrument_bottom_sheet`, `bottom_navigation`, `fab_mob`, `fab_record_track` +- Show `fab_recenter` (visibility VISIBLE, fade in 150ms) + +When `isFollowing` → `true`: +- Hide `fab_recenter` (fade out 150ms, then GONE) +- Fade in (alpha 1, 150ms): `instrument_bottom_sheet`, `bottom_navigation`, `fab_mob`, `fab_record_track` + +## Files to Change + +| File | Change | +|------|--------| +| `MapHandler.kt` | Add `isFollowing` StateFlow, `lastLat`/`lastLon` storage, `recenter()`, guard in `centerOnLocation()`, register `OnCameraMoveStartedListener` | +| `activity_main.xml` | Add `fab_recenter` pill button | +| `MainActivity.kt` | Observe `mapHandler.isFollowing`, animate UI in/out, wire `fab_recenter` click | + +## Out of Scope + +- Tapping the map (without gesture) to restore UI — not requested +- Timeout to auto-restore UI — not requested +- Zoom-level persistence across recenter — not requested -- cgit v1.2.3 From be56cf32a68ee1b0df2966ff39fd9751fd6afd7a Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 3 Apr 2026 07:25:13 +0000 Subject: feat(instruments): replace simulation with real GPS and barometer data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop VMG, Polar %, and PolarDiagramView — no NMEA source on boat - Shrink grid to 3×2 (AWS/HDG/BSP / TWS/COG/SOG) - Move Depth + Baro to expanded section side by side - Initialize all instruments to "—" on startup - Wire LocationService.locationFlow → SOG + COG display (real GPS) - Wire LocationService.barometerStatus → Baro display (device sensor) - Delete startInstrumentSimulation() fake loop entirely Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 69 +++++------------ .../kotlin/org/terst/nav/ui/InstrumentHandler.kt | 36 +-------- .../main/res/layout/layout_instruments_sheet.xml | 88 +++++++--------------- 3 files changed, 51 insertions(+), 142 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 252761e..c48cec2 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -11,7 +11,6 @@ import android.os.Bundle import android.util.Log import android.view.View import android.widget.FrameLayout -import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -22,15 +21,12 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.button.MaterialButton import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import java.util.Locale import org.maplibre.android.MapLibre import org.maplibre.android.maps.MapView import org.maplibre.android.maps.Style @@ -185,20 +181,14 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { valueCog = findViewById(R.id.value_cog), valueBsp = findViewById(R.id.value_bsp), valueSog = findViewById(R.id.value_sog), - valueVmg = findViewById(R.id.value_vmg), valueDepth = findViewById(R.id.value_depth), - valuePolarPct = findViewById(R.id.value_polar_pct), - valueBaro = findViewById(R.id.value_baro), - labelTrend = null, // simplified - barometerTrendView = null, // simplified - polarDiagramView = findViewById(R.id.polar_diagram_view) + valueBaro = findViewById(R.id.value_baro) + ) + instrumentHandler?.updateDisplay( + aws = "—", tws = "—", hdg = "—", + cog = "—", bsp = "—", sog = "—", + depth = "—", baro = "—" ) - - // anchorWatchHandler is initialized when the anchor config UI is available - - val mockPolarTable = createMockPolarTable() - findViewById(R.id.polar_diagram_view).setPolarTable(mockPolarTable) - startInstrumentSimulation(mockPolarTable) } // Helper to convert dp to px @@ -277,7 +267,19 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { LocationService.locationFlow.collect { gpsData -> mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude) val sogKnots = gpsData.speedOverGround * 1.94384 - viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, sogKnots, gpsData.courseOverGround.toDouble()) + val cogDeg = gpsData.courseOverGround + viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, sogKnots, cogDeg.toDouble()) + instrumentHandler?.updateDisplay( + sog = "%.1f".format(Locale.getDefault(), sogKnots), + cog = "%.0f°".format(Locale.getDefault(), cogDeg) + ) + } + } + lifecycleScope.launch { + LocationService.barometerStatus.collect { status -> + if (status.history.isNotEmpty()) { + instrumentHandler?.updateDisplay(baro = status.formatPressure()) + } } } lifecycleScope.launch { @@ -292,28 +294,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } } - private fun startInstrumentSimulation(polarTable: PolarTable) { - lifecycleScope.launch { - var simulatedTws = 8.0 - var simulatedTwa = 40.0 - while (true) { - val bsp = polarTable.interpolateBsp(simulatedTws, simulatedTwa) - instrumentHandler?.updateDisplay( - aws = "%.1f".format(Locale.getDefault(), simulatedTws * 1.1), - tws = "%.1f".format(Locale.getDefault(), simulatedTws), - bsp = "%.1f".format(Locale.getDefault(), bsp), - sog = "%.1f".format(Locale.getDefault(), bsp * 0.95), - vmg = "%.1f".format(Locale.getDefault(), polarTable.curves.firstOrNull { it.twS == simulatedTws }?.calculateVmg(simulatedTwa, bsp) ?: 0.0), - polarPct = "%.0f%%".format(Locale.getDefault(), polarTable.calculatePolarPercentage(simulatedTws, simulatedTwa, bsp)), - baro = "1013.2" - ) - instrumentHandler?.updatePolarDiagram(simulatedTws, simulatedTwa, bsp) - simulatedTwa = (simulatedTwa + 0.5).let { if (it > 170) 40.0 else it } - delay(1000) - } - } - } - private fun rasterizeDrawable(drawableId: Int): Bitmap { val drawable = ContextCompat.getDrawable(this, drawableId)!! val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) @@ -323,15 +303,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { return bitmap } - private fun createMockPolarTable(): PolarTable { - val curves = listOf(6.0, 8.0, 10.0).map { tws -> - PolarCurve(tws, listOf(30.0, 45.0, 60.0, 90.0, 120.0, 150.0, 180.0).map { twa -> - PolarPoint(twa, tws * (0.4 + twa / 200.0)) - }) - } - return PolarTable(curves) - } - private fun fadeOut(vararg views: View, gone: Boolean = false) { views.forEach { v -> v.animate().alpha(0f).setDuration(150).withEndAction { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt index 2f72153..7e09756 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt @@ -1,11 +1,6 @@ package org.terst.nav.ui import android.widget.TextView -import org.terst.nav.BarometerReading -import org.terst.nav.BarometerTrendView -import org.terst.nav.PolarDiagramView -import org.terst.nav.R -import java.util.Locale /** * Handles the display of instrument data in the UI. @@ -17,16 +12,11 @@ class InstrumentHandler( private val valueCog: TextView, private val valueBsp: TextView, private val valueSog: TextView, - private val valueVmg: TextView, private val valueDepth: TextView, - private val valuePolarPct: TextView, - private val valueBaro: TextView, - private val labelTrend: TextView?, - private val barometerTrendView: BarometerTrendView?, - private val polarDiagramView: PolarDiagramView + private val valueBaro: TextView ) { /** - * Updates the text displays for various instruments. + * Updates the text displays for the given instruments. Null arguments leave the current value unchanged. */ fun updateDisplay( aws: String? = null, @@ -35,11 +25,8 @@ class InstrumentHandler( cog: String? = null, bsp: String? = null, sog: String? = null, - vmg: String? = null, depth: String? = null, - polarPct: String? = null, - baro: String? = null, - trend: String? = null + baro: String? = null ) { aws?.let { valueAws.text = it } tws?.let { valueTws.text = it } @@ -47,24 +34,7 @@ class InstrumentHandler( cog?.let { valueCog.text = it } bsp?.let { valueBsp.text = it } sog?.let { valueSog.text = it } - vmg?.let { valueVmg.text = it } depth?.let { valueDepth.text = it } - polarPct?.let { valuePolarPct.text = it } baro?.let { valueBaro.text = it } - trend?.let { labelTrend?.text = it } - } - - /** - * Updates the polar diagram view. - */ - fun updatePolarDiagram(tws: Double, twa: Double, bsp: Double) { - polarDiagramView.setCurrentPerformance(tws, twa, bsp) - } - - /** - * Updates the barometer trend chart. - */ - fun updateBarometerTrend(history: List) { - barometerTrendView?.setHistory(history) } } diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml index 0a84418..c651ba2 100644 --- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml +++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml @@ -16,14 +16,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> - + @@ -35,7 +35,7 @@ android:gravity="center" android:padding="8dp"> - + @@ -47,7 +47,7 @@ android:gravity="center" android:padding="8dp"> - + @@ -59,7 +59,7 @@ android:gravity="center" android:padding="8dp"> - + @@ -71,7 +71,7 @@ android:gravity="center" android:padding="8dp"> - + @@ -83,7 +83,7 @@ android:gravity="center" android:padding="8dp"> - + @@ -95,72 +95,40 @@ android:gravity="center" android:padding="8dp"> - + - - - - - + + + + - + android:layout_weight="1" + android:orientation="vertical"> - + - - - + android:layout_weight="1" + android:orientation="vertical"> + + - - - - - - - - + -- cgit v1.2.3 From 9417a7c6b08da362ad97e85973b7570e05d4f0b5 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 3 Apr 2026 07:38:22 +0000 Subject: feat(instruments): add forecast wind, wave, swell and current from Open-Meteo - Add swell params to MarineApiService request - Add swell fields to MarineHourly model - Add MarineConditions snapshot model - Add WeatherRepository.fetchCurrentConditions() (first forecast hour) - Add MainViewModel.loadConditions() + marineConditions StateFlow - Add Forecast section to instrument sheet: Curr / Wave / Swell - Populate TWS from forecast wind speed on first GPS fix - Trigger loadConditions() once on first GPS position received Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 34 ++++++++++- .../org/terst/nav/data/api/MarineApiService.kt | 2 +- .../org/terst/nav/data/model/MarineConditions.kt | 17 ++++++ .../org/terst/nav/data/model/MarineResponse.kt | 3 + .../terst/nav/data/repository/WeatherRepository.kt | 24 ++++++++ .../kotlin/org/terst/nav/ui/InstrumentHandler.kt | 28 ++++++++- .../main/kotlin/org/terst/nav/ui/MainViewModel.kt | 15 +++++ .../main/res/layout/layout_instruments_sheet.xml | 66 ++++++++++++++++++++++ 8 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index c48cec2..7c0cd9e 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -182,13 +182,24 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { valueBsp = findViewById(R.id.value_bsp), valueSog = findViewById(R.id.value_sog), valueDepth = findViewById(R.id.value_depth), - valueBaro = findViewById(R.id.value_baro) + valueBaro = findViewById(R.id.value_baro), + valueCurrSpd = findViewById(R.id.value_curr_spd), + valueCurrDir = findViewById(R.id.value_curr_dir), + valueWaveHt = findViewById(R.id.value_wave_ht), + valueWaveDir = findViewById(R.id.value_wave_dir), + valueSwellHt = findViewById(R.id.value_swell_ht), + valueSwellPer = findViewById(R.id.value_swell_per) ) instrumentHandler?.updateDisplay( aws = "—", tws = "—", hdg = "—", cog = "—", bsp = "—", sog = "—", depth = "—", baro = "—" ) + instrumentHandler?.updateConditions( + currSpd = "—", currDir = "—", + waveHt = "—", waveDir = "—", + swellHt = "—", swellPer = "—" + ) } // Helper to convert dp to px @@ -263,6 +274,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } private fun observeDataSources() { + var conditionsLoaded = false lifecycleScope.launch { LocationService.locationFlow.collect { gpsData -> mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude) @@ -273,6 +285,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { sog = "%.1f".format(Locale.getDefault(), sogKnots), cog = "%.0f°".format(Locale.getDefault(), cogDeg) ) + if (!conditionsLoaded) { + conditionsLoaded = true + viewModel.loadConditions(gpsData.latitude, gpsData.longitude) + } } } lifecycleScope.launch { @@ -282,6 +298,22 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } } } + lifecycleScope.launch { + viewModel.marineConditions.collect { c -> + if (c == null) return@collect + instrumentHandler?.updateDisplay( + tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) } ?: "—" + ) + instrumentHandler?.updateConditions( + currSpd = c.currentSpeedKt?.let { "%.1f kn".format(Locale.getDefault(), it) } ?: "—", + currDir = c.currentDirDeg?.let { "%.0f°".format(Locale.getDefault(), it) } ?: "—", + waveHt = c.waveHeightM?.let { "%.1f m".format(Locale.getDefault(), it) } ?: "—", + waveDir = c.waveDirDeg?.let { "%.0f°".format(Locale.getDefault(), it) } ?: "—", + swellHt = c.swellHeightM?.let { "%.1f m".format(Locale.getDefault(), it) } ?: "—", + swellPer = c.swellPeriodS?.let { "%.0f s".format(Locale.getDefault(), it) } ?: "—" + ) + } + } lifecycleScope.launch { LocationService.anchorWatchState.collect { state -> safetyFragment.updateAnchorStatus(if (state.isActive) "Active: ${state.watchCircleRadiusMeters}m" else "Inactive") diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/api/MarineApiService.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/api/MarineApiService.kt index 67c14f8..5a7d2e2 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/data/api/MarineApiService.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/data/api/MarineApiService.kt @@ -11,7 +11,7 @@ interface MarineApiService { @Query("latitude") latitude: Double, @Query("longitude") longitude: Double, @Query("hourly") hourly: String = - "wave_height,wave_direction,ocean_current_velocity,ocean_current_direction", + "wave_height,wave_direction,swell_wave_height,swell_wave_direction,swell_wave_period,ocean_current_velocity,ocean_current_direction", @Query("forecast_days") forecastDays: Int = 7 ): MarineResponse } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt new file mode 100644 index 0000000..3cde023 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt @@ -0,0 +1,17 @@ +package org.terst.nav.data.model + +/** + * Snapshot of current marine conditions derived from the first forecast hour. + * All fields are nullable — null means the model returned no data for that parameter. + */ +data class MarineConditions( + val windSpeedKt: Double?, + val windDirDeg: Double?, + val waveHeightM: Double?, + val waveDirDeg: Double?, + val swellHeightM: Double?, + val swellDirDeg: Double?, + val swellPeriodS: Double?, + val currentSpeedKt: Double?, // ocean_current_velocity converted from m/s to knots + val currentDirDeg: Double? +) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineResponse.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineResponse.kt index ab9799b..cf5216d 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineResponse.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineResponse.kt @@ -15,6 +15,9 @@ data class MarineHourly( @Json(name = "time") val time: List, @Json(name = "wave_height") val waveHeight: List, @Json(name = "wave_direction") val waveDirection: List, + @Json(name = "swell_wave_height") val swellWaveHeight: List, + @Json(name = "swell_wave_direction") val swellWaveDirection: List, + @Json(name = "swell_wave_period") val swellWavePeriod: List, @Json(name = "ocean_current_velocity") val oceanCurrentVelocity: List, @Json(name = "ocean_current_direction") val oceanCurrentDirection: List ) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt index ee586a5..b70ea8c 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt @@ -3,6 +3,7 @@ package org.terst.nav.data.repository import org.terst.nav.data.api.MarineApiService import org.terst.nav.data.api.WeatherApiService import org.terst.nav.data.model.ForecastItem +import org.terst.nav.data.model.MarineConditions import org.terst.nav.data.model.WindArrow class WeatherRepository( @@ -47,6 +48,29 @@ class WeatherRepository( ) } + /** + * Fetches current marine conditions (first forecast hour) for [lat]/[lon]. + * Ocean current velocity is converted from m/s to knots. + */ + suspend fun fetchCurrentConditions(lat: Double, lon: Double): Result = + runCatching { + val weather = weatherApi.getWeatherForecast(lat, lon, forecastDays = 1) + val marine = marineApi.getMarineForecast(lat, lon) + val w = weather.hourly + val m = marine.hourly + MarineConditions( + windSpeedKt = w.windspeed10m.firstOrNull(), + windDirDeg = w.winddirection10m.firstOrNull(), + waveHeightM = m.waveHeight.firstOrNull(), + waveDirDeg = m.waveDirection.firstOrNull(), + swellHeightM = m.swellWaveHeight.firstOrNull(), + swellDirDeg = m.swellWaveDirection.firstOrNull(), + swellPeriodS = m.swellWavePeriod.firstOrNull(), + currentSpeedKt = m.oceanCurrentVelocity.firstOrNull()?.let { it * 1.94384 }, + currentDirDeg = m.oceanCurrentDirection.firstOrNull() + ) + } + companion object { /** Factory using the shared ApiClient singletons. */ fun create(): WeatherRepository { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt index 7e09756..91582c0 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt @@ -13,7 +13,13 @@ class InstrumentHandler( private val valueBsp: TextView, private val valueSog: TextView, private val valueDepth: TextView, - private val valueBaro: TextView + private val valueBaro: TextView, + private val valueCurrSpd: TextView, + private val valueCurrDir: TextView, + private val valueWaveHt: TextView, + private val valueWaveDir: TextView, + private val valueSwellHt: TextView, + private val valueSwellPer: TextView ) { /** * Updates the text displays for the given instruments. Null arguments leave the current value unchanged. @@ -37,4 +43,24 @@ class InstrumentHandler( depth?.let { valueDepth.text = it } baro?.let { valueBaro.text = it } } + + /** + * Updates the forecast conditions section (Curr, Wave, Swell). + * Null arguments leave the current value unchanged. + */ + fun updateConditions( + currSpd: String? = null, + currDir: String? = null, + waveHt: String? = null, + waveDir: String? = null, + swellHt: String? = null, + swellPer: String? = null + ) { + currSpd?.let { valueCurrSpd.text = it } + currDir?.let { valueCurrDir.text = it } + waveHt?.let { valueWaveHt.text = it } + waveDir?.let { valueWaveDir.text = it } + swellHt?.let { valueSwellHt.text = it } + swellPer?.let { valueSwellPer.text = it } + } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt index 0efff52..0431f31 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt @@ -9,6 +9,7 @@ import org.terst.nav.data.api.AisHubApiService import org.terst.nav.track.TrackPoint import org.terst.nav.track.TrackRepository import org.terst.nav.data.model.ForecastItem +import org.terst.nav.data.model.MarineConditions import org.terst.nav.data.model.WindArrow import org.terst.nav.data.repository.WeatherRepository import com.squareup.moshi.Moshi @@ -40,6 +41,9 @@ class MainViewModel( private val _forecast = MutableStateFlow>(emptyList()) val forecast: StateFlow> = _forecast + private val _marineConditions = MutableStateFlow(null) + val marineConditions: StateFlow = _marineConditions.asStateFlow() + private val _aisTargets = MutableStateFlow>(emptyList()) val aisTargets: StateFlow> = _aisTargets.asStateFlow() @@ -88,6 +92,17 @@ class MainViewModel( .create(AisHubApiService::class.java) } + /** + * Fetches current conditions snapshot for [lat]/[lon] and exposes it via [marineConditions]. + * Silently ignored on network failure — the UI keeps showing dashes. + */ + fun loadConditions(lat: Double, lon: Double) { + viewModelScope.launch { + repository.fetchCurrentConditions(lat, lon) + .onSuccess { _marineConditions.value = it } + } + } + /** * Fetch weather and marine data for [lat]/[lon] in parallel. * Called once the device location is known. diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml index c651ba2..a6b74b0 100644 --- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml +++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml @@ -131,4 +131,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3 From e182619ce43bddea8dbee73592e3318fa9fbfc71 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 4 Apr 2026 02:31:54 +0000 Subject: feat(tripreport): add AI trip narrative generator with multiple styles - Consolidate track data, weather, and log entries into a TripSummary - Implement TripReportGenerator with Professional, Adventurous, Journal, and Pirate styles - Add TripReportFragment and ViewModel for UI interaction - Share TrackRepository and LogbookRepository via NavApplication singleton - Fix compilation error in MainViewModel rich metadata recording Co-Authored-By: Gemini CLI --- .../main/kotlin/org/terst/nav/NavApplication.kt | 2 + .../org/terst/nav/tripreport/TripReportFragment.kt | 86 +++++++++++++++ .../terst/nav/tripreport/TripReportGenerator.kt | 117 +++++++++++++++++++++ .../terst/nav/tripreport/TripReportViewModel.kt | 54 ++++++++++ .../main/kotlin/org/terst/nav/ui/MainViewModel.kt | 11 +- .../org/terst/nav/ui/voicelog/VoiceLogFragment.kt | 10 +- .../src/main/res/layout/fragment_trip_report.xml | 114 ++++++++++++++++++++ .../app/src/main/res/layout/fragment_voice_log.xml | 14 +++ 8 files changed, 402 insertions(+), 6 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportViewModel.kt create mode 100644 android-app/app/src/main/res/layout/fragment_trip_report.xml (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt index 3b8b596..9b8cb8a 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt @@ -12,6 +12,8 @@ import java.util.Locale class NavApplication : Application() { companion object { + val logbookRepository = org.terst.nav.logbook.InMemoryLogbookRepository() + val trackRepository = org.terst.nav.track.TrackRepository() var isTesting: Boolean = false get() { if (field) return true diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt new file mode 100644 index 0000000..e7a425f --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt @@ -0,0 +1,86 @@ +package org.terst.nav.tripreport + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.google.android.material.button.MaterialButton +import com.google.android.material.chip.ChipGroup +import kotlinx.coroutines.launch +import org.terst.nav.NavApplication +import org.terst.nav.R + +class TripReportFragment : Fragment() { + + private val viewModel by lazy { + TripReportViewModel( + trackRepository = NavApplication.trackRepository, + logbookRepository = NavApplication.logbookRepository + ) + } + + private lateinit var tvNarrativeContent: TextView + private lateinit var btnRefresh: MaterialButton + private lateinit var chipGroup: ChipGroup + private lateinit var progress: ProgressBar + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_trip_report, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + tvNarrativeContent = view.findViewById(R.id.tv_narrative_content) + btnRefresh = view.findViewById(R.id.btn_refresh_report) + chipGroup = view.findViewById(R.id.chip_group_styles) + progress = view.findViewById(R.id.progress_report) + + btnRefresh.setOnClickListener { viewModel.generateReport() } + + chipGroup.setOnCheckedStateChangeListener { _, checkedIds -> + val style = when (checkedIds.firstOrNull()) { + R.id.chip_adventurous -> NarrativeStyle.ADVENTUROUS + R.id.chip_journal -> NarrativeStyle.JOURNAL + R.id.chip_pirate -> NarrativeStyle.PIRATE + else -> NarrativeStyle.PROFESSIONAL + } + viewModel.setStyle(style) + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.state.collect { state -> renderState(state) } + } + + // Initial generation + viewModel.generateReport() + } + + private fun renderState(state: TripReportState) { + when (state) { + is TripReportState.Idle -> { + progress.visibility = View.GONE + } + is TripReportState.Loading -> { + progress.visibility = View.VISIBLE + btnRefresh.isEnabled = false + } + is TripReportState.Success -> { + progress.visibility = View.GONE + btnRefresh.isEnabled = true + tvNarrativeContent.text = state.narrative + } + is TripReportState.Error -> { + progress.visibility = View.GONE + btnRefresh.isEnabled = true + tvNarrativeContent.text = "Error: ${state.message}" + } + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt new file mode 100644 index 0000000..bbf00b1 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt @@ -0,0 +1,117 @@ +package org.terst.nav.tripreport + +import org.terst.nav.logbook.LogEntry +import org.terst.nav.track.TrackPoint + +enum class NarrativeStyle { + PROFESSIONAL, + ADVENTUROUS, + JOURNAL, + PIRATE +} + +data class TripSummary( + val startTimeMs: Long, + val endTimeMs: Long, + val distanceNm: Double, + val maxSogKts: Double, + val avgSogKts: Double, + val minAirTempC: Double?, + val maxAirTempC: Double?, + val maxWaveHeightM: Double?, + val logEntries: List, + val points: List +) + +class TripReportGenerator { + + fun generateSummary(points: List, logEntries: List): TripSummary { + if (points.isEmpty()) { + return TripSummary(0, 0, 0.0, 0.0, 0.0, null, null, null, logEntries, points) + } + + val startTime = points.first().timestampMs + val endTime = points.last().timestampMs + + var totalDist = 0.0 + for (i in 0 until points.size - 1) { + totalDist += calculateDistance(points[i].lat, points[i].lon, points[i+1].lat, points[i+1].lon) + } + val distanceNm = totalDist / 1852.0 // meters to nautical miles + + val maxSog = points.maxOf { it.sogKnots } + val avgSog = points.map { it.sogKnots }.average() + + val airTemps = points.mapNotNull { it.airTempC } + val minTemp = airTemps.minOrNull() + val maxTemp = airTemps.maxOrNull() + val maxWave = points.mapNotNull { it.waveHeightM }.maxOrNull() + + return TripSummary( + startTimeMs = startTime, + endTimeMs = endTime, + distanceNm = distanceNm, + maxSogKts = maxSog, + avgSogKts = avgSog, + minAirTempC = minTemp, + maxAirTempC = maxTemp, + maxWaveHeightM = maxWave, + logEntries = logEntries.filter { it.timestampMs in startTime..endTime }, + points = points + ) + } + + private fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { + val r = 6371e3 // Earth radius in meters + val phi1 = Math.toRadians(lat1) + val phi2 = Math.toRadians(lat2) + val deltaPhi = Math.toRadians(lat2 - lat1) + val deltaLambda = Math.toRadians(lon2 - lon1) + + val a = Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) + + Math.cos(phi1) * Math.cos(phi2) * + Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2) + val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) + + return r * c + } + + fun generateNarrative(summary: TripSummary, style: NarrativeStyle): String { + val durationHrs = (summary.endTimeMs - summary.startTimeMs) / 3600000.0 + val baseFactualString = "Trip from ${java.util.Date(summary.startTimeMs)} to ${java.util.Date(summary.endTimeMs)}. " + + "Distance: %.1f nm. Max SOG: %.1f kts. Avg SOG: %.1f kts. ".format(summary.distanceNm, summary.maxSogKts, summary.avgSogKts) + + (summary.maxWaveHeightM?.let { "Max waves: %.1fm. ".format(it) } ?: "") + + "Events: ${summary.logEntries.joinToString { it.text }}" + + return when (style) { + NarrativeStyle.PROFESSIONAL -> { + "VOYAGE SUMMARY\n" + + "Duration: %.1f hours\n".format(durationHrs) + + "Total Distance: %.1f NM\n".format(summary.distanceNm) + + "Vessel Performance: Avg Speed %.1f kts, Max Speed %.1f kts\n".format(summary.avgSogKts, summary.maxSogKts) + + "Meteorological Data: " + (summary.maxWaveHeightM?.let { "Significant wave height reached %.1fm." .format(it)} ?: "No wave data recorded.") + "\n" + + "Key Events:\n" + summary.logEntries.joinToString("\n") { "- ${it.text}" } + } + NarrativeStyle.ADVENTUROUS -> { + "WHAT A TRIP! We covered %.1f nautical miles of open water.\n".format(summary.distanceNm) + + "We hit a top speed of %.1f knots! ".format(summary.maxSogKts) + + (summary.maxWaveHeightM?.let { "The sea was alive with waves up to %.1fm high! ".format(it) } ?: "") + "\n" + + "During our journey, we logged some great moments:\n" + + summary.logEntries.joinToString("\n") { "🔥 ${it.text}" } + } + NarrativeStyle.JOURNAL -> { + "Reflecting on our time at sea. We traveled %.1f miles over %.1f hours.\n".format(summary.distanceNm, durationHrs) + + "The average pace was steady at %.1f knots. ".format(summary.avgSogKts) + + "I remember writing down: " + summary.logEntries.firstOrNull()?.text + "... " + + "It was a meaningful passage." + } + NarrativeStyle.PIRATE -> { + "AHOY! We've sailed %.1f leagues (well, nautical miles) across the briney deep!\n".format(summary.distanceNm) + + "The wind caught our sails and we flew at %.1f knots!\n".format(summary.maxSogKts) + + "Listen to the tales from the log:\n" + + summary.logEntries.joinToString("\n") { "🏴‍☠️ ${it.text}" } + "\n" + + "Arr, it was a fine voyage indeed!" + } + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportViewModel.kt new file mode 100644 index 0000000..e474cd2 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportViewModel.kt @@ -0,0 +1,54 @@ +package org.terst.nav.tripreport + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.terst.nav.logbook.InMemoryLogbookRepository +import org.terst.nav.track.TrackRepository + +sealed class TripReportState { + object Idle : TripReportState() + object Loading : TripReportState() + data class Success(val summary: TripSummary, val narrative: String) : TripReportState() + data class Error(val message: String) : TripReportState() +} + +class TripReportViewModel( + private val trackRepository: TrackRepository, + private val logbookRepository: InMemoryLogbookRepository, + private val generator: TripReportGenerator = TripReportGenerator() +) : ViewModel() { + + private val _state = MutableStateFlow(TripReportState.Idle) + val state: StateFlow = _state.asStateFlow() + + private val _selectedStyle = MutableStateFlow(NarrativeStyle.PROFESSIONAL) + val selectedStyle: StateFlow = _selectedStyle.asStateFlow() + + fun setStyle(style: NarrativeStyle) { + _selectedStyle.value = style + generateReport() + } + + fun generateReport() { + viewModelScope.launch { + _state.value = TripReportState.Loading + try { + val points = trackRepository.getPoints() + if (points.isEmpty()) { + _state.value = TripReportState.Error("No track data available. Start recording a track first.") + return@launch + } + val logEntries = logbookRepository.getAll() + val summary = generator.generateSummary(points, logEntries) + val narrative = generator.generateNarrative(summary, _selectedStyle.value) + _state.value = TripReportState.Success(summary, narrative) + } catch (e: Exception) { + _state.value = TripReportState.Error(e.message ?: "Unknown error generating report") + } + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt index a81a76f..7caabe7 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt @@ -49,7 +49,7 @@ class MainViewModel( private val aisRepository = AisRepository() - private val trackRepository = TrackRepository() + private val trackRepository = org.terst.nav.NavApplication.trackRepository private val _isRecording = MutableStateFlow(false) val isRecording: StateFlow = _isRecording.asStateFlow() @@ -70,13 +70,14 @@ class MainViewModel( fun addGpsPoint(lat: Double, lon: Double, sogKnots: Double, cogDeg: Double) { val conditions = _marineConditions.value + val forecast = _forecast.value.firstOrNull() val point = TrackPoint( lat = lat, lon = lon, sogKnots = sogKnots, cogDeg = cogDeg, - airTempC = conditions?.airTemp, - waveHeightM = conditions?.waveHeight, - currentSpeedKts = conditions?.currentSpeed, - currentDirDeg = conditions?.currentDir, + airTempC = forecast?.tempC, + waveHeightM = conditions?.waveHeightM, + currentSpeedKts = conditions?.currentSpeedKt, + currentDirDeg = conditions?.currentDirDeg, timestampMs = System.currentTimeMillis() ) if (trackRepository.addPoint(point)) { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt index ef48d37..86fd67c 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt @@ -28,7 +28,7 @@ class VoiceLogFragment : Fragment() { private lateinit var speechRecognizer: SpeechRecognizer private val viewModel by lazy { - VoiceLogViewModel(repository = InMemoryLogbookRepository()) + VoiceLogViewModel(repository = org.terst.nav.NavApplication.logbookRepository) } private lateinit var tvStatus: TextView @@ -38,6 +38,7 @@ class VoiceLogFragment : Fragment() { private lateinit var btnSave: Button private lateinit var btnRetry: Button private lateinit var tvSavedConfirmation: TextView + private lateinit var btnGenerateReport: MaterialButton override fun onCreateView( inflater: LayoutInflater, @@ -55,12 +56,19 @@ class VoiceLogFragment : Fragment() { btnSave = view.findViewById(R.id.btn_save) btnRetry = view.findViewById(R.id.btn_retry) tvSavedConfirmation = view.findViewById(R.id.tv_saved_confirmation) + btnGenerateReport = view.findViewById(R.id.btn_generate_report) setupSpeechRecognizer() fabMic.setOnClickListener { startListening() } btnSave.setOnClickListener { viewModel.confirmAndSave() } btnRetry.setOnClickListener { viewModel.retry() } + btnGenerateReport.setOnClickListener { + parentFragmentManager.beginTransaction() + .replace(R.id.fragment_container, org.terst.nav.tripreport.TripReportFragment()) + .addToBackStack(null) + .commit() + } viewLifecycleOwner.lifecycleScope.launch { viewModel.state.collect { state -> renderState(state) } diff --git a/android-app/app/src/main/res/layout/fragment_trip_report.xml b/android-app/app/src/main/res/layout/fragment_trip_report.xml new file mode 100644 index 0000000..1ce0bde --- /dev/null +++ b/android-app/app/src/main/res/layout/fragment_trip_report.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/fragment_voice_log.xml b/android-app/app/src/main/res/layout/fragment_voice_log.xml index e5f864c..6d136be 100644 --- a/android-app/app/src/main/res/layout/fragment_voice_log.xml +++ b/android-app/app/src/main/res/layout/fragment_voice_log.xml @@ -63,4 +63,18 @@ android:text="" android:textSize="14sp" android:layout_marginTop="16dp" /> + + + + -- cgit v1.2.3 From 91645cc9029798d78f6d5630ff9ec121e391ec49 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 4 Apr 2026 02:38:51 +0000 Subject: feat(tripreport): add pre-trip planning and past track visualization - Fix compilation errors (missing imports for PropertyFactory and MaterialButton) - Implement Pre-Trip Report with weather summary, boat profile, and sail suggestions - Differentiate between active track (solid red) and past tracks (dotted red) on map - Add navigation to Pre-Trip Report from Safety Dashboard - Robustify track storage to preserve multiple tracks in session Co-Authored-By: Gemini CLI --- .remember/logs/autonomous/save-024411.log | 4 + .remember/logs/autonomous/save-024416.log | 0 .remember/logs/autonomous/save-024420.log | 0 .remember/logs/autonomous/save-024427.log | 0 .remember/logs/autonomous/save-024809.log | 4 + .remember/logs/autonomous/save-024812.log | 0 .remember/logs/autonomous/save-024815.log | 0 .remember/logs/autonomous/save-024825.log | 0 .remember/logs/autonomous/save-024827.log | 0 .remember/logs/autonomous/save-024829.log | 0 .remember/logs/autonomous/save-024831.log | 0 .remember/logs/autonomous/save-024833.log | 0 .remember/logs/autonomous/save-024834.log | 0 .remember/logs/autonomous/save-024836.log | 0 .remember/logs/autonomous/save-024837.log | 0 .remember/logs/autonomous/save-024838.log | 0 .remember/logs/autonomous/save-024839.log | 0 .remember/logs/autonomous/save-024841.log | 0 .remember/logs/autonomous/save-024842.log | 0 .remember/logs/autonomous/save-024844.log | 0 .remember/logs/autonomous/save-024904.log | 0 .remember/logs/autonomous/save-025229.log | 4 + .remember/logs/autonomous/save-025235.log | 0 .remember/logs/autonomous/save-025455.log | 4 + .remember/logs/autonomous/save-025805.log | 4 + .remember/logs/autonomous/save-025812.log | 0 .remember/logs/autonomous/save-025820.log | 0 .remember/logs/autonomous/save-030100.log | 4 + .remember/logs/autonomous/save-030116.log | 0 .remember/logs/autonomous/save-030125.log | 0 .remember/logs/autonomous/save-030130.log | 0 .remember/logs/autonomous/save-030329.log | 4 + .remember/logs/autonomous/save-030519.log | 0 .remember/logs/autonomous/save-030522.log | 0 .remember/logs/autonomous/save-030525.log | 0 .remember/logs/autonomous/save-030636.log | 4 + .remember/logs/autonomous/save-030656.log | 0 .remember/logs/autonomous/save-030705.log | 0 .remember/logs/autonomous/save-030707.log | 0 .remember/logs/autonomous/save-030709.log | 0 .remember/logs/autonomous/save-030718.log | 0 .remember/logs/autonomous/save-030721.log | 0 .remember/logs/autonomous/save-030726.log | 0 .remember/logs/autonomous/save-030729.log | 0 .remember/logs/autonomous/save-030734.log | 0 .remember/logs/autonomous/save-030738.log | 0 .remember/logs/autonomous/save-030741.log | 0 .remember/logs/autonomous/save-030747.log | 0 .remember/logs/autonomous/save-030807.log | 0 .remember/logs/autonomous/save-030813.log | 0 .remember/logs/autonomous/save-030815.log | 0 .remember/logs/autonomous/save-030817.log | 0 .remember/logs/autonomous/save-030821.log | 0 .remember/logs/autonomous/save-030823.log | 0 .remember/logs/autonomous/save-030825.log | 0 .remember/logs/autonomous/save-030827.log | 0 .remember/logs/autonomous/save-030832.log | 0 .remember/logs/autonomous/save-030833.log | 0 .remember/logs/autonomous/save-030836.log | 4 + .remember/logs/autonomous/save-030845.log | 0 .remember/logs/autonomous/save-030851.log | 0 .remember/logs/autonomous/save-030902.log | 0 .remember/logs/autonomous/save-030904.log | 0 .remember/logs/autonomous/save-030932.log | 0 .remember/logs/autonomous/save-030953.log | 0 .remember/logs/autonomous/save-030957.log | 0 .remember/logs/autonomous/save-030959.log | 0 .remember/logs/autonomous/save-031000.log | 0 .remember/logs/autonomous/save-031002.log | 0 .remember/logs/autonomous/save-031006.log | 0 .remember/logs/autonomous/save-031015.log | 0 .remember/logs/autonomous/save-031016.log | 0 .remember/logs/autonomous/save-031019.log | 0 .remember/logs/autonomous/save-031021.log | 0 .remember/logs/autonomous/save-031024.log | 0 .remember/logs/autonomous/save-031046.log | 0 .remember/logs/autonomous/save-031054.log | 0 .remember/logs/autonomous/save-031058.log | 0 .remember/logs/autonomous/save-031102.log | 0 .remember/logs/autonomous/save-031106.log | 0 .remember/logs/autonomous/save-031111.log | 0 .remember/logs/autonomous/save-031113.log | 0 .remember/logs/autonomous/save-031117.log | 0 .remember/logs/autonomous/save-031125.log | 0 .remember/logs/autonomous/save-031133.log | 0 .remember/logs/autonomous/save-031140.log | 0 .remember/logs/autonomous/save-031154.log | 0 .remember/logs/autonomous/save-031155.log | 0 .remember/logs/autonomous/save-031156.log | 0 .remember/logs/autonomous/save-031158.log | 0 .remember/logs/autonomous/save-031159.log | 0 .remember/logs/autonomous/save-031200.log | 0 .remember/logs/autonomous/save-031228.log | 0 .remember/logs/autonomous/save-031242.log | 0 .remember/logs/autonomous/save-031252.log | 4 + .remember/logs/autonomous/save-031256.log | 0 .remember/logs/autonomous/save-031259.log | 0 .remember/logs/autonomous/save-031305.log | 0 .remember/logs/autonomous/save-031313.log | 0 .remember/logs/autonomous/save-031324.log | 0 .remember/logs/autonomous/save-031331.log | 0 .remember/logs/autonomous/save-031334.log | 0 .remember/logs/autonomous/save-033329.log | 4 + .remember/logs/autonomous/save-033333.log | 0 .remember/logs/autonomous/save-033343.log | 0 .remember/logs/autonomous/save-033345.log | 0 .remember/logs/autonomous/save-033346.log | 0 .remember/logs/autonomous/save-033347.log | 0 .remember/logs/autonomous/save-033349.log | 0 .remember/logs/autonomous/save-033350.log | 0 .remember/logs/autonomous/save-033354.log | 0 .remember/logs/autonomous/save-033355.log | 0 .remember/logs/autonomous/save-033357.log | 0 .remember/logs/autonomous/save-033400.log | 0 .remember/logs/autonomous/save-033403.log | 0 .remember/logs/autonomous/save-033407.log | 0 .remember/logs/autonomous/save-033410.log | 0 .remember/logs/autonomous/save-033413.log | 0 .remember/logs/autonomous/save-033416.log | 0 .remember/logs/autonomous/save-033434.log | 0 .remember/logs/autonomous/save-033436.log | 0 .remember/logs/autonomous/save-033437.log | 0 .remember/logs/autonomous/save-033438.log | 0 .remember/logs/autonomous/save-033440.log | 0 .remember/logs/autonomous/save-033441.log | 0 .remember/logs/autonomous/save-033445.log | 0 .remember/logs/autonomous/save-033446.log | 0 .remember/logs/autonomous/save-033447.log | 0 .remember/logs/autonomous/save-033449.log | 0 .remember/logs/autonomous/save-033450.log | 0 .remember/logs/autonomous/save-033453.log | 0 .remember/logs/autonomous/save-033455.log | 0 .remember/logs/autonomous/save-033458.log | 0 .remember/logs/autonomous/save-033459.log | 0 .remember/logs/autonomous/save-033502.log | 0 .remember/logs/autonomous/save-033503.log | 0 .remember/logs/autonomous/save-033504.log | 0 .remember/logs/autonomous/save-033505.log | 0 .remember/logs/autonomous/save-033508.log | 0 .remember/logs/autonomous/save-033511.log | 0 .remember/logs/autonomous/save-033517.log | 0 .remember/logs/autonomous/save-033522.log | 0 .remember/logs/autonomous/save-033536.log | 4 + .remember/logs/autonomous/save-033540.log | 0 .remember/logs/autonomous/save-033541.log | 0 .remember/logs/autonomous/save-033552.log | 0 .remember/logs/autonomous/save-033633.log | 0 .remember/logs/autonomous/save-033634.log | 0 .remember/logs/autonomous/save-033642.log | 0 .remember/logs/autonomous/save-033644.log | 0 .remember/logs/autonomous/save-033645.log | 0 .remember/logs/autonomous/save-033647.log | 0 .remember/logs/autonomous/save-033649.log | 0 .remember/logs/autonomous/save-033650.log | 0 .remember/logs/autonomous/save-033651.log | 0 .remember/logs/autonomous/save-033652.log | 0 .remember/logs/autonomous/save-033653.log | 0 .remember/logs/autonomous/save-033654.log | 0 .remember/logs/autonomous/save-033655.log | 0 .remember/logs/autonomous/save-033656.log | 0 .remember/logs/autonomous/save-033657.log | 0 .remember/logs/autonomous/save-033658.log | 0 .remember/logs/autonomous/save-033700.log | 0 .remember/logs/autonomous/save-033701.log | 0 .remember/logs/autonomous/save-033703.log | 0 .remember/logs/autonomous/save-033724.log | 0 .remember/logs/autonomous/save-033726.log | 0 .remember/logs/autonomous/save-033727.log | 0 .remember/logs/autonomous/save-033728.log | 0 .remember/logs/autonomous/save-033730.log | 0 .remember/logs/autonomous/save-033738.log | 4 + .remember/logs/autonomous/save-033749.log | 0 .remember/logs/autonomous/save-033752.log | 0 .remember/logs/autonomous/save-033753.log | 0 .remember/logs/autonomous/save-033804.log | 0 .remember/logs/autonomous/save-033808.log | 0 .remember/logs/autonomous/save-033810.log | 0 .remember/logs/autonomous/save-033813.log | 0 .remember/logs/autonomous/save-033820.log | 0 .remember/logs/autonomous/save-033823.log | 0 .remember/logs/autonomous/save-033828.log | 0 .remember/logs/autonomous/save-033830.log | 0 .remember/logs/autonomous/save-033833.log | 0 .remember/logs/autonomous/save-033841.log | 0 .remember/logs/autonomous/save-033844.log | 0 .remember/logs/autonomous/save-033853.log | 0 .remember/logs/autonomous/save-033955.log | 4 + .remember/logs/autonomous/save-033956.log | 0 .remember/logs/autonomous/save-033957.log | 0 .remember/logs/autonomous/save-034000.log | 0 .remember/logs/autonomous/save-034005.log | 0 .remember/logs/autonomous/save-034020.log | 0 .remember/logs/autonomous/save-034022.log | 0 .remember/logs/autonomous/save-034023.log | 0 .remember/logs/autonomous/save-034024.log | 0 .remember/logs/autonomous/save-034026.log | 0 .remember/logs/autonomous/save-034029.log | 0 .remember/logs/autonomous/save-034032.log | 0 .remember/logs/autonomous/save-034033.log | 0 .remember/logs/autonomous/save-034034.log | 0 .remember/logs/autonomous/save-034035.log | 0 .remember/logs/autonomous/save-034036.log | 0 .remember/logs/autonomous/save-034039.log | 0 .remember/logs/autonomous/save-034042.log | 0 .remember/logs/autonomous/save-034048.log | 0 .remember/logs/autonomous/save-034059.log | 0 .remember/logs/autonomous/save-034101.log | 0 .remember/logs/autonomous/save-034942.log | 4 + .remember/logs/autonomous/save-034944.log | 0 .remember/logs/autonomous/save-034946.log | 0 .remember/logs/autonomous/save-034948.log | 0 .remember/logs/autonomous/save-034949.log | 0 .remember/logs/autonomous/save-034950.log | 0 .remember/logs/autonomous/save-034951.log | 0 .remember/logs/autonomous/save-035003.log | 0 .remember/logs/autonomous/save-035007.log | 0 .remember/logs/autonomous/save-035049.log | 0 .remember/logs/autonomous/save-035051.log | 0 .remember/logs/autonomous/save-035052.log | 0 .remember/logs/autonomous/save-035054.log | 0 .remember/logs/autonomous/save-035056.log | 0 .remember/logs/autonomous/save-035058.log | 0 .remember/logs/autonomous/save-035059.log | 0 .remember/logs/autonomous/save-035101.log | 0 .remember/logs/autonomous/save-035103.log | 0 .remember/logs/autonomous/save-035105.log | 0 .remember/logs/autonomous/save-035222.log | 0 .remember/logs/autonomous/save-035223.log | 0 .remember/logs/autonomous/save-035224.log | 0 .remember/logs/autonomous/save-035226.log | 0 .remember/logs/autonomous/save-035228.log | 0 .remember/logs/autonomous/save-035230.log | 0 .remember/logs/autonomous/save-035232.log | 0 .remember/logs/autonomous/save-035237.log | 0 .remember/logs/autonomous/save-035240.log | 0 .remember/logs/autonomous/save-035244.log | 0 .remember/logs/autonomous/save-035249.log | 0 .remember/logs/autonomous/save-035250.log | 0 .remember/logs/autonomous/save-035256.log | 0 .remember/logs/autonomous/save-035343.log | 0 .remember/logs/autonomous/save-035345.log | 0 .remember/logs/autonomous/save-035355.log | 0 .remember/logs/autonomous/save-035401.log | 0 .remember/logs/autonomous/save-035405.log | 0 .remember/logs/autonomous/save-035412.log | 0 .remember/logs/autonomous/save-035416.log | 0 .remember/logs/autonomous/save-035515.log | 4 + .remember/logs/autonomous/save-035523.log | 0 .remember/logs/autonomous/save-035527.log | 0 .remember/logs/autonomous/save-035529.log | 0 .remember/logs/autonomous/save-035535.log | 0 .remember/logs/autonomous/save-035556.log | 0 .remember/logs/autonomous/save-035559.log | 0 .remember/logs/autonomous/save-035602.log | 0 .remember/logs/autonomous/save-035610.log | 0 .remember/logs/autonomous/save-035617.log | 0 .remember/logs/autonomous/save-035619.log | 0 .remember/logs/autonomous/save-035624.log | 0 .remember/logs/autonomous/save-035721.log | 4 + .remember/logs/autonomous/save-035919.log | 0 .remember/logs/autonomous/save-035930.log | 0 .remember/logs/autonomous/save-035934.log | 0 .remember/logs/autonomous/save-035937.log | 0 .remember/logs/autonomous/save-035943.log | 0 .remember/logs/autonomous/save-035945.log | 0 .remember/logs/autonomous/save-035954.log | 0 .remember/logs/autonomous/save-035958.log | 0 .remember/logs/autonomous/save-035959.log | 0 .remember/logs/autonomous/save-040001.log | 0 .remember/logs/autonomous/save-040006.log | 0 .remember/logs/autonomous/save-040013.log | 0 .remember/logs/autonomous/save-040026.log | 0 .remember/logs/autonomous/save-040037.log | 0 .remember/logs/autonomous/save-040038.log | 0 .remember/logs/autonomous/save-040042.log | 0 .remember/logs/autonomous/save-040043.log | 0 .remember/logs/autonomous/save-040046.log | 0 .remember/logs/autonomous/save-040048.log | 0 .remember/logs/autonomous/save-040110.log | 0 .remember/logs/autonomous/save-040111.log | 0 .remember/logs/autonomous/save-040117.log | 0 .remember/logs/autonomous/save-040121.log | 0 .remember/logs/autonomous/save-040126.log | 0 .remember/logs/autonomous/save-070753.log | 4 + .remember/logs/autonomous/save-070801.log | 0 .remember/logs/autonomous/save-070805.log | 0 .remember/logs/autonomous/save-070809.log | 0 .remember/logs/autonomous/save-070823.log | 0 .remember/logs/autonomous/save-070826.log | 0 .remember/logs/autonomous/save-070831.log | 0 .remember/logs/autonomous/save-071107.log | 0 .remember/logs/autonomous/save-071110.log | 0 .remember/logs/autonomous/save-071112.log | 0 .remember/logs/autonomous/save-071113.log | 0 .remember/logs/autonomous/save-071116.log | 0 .remember/logs/autonomous/save-071118.log | 0 .remember/logs/autonomous/save-071727.log | 0 .remember/logs/autonomous/save-071733.log | 0 .remember/logs/autonomous/save-071821.log | 0 .remember/logs/autonomous/save-071827.log | 0 .remember/logs/autonomous/save-071835.log | 0 .remember/logs/autonomous/save-071844.log | 0 .remember/logs/autonomous/save-071857.log | 0 .remember/logs/autonomous/save-071906.log | 0 .remember/logs/autonomous/save-071910.log | 0 .remember/logs/autonomous/save-072329.log | 4 + .remember/logs/autonomous/save-072432.log | 0 .remember/logs/autonomous/save-072434.log | 0 .remember/logs/autonomous/save-072446.log | 0 .remember/logs/autonomous/save-072453.log | 0 .remember/logs/autonomous/save-072515.log | 0 .remember/logs/autonomous/save-072624.log | 4 + .remember/logs/autonomous/save-072625.log | 0 .remember/logs/autonomous/save-072858.log | 4 + .remember/logs/autonomous/save-072859.log | 0 .remember/logs/autonomous/save-072942.log | 0 .remember/logs/autonomous/save-073019.log | 0 .remember/logs/autonomous/save-073026.log | 0 .remember/logs/autonomous/save-073031.log | 0 .remember/logs/autonomous/save-073034.log | 0 .remember/logs/autonomous/save-073041.log | 0 .remember/logs/autonomous/save-073046.log | 0 .remember/logs/autonomous/save-073050.log | 0 .remember/logs/autonomous/save-073055.log | 0 .remember/logs/autonomous/save-073114.log | 4 + .remember/logs/autonomous/save-073125.log | 0 .remember/logs/autonomous/save-073134.log | 0 .remember/logs/autonomous/save-073147.log | 0 .remember/logs/autonomous/save-073221.log | 0 .remember/logs/autonomous/save-073812.log | 4 + .remember/logs/autonomous/save-073824.log | 0 .remember/logs/autonomous/save-074034.log | 4 + .remember/logs/autonomous/save-074044.log | 0 .remember/logs/autonomous/save-074048.log | 0 .remember/logs/autonomous/save-074049.log | 0 .remember/logs/autonomous/save-074052.log | 0 .remember/logs/autonomous/save-074053.log | 0 .remember/logs/autonomous/save-074056.log | 0 .remember/logs/autonomous/save-074059.log | 0 .remember/logs/autonomous/save-074100.log | 0 .remember/logs/autonomous/save-074101.log | 0 .remember/logs/autonomous/save-074103.log | 0 .remember/logs/autonomous/save-074104.log | 0 .remember/logs/autonomous/save-074235.log | 4 + .remember/logs/autonomous/save-074241.log | 0 .remember/logs/autonomous/save-074720.log | 4 + .remember/logs/autonomous/save-074721.log | 0 .remember/logs/autonomous/save-074724.log | 0 .remember/logs/autonomous/save-074725.log | 0 .remember/logs/autonomous/save-074730.log | 0 .remember/logs/autonomous/save-074733.log | 0 .remember/logs/autonomous/save-074738.log | 0 .remember/logs/autonomous/save-074741.log | 0 .remember/logs/autonomous/save-074747.log | 0 .remember/logs/autonomous/save-074748.log | 0 .remember/logs/autonomous/save-074753.log | 0 .remember/logs/autonomous/save-074754.log | 0 .remember/logs/autonomous/save-074756.log | 0 .remember/logs/autonomous/save-074757.log | 0 .remember/logs/autonomous/save-074800.log | 0 .remember/logs/autonomous/save-074802.log | 0 .remember/logs/autonomous/save-074805.log | 0 .remember/logs/autonomous/save-074806.log | 0 .remember/logs/autonomous/save-074929.log | 4 + .remember/logs/autonomous/save-074933.log | 0 .remember/logs/autonomous/save-074934.log | 0 .remember/logs/autonomous/save-075012.log | 0 .remember/logs/autonomous/save-075015.log | 0 .remember/logs/autonomous/save-075019.log | 0 .remember/logs/autonomous/save-075020.log | 0 .remember/logs/autonomous/save-075902.log | 4 + .remember/logs/autonomous/save-075906.log | 0 .remember/logs/autonomous/save-075909.log | 0 .remember/logs/autonomous/save-075911.log | 0 .remember/logs/autonomous/save-075913.log | 0 .remember/logs/autonomous/save-075915.log | 0 .remember/logs/autonomous/save-080457.log | 4 + .remember/logs/autonomous/save-080507.log | 0 .remember/logs/autonomous/save-080512.log | 0 .remember/logs/autonomous/save-080518.log | 0 .remember/logs/autonomous/save-080525.log | 0 .remember/logs/autonomous/save-082819.log | 4 + .remember/logs/autonomous/save-082830.log | 0 .remember/logs/autonomous/save-082833.log | 0 .remember/logs/autonomous/save-082835.log | 0 .remember/logs/autonomous/save-082846.log | 0 .remember/logs/autonomous/save-082855.log | 0 .remember/logs/autonomous/save-082901.log | 0 .remember/logs/autonomous/save-082906.log | 0 .remember/logs/autonomous/save-082910.log | 0 .remember/logs/autonomous/save-082913.log | 0 .remember/logs/autonomous/save-082925.log | 0 .remember/logs/autonomous/save-082942.log | 0 .remember/logs/autonomous/save-082955.log | 0 .remember/logs/autonomous/save-082958.log | 0 .remember/logs/autonomous/save-083253.log | 0 .remember/logs/autonomous/save-083310.log | 0 .remember/logs/autonomous/save-083321.log | 0 .remember/logs/autonomous/save-083329.log | 0 .remember/logs/autonomous/save-084012.log | 0 .remember/logs/autonomous/save-084017.log | 0 .remember/logs/autonomous/save-084022.log | 0 .remember/logs/autonomous/save-084038.log | 0 .remember/logs/autonomous/save-084043.log | 0 .remember/logs/autonomous/save-090822.log | 0 .remember/logs/autonomous/save-090828.log | 0 .remember/logs/autonomous/save-090842.log | 4 + .remember/logs/autonomous/save-090850.log | 0 .remember/logs/autonomous/save-090902.log | 0 .remember/logs/autonomous/save-090925.log | 0 .remember/logs/autonomous/save-090934.log | 0 .remember/logs/autonomous/save-090945.log | 0 .remember/logs/autonomous/save-091016.log | 0 .remember/logs/autonomous/save-091120.log | 4 + .remember/logs/autonomous/save-091132.log | 0 .remember/logs/autonomous/save-091140.log | 0 .remember/logs/autonomous/save-091217.log | 0 .remember/logs/autonomous/save-204803.log | 4 + .remember/logs/autonomous/save-204808.log | 0 .remember/logs/autonomous/save-204816.log | 0 .remember/logs/autonomous/save-204823.log | 0 .remember/logs/autonomous/save-204910.log | 0 .remember/logs/autonomous/save-204915.log | 0 .remember/logs/autonomous/save-204919.log | 0 .remember/logs/autonomous/save-204922.log | 0 .remember/logs/autonomous/save-204923.log | 0 .remember/logs/autonomous/save-204930.log | 0 .remember/logs/autonomous/save-204933.log | 0 .remember/logs/autonomous/save-204934.log | 0 .remember/logs/autonomous/save-204936.log | 0 .remember/logs/autonomous/save-204942.log | 0 .remember/logs/autonomous/save-204943.log | 0 .remember/logs/autonomous/save-204947.log | 0 .remember/logs/autonomous/save-204950.log | 0 .remember/logs/autonomous/save-204954.log | 0 .remember/logs/autonomous/save-205001.log | 0 .remember/logs/autonomous/save-205014.log | 4 + .remember/logs/autonomous/save-205021.log | 0 .remember/logs/autonomous/save-205024.log | 0 .remember/logs/autonomous/save-205031.log | 0 .remember/logs/autonomous/save-205045.log | 0 .remember/logs/autonomous/save-205047.log | 0 .remember/logs/autonomous/save-205052.log | 0 .remember/logs/autonomous/save-205053.log | 0 .remember/logs/autonomous/save-205054.log | 0 .remember/logs/autonomous/save-205055.log | 0 .remember/logs/autonomous/save-205056.log | 0 .remember/logs/autonomous/save-205057.log | 0 .remember/logs/autonomous/save-205101.log | 0 .remember/logs/autonomous/save-205102.log | 0 .remember/logs/autonomous/save-205103.log | 0 .remember/logs/autonomous/save-205104.log | 0 .remember/logs/autonomous/save-205105.log | 0 .remember/logs/autonomous/save-205107.log | 0 .remember/logs/autonomous/save-205108.log | 0 .remember/logs/autonomous/save-205109.log | 0 .remember/logs/autonomous/save-205111.log | 0 .remember/logs/autonomous/save-205117.log | 0 .remember/logs/autonomous/save-205118.log | 0 .remember/logs/autonomous/save-205119.log | 0 .remember/logs/autonomous/save-205125.log | 0 .remember/logs/autonomous/save-205126.log | 0 .remember/logs/autonomous/save-205129.log | 0 .remember/logs/autonomous/save-205141.log | 0 .remember/logs/autonomous/save-205142.log | 0 .remember/logs/autonomous/save-205147.log | 0 .remember/logs/autonomous/save-205148.log | 0 .remember/logs/autonomous/save-205153.log | 0 .remember/logs/autonomous/save-205157.log | 0 .remember/logs/autonomous/save-205158.log | 0 .remember/logs/autonomous/save-205201.log | 0 .remember/logs/autonomous/save-205204.log | 0 .remember/logs/autonomous/save-205206.log | 0 .remember/logs/autonomous/save-205210.log | 0 .remember/logs/autonomous/save-205212.log | 0 .remember/logs/autonomous/save-205217.log | 4 + .remember/logs/autonomous/save-205224.log | 0 .remember/logs/autonomous/save-205229.log | 0 .remember/logs/autonomous/save-205230.log | 0 .remember/logs/autonomous/save-205234.log | 0 .remember/logs/autonomous/save-205241.log | 0 .remember/logs/autonomous/save-205246.log | 0 .remember/logs/autonomous/save-205254.log | 0 .remember/logs/autonomous/save-205258.log | 0 .remember/logs/autonomous/save-205309.log | 0 .remember/logs/autonomous/save-205316.log | 0 .remember/logs/autonomous/save-205324.log | 0 .remember/logs/autonomous/save-205326.log | 0 .remember/logs/autonomous/save-205332.log | 0 .remember/logs/autonomous/save-205333.log | 0 .remember/logs/autonomous/save-205338.log | 0 .remember/logs/autonomous/save-205340.log | 0 .remember/logs/autonomous/save-205344.log | 0 .remember/logs/autonomous/save-205346.log | 0 .remember/logs/autonomous/save-205357.log | 0 .remember/logs/autonomous/save-205402.log | 0 .remember/logs/autonomous/save-205408.log | 0 .remember/logs/autonomous/save-205412.log | 0 .remember/logs/autonomous/save-205417.log | 4 + .remember/logs/autonomous/save-205425.log | 0 .remember/logs/autonomous/save-205445.log | 0 .remember/logs/autonomous/save-205449.log | 0 .remember/logs/autonomous/save-205454.log | 0 .remember/logs/autonomous/save-205502.log | 0 .remember/logs/autonomous/save-205525.log | 0 .remember/logs/autonomous/save-205538.log | 0 .remember/logs/autonomous/save-205555.log | 0 .remember/logs/autonomous/save-205613.log | 0 .remember/logs/autonomous/save-205614.log | 0 .remember/logs/autonomous/save-205615.log | 0 .remember/logs/autonomous/save-205616.log | 0 .remember/logs/autonomous/save-205617.log | 4 + .remember/logs/autonomous/save-205618.log | 0 .remember/logs/autonomous/save-205619.log | 0 .remember/logs/autonomous/save-205620.log | 0 .remember/logs/autonomous/save-205621.log | 0 .remember/logs/autonomous/save-205624.log | 0 .remember/logs/autonomous/save-205625.log | 0 .remember/logs/autonomous/save-205626.log | 0 .remember/logs/autonomous/save-205627.log | 0 .remember/logs/autonomous/save-205628.log | 0 .remember/logs/autonomous/save-205630.log | 0 .remember/logs/autonomous/save-205631.log | 0 .remember/logs/autonomous/save-205632.log | 0 .remember/logs/autonomous/save-205633.log | 0 .remember/logs/autonomous/save-205634.log | 0 .remember/logs/autonomous/save-205636.log | 0 .remember/logs/autonomous/save-205640.log | 0 .remember/logs/autonomous/save-205641.log | 0 .remember/logs/autonomous/save-205645.log | 0 .remember/logs/autonomous/save-205650.log | 0 .remember/logs/autonomous/save-205651.log | 0 .remember/logs/autonomous/save-205657.log | 0 .remember/logs/autonomous/save-205700.log | 0 .remember/logs/autonomous/save-205702.log | 0 .remember/logs/autonomous/save-205703.log | 0 .remember/logs/autonomous/save-205704.log | 0 .remember/logs/autonomous/save-205708.log | 0 .remember/logs/autonomous/save-205710.log | 0 .remember/logs/autonomous/save-205713.log | 0 .remember/logs/autonomous/save-205714.log | 0 .remember/logs/autonomous/save-205715.log | 0 .remember/logs/autonomous/save-205716.log | 0 .remember/logs/autonomous/save-205717.log | 0 .remember/logs/autonomous/save-205718.log | 0 .remember/logs/autonomous/save-205720.log | 0 .remember/logs/autonomous/save-205722.log | 0 .remember/logs/autonomous/save-205724.log | 0 .remember/logs/autonomous/save-205725.log | 0 .remember/logs/autonomous/save-205730.log | 0 .remember/logs/autonomous/save-205732.log | 0 .remember/logs/autonomous/save-205740.log | 0 .remember/logs/autonomous/save-205746.log | 0 .remember/logs/autonomous/save-205748.log | 0 .remember/logs/autonomous/save-205749.log | 0 .remember/logs/autonomous/save-205757.log | 0 .remember/logs/autonomous/save-205811.log | 0 .remember/logs/autonomous/save-205812.log | 0 .remember/logs/autonomous/save-205815.log | 0 .remember/logs/autonomous/save-205823.log | 4 + .remember/logs/autonomous/save-205829.log | 0 .remember/logs/autonomous/save-205832.log | 0 .remember/logs/autonomous/save-205837.log | 0 .remember/logs/autonomous/save-205840.log | 0 .remember/logs/autonomous/save-212802.log | 4 + .remember/logs/autonomous/save-212806.log | 0 .remember/logs/autonomous/save-215225.log | 4 + .remember/logs/autonomous/save-215227.log | 0 .remember/logs/autonomous/save-215252.log | 0 .remember/logs/autonomous/save-215255.log | 0 .remember/logs/autonomous/save-215300.log | 0 .remember/logs/autonomous/save-215302.log | 0 .remember/logs/autonomous/save-215308.log | 0 .remember/logs/autonomous/save-215325.log | 0 .remember/logs/autonomous/save-222708.log | 4 + .remember/logs/autonomous/save-222716.log | 0 .remember/logs/autonomous/save-223848.log | 4 + .remember/logs/autonomous/save-223853.log | 0 .remember/logs/autonomous/save-224350.log | 4 + .remember/logs/autonomous/save-224446.log | 0 .remember/logs/autonomous/save-224449.log | 0 .remember/logs/autonomous/save-224535.log | 0 .remember/logs/autonomous/save-224609.log | 0 .remember/logs/autonomous/save-224828.log | 4 + .remember/logs/autonomous/save-224831.log | 0 .remember/logs/autonomous/save-224836.log | 0 .remember/logs/autonomous/save-224844.log | 0 .remember/logs/autonomous/save-225428.log | 4 + .remember/logs/autonomous/save-225458.log | 0 .remember/logs/autonomous/save-225506.log | 0 .remember/logs/autonomous/save-225511.log | 0 .remember/logs/autonomous/save-225535.log | 0 .remember/logs/autonomous/save-225540.log | 0 .remember/logs/autonomous/save-231628.log | 0 .remember/logs/autonomous/save-231633.log | 0 .remember/logs/autonomous/save-231641.log | 0 .remember/logs/autonomous/save-231645.log | 0 .remember/logs/autonomous/save-231646.log | 0 .remember/logs/autonomous/save-231649.log | 0 .remember/logs/autonomous/save-231654.log | 0 .remember/logs/autonomous/save-231659.log | 0 .remember/logs/autonomous/save-231700.log | 0 .remember/logs/autonomous/save-231703.log | 0 .remember/logs/autonomous/save-231704.log | 0 .remember/logs/autonomous/save-231720.log | 0 .remember/logs/autonomous/save-231724.log | 0 .remember/logs/autonomous/save-231756.log | 0 .remember/logs/autonomous/save-231757.log | 0 .remember/logs/autonomous/save-231800.log | 0 .remember/logs/autonomous/save-231909.log | 0 .remember/logs/autonomous/save-231920.log | 0 .remember/tmp/save-session.pid | 1 + all_logs.txt | 0 .../kotlin-compiler-5367790450239390534.salive | 0 .../kotlin-compiler-8331847918581383171.salive | 0 .../src/main/kotlin/org/terst/nav/MainActivity.kt | 4 +- .../kotlin/org/terst/nav/track/TrackRepository.kt | 14 +- .../org/terst/nav/tripreport/PreTripModels.kt | 43 ++ .../terst/nav/tripreport/PreTripReportFragment.kt | 102 ++++ .../terst/nav/tripreport/PreTripReportGenerator.kt | 66 +++ .../terst/nav/tripreport/PreTripReportViewModel.kt | 51 ++ .../main/kotlin/org/terst/nav/ui/MainViewModel.kt | 5 + .../src/main/kotlin/org/terst/nav/ui/MapHandler.kt | 55 +- .../org/terst/nav/ui/safety/SafetyFragment.kt | 7 + .../org/terst/nav/ui/voicelog/VoiceLogFragment.kt | 1 + .../main/res/layout/fragment_pretrip_report.xml | 144 +++++ .../app/src/main/res/layout/fragment_safety.xml | 9 + build.log | 606 +++++++++++++++++++++ new_build.log | 606 +++++++++++++++++++++ 629 files changed, 1876 insertions(+), 18 deletions(-) create mode 100644 .remember/logs/autonomous/save-024411.log create mode 100644 .remember/logs/autonomous/save-024416.log create mode 100644 .remember/logs/autonomous/save-024420.log create mode 100644 .remember/logs/autonomous/save-024427.log create mode 100644 .remember/logs/autonomous/save-024809.log create mode 100644 .remember/logs/autonomous/save-024812.log create mode 100644 .remember/logs/autonomous/save-024815.log create mode 100644 .remember/logs/autonomous/save-024825.log create mode 100644 .remember/logs/autonomous/save-024827.log create mode 100644 .remember/logs/autonomous/save-024829.log create mode 100644 .remember/logs/autonomous/save-024831.log create mode 100644 .remember/logs/autonomous/save-024833.log create mode 100644 .remember/logs/autonomous/save-024834.log create mode 100644 .remember/logs/autonomous/save-024836.log create mode 100644 .remember/logs/autonomous/save-024837.log create mode 100644 .remember/logs/autonomous/save-024838.log create mode 100644 .remember/logs/autonomous/save-024839.log create mode 100644 .remember/logs/autonomous/save-024841.log create mode 100644 .remember/logs/autonomous/save-024842.log create mode 100644 .remember/logs/autonomous/save-024844.log create mode 100644 .remember/logs/autonomous/save-024904.log create mode 100644 .remember/logs/autonomous/save-025229.log create mode 100644 .remember/logs/autonomous/save-025235.log create mode 100644 .remember/logs/autonomous/save-025455.log create mode 100644 .remember/logs/autonomous/save-025805.log create mode 100644 .remember/logs/autonomous/save-025812.log create mode 100644 .remember/logs/autonomous/save-025820.log create mode 100644 .remember/logs/autonomous/save-030100.log create mode 100644 .remember/logs/autonomous/save-030116.log create mode 100644 .remember/logs/autonomous/save-030125.log create mode 100644 .remember/logs/autonomous/save-030130.log create mode 100644 .remember/logs/autonomous/save-030329.log create mode 100644 .remember/logs/autonomous/save-030519.log create mode 100644 .remember/logs/autonomous/save-030522.log create mode 100644 .remember/logs/autonomous/save-030525.log create mode 100644 .remember/logs/autonomous/save-030636.log create mode 100644 .remember/logs/autonomous/save-030656.log create mode 100644 .remember/logs/autonomous/save-030705.log create mode 100644 .remember/logs/autonomous/save-030707.log create mode 100644 .remember/logs/autonomous/save-030709.log create mode 100644 .remember/logs/autonomous/save-030718.log create mode 100644 .remember/logs/autonomous/save-030721.log create mode 100644 .remember/logs/autonomous/save-030726.log create mode 100644 .remember/logs/autonomous/save-030729.log create mode 100644 .remember/logs/autonomous/save-030734.log create mode 100644 .remember/logs/autonomous/save-030738.log create mode 100644 .remember/logs/autonomous/save-030741.log create mode 100644 .remember/logs/autonomous/save-030747.log create mode 100644 .remember/logs/autonomous/save-030807.log create mode 100644 .remember/logs/autonomous/save-030813.log create mode 100644 .remember/logs/autonomous/save-030815.log create mode 100644 .remember/logs/autonomous/save-030817.log create mode 100644 .remember/logs/autonomous/save-030821.log create mode 100644 .remember/logs/autonomous/save-030823.log create mode 100644 .remember/logs/autonomous/save-030825.log create mode 100644 .remember/logs/autonomous/save-030827.log create mode 100644 .remember/logs/autonomous/save-030832.log create mode 100644 .remember/logs/autonomous/save-030833.log create mode 100644 .remember/logs/autonomous/save-030836.log create mode 100644 .remember/logs/autonomous/save-030845.log create mode 100644 .remember/logs/autonomous/save-030851.log create mode 100644 .remember/logs/autonomous/save-030902.log create mode 100644 .remember/logs/autonomous/save-030904.log create mode 100644 .remember/logs/autonomous/save-030932.log create mode 100644 .remember/logs/autonomous/save-030953.log create mode 100644 .remember/logs/autonomous/save-030957.log create mode 100644 .remember/logs/autonomous/save-030959.log create mode 100644 .remember/logs/autonomous/save-031000.log create mode 100644 .remember/logs/autonomous/save-031002.log create mode 100644 .remember/logs/autonomous/save-031006.log create mode 100644 .remember/logs/autonomous/save-031015.log create mode 100644 .remember/logs/autonomous/save-031016.log create mode 100644 .remember/logs/autonomous/save-031019.log create mode 100644 .remember/logs/autonomous/save-031021.log create mode 100644 .remember/logs/autonomous/save-031024.log create mode 100644 .remember/logs/autonomous/save-031046.log create mode 100644 .remember/logs/autonomous/save-031054.log create mode 100644 .remember/logs/autonomous/save-031058.log create mode 100644 .remember/logs/autonomous/save-031102.log create mode 100644 .remember/logs/autonomous/save-031106.log create mode 100644 .remember/logs/autonomous/save-031111.log create mode 100644 .remember/logs/autonomous/save-031113.log create mode 100644 .remember/logs/autonomous/save-031117.log create mode 100644 .remember/logs/autonomous/save-031125.log create mode 100644 .remember/logs/autonomous/save-031133.log create mode 100644 .remember/logs/autonomous/save-031140.log create mode 100644 .remember/logs/autonomous/save-031154.log create mode 100644 .remember/logs/autonomous/save-031155.log create mode 100644 .remember/logs/autonomous/save-031156.log create mode 100644 .remember/logs/autonomous/save-031158.log create mode 100644 .remember/logs/autonomous/save-031159.log create mode 100644 .remember/logs/autonomous/save-031200.log create mode 100644 .remember/logs/autonomous/save-031228.log create mode 100644 .remember/logs/autonomous/save-031242.log create mode 100644 .remember/logs/autonomous/save-031252.log create mode 100644 .remember/logs/autonomous/save-031256.log create mode 100644 .remember/logs/autonomous/save-031259.log create mode 100644 .remember/logs/autonomous/save-031305.log create mode 100644 .remember/logs/autonomous/save-031313.log create mode 100644 .remember/logs/autonomous/save-031324.log create mode 100644 .remember/logs/autonomous/save-031331.log create mode 100644 .remember/logs/autonomous/save-031334.log create mode 100644 .remember/logs/autonomous/save-033329.log create mode 100644 .remember/logs/autonomous/save-033333.log create mode 100644 .remember/logs/autonomous/save-033343.log create mode 100644 .remember/logs/autonomous/save-033345.log create mode 100644 .remember/logs/autonomous/save-033346.log create mode 100644 .remember/logs/autonomous/save-033347.log create mode 100644 .remember/logs/autonomous/save-033349.log create mode 100644 .remember/logs/autonomous/save-033350.log create mode 100644 .remember/logs/autonomous/save-033354.log create mode 100644 .remember/logs/autonomous/save-033355.log create mode 100644 .remember/logs/autonomous/save-033357.log create mode 100644 .remember/logs/autonomous/save-033400.log create mode 100644 .remember/logs/autonomous/save-033403.log create mode 100644 .remember/logs/autonomous/save-033407.log create mode 100644 .remember/logs/autonomous/save-033410.log create mode 100644 .remember/logs/autonomous/save-033413.log create mode 100644 .remember/logs/autonomous/save-033416.log create mode 100644 .remember/logs/autonomous/save-033434.log create mode 100644 .remember/logs/autonomous/save-033436.log create mode 100644 .remember/logs/autonomous/save-033437.log create mode 100644 .remember/logs/autonomous/save-033438.log create mode 100644 .remember/logs/autonomous/save-033440.log create mode 100644 .remember/logs/autonomous/save-033441.log create mode 100644 .remember/logs/autonomous/save-033445.log create mode 100644 .remember/logs/autonomous/save-033446.log create mode 100644 .remember/logs/autonomous/save-033447.log create mode 100644 .remember/logs/autonomous/save-033449.log create mode 100644 .remember/logs/autonomous/save-033450.log create mode 100644 .remember/logs/autonomous/save-033453.log create mode 100644 .remember/logs/autonomous/save-033455.log create mode 100644 .remember/logs/autonomous/save-033458.log create mode 100644 .remember/logs/autonomous/save-033459.log create mode 100644 .remember/logs/autonomous/save-033502.log create mode 100644 .remember/logs/autonomous/save-033503.log create mode 100644 .remember/logs/autonomous/save-033504.log create mode 100644 .remember/logs/autonomous/save-033505.log create mode 100644 .remember/logs/autonomous/save-033508.log create mode 100644 .remember/logs/autonomous/save-033511.log create mode 100644 .remember/logs/autonomous/save-033517.log create mode 100644 .remember/logs/autonomous/save-033522.log create mode 100644 .remember/logs/autonomous/save-033536.log create mode 100644 .remember/logs/autonomous/save-033540.log create mode 100644 .remember/logs/autonomous/save-033541.log create mode 100644 .remember/logs/autonomous/save-033552.log create mode 100644 .remember/logs/autonomous/save-033633.log create mode 100644 .remember/logs/autonomous/save-033634.log create mode 100644 .remember/logs/autonomous/save-033642.log create mode 100644 .remember/logs/autonomous/save-033644.log create mode 100644 .remember/logs/autonomous/save-033645.log create mode 100644 .remember/logs/autonomous/save-033647.log create mode 100644 .remember/logs/autonomous/save-033649.log create mode 100644 .remember/logs/autonomous/save-033650.log create mode 100644 .remember/logs/autonomous/save-033651.log create mode 100644 .remember/logs/autonomous/save-033652.log create mode 100644 .remember/logs/autonomous/save-033653.log create mode 100644 .remember/logs/autonomous/save-033654.log create mode 100644 .remember/logs/autonomous/save-033655.log create mode 100644 .remember/logs/autonomous/save-033656.log create mode 100644 .remember/logs/autonomous/save-033657.log create mode 100644 .remember/logs/autonomous/save-033658.log create mode 100644 .remember/logs/autonomous/save-033700.log create mode 100644 .remember/logs/autonomous/save-033701.log create mode 100644 .remember/logs/autonomous/save-033703.log create mode 100644 .remember/logs/autonomous/save-033724.log create mode 100644 .remember/logs/autonomous/save-033726.log create mode 100644 .remember/logs/autonomous/save-033727.log create mode 100644 .remember/logs/autonomous/save-033728.log create mode 100644 .remember/logs/autonomous/save-033730.log create mode 100644 .remember/logs/autonomous/save-033738.log create mode 100644 .remember/logs/autonomous/save-033749.log create mode 100644 .remember/logs/autonomous/save-033752.log create mode 100644 .remember/logs/autonomous/save-033753.log create mode 100644 .remember/logs/autonomous/save-033804.log create mode 100644 .remember/logs/autonomous/save-033808.log create mode 100644 .remember/logs/autonomous/save-033810.log create mode 100644 .remember/logs/autonomous/save-033813.log create mode 100644 .remember/logs/autonomous/save-033820.log create mode 100644 .remember/logs/autonomous/save-033823.log create mode 100644 .remember/logs/autonomous/save-033828.log create mode 100644 .remember/logs/autonomous/save-033830.log create mode 100644 .remember/logs/autonomous/save-033833.log create mode 100644 .remember/logs/autonomous/save-033841.log create mode 100644 .remember/logs/autonomous/save-033844.log create mode 100644 .remember/logs/autonomous/save-033853.log create mode 100644 .remember/logs/autonomous/save-033955.log create mode 100644 .remember/logs/autonomous/save-033956.log create mode 100644 .remember/logs/autonomous/save-033957.log create mode 100644 .remember/logs/autonomous/save-034000.log create mode 100644 .remember/logs/autonomous/save-034005.log create mode 100644 .remember/logs/autonomous/save-034020.log create mode 100644 .remember/logs/autonomous/save-034022.log create mode 100644 .remember/logs/autonomous/save-034023.log create mode 100644 .remember/logs/autonomous/save-034024.log create mode 100644 .remember/logs/autonomous/save-034026.log create mode 100644 .remember/logs/autonomous/save-034029.log create mode 100644 .remember/logs/autonomous/save-034032.log create mode 100644 .remember/logs/autonomous/save-034033.log create mode 100644 .remember/logs/autonomous/save-034034.log create mode 100644 .remember/logs/autonomous/save-034035.log create mode 100644 .remember/logs/autonomous/save-034036.log create mode 100644 .remember/logs/autonomous/save-034039.log create mode 100644 .remember/logs/autonomous/save-034042.log create mode 100644 .remember/logs/autonomous/save-034048.log create mode 100644 .remember/logs/autonomous/save-034059.log create mode 100644 .remember/logs/autonomous/save-034101.log create mode 100644 .remember/logs/autonomous/save-034942.log create mode 100644 .remember/logs/autonomous/save-034944.log create mode 100644 .remember/logs/autonomous/save-034946.log create mode 100644 .remember/logs/autonomous/save-034948.log create mode 100644 .remember/logs/autonomous/save-034949.log create mode 100644 .remember/logs/autonomous/save-034950.log create mode 100644 .remember/logs/autonomous/save-034951.log create mode 100644 .remember/logs/autonomous/save-035003.log create mode 100644 .remember/logs/autonomous/save-035007.log create mode 100644 .remember/logs/autonomous/save-035049.log create mode 100644 .remember/logs/autonomous/save-035051.log create mode 100644 .remember/logs/autonomous/save-035052.log create mode 100644 .remember/logs/autonomous/save-035054.log create mode 100644 .remember/logs/autonomous/save-035056.log create mode 100644 .remember/logs/autonomous/save-035058.log create mode 100644 .remember/logs/autonomous/save-035059.log create mode 100644 .remember/logs/autonomous/save-035101.log create mode 100644 .remember/logs/autonomous/save-035103.log create mode 100644 .remember/logs/autonomous/save-035105.log create mode 100644 .remember/logs/autonomous/save-035222.log create mode 100644 .remember/logs/autonomous/save-035223.log create mode 100644 .remember/logs/autonomous/save-035224.log create mode 100644 .remember/logs/autonomous/save-035226.log create mode 100644 .remember/logs/autonomous/save-035228.log create mode 100644 .remember/logs/autonomous/save-035230.log create mode 100644 .remember/logs/autonomous/save-035232.log create mode 100644 .remember/logs/autonomous/save-035237.log create mode 100644 .remember/logs/autonomous/save-035240.log create mode 100644 .remember/logs/autonomous/save-035244.log create mode 100644 .remember/logs/autonomous/save-035249.log create mode 100644 .remember/logs/autonomous/save-035250.log create mode 100644 .remember/logs/autonomous/save-035256.log create mode 100644 .remember/logs/autonomous/save-035343.log create mode 100644 .remember/logs/autonomous/save-035345.log create mode 100644 .remember/logs/autonomous/save-035355.log create mode 100644 .remember/logs/autonomous/save-035401.log create mode 100644 .remember/logs/autonomous/save-035405.log create mode 100644 .remember/logs/autonomous/save-035412.log create mode 100644 .remember/logs/autonomous/save-035416.log create mode 100644 .remember/logs/autonomous/save-035515.log create mode 100644 .remember/logs/autonomous/save-035523.log create mode 100644 .remember/logs/autonomous/save-035527.log create mode 100644 .remember/logs/autonomous/save-035529.log create mode 100644 .remember/logs/autonomous/save-035535.log create mode 100644 .remember/logs/autonomous/save-035556.log create mode 100644 .remember/logs/autonomous/save-035559.log create mode 100644 .remember/logs/autonomous/save-035602.log create mode 100644 .remember/logs/autonomous/save-035610.log create mode 100644 .remember/logs/autonomous/save-035617.log create mode 100644 .remember/logs/autonomous/save-035619.log create mode 100644 .remember/logs/autonomous/save-035624.log create mode 100644 .remember/logs/autonomous/save-035721.log create mode 100644 .remember/logs/autonomous/save-035919.log create mode 100644 .remember/logs/autonomous/save-035930.log create mode 100644 .remember/logs/autonomous/save-035934.log create mode 100644 .remember/logs/autonomous/save-035937.log create mode 100644 .remember/logs/autonomous/save-035943.log create mode 100644 .remember/logs/autonomous/save-035945.log create mode 100644 .remember/logs/autonomous/save-035954.log create mode 100644 .remember/logs/autonomous/save-035958.log create mode 100644 .remember/logs/autonomous/save-035959.log create mode 100644 .remember/logs/autonomous/save-040001.log create mode 100644 .remember/logs/autonomous/save-040006.log create mode 100644 .remember/logs/autonomous/save-040013.log create mode 100644 .remember/logs/autonomous/save-040026.log create mode 100644 .remember/logs/autonomous/save-040037.log create mode 100644 .remember/logs/autonomous/save-040038.log create mode 100644 .remember/logs/autonomous/save-040042.log create mode 100644 .remember/logs/autonomous/save-040043.log create mode 100644 .remember/logs/autonomous/save-040046.log create mode 100644 .remember/logs/autonomous/save-040048.log create mode 100644 .remember/logs/autonomous/save-040110.log create mode 100644 .remember/logs/autonomous/save-040111.log create mode 100644 .remember/logs/autonomous/save-040117.log create mode 100644 .remember/logs/autonomous/save-040121.log create mode 100644 .remember/logs/autonomous/save-040126.log create mode 100644 .remember/logs/autonomous/save-070753.log create mode 100644 .remember/logs/autonomous/save-070801.log create mode 100644 .remember/logs/autonomous/save-070805.log create mode 100644 .remember/logs/autonomous/save-070809.log create mode 100644 .remember/logs/autonomous/save-070823.log create mode 100644 .remember/logs/autonomous/save-070826.log create mode 100644 .remember/logs/autonomous/save-070831.log create mode 100644 .remember/logs/autonomous/save-071107.log create mode 100644 .remember/logs/autonomous/save-071110.log create mode 100644 .remember/logs/autonomous/save-071112.log create mode 100644 .remember/logs/autonomous/save-071113.log create mode 100644 .remember/logs/autonomous/save-071116.log create mode 100644 .remember/logs/autonomous/save-071118.log create mode 100644 .remember/logs/autonomous/save-071727.log create mode 100644 .remember/logs/autonomous/save-071733.log create mode 100644 .remember/logs/autonomous/save-071821.log create mode 100644 .remember/logs/autonomous/save-071827.log create mode 100644 .remember/logs/autonomous/save-071835.log create mode 100644 .remember/logs/autonomous/save-071844.log create mode 100644 .remember/logs/autonomous/save-071857.log create mode 100644 .remember/logs/autonomous/save-071906.log create mode 100644 .remember/logs/autonomous/save-071910.log create mode 100644 .remember/logs/autonomous/save-072329.log create mode 100644 .remember/logs/autonomous/save-072432.log create mode 100644 .remember/logs/autonomous/save-072434.log create mode 100644 .remember/logs/autonomous/save-072446.log create mode 100644 .remember/logs/autonomous/save-072453.log create mode 100644 .remember/logs/autonomous/save-072515.log create mode 100644 .remember/logs/autonomous/save-072624.log create mode 100644 .remember/logs/autonomous/save-072625.log create mode 100644 .remember/logs/autonomous/save-072858.log create mode 100644 .remember/logs/autonomous/save-072859.log create mode 100644 .remember/logs/autonomous/save-072942.log create mode 100644 .remember/logs/autonomous/save-073019.log create mode 100644 .remember/logs/autonomous/save-073026.log create mode 100644 .remember/logs/autonomous/save-073031.log create mode 100644 .remember/logs/autonomous/save-073034.log create mode 100644 .remember/logs/autonomous/save-073041.log create mode 100644 .remember/logs/autonomous/save-073046.log create mode 100644 .remember/logs/autonomous/save-073050.log create mode 100644 .remember/logs/autonomous/save-073055.log create mode 100644 .remember/logs/autonomous/save-073114.log create mode 100644 .remember/logs/autonomous/save-073125.log create mode 100644 .remember/logs/autonomous/save-073134.log create mode 100644 .remember/logs/autonomous/save-073147.log create mode 100644 .remember/logs/autonomous/save-073221.log create mode 100644 .remember/logs/autonomous/save-073812.log create mode 100644 .remember/logs/autonomous/save-073824.log create mode 100644 .remember/logs/autonomous/save-074034.log create mode 100644 .remember/logs/autonomous/save-074044.log create mode 100644 .remember/logs/autonomous/save-074048.log create mode 100644 .remember/logs/autonomous/save-074049.log create mode 100644 .remember/logs/autonomous/save-074052.log create mode 100644 .remember/logs/autonomous/save-074053.log create mode 100644 .remember/logs/autonomous/save-074056.log create mode 100644 .remember/logs/autonomous/save-074059.log create mode 100644 .remember/logs/autonomous/save-074100.log create mode 100644 .remember/logs/autonomous/save-074101.log create mode 100644 .remember/logs/autonomous/save-074103.log create mode 100644 .remember/logs/autonomous/save-074104.log create mode 100644 .remember/logs/autonomous/save-074235.log create mode 100644 .remember/logs/autonomous/save-074241.log create mode 100644 .remember/logs/autonomous/save-074720.log create mode 100644 .remember/logs/autonomous/save-074721.log create mode 100644 .remember/logs/autonomous/save-074724.log create mode 100644 .remember/logs/autonomous/save-074725.log create mode 100644 .remember/logs/autonomous/save-074730.log create mode 100644 .remember/logs/autonomous/save-074733.log create mode 100644 .remember/logs/autonomous/save-074738.log create mode 100644 .remember/logs/autonomous/save-074741.log create mode 100644 .remember/logs/autonomous/save-074747.log create mode 100644 .remember/logs/autonomous/save-074748.log create mode 100644 .remember/logs/autonomous/save-074753.log create mode 100644 .remember/logs/autonomous/save-074754.log create mode 100644 .remember/logs/autonomous/save-074756.log create mode 100644 .remember/logs/autonomous/save-074757.log create mode 100644 .remember/logs/autonomous/save-074800.log create mode 100644 .remember/logs/autonomous/save-074802.log create mode 100644 .remember/logs/autonomous/save-074805.log create mode 100644 .remember/logs/autonomous/save-074806.log create mode 100644 .remember/logs/autonomous/save-074929.log create mode 100644 .remember/logs/autonomous/save-074933.log create mode 100644 .remember/logs/autonomous/save-074934.log create mode 100644 .remember/logs/autonomous/save-075012.log create mode 100644 .remember/logs/autonomous/save-075015.log create mode 100644 .remember/logs/autonomous/save-075019.log create mode 100644 .remember/logs/autonomous/save-075020.log create mode 100644 .remember/logs/autonomous/save-075902.log create mode 100644 .remember/logs/autonomous/save-075906.log create mode 100644 .remember/logs/autonomous/save-075909.log create mode 100644 .remember/logs/autonomous/save-075911.log create mode 100644 .remember/logs/autonomous/save-075913.log create mode 100644 .remember/logs/autonomous/save-075915.log create mode 100644 .remember/logs/autonomous/save-080457.log create mode 100644 .remember/logs/autonomous/save-080507.log create mode 100644 .remember/logs/autonomous/save-080512.log create mode 100644 .remember/logs/autonomous/save-080518.log create mode 100644 .remember/logs/autonomous/save-080525.log create mode 100644 .remember/logs/autonomous/save-082819.log create mode 100644 .remember/logs/autonomous/save-082830.log create mode 100644 .remember/logs/autonomous/save-082833.log create mode 100644 .remember/logs/autonomous/save-082835.log create mode 100644 .remember/logs/autonomous/save-082846.log create mode 100644 .remember/logs/autonomous/save-082855.log create mode 100644 .remember/logs/autonomous/save-082901.log create mode 100644 .remember/logs/autonomous/save-082906.log create mode 100644 .remember/logs/autonomous/save-082910.log create mode 100644 .remember/logs/autonomous/save-082913.log create mode 100644 .remember/logs/autonomous/save-082925.log create mode 100644 .remember/logs/autonomous/save-082942.log create mode 100644 .remember/logs/autonomous/save-082955.log create mode 100644 .remember/logs/autonomous/save-082958.log create mode 100644 .remember/logs/autonomous/save-083253.log create mode 100644 .remember/logs/autonomous/save-083310.log create mode 100644 .remember/logs/autonomous/save-083321.log create mode 100644 .remember/logs/autonomous/save-083329.log create mode 100644 .remember/logs/autonomous/save-084012.log create mode 100644 .remember/logs/autonomous/save-084017.log create mode 100644 .remember/logs/autonomous/save-084022.log create mode 100644 .remember/logs/autonomous/save-084038.log create mode 100644 .remember/logs/autonomous/save-084043.log create mode 100644 .remember/logs/autonomous/save-090822.log create mode 100644 .remember/logs/autonomous/save-090828.log create mode 100644 .remember/logs/autonomous/save-090842.log create mode 100644 .remember/logs/autonomous/save-090850.log create mode 100644 .remember/logs/autonomous/save-090902.log create mode 100644 .remember/logs/autonomous/save-090925.log create mode 100644 .remember/logs/autonomous/save-090934.log create mode 100644 .remember/logs/autonomous/save-090945.log create mode 100644 .remember/logs/autonomous/save-091016.log create mode 100644 .remember/logs/autonomous/save-091120.log create mode 100644 .remember/logs/autonomous/save-091132.log create mode 100644 .remember/logs/autonomous/save-091140.log create mode 100644 .remember/logs/autonomous/save-091217.log create mode 100644 .remember/logs/autonomous/save-204803.log create mode 100644 .remember/logs/autonomous/save-204808.log create mode 100644 .remember/logs/autonomous/save-204816.log create mode 100644 .remember/logs/autonomous/save-204823.log create mode 100644 .remember/logs/autonomous/save-204910.log create mode 100644 .remember/logs/autonomous/save-204915.log create mode 100644 .remember/logs/autonomous/save-204919.log create mode 100644 .remember/logs/autonomous/save-204922.log create mode 100644 .remember/logs/autonomous/save-204923.log create mode 100644 .remember/logs/autonomous/save-204930.log create mode 100644 .remember/logs/autonomous/save-204933.log create mode 100644 .remember/logs/autonomous/save-204934.log create mode 100644 .remember/logs/autonomous/save-204936.log create mode 100644 .remember/logs/autonomous/save-204942.log create mode 100644 .remember/logs/autonomous/save-204943.log create mode 100644 .remember/logs/autonomous/save-204947.log create mode 100644 .remember/logs/autonomous/save-204950.log create mode 100644 .remember/logs/autonomous/save-204954.log create mode 100644 .remember/logs/autonomous/save-205001.log create mode 100644 .remember/logs/autonomous/save-205014.log create mode 100644 .remember/logs/autonomous/save-205021.log create mode 100644 .remember/logs/autonomous/save-205024.log create mode 100644 .remember/logs/autonomous/save-205031.log create mode 100644 .remember/logs/autonomous/save-205045.log create mode 100644 .remember/logs/autonomous/save-205047.log create mode 100644 .remember/logs/autonomous/save-205052.log create mode 100644 .remember/logs/autonomous/save-205053.log create mode 100644 .remember/logs/autonomous/save-205054.log create mode 100644 .remember/logs/autonomous/save-205055.log create mode 100644 .remember/logs/autonomous/save-205056.log create mode 100644 .remember/logs/autonomous/save-205057.log create mode 100644 .remember/logs/autonomous/save-205101.log create mode 100644 .remember/logs/autonomous/save-205102.log create mode 100644 .remember/logs/autonomous/save-205103.log create mode 100644 .remember/logs/autonomous/save-205104.log create mode 100644 .remember/logs/autonomous/save-205105.log create mode 100644 .remember/logs/autonomous/save-205107.log create mode 100644 .remember/logs/autonomous/save-205108.log create mode 100644 .remember/logs/autonomous/save-205109.log create mode 100644 .remember/logs/autonomous/save-205111.log create mode 100644 .remember/logs/autonomous/save-205117.log create mode 100644 .remember/logs/autonomous/save-205118.log create mode 100644 .remember/logs/autonomous/save-205119.log create mode 100644 .remember/logs/autonomous/save-205125.log create mode 100644 .remember/logs/autonomous/save-205126.log create mode 100644 .remember/logs/autonomous/save-205129.log create mode 100644 .remember/logs/autonomous/save-205141.log create mode 100644 .remember/logs/autonomous/save-205142.log create mode 100644 .remember/logs/autonomous/save-205147.log create mode 100644 .remember/logs/autonomous/save-205148.log create mode 100644 .remember/logs/autonomous/save-205153.log create mode 100644 .remember/logs/autonomous/save-205157.log create mode 100644 .remember/logs/autonomous/save-205158.log create mode 100644 .remember/logs/autonomous/save-205201.log create mode 100644 .remember/logs/autonomous/save-205204.log create mode 100644 .remember/logs/autonomous/save-205206.log create mode 100644 .remember/logs/autonomous/save-205210.log create mode 100644 .remember/logs/autonomous/save-205212.log create mode 100644 .remember/logs/autonomous/save-205217.log create mode 100644 .remember/logs/autonomous/save-205224.log create mode 100644 .remember/logs/autonomous/save-205229.log create mode 100644 .remember/logs/autonomous/save-205230.log create mode 100644 .remember/logs/autonomous/save-205234.log create mode 100644 .remember/logs/autonomous/save-205241.log create mode 100644 .remember/logs/autonomous/save-205246.log create mode 100644 .remember/logs/autonomous/save-205254.log create mode 100644 .remember/logs/autonomous/save-205258.log create mode 100644 .remember/logs/autonomous/save-205309.log create mode 100644 .remember/logs/autonomous/save-205316.log create mode 100644 .remember/logs/autonomous/save-205324.log create mode 100644 .remember/logs/autonomous/save-205326.log create mode 100644 .remember/logs/autonomous/save-205332.log create mode 100644 .remember/logs/autonomous/save-205333.log create mode 100644 .remember/logs/autonomous/save-205338.log create mode 100644 .remember/logs/autonomous/save-205340.log create mode 100644 .remember/logs/autonomous/save-205344.log create mode 100644 .remember/logs/autonomous/save-205346.log create mode 100644 .remember/logs/autonomous/save-205357.log create mode 100644 .remember/logs/autonomous/save-205402.log create mode 100644 .remember/logs/autonomous/save-205408.log create mode 100644 .remember/logs/autonomous/save-205412.log create mode 100644 .remember/logs/autonomous/save-205417.log create mode 100644 .remember/logs/autonomous/save-205425.log create mode 100644 .remember/logs/autonomous/save-205445.log create mode 100644 .remember/logs/autonomous/save-205449.log create mode 100644 .remember/logs/autonomous/save-205454.log create mode 100644 .remember/logs/autonomous/save-205502.log create mode 100644 .remember/logs/autonomous/save-205525.log create mode 100644 .remember/logs/autonomous/save-205538.log create mode 100644 .remember/logs/autonomous/save-205555.log create mode 100644 .remember/logs/autonomous/save-205613.log create mode 100644 .remember/logs/autonomous/save-205614.log create mode 100644 .remember/logs/autonomous/save-205615.log create mode 100644 .remember/logs/autonomous/save-205616.log create mode 100644 .remember/logs/autonomous/save-205617.log create mode 100644 .remember/logs/autonomous/save-205618.log create mode 100644 .remember/logs/autonomous/save-205619.log create mode 100644 .remember/logs/autonomous/save-205620.log create mode 100644 .remember/logs/autonomous/save-205621.log create mode 100644 .remember/logs/autonomous/save-205624.log create mode 100644 .remember/logs/autonomous/save-205625.log create mode 100644 .remember/logs/autonomous/save-205626.log create mode 100644 .remember/logs/autonomous/save-205627.log create mode 100644 .remember/logs/autonomous/save-205628.log create mode 100644 .remember/logs/autonomous/save-205630.log create mode 100644 .remember/logs/autonomous/save-205631.log create mode 100644 .remember/logs/autonomous/save-205632.log create mode 100644 .remember/logs/autonomous/save-205633.log create mode 100644 .remember/logs/autonomous/save-205634.log create mode 100644 .remember/logs/autonomous/save-205636.log create mode 100644 .remember/logs/autonomous/save-205640.log create mode 100644 .remember/logs/autonomous/save-205641.log create mode 100644 .remember/logs/autonomous/save-205645.log create mode 100644 .remember/logs/autonomous/save-205650.log create mode 100644 .remember/logs/autonomous/save-205651.log create mode 100644 .remember/logs/autonomous/save-205657.log create mode 100644 .remember/logs/autonomous/save-205700.log create mode 100644 .remember/logs/autonomous/save-205702.log create mode 100644 .remember/logs/autonomous/save-205703.log create mode 100644 .remember/logs/autonomous/save-205704.log create mode 100644 .remember/logs/autonomous/save-205708.log create mode 100644 .remember/logs/autonomous/save-205710.log create mode 100644 .remember/logs/autonomous/save-205713.log create mode 100644 .remember/logs/autonomous/save-205714.log create mode 100644 .remember/logs/autonomous/save-205715.log create mode 100644 .remember/logs/autonomous/save-205716.log create mode 100644 .remember/logs/autonomous/save-205717.log create mode 100644 .remember/logs/autonomous/save-205718.log create mode 100644 .remember/logs/autonomous/save-205720.log create mode 100644 .remember/logs/autonomous/save-205722.log create mode 100644 .remember/logs/autonomous/save-205724.log create mode 100644 .remember/logs/autonomous/save-205725.log create mode 100644 .remember/logs/autonomous/save-205730.log create mode 100644 .remember/logs/autonomous/save-205732.log create mode 100644 .remember/logs/autonomous/save-205740.log create mode 100644 .remember/logs/autonomous/save-205746.log create mode 100644 .remember/logs/autonomous/save-205748.log create mode 100644 .remember/logs/autonomous/save-205749.log create mode 100644 .remember/logs/autonomous/save-205757.log create mode 100644 .remember/logs/autonomous/save-205811.log create mode 100644 .remember/logs/autonomous/save-205812.log create mode 100644 .remember/logs/autonomous/save-205815.log create mode 100644 .remember/logs/autonomous/save-205823.log create mode 100644 .remember/logs/autonomous/save-205829.log create mode 100644 .remember/logs/autonomous/save-205832.log create mode 100644 .remember/logs/autonomous/save-205837.log create mode 100644 .remember/logs/autonomous/save-205840.log create mode 100644 .remember/logs/autonomous/save-212802.log create mode 100644 .remember/logs/autonomous/save-212806.log create mode 100644 .remember/logs/autonomous/save-215225.log create mode 100644 .remember/logs/autonomous/save-215227.log create mode 100644 .remember/logs/autonomous/save-215252.log create mode 100644 .remember/logs/autonomous/save-215255.log create mode 100644 .remember/logs/autonomous/save-215300.log create mode 100644 .remember/logs/autonomous/save-215302.log create mode 100644 .remember/logs/autonomous/save-215308.log create mode 100644 .remember/logs/autonomous/save-215325.log create mode 100644 .remember/logs/autonomous/save-222708.log create mode 100644 .remember/logs/autonomous/save-222716.log create mode 100644 .remember/logs/autonomous/save-223848.log create mode 100644 .remember/logs/autonomous/save-223853.log create mode 100644 .remember/logs/autonomous/save-224350.log create mode 100644 .remember/logs/autonomous/save-224446.log create mode 100644 .remember/logs/autonomous/save-224449.log create mode 100644 .remember/logs/autonomous/save-224535.log create mode 100644 .remember/logs/autonomous/save-224609.log create mode 100644 .remember/logs/autonomous/save-224828.log create mode 100644 .remember/logs/autonomous/save-224831.log create mode 100644 .remember/logs/autonomous/save-224836.log create mode 100644 .remember/logs/autonomous/save-224844.log create mode 100644 .remember/logs/autonomous/save-225428.log create mode 100644 .remember/logs/autonomous/save-225458.log create mode 100644 .remember/logs/autonomous/save-225506.log create mode 100644 .remember/logs/autonomous/save-225511.log create mode 100644 .remember/logs/autonomous/save-225535.log create mode 100644 .remember/logs/autonomous/save-225540.log create mode 100644 .remember/logs/autonomous/save-231628.log create mode 100644 .remember/logs/autonomous/save-231633.log create mode 100644 .remember/logs/autonomous/save-231641.log create mode 100644 .remember/logs/autonomous/save-231645.log create mode 100644 .remember/logs/autonomous/save-231646.log create mode 100644 .remember/logs/autonomous/save-231649.log create mode 100644 .remember/logs/autonomous/save-231654.log create mode 100644 .remember/logs/autonomous/save-231659.log create mode 100644 .remember/logs/autonomous/save-231700.log create mode 100644 .remember/logs/autonomous/save-231703.log create mode 100644 .remember/logs/autonomous/save-231704.log create mode 100644 .remember/logs/autonomous/save-231720.log create mode 100644 .remember/logs/autonomous/save-231724.log create mode 100644 .remember/logs/autonomous/save-231756.log create mode 100644 .remember/logs/autonomous/save-231757.log create mode 100644 .remember/logs/autonomous/save-231800.log create mode 100644 .remember/logs/autonomous/save-231909.log create mode 100644 .remember/logs/autonomous/save-231920.log create mode 100644 .remember/tmp/save-session.pid create mode 100644 all_logs.txt create mode 100644 android-app/.kotlin/sessions/kotlin-compiler-5367790450239390534.salive create mode 100644 android-app/.kotlin/sessions/kotlin-compiler-8331847918581383171.salive create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt create mode 100644 android-app/app/src/main/res/layout/fragment_pretrip_report.xml create mode 100644 build.log create mode 100644 new_build.log (limited to 'android-app/app/src/main/res/layout') diff --git a/.remember/logs/autonomous/save-024411.log b/.remember/logs/autonomous/save-024411.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-024411.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-024416.log b/.remember/logs/autonomous/save-024416.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024420.log b/.remember/logs/autonomous/save-024420.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024427.log b/.remember/logs/autonomous/save-024427.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024809.log b/.remember/logs/autonomous/save-024809.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-024809.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-024812.log b/.remember/logs/autonomous/save-024812.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024815.log b/.remember/logs/autonomous/save-024815.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024825.log b/.remember/logs/autonomous/save-024825.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024827.log b/.remember/logs/autonomous/save-024827.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024829.log b/.remember/logs/autonomous/save-024829.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024831.log b/.remember/logs/autonomous/save-024831.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024833.log b/.remember/logs/autonomous/save-024833.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024834.log b/.remember/logs/autonomous/save-024834.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024836.log b/.remember/logs/autonomous/save-024836.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024837.log b/.remember/logs/autonomous/save-024837.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024838.log b/.remember/logs/autonomous/save-024838.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024839.log b/.remember/logs/autonomous/save-024839.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024841.log b/.remember/logs/autonomous/save-024841.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024842.log b/.remember/logs/autonomous/save-024842.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024844.log b/.remember/logs/autonomous/save-024844.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-024904.log b/.remember/logs/autonomous/save-024904.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-025229.log b/.remember/logs/autonomous/save-025229.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-025229.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-025235.log b/.remember/logs/autonomous/save-025235.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-025455.log b/.remember/logs/autonomous/save-025455.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-025455.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-025805.log b/.remember/logs/autonomous/save-025805.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-025805.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-025812.log b/.remember/logs/autonomous/save-025812.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-025820.log b/.remember/logs/autonomous/save-025820.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030100.log b/.remember/logs/autonomous/save-030100.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-030100.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-030116.log b/.remember/logs/autonomous/save-030116.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030125.log b/.remember/logs/autonomous/save-030125.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030130.log b/.remember/logs/autonomous/save-030130.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030329.log b/.remember/logs/autonomous/save-030329.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-030329.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-030519.log b/.remember/logs/autonomous/save-030519.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030522.log b/.remember/logs/autonomous/save-030522.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030525.log b/.remember/logs/autonomous/save-030525.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030636.log b/.remember/logs/autonomous/save-030636.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-030636.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-030656.log b/.remember/logs/autonomous/save-030656.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030705.log b/.remember/logs/autonomous/save-030705.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030707.log b/.remember/logs/autonomous/save-030707.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030709.log b/.remember/logs/autonomous/save-030709.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030718.log b/.remember/logs/autonomous/save-030718.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030721.log b/.remember/logs/autonomous/save-030721.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030726.log b/.remember/logs/autonomous/save-030726.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030729.log b/.remember/logs/autonomous/save-030729.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030734.log b/.remember/logs/autonomous/save-030734.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030738.log b/.remember/logs/autonomous/save-030738.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030741.log b/.remember/logs/autonomous/save-030741.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030747.log b/.remember/logs/autonomous/save-030747.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030807.log b/.remember/logs/autonomous/save-030807.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030813.log b/.remember/logs/autonomous/save-030813.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030815.log b/.remember/logs/autonomous/save-030815.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030817.log b/.remember/logs/autonomous/save-030817.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030821.log b/.remember/logs/autonomous/save-030821.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030823.log b/.remember/logs/autonomous/save-030823.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030825.log b/.remember/logs/autonomous/save-030825.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030827.log b/.remember/logs/autonomous/save-030827.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030832.log b/.remember/logs/autonomous/save-030832.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030833.log b/.remember/logs/autonomous/save-030833.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030836.log b/.remember/logs/autonomous/save-030836.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-030836.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-030845.log b/.remember/logs/autonomous/save-030845.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030851.log b/.remember/logs/autonomous/save-030851.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030902.log b/.remember/logs/autonomous/save-030902.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030904.log b/.remember/logs/autonomous/save-030904.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030932.log b/.remember/logs/autonomous/save-030932.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030953.log b/.remember/logs/autonomous/save-030953.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030957.log b/.remember/logs/autonomous/save-030957.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-030959.log b/.remember/logs/autonomous/save-030959.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031000.log b/.remember/logs/autonomous/save-031000.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031002.log b/.remember/logs/autonomous/save-031002.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031006.log b/.remember/logs/autonomous/save-031006.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031015.log b/.remember/logs/autonomous/save-031015.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031016.log b/.remember/logs/autonomous/save-031016.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031019.log b/.remember/logs/autonomous/save-031019.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031021.log b/.remember/logs/autonomous/save-031021.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031024.log b/.remember/logs/autonomous/save-031024.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031046.log b/.remember/logs/autonomous/save-031046.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031054.log b/.remember/logs/autonomous/save-031054.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031058.log b/.remember/logs/autonomous/save-031058.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031102.log b/.remember/logs/autonomous/save-031102.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031106.log b/.remember/logs/autonomous/save-031106.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031111.log b/.remember/logs/autonomous/save-031111.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031113.log b/.remember/logs/autonomous/save-031113.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031117.log b/.remember/logs/autonomous/save-031117.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031125.log b/.remember/logs/autonomous/save-031125.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031133.log b/.remember/logs/autonomous/save-031133.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031140.log b/.remember/logs/autonomous/save-031140.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031154.log b/.remember/logs/autonomous/save-031154.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031155.log b/.remember/logs/autonomous/save-031155.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031156.log b/.remember/logs/autonomous/save-031156.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031158.log b/.remember/logs/autonomous/save-031158.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031159.log b/.remember/logs/autonomous/save-031159.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031200.log b/.remember/logs/autonomous/save-031200.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031228.log b/.remember/logs/autonomous/save-031228.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031242.log b/.remember/logs/autonomous/save-031242.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031252.log b/.remember/logs/autonomous/save-031252.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-031252.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-031256.log b/.remember/logs/autonomous/save-031256.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031259.log b/.remember/logs/autonomous/save-031259.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031305.log b/.remember/logs/autonomous/save-031305.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031313.log b/.remember/logs/autonomous/save-031313.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031324.log b/.remember/logs/autonomous/save-031324.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031331.log b/.remember/logs/autonomous/save-031331.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-031334.log b/.remember/logs/autonomous/save-031334.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033329.log b/.remember/logs/autonomous/save-033329.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-033329.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-033333.log b/.remember/logs/autonomous/save-033333.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033343.log b/.remember/logs/autonomous/save-033343.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033345.log b/.remember/logs/autonomous/save-033345.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033346.log b/.remember/logs/autonomous/save-033346.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033347.log b/.remember/logs/autonomous/save-033347.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033349.log b/.remember/logs/autonomous/save-033349.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033350.log b/.remember/logs/autonomous/save-033350.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033354.log b/.remember/logs/autonomous/save-033354.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033355.log b/.remember/logs/autonomous/save-033355.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033357.log b/.remember/logs/autonomous/save-033357.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033400.log b/.remember/logs/autonomous/save-033400.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033403.log b/.remember/logs/autonomous/save-033403.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033407.log b/.remember/logs/autonomous/save-033407.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033410.log b/.remember/logs/autonomous/save-033410.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033413.log b/.remember/logs/autonomous/save-033413.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033416.log b/.remember/logs/autonomous/save-033416.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033434.log b/.remember/logs/autonomous/save-033434.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033436.log b/.remember/logs/autonomous/save-033436.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033437.log b/.remember/logs/autonomous/save-033437.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033438.log b/.remember/logs/autonomous/save-033438.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033440.log b/.remember/logs/autonomous/save-033440.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033441.log b/.remember/logs/autonomous/save-033441.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033445.log b/.remember/logs/autonomous/save-033445.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033446.log b/.remember/logs/autonomous/save-033446.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033447.log b/.remember/logs/autonomous/save-033447.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033449.log b/.remember/logs/autonomous/save-033449.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033450.log b/.remember/logs/autonomous/save-033450.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033453.log b/.remember/logs/autonomous/save-033453.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033455.log b/.remember/logs/autonomous/save-033455.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033458.log b/.remember/logs/autonomous/save-033458.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033459.log b/.remember/logs/autonomous/save-033459.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033502.log b/.remember/logs/autonomous/save-033502.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033503.log b/.remember/logs/autonomous/save-033503.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033504.log b/.remember/logs/autonomous/save-033504.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033505.log b/.remember/logs/autonomous/save-033505.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033508.log b/.remember/logs/autonomous/save-033508.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033511.log b/.remember/logs/autonomous/save-033511.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033517.log b/.remember/logs/autonomous/save-033517.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033522.log b/.remember/logs/autonomous/save-033522.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033536.log b/.remember/logs/autonomous/save-033536.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-033536.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-033540.log b/.remember/logs/autonomous/save-033540.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033541.log b/.remember/logs/autonomous/save-033541.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033552.log b/.remember/logs/autonomous/save-033552.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033633.log b/.remember/logs/autonomous/save-033633.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033634.log b/.remember/logs/autonomous/save-033634.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033642.log b/.remember/logs/autonomous/save-033642.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033644.log b/.remember/logs/autonomous/save-033644.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033645.log b/.remember/logs/autonomous/save-033645.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033647.log b/.remember/logs/autonomous/save-033647.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033649.log b/.remember/logs/autonomous/save-033649.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033650.log b/.remember/logs/autonomous/save-033650.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033651.log b/.remember/logs/autonomous/save-033651.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033652.log b/.remember/logs/autonomous/save-033652.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033653.log b/.remember/logs/autonomous/save-033653.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033654.log b/.remember/logs/autonomous/save-033654.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033655.log b/.remember/logs/autonomous/save-033655.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033656.log b/.remember/logs/autonomous/save-033656.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033657.log b/.remember/logs/autonomous/save-033657.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033658.log b/.remember/logs/autonomous/save-033658.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033700.log b/.remember/logs/autonomous/save-033700.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033701.log b/.remember/logs/autonomous/save-033701.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033703.log b/.remember/logs/autonomous/save-033703.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033724.log b/.remember/logs/autonomous/save-033724.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033726.log b/.remember/logs/autonomous/save-033726.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033727.log b/.remember/logs/autonomous/save-033727.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033728.log b/.remember/logs/autonomous/save-033728.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033730.log b/.remember/logs/autonomous/save-033730.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033738.log b/.remember/logs/autonomous/save-033738.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-033738.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-033749.log b/.remember/logs/autonomous/save-033749.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033752.log b/.remember/logs/autonomous/save-033752.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033753.log b/.remember/logs/autonomous/save-033753.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033804.log b/.remember/logs/autonomous/save-033804.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033808.log b/.remember/logs/autonomous/save-033808.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033810.log b/.remember/logs/autonomous/save-033810.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033813.log b/.remember/logs/autonomous/save-033813.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033820.log b/.remember/logs/autonomous/save-033820.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033823.log b/.remember/logs/autonomous/save-033823.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033828.log b/.remember/logs/autonomous/save-033828.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033830.log b/.remember/logs/autonomous/save-033830.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033833.log b/.remember/logs/autonomous/save-033833.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033841.log b/.remember/logs/autonomous/save-033841.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033844.log b/.remember/logs/autonomous/save-033844.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033853.log b/.remember/logs/autonomous/save-033853.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033955.log b/.remember/logs/autonomous/save-033955.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-033955.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-033956.log b/.remember/logs/autonomous/save-033956.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-033957.log b/.remember/logs/autonomous/save-033957.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034000.log b/.remember/logs/autonomous/save-034000.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034005.log b/.remember/logs/autonomous/save-034005.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034020.log b/.remember/logs/autonomous/save-034020.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034022.log b/.remember/logs/autonomous/save-034022.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034023.log b/.remember/logs/autonomous/save-034023.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034024.log b/.remember/logs/autonomous/save-034024.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034026.log b/.remember/logs/autonomous/save-034026.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034029.log b/.remember/logs/autonomous/save-034029.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034032.log b/.remember/logs/autonomous/save-034032.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034033.log b/.remember/logs/autonomous/save-034033.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034034.log b/.remember/logs/autonomous/save-034034.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034035.log b/.remember/logs/autonomous/save-034035.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034036.log b/.remember/logs/autonomous/save-034036.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034039.log b/.remember/logs/autonomous/save-034039.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034042.log b/.remember/logs/autonomous/save-034042.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034048.log b/.remember/logs/autonomous/save-034048.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034059.log b/.remember/logs/autonomous/save-034059.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034101.log b/.remember/logs/autonomous/save-034101.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034942.log b/.remember/logs/autonomous/save-034942.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-034942.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-034944.log b/.remember/logs/autonomous/save-034944.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034946.log b/.remember/logs/autonomous/save-034946.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034948.log b/.remember/logs/autonomous/save-034948.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034949.log b/.remember/logs/autonomous/save-034949.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034950.log b/.remember/logs/autonomous/save-034950.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-034951.log b/.remember/logs/autonomous/save-034951.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035003.log b/.remember/logs/autonomous/save-035003.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035007.log b/.remember/logs/autonomous/save-035007.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035049.log b/.remember/logs/autonomous/save-035049.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035051.log b/.remember/logs/autonomous/save-035051.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035052.log b/.remember/logs/autonomous/save-035052.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035054.log b/.remember/logs/autonomous/save-035054.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035056.log b/.remember/logs/autonomous/save-035056.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035058.log b/.remember/logs/autonomous/save-035058.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035059.log b/.remember/logs/autonomous/save-035059.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035101.log b/.remember/logs/autonomous/save-035101.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035103.log b/.remember/logs/autonomous/save-035103.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035105.log b/.remember/logs/autonomous/save-035105.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035222.log b/.remember/logs/autonomous/save-035222.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035223.log b/.remember/logs/autonomous/save-035223.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035224.log b/.remember/logs/autonomous/save-035224.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035226.log b/.remember/logs/autonomous/save-035226.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035228.log b/.remember/logs/autonomous/save-035228.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035230.log b/.remember/logs/autonomous/save-035230.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035232.log b/.remember/logs/autonomous/save-035232.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035237.log b/.remember/logs/autonomous/save-035237.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035240.log b/.remember/logs/autonomous/save-035240.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035244.log b/.remember/logs/autonomous/save-035244.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035249.log b/.remember/logs/autonomous/save-035249.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035250.log b/.remember/logs/autonomous/save-035250.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035256.log b/.remember/logs/autonomous/save-035256.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035343.log b/.remember/logs/autonomous/save-035343.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035345.log b/.remember/logs/autonomous/save-035345.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035355.log b/.remember/logs/autonomous/save-035355.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035401.log b/.remember/logs/autonomous/save-035401.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035405.log b/.remember/logs/autonomous/save-035405.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035412.log b/.remember/logs/autonomous/save-035412.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035416.log b/.remember/logs/autonomous/save-035416.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035515.log b/.remember/logs/autonomous/save-035515.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-035515.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-035523.log b/.remember/logs/autonomous/save-035523.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035527.log b/.remember/logs/autonomous/save-035527.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035529.log b/.remember/logs/autonomous/save-035529.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035535.log b/.remember/logs/autonomous/save-035535.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035556.log b/.remember/logs/autonomous/save-035556.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035559.log b/.remember/logs/autonomous/save-035559.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035602.log b/.remember/logs/autonomous/save-035602.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035610.log b/.remember/logs/autonomous/save-035610.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035617.log b/.remember/logs/autonomous/save-035617.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035619.log b/.remember/logs/autonomous/save-035619.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035624.log b/.remember/logs/autonomous/save-035624.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035721.log b/.remember/logs/autonomous/save-035721.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-035721.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-035919.log b/.remember/logs/autonomous/save-035919.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035930.log b/.remember/logs/autonomous/save-035930.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035934.log b/.remember/logs/autonomous/save-035934.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035937.log b/.remember/logs/autonomous/save-035937.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035943.log b/.remember/logs/autonomous/save-035943.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035945.log b/.remember/logs/autonomous/save-035945.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035954.log b/.remember/logs/autonomous/save-035954.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035958.log b/.remember/logs/autonomous/save-035958.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-035959.log b/.remember/logs/autonomous/save-035959.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040001.log b/.remember/logs/autonomous/save-040001.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040006.log b/.remember/logs/autonomous/save-040006.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040013.log b/.remember/logs/autonomous/save-040013.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040026.log b/.remember/logs/autonomous/save-040026.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040037.log b/.remember/logs/autonomous/save-040037.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040038.log b/.remember/logs/autonomous/save-040038.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040042.log b/.remember/logs/autonomous/save-040042.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040043.log b/.remember/logs/autonomous/save-040043.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040046.log b/.remember/logs/autonomous/save-040046.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040048.log b/.remember/logs/autonomous/save-040048.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040110.log b/.remember/logs/autonomous/save-040110.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040111.log b/.remember/logs/autonomous/save-040111.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040117.log b/.remember/logs/autonomous/save-040117.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040121.log b/.remember/logs/autonomous/save-040121.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-040126.log b/.remember/logs/autonomous/save-040126.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-070753.log b/.remember/logs/autonomous/save-070753.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-070753.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-070801.log b/.remember/logs/autonomous/save-070801.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-070805.log b/.remember/logs/autonomous/save-070805.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-070809.log b/.remember/logs/autonomous/save-070809.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-070823.log b/.remember/logs/autonomous/save-070823.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-070826.log b/.remember/logs/autonomous/save-070826.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-070831.log b/.remember/logs/autonomous/save-070831.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071107.log b/.remember/logs/autonomous/save-071107.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071110.log b/.remember/logs/autonomous/save-071110.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071112.log b/.remember/logs/autonomous/save-071112.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071113.log b/.remember/logs/autonomous/save-071113.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071116.log b/.remember/logs/autonomous/save-071116.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071118.log b/.remember/logs/autonomous/save-071118.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071727.log b/.remember/logs/autonomous/save-071727.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071733.log b/.remember/logs/autonomous/save-071733.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071821.log b/.remember/logs/autonomous/save-071821.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071827.log b/.remember/logs/autonomous/save-071827.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071835.log b/.remember/logs/autonomous/save-071835.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071844.log b/.remember/logs/autonomous/save-071844.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071857.log b/.remember/logs/autonomous/save-071857.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071906.log b/.remember/logs/autonomous/save-071906.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-071910.log b/.remember/logs/autonomous/save-071910.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-072329.log b/.remember/logs/autonomous/save-072329.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-072329.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-072432.log b/.remember/logs/autonomous/save-072432.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-072434.log b/.remember/logs/autonomous/save-072434.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-072446.log b/.remember/logs/autonomous/save-072446.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-072453.log b/.remember/logs/autonomous/save-072453.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-072515.log b/.remember/logs/autonomous/save-072515.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-072624.log b/.remember/logs/autonomous/save-072624.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-072624.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-072625.log b/.remember/logs/autonomous/save-072625.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-072858.log b/.remember/logs/autonomous/save-072858.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-072858.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-072859.log b/.remember/logs/autonomous/save-072859.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-072942.log b/.remember/logs/autonomous/save-072942.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073019.log b/.remember/logs/autonomous/save-073019.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073026.log b/.remember/logs/autonomous/save-073026.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073031.log b/.remember/logs/autonomous/save-073031.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073034.log b/.remember/logs/autonomous/save-073034.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073041.log b/.remember/logs/autonomous/save-073041.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073046.log b/.remember/logs/autonomous/save-073046.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073050.log b/.remember/logs/autonomous/save-073050.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073055.log b/.remember/logs/autonomous/save-073055.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073114.log b/.remember/logs/autonomous/save-073114.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-073114.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-073125.log b/.remember/logs/autonomous/save-073125.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073134.log b/.remember/logs/autonomous/save-073134.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073147.log b/.remember/logs/autonomous/save-073147.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073221.log b/.remember/logs/autonomous/save-073221.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-073812.log b/.remember/logs/autonomous/save-073812.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-073812.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-073824.log b/.remember/logs/autonomous/save-073824.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074034.log b/.remember/logs/autonomous/save-074034.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-074034.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-074044.log b/.remember/logs/autonomous/save-074044.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074048.log b/.remember/logs/autonomous/save-074048.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074049.log b/.remember/logs/autonomous/save-074049.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074052.log b/.remember/logs/autonomous/save-074052.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074053.log b/.remember/logs/autonomous/save-074053.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074056.log b/.remember/logs/autonomous/save-074056.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074059.log b/.remember/logs/autonomous/save-074059.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074100.log b/.remember/logs/autonomous/save-074100.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074101.log b/.remember/logs/autonomous/save-074101.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074103.log b/.remember/logs/autonomous/save-074103.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074104.log b/.remember/logs/autonomous/save-074104.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074235.log b/.remember/logs/autonomous/save-074235.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-074235.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-074241.log b/.remember/logs/autonomous/save-074241.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074720.log b/.remember/logs/autonomous/save-074720.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-074720.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-074721.log b/.remember/logs/autonomous/save-074721.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074724.log b/.remember/logs/autonomous/save-074724.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074725.log b/.remember/logs/autonomous/save-074725.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074730.log b/.remember/logs/autonomous/save-074730.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074733.log b/.remember/logs/autonomous/save-074733.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074738.log b/.remember/logs/autonomous/save-074738.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074741.log b/.remember/logs/autonomous/save-074741.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074747.log b/.remember/logs/autonomous/save-074747.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074748.log b/.remember/logs/autonomous/save-074748.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074753.log b/.remember/logs/autonomous/save-074753.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074754.log b/.remember/logs/autonomous/save-074754.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074756.log b/.remember/logs/autonomous/save-074756.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074757.log b/.remember/logs/autonomous/save-074757.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074800.log b/.remember/logs/autonomous/save-074800.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074802.log b/.remember/logs/autonomous/save-074802.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074805.log b/.remember/logs/autonomous/save-074805.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074806.log b/.remember/logs/autonomous/save-074806.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074929.log b/.remember/logs/autonomous/save-074929.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-074929.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-074933.log b/.remember/logs/autonomous/save-074933.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-074934.log b/.remember/logs/autonomous/save-074934.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075012.log b/.remember/logs/autonomous/save-075012.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075015.log b/.remember/logs/autonomous/save-075015.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075019.log b/.remember/logs/autonomous/save-075019.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075020.log b/.remember/logs/autonomous/save-075020.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075902.log b/.remember/logs/autonomous/save-075902.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-075902.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-075906.log b/.remember/logs/autonomous/save-075906.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075909.log b/.remember/logs/autonomous/save-075909.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075911.log b/.remember/logs/autonomous/save-075911.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075913.log b/.remember/logs/autonomous/save-075913.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-075915.log b/.remember/logs/autonomous/save-075915.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-080457.log b/.remember/logs/autonomous/save-080457.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-080457.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-080507.log b/.remember/logs/autonomous/save-080507.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-080512.log b/.remember/logs/autonomous/save-080512.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-080518.log b/.remember/logs/autonomous/save-080518.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-080525.log b/.remember/logs/autonomous/save-080525.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082819.log b/.remember/logs/autonomous/save-082819.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-082819.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-082830.log b/.remember/logs/autonomous/save-082830.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082833.log b/.remember/logs/autonomous/save-082833.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082835.log b/.remember/logs/autonomous/save-082835.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082846.log b/.remember/logs/autonomous/save-082846.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082855.log b/.remember/logs/autonomous/save-082855.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082901.log b/.remember/logs/autonomous/save-082901.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082906.log b/.remember/logs/autonomous/save-082906.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082910.log b/.remember/logs/autonomous/save-082910.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082913.log b/.remember/logs/autonomous/save-082913.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082925.log b/.remember/logs/autonomous/save-082925.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082942.log b/.remember/logs/autonomous/save-082942.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082955.log b/.remember/logs/autonomous/save-082955.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-082958.log b/.remember/logs/autonomous/save-082958.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-083253.log b/.remember/logs/autonomous/save-083253.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-083310.log b/.remember/logs/autonomous/save-083310.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-083321.log b/.remember/logs/autonomous/save-083321.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-083329.log b/.remember/logs/autonomous/save-083329.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-084012.log b/.remember/logs/autonomous/save-084012.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-084017.log b/.remember/logs/autonomous/save-084017.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-084022.log b/.remember/logs/autonomous/save-084022.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-084038.log b/.remember/logs/autonomous/save-084038.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-084043.log b/.remember/logs/autonomous/save-084043.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-090822.log b/.remember/logs/autonomous/save-090822.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-090828.log b/.remember/logs/autonomous/save-090828.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-090842.log b/.remember/logs/autonomous/save-090842.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-090842.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-090850.log b/.remember/logs/autonomous/save-090850.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-090902.log b/.remember/logs/autonomous/save-090902.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-090925.log b/.remember/logs/autonomous/save-090925.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-090934.log b/.remember/logs/autonomous/save-090934.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-090945.log b/.remember/logs/autonomous/save-090945.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-091016.log b/.remember/logs/autonomous/save-091016.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-091120.log b/.remember/logs/autonomous/save-091120.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-091120.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-091132.log b/.remember/logs/autonomous/save-091132.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-091140.log b/.remember/logs/autonomous/save-091140.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-091217.log b/.remember/logs/autonomous/save-091217.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204803.log b/.remember/logs/autonomous/save-204803.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-204803.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-204808.log b/.remember/logs/autonomous/save-204808.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204816.log b/.remember/logs/autonomous/save-204816.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204823.log b/.remember/logs/autonomous/save-204823.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204910.log b/.remember/logs/autonomous/save-204910.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204915.log b/.remember/logs/autonomous/save-204915.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204919.log b/.remember/logs/autonomous/save-204919.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204922.log b/.remember/logs/autonomous/save-204922.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204923.log b/.remember/logs/autonomous/save-204923.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204930.log b/.remember/logs/autonomous/save-204930.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204933.log b/.remember/logs/autonomous/save-204933.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204934.log b/.remember/logs/autonomous/save-204934.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204936.log b/.remember/logs/autonomous/save-204936.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204942.log b/.remember/logs/autonomous/save-204942.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204943.log b/.remember/logs/autonomous/save-204943.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204947.log b/.remember/logs/autonomous/save-204947.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204950.log b/.remember/logs/autonomous/save-204950.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-204954.log b/.remember/logs/autonomous/save-204954.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205001.log b/.remember/logs/autonomous/save-205001.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205014.log b/.remember/logs/autonomous/save-205014.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-205014.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-205021.log b/.remember/logs/autonomous/save-205021.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205024.log b/.remember/logs/autonomous/save-205024.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205031.log b/.remember/logs/autonomous/save-205031.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205045.log b/.remember/logs/autonomous/save-205045.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205047.log b/.remember/logs/autonomous/save-205047.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205052.log b/.remember/logs/autonomous/save-205052.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205053.log b/.remember/logs/autonomous/save-205053.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205054.log b/.remember/logs/autonomous/save-205054.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205055.log b/.remember/logs/autonomous/save-205055.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205056.log b/.remember/logs/autonomous/save-205056.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205057.log b/.remember/logs/autonomous/save-205057.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205101.log b/.remember/logs/autonomous/save-205101.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205102.log b/.remember/logs/autonomous/save-205102.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205103.log b/.remember/logs/autonomous/save-205103.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205104.log b/.remember/logs/autonomous/save-205104.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205105.log b/.remember/logs/autonomous/save-205105.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205107.log b/.remember/logs/autonomous/save-205107.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205108.log b/.remember/logs/autonomous/save-205108.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205109.log b/.remember/logs/autonomous/save-205109.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205111.log b/.remember/logs/autonomous/save-205111.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205117.log b/.remember/logs/autonomous/save-205117.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205118.log b/.remember/logs/autonomous/save-205118.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205119.log b/.remember/logs/autonomous/save-205119.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205125.log b/.remember/logs/autonomous/save-205125.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205126.log b/.remember/logs/autonomous/save-205126.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205129.log b/.remember/logs/autonomous/save-205129.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205141.log b/.remember/logs/autonomous/save-205141.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205142.log b/.remember/logs/autonomous/save-205142.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205147.log b/.remember/logs/autonomous/save-205147.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205148.log b/.remember/logs/autonomous/save-205148.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205153.log b/.remember/logs/autonomous/save-205153.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205157.log b/.remember/logs/autonomous/save-205157.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205158.log b/.remember/logs/autonomous/save-205158.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205201.log b/.remember/logs/autonomous/save-205201.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205204.log b/.remember/logs/autonomous/save-205204.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205206.log b/.remember/logs/autonomous/save-205206.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205210.log b/.remember/logs/autonomous/save-205210.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205212.log b/.remember/logs/autonomous/save-205212.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205217.log b/.remember/logs/autonomous/save-205217.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-205217.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-205224.log b/.remember/logs/autonomous/save-205224.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205229.log b/.remember/logs/autonomous/save-205229.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205230.log b/.remember/logs/autonomous/save-205230.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205234.log b/.remember/logs/autonomous/save-205234.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205241.log b/.remember/logs/autonomous/save-205241.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205246.log b/.remember/logs/autonomous/save-205246.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205254.log b/.remember/logs/autonomous/save-205254.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205258.log b/.remember/logs/autonomous/save-205258.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205309.log b/.remember/logs/autonomous/save-205309.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205316.log b/.remember/logs/autonomous/save-205316.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205324.log b/.remember/logs/autonomous/save-205324.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205326.log b/.remember/logs/autonomous/save-205326.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205332.log b/.remember/logs/autonomous/save-205332.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205333.log b/.remember/logs/autonomous/save-205333.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205338.log b/.remember/logs/autonomous/save-205338.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205340.log b/.remember/logs/autonomous/save-205340.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205344.log b/.remember/logs/autonomous/save-205344.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205346.log b/.remember/logs/autonomous/save-205346.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205357.log b/.remember/logs/autonomous/save-205357.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205402.log b/.remember/logs/autonomous/save-205402.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205408.log b/.remember/logs/autonomous/save-205408.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205412.log b/.remember/logs/autonomous/save-205412.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205417.log b/.remember/logs/autonomous/save-205417.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-205417.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-205425.log b/.remember/logs/autonomous/save-205425.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205445.log b/.remember/logs/autonomous/save-205445.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205449.log b/.remember/logs/autonomous/save-205449.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205454.log b/.remember/logs/autonomous/save-205454.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205502.log b/.remember/logs/autonomous/save-205502.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205525.log b/.remember/logs/autonomous/save-205525.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205538.log b/.remember/logs/autonomous/save-205538.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205555.log b/.remember/logs/autonomous/save-205555.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205613.log b/.remember/logs/autonomous/save-205613.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205614.log b/.remember/logs/autonomous/save-205614.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205615.log b/.remember/logs/autonomous/save-205615.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205616.log b/.remember/logs/autonomous/save-205616.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205617.log b/.remember/logs/autonomous/save-205617.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-205617.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-205618.log b/.remember/logs/autonomous/save-205618.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205619.log b/.remember/logs/autonomous/save-205619.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205620.log b/.remember/logs/autonomous/save-205620.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205621.log b/.remember/logs/autonomous/save-205621.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205624.log b/.remember/logs/autonomous/save-205624.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205625.log b/.remember/logs/autonomous/save-205625.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205626.log b/.remember/logs/autonomous/save-205626.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205627.log b/.remember/logs/autonomous/save-205627.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205628.log b/.remember/logs/autonomous/save-205628.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205630.log b/.remember/logs/autonomous/save-205630.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205631.log b/.remember/logs/autonomous/save-205631.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205632.log b/.remember/logs/autonomous/save-205632.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205633.log b/.remember/logs/autonomous/save-205633.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205634.log b/.remember/logs/autonomous/save-205634.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205636.log b/.remember/logs/autonomous/save-205636.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205640.log b/.remember/logs/autonomous/save-205640.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205641.log b/.remember/logs/autonomous/save-205641.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205645.log b/.remember/logs/autonomous/save-205645.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205650.log b/.remember/logs/autonomous/save-205650.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205651.log b/.remember/logs/autonomous/save-205651.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205657.log b/.remember/logs/autonomous/save-205657.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205700.log b/.remember/logs/autonomous/save-205700.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205702.log b/.remember/logs/autonomous/save-205702.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205703.log b/.remember/logs/autonomous/save-205703.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205704.log b/.remember/logs/autonomous/save-205704.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205708.log b/.remember/logs/autonomous/save-205708.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205710.log b/.remember/logs/autonomous/save-205710.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205713.log b/.remember/logs/autonomous/save-205713.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205714.log b/.remember/logs/autonomous/save-205714.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205715.log b/.remember/logs/autonomous/save-205715.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205716.log b/.remember/logs/autonomous/save-205716.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205717.log b/.remember/logs/autonomous/save-205717.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205718.log b/.remember/logs/autonomous/save-205718.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205720.log b/.remember/logs/autonomous/save-205720.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205722.log b/.remember/logs/autonomous/save-205722.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205724.log b/.remember/logs/autonomous/save-205724.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205725.log b/.remember/logs/autonomous/save-205725.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205730.log b/.remember/logs/autonomous/save-205730.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205732.log b/.remember/logs/autonomous/save-205732.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205740.log b/.remember/logs/autonomous/save-205740.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205746.log b/.remember/logs/autonomous/save-205746.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205748.log b/.remember/logs/autonomous/save-205748.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205749.log b/.remember/logs/autonomous/save-205749.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205757.log b/.remember/logs/autonomous/save-205757.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205811.log b/.remember/logs/autonomous/save-205811.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205812.log b/.remember/logs/autonomous/save-205812.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205815.log b/.remember/logs/autonomous/save-205815.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205823.log b/.remember/logs/autonomous/save-205823.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-205823.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-205829.log b/.remember/logs/autonomous/save-205829.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205832.log b/.remember/logs/autonomous/save-205832.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205837.log b/.remember/logs/autonomous/save-205837.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-205840.log b/.remember/logs/autonomous/save-205840.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-212802.log b/.remember/logs/autonomous/save-212802.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-212802.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-212806.log b/.remember/logs/autonomous/save-212806.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-215225.log b/.remember/logs/autonomous/save-215225.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-215225.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-215227.log b/.remember/logs/autonomous/save-215227.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-215252.log b/.remember/logs/autonomous/save-215252.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-215255.log b/.remember/logs/autonomous/save-215255.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-215300.log b/.remember/logs/autonomous/save-215300.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-215302.log b/.remember/logs/autonomous/save-215302.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-215308.log b/.remember/logs/autonomous/save-215308.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-215325.log b/.remember/logs/autonomous/save-215325.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-222708.log b/.remember/logs/autonomous/save-222708.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-222708.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-222716.log b/.remember/logs/autonomous/save-222716.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-223848.log b/.remember/logs/autonomous/save-223848.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-223848.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-223853.log b/.remember/logs/autonomous/save-223853.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-224350.log b/.remember/logs/autonomous/save-224350.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-224350.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-224446.log b/.remember/logs/autonomous/save-224446.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-224449.log b/.remember/logs/autonomous/save-224449.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-224535.log b/.remember/logs/autonomous/save-224535.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-224609.log b/.remember/logs/autonomous/save-224609.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-224828.log b/.remember/logs/autonomous/save-224828.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-224828.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-224831.log b/.remember/logs/autonomous/save-224831.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-224836.log b/.remember/logs/autonomous/save-224836.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-224844.log b/.remember/logs/autonomous/save-224844.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-225428.log b/.remember/logs/autonomous/save-225428.log new file mode 100644 index 0000000..189a2f2 --- /dev/null +++ b/.remember/logs/autonomous/save-225428.log @@ -0,0 +1,4 @@ +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 135: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 140: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 142: [: : integer expression expected +/root/.claude/plugins/cache/claude-plugins-official/remember/0.1.0/scripts/save-session.sh: line 164: cd: /root/.claude/plugins/cache/claude-plugins-official/.claude/remember: No such file or directory diff --git a/.remember/logs/autonomous/save-225458.log b/.remember/logs/autonomous/save-225458.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-225506.log b/.remember/logs/autonomous/save-225506.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-225511.log b/.remember/logs/autonomous/save-225511.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-225535.log b/.remember/logs/autonomous/save-225535.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-225540.log b/.remember/logs/autonomous/save-225540.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231628.log b/.remember/logs/autonomous/save-231628.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231633.log b/.remember/logs/autonomous/save-231633.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231641.log b/.remember/logs/autonomous/save-231641.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231645.log b/.remember/logs/autonomous/save-231645.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231646.log b/.remember/logs/autonomous/save-231646.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231649.log b/.remember/logs/autonomous/save-231649.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231654.log b/.remember/logs/autonomous/save-231654.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231659.log b/.remember/logs/autonomous/save-231659.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231700.log b/.remember/logs/autonomous/save-231700.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231703.log b/.remember/logs/autonomous/save-231703.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231704.log b/.remember/logs/autonomous/save-231704.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231720.log b/.remember/logs/autonomous/save-231720.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231724.log b/.remember/logs/autonomous/save-231724.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231756.log b/.remember/logs/autonomous/save-231756.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231757.log b/.remember/logs/autonomous/save-231757.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231800.log b/.remember/logs/autonomous/save-231800.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231909.log b/.remember/logs/autonomous/save-231909.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/logs/autonomous/save-231920.log b/.remember/logs/autonomous/save-231920.log new file mode 100644 index 0000000..e69de29 diff --git a/.remember/tmp/save-session.pid b/.remember/tmp/save-session.pid new file mode 100644 index 0000000..efca6cd --- /dev/null +++ b/.remember/tmp/save-session.pid @@ -0,0 +1 @@ +3500589 diff --git a/all_logs.txt b/all_logs.txt new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.kotlin/sessions/kotlin-compiler-5367790450239390534.salive b/android-app/.kotlin/sessions/kotlin-compiler-5367790450239390534.salive new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.kotlin/sessions/kotlin-compiler-8331847918581383171.salive b/android-app/.kotlin/sessions/kotlin-compiler-8331847918581383171.salive new file mode 100644 index 0000000..e69de29 diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 66aa3e0..0f2eb91 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -30,6 +30,7 @@ import java.util.Locale import org.maplibre.android.MapLibre import org.maplibre.android.maps.MapView import org.maplibre.android.maps.Style +import org.maplibre.android.style.layers.PropertyFactory import org.maplibre.android.style.layers.RasterLayer import org.maplibre.android.style.sources.RasterSource import org.maplibre.android.style.sources.TileSet @@ -335,7 +336,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { lifecycleScope.launch { loadedStyleFlow.filterNotNull() .combine(viewModel.trackPoints) { style, points -> style to points } - .collect { (style, points) -> mapHandler?.updateTrackLayer(style, points) } + .combine(viewModel.pastTracks) { (style, active), past -> Triple(style, active, past) } + .collect { (style, active, past) -> mapHandler?.updateTrackLayer(style, active, past) } } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index 7953822..85dd2dd 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -5,22 +5,28 @@ class TrackRepository { var isRecording: Boolean = false private set - private val points = mutableListOf() + private val activePoints = mutableListOf() + private val pastTracks = mutableListOf>() fun startTrack() { - points.clear() + activePoints.clear() isRecording = true } fun stopTrack() { + if (isRecording && activePoints.isNotEmpty()) { + pastTracks.add(activePoints.toList()) + } isRecording = false } fun addPoint(point: TrackPoint): Boolean { if (!isRecording) return false - points.add(point) + activePoints.add(point) return true } - fun getPoints(): List = points.toList() + fun getPoints(): List = activePoints.toList() + + fun getPastTracks(): List> = pastTracks.toList() } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt new file mode 100644 index 0000000..2362079 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt @@ -0,0 +1,43 @@ +package org.terst.nav.tripreport + +enum class BoatType { + MONOHULL, + MULTIHULL +} + +enum class RigType { + SLOOP, + CUTTER, + KETCH +} + +data class BoatProfile( + val name: String, + val lengthFt: Double, + val type: BoatType, + val rig: RigType, + val hasSpinnaker: Boolean = false, + val hasGennaker: Boolean = false +) + +data class PreTripSummary( + val timestampMs: Long, + val lat: Double, + val lon: Double, + val windSpeedKt: Double, + val windDirDeg: Double, + val waveHeightM: Double?, + val weatherDescription: String, + val boatProfile: BoatProfile +) + +data class SailSuggestion( + val sailName: String, + val action: String // e.g., "Full Main", "1 Reef", "Furl" +) + +data class PreTripReport( + val summary: PreTripSummary, + val routingSuggestion: String, + val sailPlan: List +) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt new file mode 100644 index 0000000..819485f --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt @@ -0,0 +1,102 @@ +package org.terst.nav.tripreport + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.google.android.material.button.MaterialButton +import com.google.android.material.card.MaterialCardView +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.terst.nav.R +import org.terst.nav.ui.MainViewModel +import java.util.Locale + +class PreTripReportFragment : Fragment() { + + private val viewModel: PreTripReportViewModel by activityViewModels() + private val mainViewModel: MainViewModel by activityViewModels() + + private lateinit var tvWeatherSummary: TextView + private lateinit var tvRoutingContent: TextView + private lateinit var tvSailPlanContent: TextView + private lateinit var cardReport: MaterialCardView + private lateinit var btnGenerate: MaterialButton + private lateinit var progress: ProgressBar + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_pretrip_report, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + tvWeatherSummary = view.findViewById(R.id.tv_weather_summary) + tvRoutingContent = view.findViewById(R.id.tv_routing_content) + tvSailPlanContent = view.findViewById(R.id.tv_sail_plan_content) + cardReport = view.findViewById(R.id.card_report) + btnGenerate = view.findViewById(R.id.btn_generate_pretrip) + progress = view.findViewById(R.id.progress_pretrip) + + btnGenerate.setOnClickListener { + generateReport() + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.state.collect { renderState(it) } + } + } + + private fun generateReport() { + viewLifecycleOwner.lifecycleScope.launch { + val forecast = mainViewModel.forecast.value.firstOrNull() + val conditions = mainViewModel.marineConditions.value + // For now, use 0,0 if no location, but ideally we'd have last known + // In a real app, we'd get this from a LocationProvider + viewModel.generate(0.0, 0.0, forecast, conditions) + } + } + + private fun renderState(state: PreTripState) { + when (state) { + is PreTripState.Loading -> { + progress.visibility = View.VISIBLE + btnGenerate.isEnabled = false + } + is PreTripState.Success -> { + progress.visibility = View.GONE + btnGenerate.isEnabled = true + cardReport.visibility = View.VISIBLE + + val r = state.report + tvWeatherSummary.text = "Wind: %.1f kts from %.0f°\nWaves: %s\nSky: %s".format( + Locale.getDefault(), + r.summary.windSpeedKt, + r.summary.windDirDeg, + r.summary.waveHeightM?.let { "%.1fm".format(it) } ?: "N/A", + r.summary.weatherDescription + ) + + tvRoutingContent.text = r.routingSuggestion + + val sailPlanText = r.sailPlan.joinToString("\n") { + "• ${it.sailName}: ${it.action}" + } + tvSailPlanContent.text = sailPlanText + } + is PreTripState.Error -> { + progress.visibility = View.GONE + btnGenerate.isEnabled = true + // Show toast or error message + } + else -> {} + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt new file mode 100644 index 0000000..2ccabfb --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt @@ -0,0 +1,66 @@ +package org.terst.nav.tripreport + +import org.terst.nav.data.model.ForecastItem +import org.terst.nav.data.model.MarineConditions + +class PreTripReportGenerator { + + fun generateReport( + lat: Double, + lon: Double, + forecast: ForecastItem?, + conditions: MarineConditions?, + boatProfile: BoatProfile + ): PreTripReport { + val summary = PreTripSummary( + timestampMs = System.currentTimeMillis(), + lat = lat, + lon = lon, + windSpeedKt = forecast?.windKt ?: 0.0, + windDirDeg = forecast?.windDirDeg ?: 0.0, + waveHeightM = conditions?.waveHeightM, + weatherDescription = forecast?.weatherDescription() ?: "Unknown", + boatProfile = boatProfile + ) + + val routing = suggestRouting(summary) + val sailPlan = suggestSailPlan(summary) + + return PreTripReport(summary, routing, sailPlan) + } + + private fun suggestRouting(summary: PreTripSummary): String { + val wind = summary.windSpeedKt + val waves = summary.waveHeightM ?: 0.0 + + return when { + wind > 35.0 -> "STORM WARNING: Winds exceed 35kts. Consider remaining in port or seeking shelter immediately." + wind > 25.0 && waves > 2.5 -> "HEAVY WEATHER: Expect challenging conditions. Coastal routing advised to minimize fetch." + wind < 5.0 -> "LIGHT WINDS: Motor-sailing likely required for efficient passage." + else -> "FAVORABLE CONDITIONS: Standard routing based on destination bearing should be effective." + } + } + + private fun suggestSailPlan(summary: PreTripSummary): List { + val wind = summary.windSpeedKt + val suggestions = mutableListOf() + + // Main sail + suggestions.add(when { + wind > 30.0 -> SailSuggestion("Main", "Deep Reef / Trysail") + wind > 22.0 -> SailSuggestion("Main", "2nd Reef") + wind > 16.0 -> SailSuggestion("Main", "1st Reef") + else -> SailSuggestion("Main", "Full Main") + }) + + // Headsail + suggestions.add(when { + wind > 25.0 -> SailSuggestion("Headsail", "Storm Jib / Furl 50%") + wind > 18.0 -> SailSuggestion("Headsail", "Working Jib / Furl 30%") + wind < 10.0 && summary.boatProfile.hasGennaker -> SailSuggestion("Gennaker", "Deploy for light air reach") + else -> SailSuggestion("Headsail", "Full Genoa") + }) + + return suggestions + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt new file mode 100644 index 0000000..9fd32c7 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt @@ -0,0 +1,51 @@ +package org.terst.nav.tripreport + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.terst.nav.data.model.ForecastItem +import org.terst.nav.data.model.MarineConditions + +sealed class PreTripState { + object Idle : PreTripState() + object Loading : PreTripState() + data class Success(val report: PreTripReport) : PreTripState() + data class Error(val message: String) : PreTripState() +} + +class PreTripReportViewModel( + private val generator: PreTripReportGenerator = PreTripReportGenerator() +) : ViewModel() { + + private val _state = MutableStateFlow(PreTripState.Idle) + val state: StateFlow = _state.asStateFlow() + + private val _boatProfile = MutableStateFlow( + BoatProfile("Default Sloop", 35.0, BoatType.MONOHULL, RigType.SLOOP) + ) + val boatProfile: StateFlow = _boatProfile.asStateFlow() + + fun updateBoatProfile(profile: BoatProfile) { + _boatProfile.value = profile + } + + fun generate( + lat: Double, + lon: Double, + forecast: ForecastItem?, + conditions: MarineConditions? + ) { + viewModelScope.launch { + _state.value = PreTripState.Loading + try { + val report = generator.generateReport(lat, lon, forecast, conditions, _boatProfile.value) + _state.value = PreTripState.Success(report) + } catch (e: Exception) { + _state.value = PreTripState.Error(e.message ?: "Failed to generate pre-trip report") + } + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt index 7caabe7..2c56b06 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt @@ -57,6 +57,9 @@ class MainViewModel( private val _trackPoints = MutableStateFlow>(emptyList()) val trackPoints: StateFlow> = _trackPoints.asStateFlow() + private val _pastTracks = MutableStateFlow>>(emptyList()) + val pastTracks: StateFlow>> = _pastTracks.asStateFlow() + fun startTrack() { trackRepository.startTrack() _trackPoints.value = emptyList() @@ -65,6 +68,8 @@ class MainViewModel( fun stopTrack() { trackRepository.stopTrack() + _pastTracks.value = trackRepository.getPastTracks() + _trackPoints.value = emptyList() _isRecording.value = false } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt index f1feaed..4f08de7 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt @@ -67,14 +67,17 @@ class MapHandler(private val maplibreMap: MapLibreMap) { private val USER_POS_LAYER_ID = "user-pos-layer" private val USER_ICON_ID = "user-icon" - private val TRACK_SOURCE_ID = "track-source" - private val TRACK_LAYER_ID = "track-line" + private val TRACK_ACTIVE_SOURCE_ID = "track-active-source" + private val TRACK_ACTIVE_LAYER_ID = "track-line-active" + private val TRACK_PAST_SOURCE_ID = "track-past-source" + private val TRACK_PAST_LAYER_ID = "track-line-past" private var anchorPointSource: GeoJsonSource? = null private var anchorCircleSource: GeoJsonSource? = null private var tidalCurrentSource: GeoJsonSource? = null private var userPosSource: GeoJsonSource? = null - private var trackSource: GeoJsonSource? = null + private var trackActiveSource: GeoJsonSource? = null + private var trackPastSource: GeoJsonSource? = null /** * Initializes map layers for anchor watch, tidal currents, and user position. @@ -199,26 +202,52 @@ class MapHandler(private val maplibreMap: MapLibreMap) { } /** - * Updates the GPS track polyline on the map. Lazily initialises the layer on first call. + * Updates the GPS track polyline on the map. Lazily initialises the layers on first call. */ - fun updateTrackLayer(style: Style, points: List) { - if (trackSource == null) { - trackSource = GeoJsonSource(TRACK_SOURCE_ID) - style.addSource(trackSource!!) - style.addLayer(LineLayer(TRACK_LAYER_ID, TRACK_SOURCE_ID).apply { + fun updateTrackLayer(style: Style, activePoints: List, pastTracks: List>) { + // Active track layer (Solid) + if (trackActiveSource == null) { + trackActiveSource = GeoJsonSource(TRACK_ACTIVE_SOURCE_ID) + style.addSource(trackActiveSource!!) + style.addLayer(LineLayer(TRACK_ACTIVE_LAYER_ID, TRACK_ACTIVE_SOURCE_ID).apply { setProperties( PropertyFactory.lineColor("#E53935"), PropertyFactory.lineWidth(4f), + PropertyFactory.lineCap("round") + ) + }) + } + + // Past tracks layer (Dotted) + if (trackPastSource == null) { + trackPastSource = GeoJsonSource(TRACK_PAST_SOURCE_ID) + style.addSource(trackPastSource!!) + style.addLayer(LineLayer(TRACK_PAST_LAYER_ID, TRACK_PAST_SOURCE_ID).apply { + setProperties( + PropertyFactory.lineColor("#E53935"), + PropertyFactory.lineWidth(3f), PropertyFactory.lineDasharray(arrayOf(1f, 2f)), PropertyFactory.lineCap("round") ) }) } - if (points.size >= 2) { - val coords = points.map { Point.fromLngLat(it.lon, it.lat) } - trackSource?.setGeoJson(Feature.fromGeometry(LineString.fromLngLats(coords))) + + // Update Active Track + if (activePoints.size >= 2) { + val coords = activePoints.map { Point.fromLngLat(it.lon, it.lat) } + trackActiveSource?.setGeoJson(Feature.fromGeometry(LineString.fromLngLats(coords))) + } else { + trackActiveSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList())) + } + + // Update Past Tracks + if (pastTracks.isNotEmpty()) { + val features = pastTracks.map { track -> + Feature.fromGeometry(LineString.fromLngLats(track.map { Point.fromLngLat(it.lon, it.lat) })) + } + trackPastSource?.setGeoJson(FeatureCollection.fromFeatures(features)) } else { - trackSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList())) + trackPastSource?.setGeoJson(FeatureCollection.fromFeatures(emptyList())) } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt index e950b5d..4bc0c7a 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt @@ -48,6 +48,13 @@ class SafetyFragment : Fragment() { view.findViewById(R.id.button_anchor_config).setOnClickListener { listener?.onConfigureAnchor() } + + view.findViewById(R.id.button_plan_trip).setOnClickListener { + parentFragmentManager.beginTransaction() + .replace(R.id.fragment_container, org.terst.nav.tripreport.PreTripReportFragment()) + .addToBackStack(null) + .commit() + } } fun updateAnchorStatus(statusText: String) { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt index 86fd67c..1c797d5 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt @@ -16,6 +16,7 @@ import android.widget.TextView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope +import com.google.android.material.button.MaterialButton import com.google.android.material.floatingactionbutton.FloatingActionButton import kotlinx.coroutines.launch import org.terst.nav.R diff --git a/android-app/app/src/main/res/layout/fragment_pretrip_report.xml b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml new file mode 100644 index 0000000..d7ede49 --- /dev/null +++ b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/fragment_safety.xml b/android-app/app/src/main/res/layout/fragment_safety.xml index 5b2397e..f90420e 100644 --- a/android-app/app/src/main/res/layout/fragment_safety.xml +++ b/android-app/app/src/main/res/layout/fragment_safety.xml @@ -104,4 +104,13 @@ + + diff --git a/build.log b/build.log new file mode 100644 index 0000000..bf8110e --- /dev/null +++ b/build.log @@ -0,0 +1,606 @@ +2026-04-03T08:08:07.3138358Z Current runner version: '2.333.1' +2026-04-03T08:08:07.3175780Z ##[group]Runner Image Provisioner +2026-04-03T08:08:07.3177392Z Hosted Compute Agent +2026-04-03T08:08:07.3178332Z Version: 20260213.493 +2026-04-03T08:08:07.3179410Z Commit: 5c115507f6dd24b8de37d8bbe0bb4509d0cc0fa3 +2026-04-03T08:08:07.3180637Z Build Date: 2026-02-13T00:28:41Z +2026-04-03T08:08:07.3181850Z Worker ID: {f209a986-40a4-4784-a422-0a44cbe8d8b6} +2026-04-03T08:08:07.3183435Z Azure Region: northcentralus +2026-04-03T08:08:07.3184364Z ##[endgroup] +2026-04-03T08:08:07.3186844Z ##[group]Operating System +2026-04-03T08:08:07.3187909Z Ubuntu +2026-04-03T08:08:07.3188638Z 24.04.4 +2026-04-03T08:08:07.3189464Z LTS +2026-04-03T08:08:07.3190258Z ##[endgroup] +2026-04-03T08:08:07.3191038Z ##[group]Runner Image +2026-04-03T08:08:07.3192290Z Image: ubuntu-24.04 +2026-04-03T08:08:07.3193304Z Version: 20260323.65.1 +2026-04-03T08:08:07.3195446Z Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20260323.65/images/ubuntu/Ubuntu2404-Readme.md +2026-04-03T08:08:07.3198248Z Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20260323.65 +2026-04-03T08:08:07.3199860Z ##[endgroup] +2026-04-03T08:08:07.3201945Z ##[group]GITHUB_TOKEN Permissions +2026-04-03T08:08:07.3205409Z Contents: read +2026-04-03T08:08:07.3206431Z Metadata: read +2026-04-03T08:08:07.3207379Z Packages: read +2026-04-03T08:08:07.3208228Z ##[endgroup] +2026-04-03T08:08:07.3211476Z Secret source: Actions +2026-04-03T08:08:07.3213019Z Prepare workflow directory +2026-04-03T08:08:07.3708690Z Prepare all required actions +2026-04-03T08:08:07.3769871Z Getting action download info +2026-04-03T08:08:07.8688505Z Download action repository 'actions/checkout@v4' (SHA:34e114876b0b11c390a56381ad16ebd13914f8d5) +2026-04-03T08:08:08.0173793Z Download action repository 'actions/setup-java@v4' (SHA:c1e323688fd81a25caa38c78aa6df2d33d3e20d9) +2026-04-03T08:08:08.5750939Z Download action repository 'actions/download-artifact@v4' (SHA:d3f86a106a0bac45b974a628896c90dbdf5c8093) +2026-04-03T08:08:09.0343372Z Download action repository 'reactivecircus/android-emulator-runner@v2' (SHA:e89f39f1abbbd05b1113a29cf4db69e7540cae5a) +2026-04-03T08:08:09.5825384Z Download action repository 'actions/upload-artifact@v4' (SHA:ea165f8d65b6e75b540449e92b4886f43607fa02) +2026-04-03T08:08:10.4880089Z Complete job name: smoke-test +2026-04-03T08:08:10.5591248Z ##[group]Run actions/checkout@v4 +2026-04-03T08:08:10.5592303Z with: +2026-04-03T08:08:10.5592706Z repository: thepeterstone/nav +2026-04-03T08:08:10.5593289Z token: *** +2026-04-03T08:08:10.5593603Z ssh-strict: true +2026-04-03T08:08:10.5593915Z ssh-user: git +2026-04-03T08:08:10.5594215Z persist-credentials: true +2026-04-03T08:08:10.5594535Z clean: true +2026-04-03T08:08:10.5594887Z sparse-checkout-cone-mode: true +2026-04-03T08:08:10.5595232Z fetch-depth: 1 +2026-04-03T08:08:10.5595523Z fetch-tags: false +2026-04-03T08:08:10.5595843Z show-progress: true +2026-04-03T08:08:10.5596097Z lfs: false +2026-04-03T08:08:10.5596398Z submodules: false +2026-04-03T08:08:10.5596763Z set-safe-directory: true +2026-04-03T08:08:10.5597365Z ##[endgroup] +2026-04-03T08:08:10.6820575Z Syncing repository: thepeterstone/nav +2026-04-03T08:08:10.6822603Z ##[group]Getting Git version info +2026-04-03T08:08:10.6823119Z Working directory is '/home/runner/work/nav/nav' +2026-04-03T08:08:10.6824045Z [command]/usr/bin/git version +2026-04-03T08:08:10.6861784Z git version 2.53.0 +2026-04-03T08:08:10.6895695Z ##[endgroup] +2026-04-03T08:08:10.6909716Z Temporarily overriding HOME='/home/runner/work/_temp/baaa6d69-e6d0-4394-b358-e495978c673b' before making global git config changes +2026-04-03T08:08:10.6913248Z Adding repository directory to the temporary git global config as a safe directory +2026-04-03T08:08:10.6929639Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/nav/nav +2026-04-03T08:08:10.6967783Z Deleting the contents of '/home/runner/work/nav/nav' +2026-04-03T08:08:10.6972960Z ##[group]Initializing the repository +2026-04-03T08:08:10.6978025Z [command]/usr/bin/git init /home/runner/work/nav/nav +2026-04-03T08:08:10.7114838Z hint: Using 'master' as the name for the initial branch. This default branch name +2026-04-03T08:08:10.7122741Z hint: will change to "main" in Git 3.0. To configure the initial branch name +2026-04-03T08:08:10.7124051Z hint: to use in all of your new repositories, which will suppress this warning, +2026-04-03T08:08:10.7124864Z hint: call: +2026-04-03T08:08:10.7125363Z hint: +2026-04-03T08:08:10.7126009Z hint: git config --global init.defaultBranch +2026-04-03T08:08:10.7126576Z hint: +2026-04-03T08:08:10.7127111Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and +2026-04-03T08:08:10.7128078Z hint: 'development'. The just-created branch can be renamed via this command: +2026-04-03T08:08:10.7128818Z hint: +2026-04-03T08:08:10.7129293Z hint: git branch -m +2026-04-03T08:08:10.7129873Z hint: +2026-04-03T08:08:10.7130844Z hint: Disable this message with "git config set advice.defaultBranchName false" +2026-04-03T08:08:10.7133347Z Initialized empty Git repository in /home/runner/work/nav/nav/.git/ +2026-04-03T08:08:10.7135196Z [command]/usr/bin/git remote add origin https://github.com/thepeterstone/nav +2026-04-03T08:08:10.7166326Z ##[endgroup] +2026-04-03T08:08:10.7169258Z ##[group]Disabling automatic garbage collection +2026-04-03T08:08:10.7172705Z [command]/usr/bin/git config --local gc.auto 0 +2026-04-03T08:08:10.7208906Z ##[endgroup] +2026-04-03T08:08:10.7211132Z ##[group]Setting up auth +2026-04-03T08:08:10.7216227Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand +2026-04-03T08:08:10.7250741Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :" +2026-04-03T08:08:10.7561926Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader +2026-04-03T08:08:10.7597768Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || :" +2026-04-03T08:08:10.7843058Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir: +2026-04-03T08:08:10.7892785Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url +2026-04-03T08:08:10.8140388Z [command]/usr/bin/git config --local http.https://github.com/.extraheader AUTHORIZATION: basic *** +2026-04-03T08:08:10.8188817Z ##[endgroup] +2026-04-03T08:08:10.8190334Z ##[group]Fetching the repository +2026-04-03T08:08:10.8191621Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +5d358cd570075d36a61f9a37bb80c64f8a0a7e2a:refs/remotes/origin/main +2026-04-03T08:08:11.9068411Z From https://github.com/thepeterstone/nav +2026-04-03T08:08:11.9069287Z * [new ref] 5d358cd570075d36a61f9a37bb80c64f8a0a7e2a -> origin/main +2026-04-03T08:08:11.9071653Z ##[endgroup] +2026-04-03T08:08:11.9072732Z ##[group]Determining the checkout info +2026-04-03T08:08:11.9073528Z ##[endgroup] +2026-04-03T08:08:11.9073922Z [command]/usr/bin/git sparse-checkout disable +2026-04-03T08:08:11.9075146Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig +2026-04-03T08:08:11.9076478Z ##[group]Checking out the ref +2026-04-03T08:08:11.9077132Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main +2026-04-03T08:08:11.9934621Z Switched to a new branch 'main' +2026-04-03T08:08:11.9936830Z branch 'main' set up to track 'origin/main'. +2026-04-03T08:08:11.9957315Z ##[endgroup] +2026-04-03T08:08:12.0001935Z [command]/usr/bin/git log -1 --format=%H +2026-04-03T08:08:12.0028992Z 5d358cd570075d36a61f9a37bb80c64f8a0a7e2a +2026-04-03T08:08:12.0368217Z ##[group]Run actions/setup-java@v4 +2026-04-03T08:08:12.0368580Z with: +2026-04-03T08:08:12.0368759Z java-version: 17 +2026-04-03T08:08:12.0368964Z distribution: temurin +2026-04-03T08:08:12.0369367Z cache: gradle +2026-04-03T08:08:12.0369548Z java-package: jdk +2026-04-03T08:08:12.0369744Z check-latest: false +2026-04-03T08:08:12.0369938Z server-id: github +2026-04-03T08:08:12.0370150Z server-username: GITHUB_ACTOR +2026-04-03T08:08:12.0370394Z server-password: GITHUB_TOKEN +2026-04-03T08:08:12.0370622Z overwrite-settings: true +2026-04-03T08:08:12.0370851Z job-status: success +2026-04-03T08:08:12.0371178Z token: *** +2026-04-03T08:08:12.0371362Z ##[endgroup] +2026-04-03T08:08:12.2402244Z ##[group]Installed distributions +2026-04-03T08:08:12.2472933Z Resolved Java 17.0.18+8 from tool-cache +2026-04-03T08:08:12.2474792Z Setting Java 17.0.18+8 as the default +2026-04-03T08:08:12.2489188Z Creating toolchains.xml for JDK version 17 from temurin +2026-04-03T08:08:12.2569647Z Writing to /home/runner/.m2/toolchains.xml +2026-04-03T08:08:12.2571410Z +2026-04-03T08:08:12.2573883Z Java configuration: +2026-04-03T08:08:12.2574799Z Distribution: temurin +2026-04-03T08:08:12.2575562Z Version: 17.0.18+8 +2026-04-03T08:08:12.2576354Z Path: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:12.2577256Z +2026-04-03T08:08:12.2578142Z ##[endgroup] +2026-04-03T08:08:12.2607452Z Creating settings.xml with server-id: github +2026-04-03T08:08:12.2613133Z Writing to /home/runner/.m2/settings.xml +2026-04-03T08:08:12.7474364Z Cache hit for: setup-java-Linux-x64-gradle-d4c15765f365f6526901b93c5ac1a269b402e810253a0ecc8c422f8b3b2ed95d +2026-04-03T08:08:13.9000403Z Received 100663296 of 573077343 (17.6%), 96.0 MBs/sec +2026-04-03T08:08:14.9002882Z Received 268435456 of 573077343 (46.8%), 127.9 MBs/sec +2026-04-03T08:08:15.8998418Z Received 486539264 of 573077343 (84.9%), 154.7 MBs/sec +2026-04-03T08:08:16.3691151Z Received 573077343 of 573077343 (100.0%), 157.5 MBs/sec +2026-04-03T08:08:16.3723298Z Cache Size: ~547 MB (573077343 B) +2026-04-03T08:08:16.3896313Z [command]/usr/bin/tar -xf /home/runner/work/_temp/25aa7b4f-1e8a-4fdb-9b47-b09c31fab97d/cache.tzst -P -C /home/runner/work/nav/nav --use-compress-program unzstd +2026-04-03T08:08:20.5782740Z Cache restored successfully +2026-04-03T08:08:20.6085524Z Cache restored from key: setup-java-Linux-x64-gradle-d4c15765f365f6526901b93c5ac1a269b402e810253a0ecc8c422f8b3b2ed95d +2026-04-03T08:08:20.6259783Z ##[group]Run chmod +x android-app/gradlew +2026-04-03T08:08:20.6260183Z chmod +x android-app/gradlew +2026-04-03T08:08:20.6294620Z shell: /usr/bin/bash -e {0} +2026-04-03T08:08:20.6294896Z env: +2026-04-03T08:08:20.6295214Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:20.6295712Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:20.6296081Z ##[endgroup] +2026-04-03T08:08:20.7006672Z ##[group]Run actions/download-artifact@v4 +2026-04-03T08:08:20.7006967Z with: +2026-04-03T08:08:20.7007146Z name: test-apks +2026-04-03T08:08:20.7007338Z path: . +2026-04-03T08:08:20.7007513Z merge-multiple: false +2026-04-03T08:08:20.7007743Z repository: thepeterstone/nav +2026-04-03T08:08:20.7007995Z run-id: 23939201588 +2026-04-03T08:08:20.7008180Z env: +2026-04-03T08:08:20.7008460Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:20.7008932Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:20.7009293Z ##[endgroup] +2026-04-03T08:08:20.9177650Z Downloading single artifact +2026-04-03T08:08:21.1041850Z Preparing to download the following artifacts: +2026-04-03T08:08:21.1043362Z - test-apks (ID: 6256757851, Size: 32186358, Expected Digest: sha256:6175c12ede2e28f689fbf4d719ce1af6a292e3b3150257e67a31122a95350540) +2026-04-03T08:08:21.1877967Z Redirecting to blob download url: https://productionresultssa18.blob.core.windows.net/actions-results/ba194748-6152-496b-be48-84b66f8c4e76/workflow-job-run-33bd8373-d342-5bbd-b72c-d37e43835c5c/artifacts/be6d339498f0e44beff448c449ca6372065982a0894aa49cd535d4dfcb3f50a9.zip +2026-04-03T08:08:21.1880207Z Starting download of artifact to: /home/runner/work/nav/nav +2026-04-03T08:08:21.3555368Z (node:2109) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead. +2026-04-03T08:08:21.3560215Z (Use `node --trace-deprecation ...` to show where the warning was created) +2026-04-03T08:08:22.5727366Z SHA256 digest of downloaded artifact is 6175c12ede2e28f689fbf4d719ce1af6a292e3b3150257e67a31122a95350540 +2026-04-03T08:08:22.5729612Z Artifact download completed successfully. +2026-04-03T08:08:22.5730194Z Total of 1 artifact(s) downloaded +2026-04-03T08:08:22.5742971Z Download artifact has finished successfully +2026-04-03T08:08:22.5861196Z ##[group]Run echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules +2026-04-03T08:08:22.5862471Z echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules +2026-04-03T08:08:22.5863404Z sudo udevadm control --reload-rules +2026-04-03T08:08:22.5863720Z sudo udevadm trigger --name-match=kvm +2026-04-03T08:08:22.5890201Z shell: /usr/bin/bash -e {0} +2026-04-03T08:08:22.5890455Z env: +2026-04-03T08:08:22.5890767Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:22.5891267Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:22.5891649Z ##[endgroup] +2026-04-03T08:08:22.6010254Z KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm" +2026-04-03T08:08:22.6408570Z ##[group]Run reactivecircus/android-emulator-runner@v2 +2026-04-03T08:08:22.6408923Z with: +2026-04-03T08:08:22.6409111Z api-level: 30 +2026-04-03T08:08:22.6409309Z arch: x86_64 +2026-04-03T08:08:22.6409494Z profile: pixel_3a +2026-04-03T08:08:22.6409887Z script: ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest +2026-04-03T08:08:22.6410370Z working-directory: android-app +2026-04-03T08:08:22.6410636Z target: default +2026-04-03T08:08:22.6410826Z cores: 2 +2026-04-03T08:08:22.6411002Z avd-name: test +2026-04-03T08:08:22.6411202Z force-avd-creation: true +2026-04-03T08:08:22.6411441Z emulator-boot-timeout: 600 +2026-04-03T08:08:22.6411675Z emulator-port: 5554 +2026-04-03T08:08:22.6412387Z emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim +2026-04-03T08:08:22.6412874Z disable-animations: true +2026-04-03T08:08:22.6413116Z disable-spellchecker: false +2026-04-03T08:08:22.6413407Z disable-linux-hw-accel: auto +2026-04-03T08:08:22.6413660Z enable-hw-keyboard: false +2026-04-03T08:08:22.6413886Z channel: stable +2026-04-03T08:08:22.6414064Z env: +2026-04-03T08:08:22.6414357Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:22.6414841Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:08:22.6415221Z ##[endgroup] +2026-04-03T08:08:22.7038969Z ##[group]Configure emulator +2026-04-03T08:08:22.7042892Z API level: 30 +2026-04-03T08:08:22.7044334Z System image API level: 30 +2026-04-03T08:08:22.7050179Z target: default +2026-04-03T08:08:22.7051934Z CPU architecture: x86_64 +2026-04-03T08:08:22.7052591Z Hardware profile: pixel_3a +2026-04-03T08:08:22.7052981Z Cores: 2 +2026-04-03T08:08:22.7053308Z RAM size: +2026-04-03T08:08:22.7053633Z Heap size: +2026-04-03T08:08:22.7053950Z SD card path or size: +2026-04-03T08:08:22.7054312Z Disk size: +2026-04-03T08:08:22.7054601Z AVD name: test +2026-04-03T08:08:22.7054935Z force avd creation: true +2026-04-03T08:08:22.7055348Z Emulator boot timeout: 600 +2026-04-03T08:08:22.7055742Z emulator port: 5554 +2026-04-03T08:08:22.7056473Z emulator options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim +2026-04-03T08:08:22.7057264Z disable animations: true +2026-04-03T08:08:22.7057850Z disable spellchecker: false +2026-04-03T08:08:22.7058322Z disable Linux hardware acceleration: false +2026-04-03T08:08:22.7058834Z enable hardware keyboard: false +2026-04-03T08:08:22.7060315Z custom working directory: android-app +2026-04-03T08:08:22.7066547Z Channel: 0 (stable) +2026-04-03T08:08:22.7067013Z Script: +2026-04-03T08:08:22.7067692Z ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest +2026-04-03T08:08:22.7068391Z Pre emulator launch script: +2026-04-03T08:08:22.7069073Z ##[endgroup] +2026-04-03T08:08:22.7069434Z ##[group]Install Android SDK +2026-04-03T08:08:22.7137600Z [command]/usr/bin/sh -c \yes | sdkmanager --licenses > /dev/null +2026-04-03T08:08:29.1209697Z Installing latest build tools, platform tools, and platform. +2026-04-03T08:08:29.1230157Z [command]/usr/bin/sh -c \sdkmanager --install 'build-tools;36.0.0' platform-tools 'platforms;android-30'> /dev/null +2026-04-03T08:08:36.2526002Z Installing latest emulator. +2026-04-03T08:08:36.2544918Z [command]/usr/bin/sh -c \sdkmanager --install emulator --channel=0 > /dev/null +2026-04-03T08:08:45.9922633Z Installing system images. +2026-04-03T08:08:45.9941832Z [command]/usr/bin/sh -c \sdkmanager --install 'system-images;android-30;default;x86_64' --channel=0 > /dev/null +2026-04-03T08:09:12.1524411Z ##[endgroup] +2026-04-03T08:09:12.1529603Z ##[group]Create AVD +2026-04-03T08:09:12.1534596Z Creating AVD. +2026-04-03T08:09:12.1574743Z [command]/usr/bin/sh -c \echo no | avdmanager create avd --force -n test --package 'system-images;android-30;default;x86_64' --device 'pixel_3a' +2026-04-03T08:09:13.2827980Z Loading local repository... +2026-04-03T08:09:13.2834498Z [========= ] 25% Loading local repository... +2026-04-03T08:09:13.2835423Z [========= ] 25% Fetch remote repository... +2026-04-03T08:09:13.2836176Z [=======================================] 100% Fetch remote repository... +2026-04-03T08:09:13.3436020Z Auto-selecting single ABI x86_64 +2026-04-03T08:09:13.9695050Z [command]/usr/bin/sh -c \printf 'hw.cpu.ncore=2\n' >> /home/runner/.android/avd/test.avd/config.ini +2026-04-03T08:09:13.9726736Z ##[endgroup] +2026-04-03T08:09:13.9729111Z ##[group]Launch Emulator +2026-04-03T08:09:13.9730778Z Starting emulator. +2026-04-03T08:09:13.9754309Z [command]/usr/bin/sh -c \/usr/local/lib/android/sdk/emulator/emulator -port 5554 -avd test -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim & +2026-04-03T08:09:13.9903562Z INFO | Android emulator version 36.5.10.0 (build_id 15081367) (CL:N/A) +2026-04-03T08:09:13.9905761Z INFO | Graphics backend: gfxstream +2026-04-03T08:09:13.9908928Z INFO | Found systemPath /usr/local/lib/android/sdk/system-images/android-30/default/x86_64/ +2026-04-03T08:09:14.3691509Z WARNING | Please update the emulator to one that supports the feature(s): Vulkan +2026-04-03T08:09:14.3692589Z INFO | Increasing RAM size to 2048MB +2026-04-03T08:09:14.3693144Z ############################################################################## +2026-04-03T08:09:14.3693888Z ## WARNING - ACTION REQUIRED ## +2026-04-03T08:09:14.3694813Z ## Consider using the '-metrics-collection' flag to help improve the ## +2026-04-03T08:09:14.3695674Z ## emulator by sending anonymized usage data. Or use the '-no-metrics' ## +2026-04-03T08:09:14.3696602Z ## flag to bypass this warning and turn off the metrics collection. ## +2026-04-03T08:09:14.3697561Z ## In a future release this warning will turn into a one-time blocking ## +2026-04-03T08:09:14.3698565Z ## prompt to ask for explicit user input regarding metrics collection. ## +2026-04-03T08:09:14.3699325Z ## ## +2026-04-03T08:09:14.3703346Z ## Please see '-help-metrics-collection' for more details. You can use ## +2026-04-03T08:09:14.3704393Z ## '-metrics-to-file' or '-metrics-to-console' flags to see what type of ## +2026-04-03T08:09:14.3705263Z ## data is being collected by emulator as part of usage statistics. ## +2026-04-03T08:09:14.3706691Z ############################################################################## +2026-04-03T08:09:14.3707315Z INFO | Guest GLES Driver: Auto (ext controls) +2026-04-03T08:09:14.3708127Z INFO | emuglConfig_init: vulkan_mode_selected:swiftshader gles_mode_selected:swiftshader +2026-04-03T08:09:14.3709046Z INFO | Checking system compatibility: +2026-04-03T08:09:14.3709592Z INFO | Checking: hasSufficientDiskSpace +2026-04-03T08:09:14.3710256Z INFO | Ok: Disk space requirements to run avd: `test` are met +2026-04-03T08:09:14.3710927Z INFO | Checking: hasSufficientHwGpu +2026-04-03T08:09:14.3711552Z INFO | Ok: Hardware GPU compatibility checks are not required +2026-04-03T08:09:14.3712502Z INFO | Checking: hasSufficientSystem +2026-04-03T08:09:14.3713274Z INFO | Warning: AVD 'test' will run more smoothly with 4 CPU cores (currently using 2) +2026-04-03T08:09:14.3714237Z USER_WARNING | AVD 'test' will run more smoothly with 4 CPU cores (currently using 2). +2026-04-03T08:09:14.3715100Z WARNING | FeatureControl is requesting a non existing feature. +2026-04-03T08:09:14.3715793Z ERROR | Unable to connect to adb daemon on port: 5037 +2026-04-03T08:09:14.3716750Z INFO | Storing crashdata in: /tmp/android-runner/emu-crash-36.5.10.db, detection is enabled for process: 2490 +2026-04-03T08:09:14.3717654Z INFO | Initializing gfxstream backend +2026-04-03T08:09:14.3718209Z INFO | android_startOpenglesRenderer: gpu info +2026-04-03T08:09:14.3719058Z INFO | +2026-04-03T08:09:14.3719582Z INFO | initIcdPaths: ICD set to 'swiftshader', using Swiftshader ICD +2026-04-03T08:09:14.3720671Z INFO | Setting ICD filenames for the loader = /usr/local/lib/android/sdk/emulator/lib64/vulkan/vk_swiftshader_icd.json +2026-04-03T08:09:14.3722311Z INFO | SharedLibrary::open for [/usr/local/lib/android/sdk/emulator/qemu/linux-x86_64/lib64/vulkan/libvulkan.so] +2026-04-03T08:09:14.3723695Z INFO | SharedLibrary::open for [/usr/local/lib/android/sdk/emulator/qemu/linux-x86_64/lib64/vulkan/libvulkan.so.1] +2026-04-03T08:09:14.3724912Z INFO | SharedLibrary::open for [/usr/local/lib/android/sdk/emulator/lib64/vulkan/libvulkan.so] +2026-04-03T08:09:14.3725925Z INFO | Added library: /usr/local/lib/android/sdk/emulator/lib64/vulkan/libvulkan.so +2026-04-03T08:09:14.3726851Z INFO | Selecting Vulkan device: SwiftShader Device (Subzero), Version: 1.3.0 +2026-04-03T08:09:14.3727557Z INFO | SharedLibrary::open for [libX11] +2026-04-03T08:09:14.3728121Z WARNING | Could not open libX11.so, try libX11.so.6 +2026-04-03T08:09:14.3728706Z INFO | SharedLibrary::open for [libX11.so.6] +2026-04-03T08:09:14.3729283Z INFO | SharedLibrary::open for [libX11-xcb] +2026-04-03T08:09:14.3729906Z WARNING | Could not open libX11-xcb.so, try libX11-xcb.so.1 +2026-04-03T08:09:14.3730552Z INFO | SharedLibrary::open for [libX11-xcb.so.1] +2026-04-03T08:09:14.3731212Z INFO | Initializing VkEmulation features: +2026-04-03T08:09:14.3731713Z INFO | glInteropSupported: false +2026-04-03T08:09:14.3732577Z INFO | useDeferredCommands: true +2026-04-03T08:09:14.3733136Z INFO | createResourceWithRequirements: true +2026-04-03T08:09:14.3733699Z INFO | useVulkanComposition: false +2026-04-03T08:09:14.3734232Z INFO | useVulkanNativeSwapchain: false +2026-04-03T08:09:14.3734791Z INFO | enable guestRenderDoc: false +2026-04-03T08:09:14.3735304Z INFO | ASTC LDR emulation mode: Gpu +2026-04-03T08:09:14.3735813Z INFO | enable ETC2 emulation: true +2026-04-03T08:09:14.3736319Z INFO | enable Ycbcr emulation: false +2026-04-03T08:09:14.3736804Z INFO | guestVulkanOnly: false +2026-04-03T08:09:14.3737344Z INFO | useDedicatedAllocations: false +2026-04-03T08:09:14.3737880Z INFO | guestVulkanMaxApiVersion: 1.3.0 +2026-04-03T08:09:14.3738478Z INFO | Graphics Adapter Vendor Google (Google Inc.) +2026-04-03T08:09:14.3739489Z INFO | Graphics Adapter Android Emulator OpenGL ES Translator (Google SwiftShader) +2026-04-03T08:09:14.3740385Z INFO | Graphics API Version OpenGL ES 3.0 (OpenGL ES 3.0 SwiftShader 4.0.0.1) +2026-04-03T08:09:14.3983946Z INFO | Graphics API Extensions GL_OES_EGL_WARNING: cannnot unmap ptr 0x7fb7a7a01000 as it is in the protected range from 0x7fb727a00000 to 0x7fb7a7c00000 +2026-04-03T08:09:14.4156529Z sync GL_OES_EGL_image GL_OES_EGL_image_external GL_OES_depth24 GL_OES_depth32 GL_OES_element_index_uint GL_OES_texture_float GL_OES_texture_float_linear GL_OES_compressed_paletted_texture GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth_texture GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_packed_depth_stencil GL_OES_vertex_half_float GL_OES_standard_derivatives GL_OES_texture_npot GL_OES_rgb8_rgba8 GL_EXT_color_buffer_float GL_EXT_color_buffer_half_float GL_EXT_texture_format_BGRA8888 GL_APPLE_texture_format_BGRA8888 +2026-04-03T08:09:14.4160307Z INFO | Graphics Device Extensions N/A +2026-04-03T08:09:14.4160926Z INFO | Disabling sparse binding feature support +2026-04-03T08:09:14.4161580Z INFO | Userspace boot properties: +2026-04-03T08:09:14.4162335Z INFO | android.bootanim=0 +2026-04-03T08:09:14.4162815Z INFO | android.qemud=1 +2026-04-03T08:09:14.4163523Z INFO | androidboot.boot_devices=pci0000:00/0000:00:03.0 pci0000:00/0000:00:06.0 +2026-04-03T08:09:14.4164295Z INFO | androidboot.hardware=ranchu +2026-04-03T08:09:14.4165119Z INFO | androidboot.hardware.vulkan=ranchu +2026-04-03T08:09:14.4165729Z INFO | androidboot.serialno=EMULATOR36X5X10X0 +2026-04-03T08:09:14.4166677Z INFO | androidboot.vbmeta.digest=aa516f360594014de86105695b13f1df5e00b81539011aafdd58e68e859134a1 +2026-04-03T08:09:14.4167615Z INFO | androidboot.vbmeta.hash_alg=sha256 +2026-04-03T08:09:14.4168174Z INFO | androidboot.vbmeta.size=6144 +2026-04-03T08:09:14.4168660Z INFO | qemu=1 +2026-04-03T08:09:14.4169042Z INFO | qemu.avd_name=test +2026-04-03T08:09:14.4169525Z INFO | qemu.camera_hq_edge_processing=0 +2026-04-03T08:09:14.4170053Z INFO | qemu.camera_protocol_ver=1 +2026-04-03T08:09:14.4170565Z INFO | qemu.dalvik.vm.heapsize=512m +2026-04-03T08:09:14.4171059Z INFO | qemu.encrypt=1 +2026-04-03T08:09:14.4171474Z INFO | qemu.gles=1 +2026-04-03T08:09:14.4171891Z INFO | qemu.gltransport=pipe +2026-04-03T08:09:14.4173069Z INFO | qemu.gltransport.drawFlushInterval=800 +2026-04-03T08:09:14.4173645Z INFO | qemu.hwcodec.avcdec=2 +2026-04-03T08:09:14.4174111Z INFO | qemu.hwcodec.hevcdec=2 +2026-04-03T08:09:14.4174607Z INFO | qemu.hwcodec.vpxdec=2 +2026-04-03T08:09:14.4175102Z INFO | qemu.opengles.version=196608 +2026-04-03T08:09:14.4175711Z INFO | qemu.settings.system.screen_off_timeout=2147483647 +2026-04-03T08:09:14.4176316Z INFO | qemu.skin=1080x2220 +2026-04-03T08:09:14.4176771Z INFO | qemu.uirenderer=skiagl +2026-04-03T08:09:14.4177234Z INFO | qemu.vsync=60 +2026-04-03T08:09:14.4177647Z INFO | qemu.wifi=1 +2026-04-03T08:09:14.4178125Z INFO | Monitoring duration of emulator setup. +2026-04-03T08:09:14.4372249Z INFO | Setting display: 0 configuration to: 1080x2220, dpi: 440x440 +2026-04-03T08:09:14.4373834Z INFO | setDisplayActiveConfig: id:0, 1080x2220 +2026-04-03T08:09:14.4376085Z INFO | emulatorSetupEnvironment: Setting up screen background view and display layout at env:1080x2220, lcd:1080x2220 +2026-04-03T08:09:14.4380820Z INFO | emulatorSetupEnvironment: Environment scene is not required +2026-04-03T08:09:14.4444543Z USER_INFO | Emulator is performing a full startup. This may take upto two minutes, or more. +2026-04-03T08:09:14.4460387Z WARNING | Failed to process .ini file /home/runner/.android/emu-update-last-check.ini for reading. +2026-04-03T08:09:14.5783685Z WARNING | Failed to process .ini file /home/runner/.android/emu-update-last-check.ini for reading. +2026-04-03T08:09:23.9828161Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:23.9860858Z * daemon not running; starting now at tcp:5037 +2026-04-03T08:09:23.9898594Z * daemon started successfully +2026-04-03T08:09:23.9913983Z adb: device offline +2026-04-03T08:09:23.9923030Z The process '/usr/local/lib/android/sdk/platform-tools/adb' failed with exit code 1 +2026-04-03T08:09:26.0018309Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:26.0050950Z adb: device offline +2026-04-03T08:09:26.0062425Z The process '/usr/local/lib/android/sdk/platform-tools/adb' failed with exit code 1 +2026-04-03T08:09:28.0117248Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:28.0173459Z adb: device offline +2026-04-03T08:09:28.0180686Z The process '/usr/local/lib/android/sdk/platform-tools/adb' failed with exit code 1 +2026-04-03T08:09:30.0259217Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:30.0359815Z adb: device offline +2026-04-03T08:09:30.0377799Z The process '/usr/local/lib/android/sdk/platform-tools/adb' failed with exit code 1 +2026-04-03T08:09:32.0457007Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:32.0723362Z +2026-04-03T08:09:34.0778440Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:34.1174154Z +2026-04-03T08:09:36.1254272Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:36.1648587Z +2026-04-03T08:09:38.1744176Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:38.2159587Z +2026-04-03T08:09:40.2254622Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:40.2701351Z +2026-04-03T08:09:42.2792635Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:42.3626333Z +2026-04-03T08:09:44.3731135Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:44.6692699Z +2026-04-03T08:09:46.6919025Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:46.9800552Z +2026-04-03T08:09:48.9948553Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-03T08:09:49.1016052Z 1 +2026-04-03T08:09:49.1035055Z Emulator booted. +2026-04-03T08:09:49.1108195Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell input keyevent 82 +2026-04-03T08:09:50.2457046Z INFO | Boot completed in 36228 ms +2026-04-03T08:09:50.2459029Z INFO | Increasing screen off timeout, logcat buffer size to 2M. +2026-04-03T08:09:54.5493257Z Disabling animations. +2026-04-03T08:09:54.5560543Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell settings put global window_animation_scale 0.0 +2026-04-03T08:09:55.6249064Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell settings put global transition_animation_scale 0.0 +2026-04-03T08:09:56.1918423Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell settings put global animator_duration_scale 0.0 +2026-04-03T08:09:56.4993048Z ##[endgroup] +2026-04-03T08:09:56.5073941Z [command]/usr/bin/sh -c ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest +2026-04-03T08:09:58.1225907Z +2026-04-03T08:09:58.1228380Z Welcome to Gradle 8.7! +2026-04-03T08:09:58.1230483Z +2026-04-03T08:09:58.1233161Z Here are the highlights of this release: +2026-04-03T08:09:58.1235434Z - Compiling and testing with Java 22 +2026-04-03T08:09:58.1237658Z - Cacheable Groovy script compilation +2026-04-03T08:09:58.1245310Z - New methods in lazy collection properties +2026-04-03T08:09:58.1265854Z +2026-04-03T08:09:58.1399885Z For more details see https://docs.gradle.org/8.7/release-notes.html +2026-04-03T08:09:58.1402513Z +2026-04-03T08:09:58.7237217Z Starting a Gradle Daemon (subsequent builds will be faster) +2026-04-03T08:10:23.8229278Z > Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED +2026-04-03T08:10:23.8257417Z > Task :app:preBuild UP-TO-DATE +2026-04-03T08:10:23.8259164Z > Task :app:preDebugBuild UP-TO-DATE +2026-04-03T08:10:26.0200665Z > Task :app:generateDebugResValues +2026-04-03T08:10:26.0205615Z > Task :app:dataBindingMergeDependencyArtifactsDebug +2026-04-03T08:10:26.0207946Z > Task :app:generateDebugResources +2026-04-03T08:10:26.0210471Z > Task :app:injectCrashlyticsMappingFileIdDebug +2026-04-03T08:10:26.1203532Z > Task :app:processDebugGoogleServices +2026-04-03T08:10:29.3225696Z > Task :app:packageDebugResources +2026-04-03T08:10:31.1184957Z > Task :app:parseDebugLocalResources +2026-04-03T08:10:31.3223319Z > Task :app:checkDebugAarMetadata +2026-04-03T08:10:31.3227165Z > Task :app:mapDebugSourceSetPaths +2026-04-03T08:10:31.3230639Z > Task :app:createDebugCompatibleScreenManifests +2026-04-03T08:10:31.3234879Z > Task :app:extractDeepLinksDebug +2026-04-03T08:10:31.5196703Z > Task :app:mergeDebugResources +2026-04-03T08:10:32.0188849Z > Task :app:processDebugMainManifest +2026-04-03T08:10:32.7188815Z > Task :app:dataBindingGenBaseClassesDebug +2026-04-03T08:10:32.8187021Z > Task :app:processDebugManifest +2026-04-03T08:10:32.9203186Z > Task :app:processDebugManifestForPackage +2026-04-03T08:10:32.9204850Z > Task :app:javaPreCompileDebug +2026-04-03T08:10:33.5235741Z > Task :app:preDebugAndroidTestBuild SKIPPED +2026-04-03T08:10:34.0186804Z > Task :app:dataBindingMergeDependencyArtifactsDebugAndroidTest +2026-04-03T08:10:34.0189399Z > Task :app:generateDebugAndroidTestResValues +2026-04-03T08:10:34.0191527Z > Task :app:generateDebugAndroidTestResources +2026-04-03T08:10:34.1197212Z > Task :app:mergeDebugAndroidTestResources +2026-04-03T08:10:34.2190403Z > Task :app:dataBindingGenBaseClassesDebugAndroidTest +2026-04-03T08:10:34.2192956Z > Task :app:checkDebugAndroidTestAarMetadata +2026-04-03T08:10:34.2195023Z > Task :app:mapDebugAndroidTestSourceSetPaths +2026-04-03T08:10:34.3207484Z > Task :app:processDebugAndroidTestManifest +2026-04-03T08:10:34.7215482Z > Task :app:processDebugAndroidTestResources +2026-04-03T08:10:34.7217747Z > Task :app:javaPreCompileDebugAndroidTest +2026-04-03T08:10:34.8188465Z > Task :app:mergeDebugShaders +2026-04-03T08:10:34.8195093Z > Task :app:compileDebugShaders NO-SOURCE +2026-04-03T08:10:34.8198800Z > Task :app:generateDebugAssets UP-TO-DATE +2026-04-03T08:10:35.0188245Z > Task :app:mergeDebugAssets +2026-04-03T08:10:35.0190649Z > Task :app:processDebugResources +2026-04-03T08:10:35.1194579Z > Task :app:compressDebugAssets +2026-04-03T08:10:38.8231232Z > Task :app:checkDebugDuplicateClasses +2026-04-03T08:10:38.8231941Z > Task :app:desugarDebugFileDependencies +2026-04-03T08:10:52.3184497Z > Task :app:kspDebugKotlin +2026-04-03T08:10:59.4185172Z > Task :app:mergeExtDexDebug +2026-04-03T08:10:59.4187293Z > Task :app:mergeLibDexDebug +2026-04-03T08:10:59.5184005Z > Task :app:mergeDebugJniLibFolders +2026-04-03T08:11:00.5221899Z > Task :app:mergeDebugNativeLibs +2026-04-03T08:11:01.0185207Z +2026-04-03T08:11:01.0202894Z > Task :app:stripDebugDebugSymbols +2026-04-03T08:11:01.0221350Z Unable to strip the following libraries, packaging them as they are: libmaplibre.so. +2026-04-03T08:11:03.3212754Z +2026-04-03T08:11:03.3213294Z > Task :app:validateSigningDebug +2026-04-03T08:11:03.3213940Z > Task :app:writeDebugAppMetadata +2026-04-03T08:11:03.3214548Z > Task :app:writeDebugSigningConfigVersions +2026-04-03T08:11:03.3215186Z > Task :app:mergeDebugAndroidTestShaders +2026-04-03T08:11:03.3215864Z > Task :app:compileDebugAndroidTestShaders NO-SOURCE +2026-04-03T08:11:03.3217151Z > Task :app:generateDebugAndroidTestAssets UP-TO-DATE +2026-04-03T08:11:03.4185748Z > Task :app:mergeDebugAndroidTestAssets +2026-04-03T08:11:03.4187874Z > Task :app:compressDebugAndroidTestAssets +2026-04-03T08:11:03.6184913Z > Task :app:checkDebugAndroidTestDuplicateClasses +2026-04-03T08:11:03.6187150Z > Task :app:desugarDebugAndroidTestFileDependencies +2026-04-03T08:11:05.0183900Z > Task :app:mergeExtDexDebugAndroidTest +2026-04-03T08:11:05.1183986Z > Task :app:mergeLibDexDebugAndroidTest +2026-04-03T08:11:05.1184699Z > Task :app:mergeDebugAndroidTestJniLibFolders +2026-04-03T08:11:05.2218804Z > Task :app:mergeDebugAndroidTestNativeLibs NO-SOURCE +2026-04-03T08:11:05.2224571Z > Task :app:stripDebugAndroidTestDebugSymbols NO-SOURCE +2026-04-03T08:11:05.2225219Z > Task :app:validateSigningDebugAndroidTest +2026-04-03T08:11:05.2226513Z > Task :app:writeDebugAndroidTestSigningConfigVersions +2026-04-03T08:11:22.3186062Z > Task :app:compileDebugKotlin +2026-04-03T08:11:30.4185762Z w: file:///home/runner/work/nav/nav/android-app/app/src/main/kotlin/org/terst/nav/AnchorAlarmManager.kt:76:26 '@Deprecated(...) fun vibrate(p0: LongArray!, p1: Int): Unit' is deprecated. Deprecated in Java. +2026-04-03T08:11:30.4189334Z w: file:///home/runner/work/nav/nav/android-app/app/src/main/kotlin/org/terst/nav/nmea/NmeaParser.kt:176:39 Condition is always 'true'. +2026-04-03T08:11:33.8256447Z +2026-04-03T08:11:33.8256985Z > Task :app:compileDebugJavaWithJavac +2026-04-03T08:11:33.9198074Z > Task :app:processDebugJavaRes +2026-04-03T08:11:33.9208855Z > Task :app:bundleDebugClassesToCompileJar +2026-04-03T08:11:36.2263036Z > Task :app:mergeDebugJavaResource +2026-04-03T08:11:39.6193980Z > Task :app:kspDebugAndroidTestKotlin +2026-04-03T08:11:42.2208149Z > Task :app:dexBuilderDebug +2026-04-03T08:11:43.8195096Z > Task :app:mergeProjectDexDebug +2026-04-03T08:11:44.0197386Z > Task :app:compileDebugAndroidTestKotlin +2026-04-03T08:11:44.0198150Z > Task :app:compileDebugAndroidTestJavaWithJavac NO-SOURCE +2026-04-03T08:11:44.0230415Z > Task :app:processDebugAndroidTestJavaRes +2026-04-03T08:11:44.2194813Z > Task :app:mergeDebugAndroidTestJavaResource +2026-04-03T08:11:44.4200975Z > Task :app:dexBuilderDebugAndroidTest +2026-04-03T08:11:44.4203596Z > Task :app:mergeProjectDexDebugAndroidTest +2026-04-03T08:11:44.7188223Z > Task :app:packageDebugAndroidTest +2026-04-03T08:11:44.8184002Z > Task :app:createDebugAndroidTestApkListingFileRedirect +2026-04-03T08:11:45.3252279Z > Task :app:packageDebug +2026-04-03T08:11:45.3258182Z > Task :app:createDebugApkListingFileRedirect +2026-04-03T08:11:53.5185519Z [EmulatorConsole]: Failed to start Emulator console for 5554 +2026-04-03T08:12:03.3199486Z +2026-04-03T08:12:03.3201693Z > Task :app:connectedDebugAndroidTest +2026-04-03T08:12:03.3204251Z Starting 11 tests on emulator-5554 - 11 +2026-04-03T08:12:04.8747045Z INFO | Created VkInstance:0x55daeb051800 for application:'maplibre-native' engine:'maplibre-native'. +2026-04-03T08:12:04.9268521Z INFO | Created VkDevice:0x55dae02a0010 for application:'maplibre-native' instance:0x55daeb051800. ASTC emulation:on CPU decoding:off. +2026-04-03T08:12:09.4801700Z INFO | Destroyed VkDevice:0x55dae02a0010 +2026-04-03T08:12:09.4805321Z INFO | Destroyed VkInstance:0x55daeb051800 for application:'maplibre-native' engine:'maplibre-native'. +2026-04-03T08:12:09.8188182Z +2026-04-03T08:12:09.8192566Z org.terst.nav.MainActivitySmokeTest > fabRecordTrack_isDisplayedWithRecordDescription[emulator-5554 - 11] FAILED  +2026-04-03T08:12:09.8195102Z +2026-04-03T08:12:22.4184060Z Tests on emulator-5554 - 11 failed: There was 1 failure(s). +2026-04-03T08:12:22.9209094Z Test run failed to complete. Instrumentation run failed due to Process crashed. +2026-04-03T08:12:23.3185241Z +2026-04-03T08:12:23.3194698Z > Task :app:connectedDebugAndroidTest FAILED +2026-04-03T08:12:23.3236748Z +2026-04-03T08:12:23.3238493Z FAILURE: Build failed with an exception. +2026-04-03T08:12:23.3239751Z +2026-04-03T08:12:23.3241245Z * What went wrong: +2026-04-03T08:12:23.3261007Z Execution failed for task ':app:connectedDebugAndroidTest'. +2026-04-03T08:12:23.3262423Z > There were failing tests. See the report at: file:///home/runner/work/nav/nav/android-app/app/build/reports/androidTests/connected/debug/index.html +2026-04-03T08:12:23.3263380Z +2026-04-03T08:12:23.3263497Z * Try: +2026-04-03T08:12:23.3263895Z > Run with --stacktrace option to get the stack trace. +2026-04-03T08:12:23.3264511Z > Run with --info or --debug option to get more log output. +2026-04-03T08:12:23.3265055Z > Run with --scan to get full insights. +2026-04-03T08:12:23.3265573Z > Get more help at https://help.gradle.org. +2026-04-03T08:12:23.3265910Z +2026-04-03T08:12:23.3266048Z BUILD FAILED in 2m 26s +2026-04-03T08:12:23.3266410Z 70 actionable tasks: 70 executed +2026-04-03T08:12:23.8534240Z ##[error]The process '/usr/bin/sh' failed with exit code 1 +2026-04-03T08:12:23.8548270Z ##[group]Terminate Emulator +2026-04-03T08:12:23.8552983Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 emu kill +2026-04-03T08:12:23.8594653Z OK: killing emulator, bye bye +2026-04-03T08:12:23.8598037Z OK +2026-04-03T08:12:23.8600498Z INFO | Wait for emulator (pid 2490) 20 seconds to shutdown gracefully before kill;you can set environment variable ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL(in seconds) to change the default value (20 seconds) +2026-04-03T08:12:23.8602282Z +2026-04-03T08:12:23.8613339Z ##[endgroup] +2026-04-03T08:12:23.8701824Z USER_INFO | Snapshots have been disabled by the user, save request is ignored. +2026-04-03T08:12:23.8713228Z INFO | Saving snapshot 'default_boot' using 1 ms +2026-04-03T08:12:24.5615482Z ERROR | stop: Not implemented +2026-04-03T08:12:24.5616573Z WARNING | Emulator client has not yet been configured.. Call configure me first! +2026-04-03T08:12:25.3971910Z ##[group]Run actions/upload-artifact@v4 +2026-04-03T08:12:25.3972723Z with: +2026-04-03T08:12:25.3972923Z name: smoke-test-results +2026-04-03T08:12:25.3973236Z path: android-app/app/build/outputs/androidTest-results/ +2026-04-03T08:12:25.3973586Z if-no-files-found: warn +2026-04-03T08:12:25.3973809Z compression-level: 6 +2026-04-03T08:12:25.3974020Z overwrite: false +2026-04-03T08:12:25.3974226Z include-hidden-files: false +2026-04-03T08:12:25.3974451Z env: +2026-04-03T08:12:25.3974740Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:12:25.3975224Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:12:25.3975624Z ANDROID_AVD_HOME: /home/runner/.android/avd +2026-04-03T08:12:25.3975883Z ##[endgroup] +2026-04-03T08:12:25.6967709Z With the provided path, there will be 9 files uploaded +2026-04-03T08:12:25.6974202Z Artifact name is valid! +2026-04-03T08:12:25.6975626Z Root directory input is valid! +2026-04-03T08:12:25.8833063Z Beginning upload of artifact content to blob storage +2026-04-03T08:12:26.0983805Z Uploaded bytes 14789 +2026-04-03T08:12:26.1443012Z Finished uploading artifact content to blob storage! +2026-04-03T08:12:26.1446796Z SHA256 digest of uploaded artifact zip is df414343b746ca239f3059febcc2d18d565d51b92e7bb5c00fe40da9b135c586 +2026-04-03T08:12:26.1449278Z Finalizing artifact upload +2026-04-03T08:12:26.2500013Z Artifact smoke-test-results.zip successfully finalized. Artifact ID 6256801517 +2026-04-03T08:12:26.2501375Z Artifact smoke-test-results has been successfully uploaded! Final size is 14789 bytes. Artifact ID is 6256801517 +2026-04-03T08:12:26.2507644Z Artifact download URL: https://github.com/thepeterstone/nav/actions/runs/23939201588/artifacts/6256801517 +2026-04-03T08:12:26.2651096Z ##[group]Run PAYLOAD=$(jq -n \ +2026-04-03T08:12:26.2651427Z PAYLOAD=$(jq -n \ +2026-04-03T08:12:26.2651689Z  --arg action "completed" \ +2026-04-03T08:12:26.2652243Z  --arg conclusion "failure" \ +2026-04-03T08:12:26.2652620Z  --arg name "Android CI/CD / smoke-test" \ +2026-04-03T08:12:26.2652955Z  --arg repo "thepeterstone/nav" \ +2026-04-03T08:12:26.2653320Z  --arg sha "5d358cd570075d36a61f9a37bb80c64f8a0a7e2a" \ +2026-04-03T08:12:26.2653868Z  --arg run_id "23939201588" \ +2026-04-03T08:12:26.2654490Z  '{action: $action, workflow_run: {conclusion: $conclusion, name: $name, head_sha: $sha, id: ($run_id | tonumber)}, repository: {full_name: $repo}}') +2026-04-03T08:12:26.2655436Z SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$CLAUDOMATOR_WEBHOOK_SECRET" | awk '{print $2}') +2026-04-03T08:12:26.2656023Z curl -sf -X POST \ +2026-04-03T08:12:26.2656608Z  -H "Content-Type: application/json" \ +2026-04-03T08:12:26.2657041Z  -H "X-GitHub-Event: workflow_run" \ +2026-04-03T08:12:26.2657492Z  -H "X-Hub-Signature-256: sha256=${SIG}" \ +2026-04-03T08:12:26.2657958Z  "${CLAUDOMATOR_WEBHOOK_URL}/api/webhooks/github" \ +2026-04-03T08:12:26.2658372Z  -d "$PAYLOAD" +2026-04-03T08:12:26.2927996Z shell: /usr/bin/bash -e {0} +2026-04-03T08:12:26.2928673Z env: +2026-04-03T08:12:26.2929286Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:12:26.2930175Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-03T08:12:26.2943967Z ANDROID_AVD_HOME: /home/runner/.android/avd +2026-04-03T08:12:26.2945175Z CLAUDOMATOR_WEBHOOK_URL: *** +2026-04-03T08:12:26.2945622Z CLAUDOMATOR_WEBHOOK_SECRET: +2026-04-03T08:12:26.2946006Z ##[endgroup] +2026-04-03T08:12:26.5221856Z {"task_id":"7a9a3d94-b13a-4514-9373-43c445b964bf"} +2026-04-03T08:12:26.5334366Z Post job cleanup. +2026-04-03T08:12:26.7495521Z Post job cleanup. +2026-04-03T08:12:26.8592321Z [command]/usr/bin/git version +2026-04-03T08:12:26.8635683Z git version 2.53.0 +2026-04-03T08:12:26.8688082Z Temporarily overriding HOME='/home/runner/work/_temp/d52085d1-ea0e-48f0-b36e-9b3e2a665795' before making global git config changes +2026-04-03T08:12:26.8690721Z Adding repository directory to the temporary git global config as a safe directory +2026-04-03T08:12:26.8695659Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/nav/nav +2026-04-03T08:12:26.8745033Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand +2026-04-03T08:12:26.8781125Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :" +2026-04-03T08:12:26.9064308Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader +2026-04-03T08:12:26.9088420Z http.https://github.com/.extraheader +2026-04-03T08:12:26.9105199Z [command]/usr/bin/git config --local --unset-all http.https://github.com/.extraheader +2026-04-03T08:12:26.9141431Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || :" +2026-04-03T08:12:26.9374709Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir: +2026-04-03T08:12:26.9408261Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url +2026-04-03T08:12:26.9768014Z Cleaning up orphan processes +2026-04-03T08:12:27.0122787Z Terminate orphan process: pid (2554) (adb) +2026-04-03T08:12:27.0303047Z Terminate orphan process: pid (2713) (java) +2026-04-03T08:12:27.0463029Z Terminate orphan process: pid (2814) (java) +2026-04-03T08:12:27.0498638Z ##[warning]Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: actions/checkout@v4, actions/download-artifact@v4, actions/setup-java@v4, actions/upload-artifact@v4. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Node.js 20 will be removed from the runner on September 16th, 2026. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ diff --git a/new_build.log b/new_build.log new file mode 100644 index 0000000..9c46f5f --- /dev/null +++ b/new_build.log @@ -0,0 +1,606 @@ +2026-04-04T01:52:24.5913552Z Current runner version: '2.333.1' +2026-04-04T01:52:24.5979073Z ##[group]Runner Image Provisioner +2026-04-04T01:52:24.5980530Z Hosted Compute Agent +2026-04-04T01:52:24.5981425Z Version: 20260213.493 +2026-04-04T01:52:24.5982509Z Commit: 5c115507f6dd24b8de37d8bbe0bb4509d0cc0fa3 +2026-04-04T01:52:24.5983685Z Build Date: 2026-02-13T00:28:41Z +2026-04-04T01:52:24.6005329Z Worker ID: {49c40993-9840-4580-a12a-3c457a218dc9} +2026-04-04T01:52:24.6006680Z Azure Region: westcentralus +2026-04-04T01:52:24.6007671Z ##[endgroup] +2026-04-04T01:52:24.6010087Z ##[group]Operating System +2026-04-04T01:52:24.6011131Z Ubuntu +2026-04-04T01:52:24.6011974Z 24.04.4 +2026-04-04T01:52:24.6012703Z LTS +2026-04-04T01:52:24.6013619Z ##[endgroup] +2026-04-04T01:52:24.6025026Z ##[group]Runner Image +2026-04-04T01:52:24.6026035Z Image: ubuntu-24.04 +2026-04-04T01:52:24.6027080Z Version: 20260329.72.1 +2026-04-04T01:52:24.6029204Z Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20260329.72/images/ubuntu/Ubuntu2404-Readme.md +2026-04-04T01:52:24.6032146Z Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20260329.72 +2026-04-04T01:52:24.6033909Z ##[endgroup] +2026-04-04T01:52:24.6056565Z ##[group]GITHUB_TOKEN Permissions +2026-04-04T01:52:24.6060103Z Contents: read +2026-04-04T01:52:24.6061003Z Metadata: read +2026-04-04T01:52:24.6062065Z Packages: read +2026-04-04T01:52:24.6062900Z ##[endgroup] +2026-04-04T01:52:24.6066453Z Secret source: Actions +2026-04-04T01:52:24.6068224Z Prepare workflow directory +2026-04-04T01:52:24.6889188Z Prepare all required actions +2026-04-04T01:52:24.7010721Z Getting action download info +2026-04-04T01:52:25.1799395Z Download action repository 'actions/checkout@v4' (SHA:34e114876b0b11c390a56381ad16ebd13914f8d5) +2026-04-04T01:52:25.3305273Z Download action repository 'actions/setup-java@v4' (SHA:c1e323688fd81a25caa38c78aa6df2d33d3e20d9) +2026-04-04T01:52:27.1394807Z Download action repository 'actions/download-artifact@v4' (SHA:d3f86a106a0bac45b974a628896c90dbdf5c8093) +2026-04-04T01:52:28.0347011Z Download action repository 'reactivecircus/android-emulator-runner@v2' (SHA:e89f39f1abbbd05b1113a29cf4db69e7540cae5a) +2026-04-04T01:52:28.8610539Z Download action repository 'actions/upload-artifact@v4' (SHA:ea165f8d65b6e75b540449e92b4886f43607fa02) +2026-04-04T01:52:29.1051474Z Complete job name: smoke-test +2026-04-04T01:52:29.1760319Z ##[group]Run actions/checkout@v4 +2026-04-04T01:52:29.1761048Z with: +2026-04-04T01:52:29.1761465Z repository: thepeterstone/nav +2026-04-04T01:52:29.1762074Z token: *** +2026-04-04T01:52:29.1762349Z ssh-strict: true +2026-04-04T01:52:29.1762677Z ssh-user: git +2026-04-04T01:52:29.1762972Z persist-credentials: true +2026-04-04T01:52:29.1763274Z clean: true +2026-04-04T01:52:29.1763623Z sparse-checkout-cone-mode: true +2026-04-04T01:52:29.1763977Z fetch-depth: 1 +2026-04-04T01:52:29.1764228Z fetch-tags: false +2026-04-04T01:52:29.1764811Z show-progress: true +2026-04-04T01:52:29.1765105Z lfs: false +2026-04-04T01:52:29.1765353Z submodules: false +2026-04-04T01:52:29.1765754Z set-safe-directory: true +2026-04-04T01:52:29.1766389Z ##[endgroup] +2026-04-04T01:52:29.3020939Z Syncing repository: thepeterstone/nav +2026-04-04T01:52:29.3023431Z ##[group]Getting Git version info +2026-04-04T01:52:29.3024681Z Working directory is '/home/runner/work/nav/nav' +2026-04-04T01:52:29.3027658Z [command]/usr/bin/git version +2026-04-04T01:52:29.3056643Z git version 2.53.0 +2026-04-04T01:52:29.3074028Z ##[endgroup] +2026-04-04T01:52:29.3091164Z Temporarily overriding HOME='/home/runner/work/_temp/e3dcfdfa-386e-4032-a411-d67cac1495e3' before making global git config changes +2026-04-04T01:52:29.3094255Z Adding repository directory to the temporary git global config as a safe directory +2026-04-04T01:52:29.3113498Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/nav/nav +2026-04-04T01:52:29.3153938Z Deleting the contents of '/home/runner/work/nav/nav' +2026-04-04T01:52:29.3159227Z ##[group]Initializing the repository +2026-04-04T01:52:29.3165324Z [command]/usr/bin/git init /home/runner/work/nav/nav +2026-04-04T01:52:29.3259688Z hint: Using 'master' as the name for the initial branch. This default branch name +2026-04-04T01:52:29.3262874Z hint: will change to "main" in Git 3.0. To configure the initial branch name +2026-04-04T01:52:29.3264209Z hint: to use in all of your new repositories, which will suppress this warning, +2026-04-04T01:52:29.3265471Z hint: call: +2026-04-04T01:52:29.3265995Z hint: +2026-04-04T01:52:29.3266921Z hint: git config --global init.defaultBranch +2026-04-04T01:52:29.3267732Z hint: +2026-04-04T01:52:29.3268506Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and +2026-04-04T01:52:29.3269774Z hint: 'development'. The just-created branch can be renamed via this command: +2026-04-04T01:52:29.3270588Z hint: +2026-04-04T01:52:29.3271070Z hint: git branch -m +2026-04-04T01:52:29.3271721Z hint: +2026-04-04T01:52:29.3272451Z hint: Disable this message with "git config set advice.defaultBranchName false" +2026-04-04T01:52:29.3276092Z Initialized empty Git repository in /home/runner/work/nav/nav/.git/ +2026-04-04T01:52:29.3281853Z [command]/usr/bin/git remote add origin https://github.com/thepeterstone/nav +2026-04-04T01:52:29.3321884Z ##[endgroup] +2026-04-04T01:52:29.3324743Z ##[group]Disabling automatic garbage collection +2026-04-04T01:52:29.3327047Z [command]/usr/bin/git config --local gc.auto 0 +2026-04-04T01:52:29.3370540Z ##[endgroup] +2026-04-04T01:52:29.3371429Z ##[group]Setting up auth +2026-04-04T01:52:29.3372210Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand +2026-04-04T01:52:29.3407096Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :" +2026-04-04T01:52:29.3719301Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader +2026-04-04T01:52:29.3755463Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || :" +2026-04-04T01:52:29.4004724Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir: +2026-04-04T01:52:29.4042764Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url +2026-04-04T01:52:29.4294824Z [command]/usr/bin/git config --local http.https://github.com/.extraheader AUTHORIZATION: basic *** +2026-04-04T01:52:29.4335793Z ##[endgroup] +2026-04-04T01:52:29.4345439Z ##[group]Fetching the repository +2026-04-04T01:52:29.4347409Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +7d4e856193954a0eba8e68b3ca5aa8f6a2dbd175:refs/remotes/origin/main +2026-04-04T01:52:31.0246269Z From https://github.com/thepeterstone/nav +2026-04-04T01:52:31.0249364Z * [new ref] 7d4e856193954a0eba8e68b3ca5aa8f6a2dbd175 -> origin/main +2026-04-04T01:52:31.0297604Z ##[endgroup] +2026-04-04T01:52:31.0298492Z ##[group]Determining the checkout info +2026-04-04T01:52:31.0298983Z ##[endgroup] +2026-04-04T01:52:31.0300338Z [command]/usr/bin/git sparse-checkout disable +2026-04-04T01:52:31.0351552Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig +2026-04-04T01:52:31.0382488Z ##[group]Checking out the ref +2026-04-04T01:52:31.0397231Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main +2026-04-04T01:52:31.1421449Z Switched to a new branch 'main' +2026-04-04T01:52:31.1433929Z branch 'main' set up to track 'origin/main'. +2026-04-04T01:52:31.1450209Z ##[endgroup] +2026-04-04T01:52:31.1493257Z [command]/usr/bin/git log -1 --format=%H +2026-04-04T01:52:31.1524865Z 7d4e856193954a0eba8e68b3ca5aa8f6a2dbd175 +2026-04-04T01:52:31.1810974Z ##[group]Run actions/setup-java@v4 +2026-04-04T01:52:31.1811284Z with: +2026-04-04T01:52:31.1811471Z java-version: 17 +2026-04-04T01:52:31.1811677Z distribution: temurin +2026-04-04T01:52:31.1812092Z cache: gradle +2026-04-04T01:52:31.1812293Z java-package: jdk +2026-04-04T01:52:31.1812489Z check-latest: false +2026-04-04T01:52:31.1812688Z server-id: github +2026-04-04T01:52:31.1812885Z server-username: GITHUB_ACTOR +2026-04-04T01:52:31.1813126Z server-password: GITHUB_TOKEN +2026-04-04T01:52:31.1813365Z overwrite-settings: true +2026-04-04T01:52:31.1813593Z job-status: success +2026-04-04T01:52:31.1813924Z token: *** +2026-04-04T01:52:31.1814107Z ##[endgroup] +2026-04-04T01:52:31.4086560Z ##[group]Installed distributions +2026-04-04T01:52:31.4175951Z Resolved Java 17.0.18+8 from tool-cache +2026-04-04T01:52:31.4186455Z Setting Java 17.0.18+8 as the default +2026-04-04T01:52:31.4202118Z Creating toolchains.xml for JDK version 17 from temurin +2026-04-04T01:52:31.4347079Z Writing to /home/runner/.m2/toolchains.xml +2026-04-04T01:52:31.4349367Z +2026-04-04T01:52:31.4359664Z Java configuration: +2026-04-04T01:52:31.4361714Z Distribution: temurin +2026-04-04T01:52:31.4363678Z Version: 17.0.18+8 +2026-04-04T01:52:31.4365561Z Path: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:31.4374863Z +2026-04-04T01:52:31.4386253Z ##[endgroup] +2026-04-04T01:52:31.4413722Z Creating settings.xml with server-id: github +2026-04-04T01:52:31.4414501Z Writing to /home/runner/.m2/settings.xml +2026-04-04T01:52:31.9916386Z Cache hit for: setup-java-Linux-x64-gradle-d4c15765f365f6526901b93c5ac1a269b402e810253a0ecc8c422f8b3b2ed95d +2026-04-04T01:52:33.2081850Z Received 25165824 of 573077343 (4.4%), 24.0 MBs/sec +2026-04-04T01:52:34.2106407Z Received 155189248 of 573077343 (27.1%), 73.9 MBs/sec +2026-04-04T01:52:35.2137476Z Received 318767104 of 573077343 (55.6%), 101.2 MBs/sec +2026-04-04T01:52:36.2172323Z Received 478150656 of 573077343 (83.4%), 113.8 MBs/sec +2026-04-04T01:52:37.2141088Z Received 568883039 of 573077343 (99.3%), 108.4 MBs/sec +2026-04-04T01:52:37.2633420Z Received 573077343 of 573077343 (100.0%), 108.1 MBs/sec +2026-04-04T01:52:37.2641168Z Cache Size: ~547 MB (573077343 B) +2026-04-04T01:52:37.2686260Z [command]/usr/bin/tar -xf /home/runner/work/_temp/fcf5b76c-1e9e-4864-a0b6-606e0704af7a/cache.tzst -P -C /home/runner/work/nav/nav --use-compress-program unzstd +2026-04-04T01:52:41.9852737Z Cache restored successfully +2026-04-04T01:52:42.0256007Z Cache restored from key: setup-java-Linux-x64-gradle-d4c15765f365f6526901b93c5ac1a269b402e810253a0ecc8c422f8b3b2ed95d +2026-04-04T01:52:42.0766228Z ##[group]Run chmod +x android-app/gradlew +2026-04-04T01:52:42.0766616Z chmod +x android-app/gradlew +2026-04-04T01:52:42.0800640Z shell: /usr/bin/bash -e {0} +2026-04-04T01:52:42.0800890Z env: +2026-04-04T01:52:42.0801188Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:42.0801677Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:42.0802030Z ##[endgroup] +2026-04-04T01:52:42.0943600Z ##[group]Run actions/download-artifact@v4 +2026-04-04T01:52:42.0943888Z with: +2026-04-04T01:52:42.0944087Z name: test-apks +2026-04-04T01:52:42.0944525Z path: . +2026-04-04T01:52:42.0944752Z merge-multiple: false +2026-04-04T01:52:42.0944981Z repository: thepeterstone/nav +2026-04-04T01:52:42.0945212Z run-id: 23968772481 +2026-04-04T01:52:42.0945401Z env: +2026-04-04T01:52:42.0945676Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:42.0946157Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:42.0946517Z ##[endgroup] +2026-04-04T01:52:42.3578268Z Downloading single artifact +2026-04-04T01:52:42.5969861Z Preparing to download the following artifacts: +2026-04-04T01:52:42.5971012Z - test-apks (ID: 6267861101, Size: 32186647, Expected Digest: sha256:beef21a9ee7517e9e75d7bdc948df1aeab6bddaaa582ebbb8de2f4bf715a6e6d) +2026-04-04T01:52:42.7509436Z Redirecting to blob download url: https://productionresultssa2.blob.core.windows.net/actions-results/e52103b9-e634-43a9-bcb4-c3270690e5fc/workflow-job-run-d5dbcacf-7496-50d1-b234-56c9e5299772/artifacts/be6d339498f0e44beff448c449ca6372065982a0894aa49cd535d4dfcb3f50a9.zip +2026-04-04T01:52:42.7513571Z Starting download of artifact to: /home/runner/work/nav/nav +2026-04-04T01:52:43.0252188Z (node:2092) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead. +2026-04-04T01:52:43.0261096Z (Use `node --trace-deprecation ...` to show where the warning was created) +2026-04-04T01:52:44.7452196Z SHA256 digest of downloaded artifact is beef21a9ee7517e9e75d7bdc948df1aeab6bddaaa582ebbb8de2f4bf715a6e6d +2026-04-04T01:52:44.7465423Z Artifact download completed successfully. +2026-04-04T01:52:44.7466195Z Total of 1 artifact(s) downloaded +2026-04-04T01:52:44.7466675Z Download artifact has finished successfully +2026-04-04T01:52:44.7590878Z ##[group]Run echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules +2026-04-04T01:52:44.7591797Z echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules +2026-04-04T01:52:44.7592374Z sudo udevadm control --reload-rules +2026-04-04T01:52:44.7592683Z sudo udevadm trigger --name-match=kvm +2026-04-04T01:52:44.7620433Z shell: /usr/bin/bash -e {0} +2026-04-04T01:52:44.7620691Z env: +2026-04-04T01:52:44.7621026Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:44.7621526Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:44.7621889Z ##[endgroup] +2026-04-04T01:52:44.7748778Z KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm" +2026-04-04T01:52:44.8149323Z ##[group]Run reactivecircus/android-emulator-runner@v2 +2026-04-04T01:52:44.8149676Z with: +2026-04-04T01:52:44.8149860Z api-level: 30 +2026-04-04T01:52:44.8150048Z arch: x86_64 +2026-04-04T01:52:44.8150237Z profile: pixel_3a +2026-04-04T01:52:44.8150626Z script: ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest +2026-04-04T01:52:44.8151105Z working-directory: android-app +2026-04-04T01:52:44.8151350Z target: default +2026-04-04T01:52:44.8151532Z cores: 2 +2026-04-04T01:52:44.8151711Z avd-name: test +2026-04-04T01:52:44.8151910Z force-avd-creation: true +2026-04-04T01:52:44.8152146Z emulator-boot-timeout: 600 +2026-04-04T01:52:44.8152375Z emulator-port: 5554 +2026-04-04T01:52:44.8152771Z emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim +2026-04-04T01:52:44.8153239Z disable-animations: true +2026-04-04T01:52:44.8153474Z disable-spellchecker: false +2026-04-04T01:52:44.8153755Z disable-linux-hw-accel: auto +2026-04-04T01:52:44.8154000Z enable-hw-keyboard: false +2026-04-04T01:52:44.8154222Z channel: stable +2026-04-04T01:52:44.8154589Z env: +2026-04-04T01:52:44.8154878Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:44.8155367Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:52:44.8155758Z ##[endgroup] +2026-04-04T01:52:44.8799273Z ##[group]Configure emulator +2026-04-04T01:52:44.8802823Z API level: 30 +2026-04-04T01:52:44.8803435Z System image API level: 30 +2026-04-04T01:52:44.8806438Z target: default +2026-04-04T01:52:44.8806808Z CPU architecture: x86_64 +2026-04-04T01:52:44.8808863Z Hardware profile: pixel_3a +2026-04-04T01:52:44.8813165Z Cores: 2 +2026-04-04T01:52:44.8815886Z RAM size: +2026-04-04T01:52:44.8817551Z Heap size: +2026-04-04T01:52:44.8819321Z SD card path or size: +2026-04-04T01:52:44.8820868Z Disk size: +2026-04-04T01:52:44.8821200Z AVD name: test +2026-04-04T01:52:44.8821593Z force avd creation: true +2026-04-04T01:52:44.8822085Z Emulator boot timeout: 600 +2026-04-04T01:52:44.8822534Z emulator port: 5554 +2026-04-04T01:52:44.8823249Z emulator options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim +2026-04-04T01:52:44.8824091Z disable animations: true +2026-04-04T01:52:44.8825170Z disable spellchecker: false +2026-04-04T01:52:44.8825650Z disable Linux hardware acceleration: false +2026-04-04T01:52:44.8826136Z enable hardware keyboard: false +2026-04-04T01:52:44.8826595Z custom working directory: android-app +2026-04-04T01:52:44.8827065Z Channel: 0 (stable) +2026-04-04T01:52:44.8827382Z Script: +2026-04-04T01:52:44.8828447Z ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest +2026-04-04T01:52:44.8829408Z Pre emulator launch script: +2026-04-04T01:52:44.8830548Z ##[endgroup] +2026-04-04T01:52:44.8831227Z ##[group]Install Android SDK +2026-04-04T01:52:44.9614945Z [command]/usr/bin/sh -c \yes | sdkmanager --licenses > /dev/null +2026-04-04T01:52:55.5031478Z Installing latest build tools, platform tools, and platform. +2026-04-04T01:52:55.5055432Z [command]/usr/bin/sh -c \sdkmanager --install 'build-tools;36.0.0' platform-tools 'platforms;android-30'> /dev/null +2026-04-04T01:53:02.8511083Z Installing latest emulator. +2026-04-04T01:53:02.8539075Z [command]/usr/bin/sh -c \sdkmanager --install emulator --channel=0 > /dev/null +2026-04-04T01:53:13.6135483Z Installing system images. +2026-04-04T01:53:13.6156819Z [command]/usr/bin/sh -c \sdkmanager --install 'system-images;android-30;default;x86_64' --channel=0 > /dev/null +2026-04-04T01:53:42.1598436Z ##[endgroup] +2026-04-04T01:53:42.1626617Z ##[group]Create AVD +2026-04-04T01:53:42.1631477Z Creating AVD. +2026-04-04T01:53:42.1678888Z [command]/usr/bin/sh -c \echo no | avdmanager create avd --force -n test --package 'system-images;android-30;default;x86_64' --device 'pixel_3a' +2026-04-04T01:53:43.2646363Z Loading local repository... +2026-04-04T01:53:43.2657084Z [========= ] 25% Loading local repository... +2026-04-04T01:53:43.2663058Z [========= ] 25% Fetch remote repository... +2026-04-04T01:53:43.2665709Z [=======================================] 100% Fetch remote repository... +2026-04-04T01:53:43.4436603Z Auto-selecting single ABI x86_64 +2026-04-04T01:53:44.0665299Z [command]/usr/bin/sh -c \printf 'hw.cpu.ncore=2\n' >> /home/runner/.android/avd/test.avd/config.ini +2026-04-04T01:53:44.0694208Z ##[endgroup] +2026-04-04T01:53:44.0696893Z ##[group]Launch Emulator +2026-04-04T01:53:44.0699939Z Starting emulator. +2026-04-04T01:53:44.0721370Z [command]/usr/bin/sh -c \/usr/local/lib/android/sdk/emulator/emulator -port 5554 -avd test -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim & +2026-04-04T01:53:44.0875861Z INFO | Android emulator version 36.5.10.0 (build_id 15081367) (CL:N/A) +2026-04-04T01:53:44.0879825Z INFO | Graphics backend: gfxstream +2026-04-04T01:53:44.0881236Z INFO | Found systemPath /usr/local/lib/android/sdk/system-images/android-30/default/x86_64/ +2026-04-04T01:53:44.5172020Z WARNING | Please update the emulator to one that supports the feature(s): Vulkan +2026-04-04T01:53:44.5172897Z INFO | Increasing RAM size to 2048MB +2026-04-04T01:53:44.5173448Z ############################################################################## +2026-04-04T01:53:44.5174107Z ## WARNING - ACTION REQUIRED ## +2026-04-04T01:53:44.5175151Z ## Consider using the '-metrics-collection' flag to help improve the ## +2026-04-04T01:53:44.5176051Z ## emulator by sending anonymized usage data. Or use the '-no-metrics' ## +2026-04-04T01:53:44.5176939Z ## flag to bypass this warning and turn off the metrics collection. ## +2026-04-04T01:53:44.5177794Z ## In a future release this warning will turn into a one-time blocking ## +2026-04-04T01:53:44.5178703Z ## prompt to ask for explicit user input regarding metrics collection. ## +2026-04-04T01:53:44.5179457Z ## ## +2026-04-04T01:53:44.5180225Z ## Please see '-help-metrics-collection' for more details. You can use ## +2026-04-04T01:53:44.5181190Z ## '-metrics-to-file' or '-metrics-to-console' flags to see what type of ## +2026-04-04T01:53:44.5182534Z ## data is being collected by emulator as part of usage statistics. ## +2026-04-04T01:53:44.5183303Z ############################################################################## +2026-04-04T01:53:44.5183904Z INFO | Guest GLES Driver: Auto (ext controls) +2026-04-04T01:53:44.5185009Z INFO | emuglConfig_init: vulkan_mode_selected:swiftshader gles_mode_selected:swiftshader +2026-04-04T01:53:44.5185824Z INFO | Checking system compatibility: +2026-04-04T01:53:44.5186380Z INFO | Checking: hasSufficientDiskSpace +2026-04-04T01:53:44.5187041Z INFO | Ok: Disk space requirements to run avd: `test` are met +2026-04-04T01:53:44.5187681Z INFO | Checking: hasSufficientHwGpu +2026-04-04T01:53:44.5188284Z INFO | Ok: Hardware GPU compatibility checks are not required +2026-04-04T01:53:44.5188926Z INFO | Checking: hasSufficientSystem +2026-04-04T01:53:44.5189666Z INFO | Warning: AVD 'test' will run more smoothly with 4 CPU cores (currently using 2) +2026-04-04T01:53:44.5190632Z USER_WARNING | AVD 'test' will run more smoothly with 4 CPU cores (currently using 2). +2026-04-04T01:53:44.5191495Z WARNING | FeatureControl is requesting a non existing feature. +2026-04-04T01:53:44.5192187Z ERROR | Unable to connect to adb daemon on port: 5037 +2026-04-04T01:53:44.5193164Z INFO | Storing crashdata in: /tmp/android-runner/emu-crash-36.5.10.db, detection is enabled for process: 2406 +2026-04-04T01:53:44.5194110Z INFO | Initializing gfxstream backend +2026-04-04T01:53:44.5195062Z INFO | android_startOpenglesRenderer: gpu info +2026-04-04T01:53:44.5195831Z INFO | +2026-04-04T01:53:44.5196377Z INFO | initIcdPaths: ICD set to 'swiftshader', using Swiftshader ICD +2026-04-04T01:53:44.5197504Z INFO | Setting ICD filenames for the loader = /usr/local/lib/android/sdk/emulator/lib64/vulkan/vk_swiftshader_icd.json +2026-04-04T01:53:44.5198891Z INFO | SharedLibrary::open for [/usr/local/lib/android/sdk/emulator/qemu/linux-x86_64/lib64/vulkan/libvulkan.so] +2026-04-04T01:53:44.5200254Z INFO | SharedLibrary::open for [/usr/local/lib/android/sdk/emulator/qemu/linux-x86_64/lib64/vulkan/libvulkan.so.1] +2026-04-04T01:53:44.5201479Z INFO | SharedLibrary::open for [/usr/local/lib/android/sdk/emulator/lib64/vulkan/libvulkan.so] +2026-04-04T01:53:44.5202508Z INFO | Added library: /usr/local/lib/android/sdk/emulator/lib64/vulkan/libvulkan.so +2026-04-04T01:53:44.5203433Z INFO | Selecting Vulkan device: SwiftShader Device (Subzero), Version: 1.3.0 +2026-04-04T01:53:44.5204154Z INFO | SharedLibrary::open for [libX11] +2026-04-04T01:53:44.5204999Z WARNING | Could not open libX11.so, try libX11.so.6 +2026-04-04T01:53:44.5205606Z INFO | SharedLibrary::open for [libX11.so.6] +2026-04-04T01:53:44.5206195Z INFO | SharedLibrary::open for [libX11-xcb] +2026-04-04T01:53:44.5206585Z WARNING | Could not open libX11-xcb.so, try libX11-xcb.so.1 +2026-04-04T01:53:44.5206965Z INFO | SharedLibrary::open for [libX11-xcb.so.1] +2026-04-04T01:53:44.5207296Z INFO | Initializing VkEmulation features: +2026-04-04T01:53:44.5207600Z INFO | glInteropSupported: false +2026-04-04T01:53:44.5207886Z INFO | useDeferredCommands: true +2026-04-04T01:53:44.5208196Z INFO | createResourceWithRequirements: true +2026-04-04T01:53:44.5208514Z INFO | useVulkanComposition: false +2026-04-04T01:53:44.5208819Z INFO | useVulkanNativeSwapchain: false +2026-04-04T01:53:44.5209125Z INFO | enable guestRenderDoc: false +2026-04-04T01:53:44.5209430Z INFO | ASTC LDR emulation mode: Gpu +2026-04-04T01:53:44.5209713Z INFO | enable ETC2 emulation: true +2026-04-04T01:53:44.5210000Z INFO | enable Ycbcr emulation: false +2026-04-04T01:53:44.5210286Z INFO | guestVulkanOnly: false +2026-04-04T01:53:44.5210574Z INFO | useDedicatedAllocations: false +2026-04-04T01:53:44.5211083Z INFO | guestVulkanMaxApiVersion: 1.3.0 +2026-04-04T01:53:44.5211414Z INFO | Graphics Adapter Vendor Google (Google Inc.) +2026-04-04T01:53:44.5211861Z INFO | Graphics Adapter Android Emulator OpenGL ES Translator (Google SwiftShader) +2026-04-04T01:53:44.5212458Z INFO | Graphics API Version OpenGL ES 3.0 (OpenGL ES 3.0 SwiftShader 4.0.0.1) +2026-04-04T01:53:44.5430543Z INFO | Graphics API Extensions GL_OES_EGL_WARNING: cannnot unmap ptr 0x7fa0b8001000 as it is in the protected range from 0x7fa038000000 to 0x7fa0b8200000 +2026-04-04T01:53:44.5579216Z sync GL_OES_EGL_image GL_OES_EGL_image_external GL_OES_depth24 GL_OES_depth32 GL_OES_element_index_uint GL_OES_texture_float GL_OES_texture_float_linear GL_OES_compressed_paletted_texture GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth_texture GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_packed_depth_stencil GL_OES_vertex_half_float GL_OES_standard_derivatives GL_OES_texture_npot GL_OES_rgb8_rgba8 GL_EXT_color_buffer_float GL_EXT_color_buffer_half_float GL_EXT_texture_format_BGRA8888 GL_APPLE_texture_format_BGRA8888 +2026-04-04T01:53:44.5584044Z INFO | Graphics Device Extensions N/A +2026-04-04T01:53:44.5586342Z INFO | Disabling sparse binding feature support +2026-04-04T01:53:44.5593625Z INFO | Userspace boot properties: +2026-04-04T01:53:44.5594117Z INFO | android.bootanim=0 +2026-04-04T01:53:44.5594886Z INFO | android.qemud=1 +2026-04-04T01:53:44.5595556Z INFO | androidboot.boot_devices=pci0000:00/0000:00:03.0 pci0000:00/0000:00:06.0 +2026-04-04T01:53:44.5596326Z INFO | androidboot.hardware=ranchu +2026-04-04T01:53:44.5597168Z INFO | androidboot.hardware.vulkan=ranchu +2026-04-04T01:53:44.5597798Z INFO | androidboot.serialno=EMULATOR36X5X10X0 +2026-04-04T01:53:44.5598719Z INFO | androidboot.vbmeta.digest=aa516f360594014de86105695b13f1df5e00b81539011aafdd58e68e859134a1 +2026-04-04T01:53:44.5599664Z INFO | androidboot.vbmeta.hash_alg=sha256 +2026-04-04T01:53:44.5600215Z INFO | androidboot.vbmeta.size=6144 +2026-04-04T01:53:44.5600796Z INFO | qemu=1 +2026-04-04T01:53:44.5601170Z INFO | qemu.avd_name=test +2026-04-04T01:53:44.5601649Z INFO | qemu.camera_hq_edge_processing=0 +2026-04-04T01:53:44.5602168Z INFO | qemu.camera_protocol_ver=1 +2026-04-04T01:53:44.5602700Z INFO | qemu.dalvik.vm.heapsize=512m +2026-04-04T01:53:44.5603200Z INFO | qemu.encrypt=1 +2026-04-04T01:53:44.5603616Z INFO | qemu.gles=1 +2026-04-04T01:53:44.5604027Z INFO | qemu.gltransport=pipe +2026-04-04T01:53:44.5604812Z INFO | qemu.gltransport.drawFlushInterval=800 +2026-04-04T01:53:44.5605382Z INFO | qemu.hwcodec.avcdec=2 +2026-04-04T01:53:44.5605853Z INFO | qemu.hwcodec.hevcdec=2 +2026-04-04T01:53:44.5606319Z INFO | qemu.hwcodec.vpxdec=2 +2026-04-04T01:53:44.5606814Z INFO | qemu.opengles.version=196608 +2026-04-04T01:53:44.5607397Z INFO | qemu.settings.system.screen_off_timeout=2147483647 +2026-04-04T01:53:44.5607964Z INFO | qemu.skin=1080x2220 +2026-04-04T01:53:44.5608396Z INFO | qemu.uirenderer=skiagl +2026-04-04T01:53:44.5608821Z INFO | qemu.vsync=60 +2026-04-04T01:53:44.5609201Z INFO | qemu.wifi=1 +2026-04-04T01:53:44.5609630Z INFO | Monitoring duration of emulator setup. +2026-04-04T01:53:44.5804703Z INFO | Setting display: 0 configuration to: 1080x2220, dpi: 440x440 +2026-04-04T01:53:44.5809214Z INFO | setDisplayActiveConfig: id:0, 1080x2220 +2026-04-04T01:53:44.5810272Z INFO | emulatorSetupEnvironment: Setting up screen background view and display layout at env:1080x2220, lcd:1080x2220 +2026-04-04T01:53:44.5811415Z INFO | emulatorSetupEnvironment: Environment scene is not required +2026-04-04T01:53:44.5870338Z USER_INFO | Emulator is performing a full startup. This may take upto two minutes, or more. +2026-04-04T01:53:44.5896280Z WARNING | Failed to process .ini file /home/runner/.android/emu-update-last-check.ini for reading. +2026-04-04T01:53:44.8465582Z WARNING | Failed to process .ini file /home/runner/.android/emu-update-last-check.ini for reading. +2026-04-04T01:53:54.0786210Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:53:54.0815792Z * daemon not running; starting now at tcp:5037 +2026-04-04T01:53:54.0849728Z * daemon started successfully +2026-04-04T01:53:54.0863280Z The process '/usr/local/lib/android/sdk/platform-tools/adb' failed with exit code 1 +2026-04-04T01:53:54.0863750Z adb: device offline +2026-04-04T01:53:56.0968546Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:53:56.0987063Z adb: device offline +2026-04-04T01:53:56.1006948Z The process '/usr/local/lib/android/sdk/platform-tools/adb' failed with exit code 1 +2026-04-04T01:53:58.1043097Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:53:58.1102814Z adb: device offline +2026-04-04T01:53:58.1111741Z The process '/usr/local/lib/android/sdk/platform-tools/adb' failed with exit code 1 +2026-04-04T01:54:00.1182629Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:00.1288435Z adb: device offline +2026-04-04T01:54:00.1299150Z The process '/usr/local/lib/android/sdk/platform-tools/adb' failed with exit code 1 +2026-04-04T01:54:02.1387192Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:02.1755374Z +2026-04-04T01:54:04.1847960Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:04.2034658Z +2026-04-04T01:54:06.2132212Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:06.2409257Z +2026-04-04T01:54:08.2489707Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:08.2753592Z +2026-04-04T01:54:10.2832469Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:10.2997839Z +2026-04-04T01:54:12.3096483Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:12.3855060Z +2026-04-04T01:54:14.3932062Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:14.4329449Z +2026-04-04T01:54:16.4476929Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell getprop sys.boot_completed +2026-04-04T01:54:16.5778163Z 1 +2026-04-04T01:54:16.5832847Z Emulator booted. +2026-04-04T01:54:16.5891341Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell input keyevent 82 +2026-04-04T01:54:20.3222967Z Disabling animations. +2026-04-04T01:54:20.3336345Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell settings put global window_animation_scale 0.0 +2026-04-04T01:54:20.3727621Z INFO | Boot completed in 36259 ms +2026-04-04T01:54:20.3729765Z INFO | Increasing screen off timeout, logcat buffer size to 2M. +2026-04-04T01:54:20.7725936Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell settings put global transition_animation_scale 0.0 +2026-04-04T01:54:20.9680271Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 shell settings put global animator_duration_scale 0.0 +2026-04-04T01:54:21.4993141Z ##[endgroup] +2026-04-04T01:54:21.5105171Z [command]/usr/bin/sh -c ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest +2026-04-04T01:54:23.1775704Z +2026-04-04T01:54:23.1777712Z Welcome to Gradle 8.7! +2026-04-04T01:54:23.1779523Z +2026-04-04T01:54:23.1805055Z Here are the highlights of this release: +2026-04-04T01:54:23.1825079Z - Compiling and testing with Java 22 +2026-04-04T01:54:23.1827075Z - Cacheable Groovy script compilation +2026-04-04T01:54:23.1829014Z - New methods in lazy collection properties +2026-04-04T01:54:23.1830783Z +2026-04-04T01:54:23.1886488Z For more details see https://docs.gradle.org/8.7/release-notes.html +2026-04-04T01:54:23.1894782Z +2026-04-04T01:54:23.8726166Z Starting a Gradle Daemon (subsequent builds will be faster) +2026-04-04T01:54:51.5807296Z > Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED +2026-04-04T01:54:51.5893804Z > Task :app:preBuild UP-TO-DATE +2026-04-04T01:54:51.5902758Z > Task :app:preDebugBuild UP-TO-DATE +2026-04-04T01:54:53.7731180Z > Task :app:generateDebugResValues +2026-04-04T01:54:53.7744785Z > Task :app:dataBindingMergeDependencyArtifactsDebug +2026-04-04T01:54:53.7749757Z > Task :app:generateDebugResources +2026-04-04T01:54:53.7750351Z > Task :app:injectCrashlyticsMappingFileIdDebug +2026-04-04T01:54:53.7750964Z > Task :app:processDebugGoogleServices +2026-04-04T01:54:58.8725338Z > Task :app:packageDebugResources +2026-04-04T01:54:59.0747108Z > Task :app:mergeDebugResources +2026-04-04T01:54:59.3727738Z > Task :app:parseDebugLocalResources +2026-04-04T01:54:59.4774586Z > Task :app:checkDebugAarMetadata +2026-04-04T01:55:00.1729004Z > Task :app:dataBindingGenBaseClassesDebug +2026-04-04T01:55:00.1733185Z > Task :app:mapDebugSourceSetPaths +2026-04-04T01:55:00.1736093Z > Task :app:createDebugCompatibleScreenManifests +2026-04-04T01:55:00.1748114Z > Task :app:extractDeepLinksDebug +2026-04-04T01:55:00.7762980Z > Task :app:processDebugMainManifest +2026-04-04T01:55:00.9727065Z > Task :app:processDebugManifest +2026-04-04T01:55:00.9729908Z > Task :app:processDebugManifestForPackage +2026-04-04T01:55:01.0730898Z > Task :app:javaPreCompileDebug +2026-04-04T01:55:01.6724601Z > Task :app:preDebugAndroidTestBuild SKIPPED +2026-04-04T01:55:02.1745682Z > Task :app:dataBindingMergeDependencyArtifactsDebugAndroidTest +2026-04-04T01:55:02.1747793Z > Task :app:generateDebugAndroidTestResValues +2026-04-04T01:55:02.1749590Z > Task :app:generateDebugAndroidTestResources +2026-04-04T01:55:02.2742380Z > Task :app:mergeDebugAndroidTestResources +2026-04-04T01:55:02.3757413Z > Task :app:dataBindingGenBaseClassesDebugAndroidTest +2026-04-04T01:55:02.3759582Z > Task :app:checkDebugAndroidTestAarMetadata +2026-04-04T01:55:02.3761575Z > Task :app:mapDebugAndroidTestSourceSetPaths +2026-04-04T01:55:02.4725268Z > Task :app:processDebugAndroidTestManifest +2026-04-04T01:55:02.7741040Z > Task :app:processDebugAndroidTestResources +2026-04-04T01:55:02.7743186Z > Task :app:javaPreCompileDebugAndroidTest +2026-04-04T01:55:02.8743421Z > Task :app:mergeDebugShaders +2026-04-04T01:55:02.8745752Z > Task :app:compileDebugShaders NO-SOURCE +2026-04-04T01:55:02.8765032Z > Task :app:generateDebugAssets UP-TO-DATE +2026-04-04T01:55:03.2726150Z > Task :app:mergeDebugAssets +2026-04-04T01:55:03.2728077Z > Task :app:compressDebugAssets +2026-04-04T01:55:03.8727616Z > Task :app:processDebugResources +2026-04-04T01:55:03.8729963Z > Task :app:checkDebugDuplicateClasses +2026-04-04T01:55:06.4739954Z > Task :app:desugarDebugFileDependencies +2026-04-04T01:55:21.7728784Z > Task :app:kspDebugKotlin +2026-04-04T01:55:28.0755445Z > Task :app:mergeExtDexDebug +2026-04-04T01:55:28.0762110Z > Task :app:mergeLibDexDebug +2026-04-04T01:55:28.1740534Z > Task :app:mergeDebugJniLibFolders +2026-04-04T01:55:29.0726891Z > Task :app:mergeDebugNativeLibs +2026-04-04T01:55:29.3724151Z +2026-04-04T01:55:29.3726623Z > Task :app:stripDebugDebugSymbols +2026-04-04T01:55:29.3728563Z Unable to strip the following libraries, packaging them as they are: libmaplibre.so. +2026-04-04T01:55:31.1731449Z +2026-04-04T01:55:31.1733344Z > Task :app:validateSigningDebug +2026-04-04T01:55:31.1735508Z > Task :app:writeDebugAppMetadata +2026-04-04T01:55:31.1737900Z > Task :app:writeDebugSigningConfigVersions +2026-04-04T01:55:31.1741221Z > Task :app:mergeDebugAndroidTestShaders +2026-04-04T01:55:31.1744194Z > Task :app:compileDebugAndroidTestShaders NO-SOURCE +2026-04-04T01:55:31.1746878Z > Task :app:generateDebugAndroidTestAssets UP-TO-DATE +2026-04-04T01:55:31.2755022Z > Task :app:mergeDebugAndroidTestAssets +2026-04-04T01:55:31.2760654Z > Task :app:compressDebugAndroidTestAssets +2026-04-04T01:55:31.4748280Z > Task :app:checkDebugAndroidTestDuplicateClasses +2026-04-04T01:55:31.4750580Z > Task :app:desugarDebugAndroidTestFileDependencies +2026-04-04T01:55:33.1737523Z > Task :app:mergeExtDexDebugAndroidTest +2026-04-04T01:55:33.1739623Z > Task :app:mergeLibDexDebugAndroidTest +2026-04-04T01:55:33.2725730Z > Task :app:mergeDebugAndroidTestJniLibFolders +2026-04-04T01:55:33.3727434Z > Task :app:mergeDebugAndroidTestNativeLibs NO-SOURCE +2026-04-04T01:55:33.3729857Z > Task :app:stripDebugAndroidTestDebugSymbols NO-SOURCE +2026-04-04T01:55:33.3736177Z > Task :app:validateSigningDebugAndroidTest +2026-04-04T01:55:33.3738369Z > Task :app:writeDebugAndroidTestSigningConfigVersions +2026-04-04T01:55:51.7724190Z > Task :app:compileDebugKotlin +2026-04-04T01:55:58.3724759Z w: file:///home/runner/work/nav/nav/android-app/app/src/main/kotlin/org/terst/nav/AnchorAlarmManager.kt:76:26 '@Deprecated(...) fun vibrate(p0: LongArray!, p1: Int): Unit' is deprecated. Deprecated in Java. +2026-04-04T01:55:58.3728382Z w: file:///home/runner/work/nav/nav/android-app/app/src/main/kotlin/org/terst/nav/nmea/NmeaParser.kt:176:39 Condition is always 'true'. +2026-04-04T01:56:01.7723441Z +2026-04-04T01:56:01.7725586Z > Task :app:compileDebugJavaWithJavac +2026-04-04T01:56:01.8722401Z > Task :app:processDebugJavaRes +2026-04-04T01:56:01.8738238Z > Task :app:bundleDebugClassesToCompileJar +2026-04-04T01:56:04.1755362Z > Task :app:kspDebugAndroidTestKotlin +2026-04-04T01:56:04.6737277Z > Task :app:mergeDebugJavaResource +2026-04-04T01:56:08.4723489Z > Task :app:compileDebugAndroidTestKotlin +2026-04-04T01:56:11.0722573Z > Task :app:dexBuilderDebug +2026-04-04T01:56:11.1724211Z > Task :app:compileDebugAndroidTestJavaWithJavac NO-SOURCE +2026-04-04T01:56:11.1726867Z > Task :app:processDebugAndroidTestJavaRes +2026-04-04T01:56:11.6723874Z > Task :app:mergeProjectDexDebug +2026-04-04T01:56:11.8723876Z > Task :app:mergeDebugAndroidTestJavaResource +2026-04-04T01:56:11.9739183Z > Task :app:dexBuilderDebugAndroidTest +2026-04-04T01:56:12.0730192Z > Task :app:mergeProjectDexDebugAndroidTest +2026-04-04T01:56:12.2724566Z > Task :app:packageDebugAndroidTest +2026-04-04T01:56:12.2726829Z > Task :app:createDebugAndroidTestApkListingFileRedirect +2026-04-04T01:56:12.9731999Z > Task :app:packageDebug +2026-04-04T01:56:12.9732873Z > Task :app:createDebugApkListingFileRedirect +2026-04-04T01:56:23.9723089Z [EmulatorConsole]: Failed to start Emulator console for 5554 +2026-04-04T01:56:33.6727510Z +2026-04-04T01:56:33.6728450Z > Task :app:connectedDebugAndroidTest +2026-04-04T01:56:33.6729708Z Starting 11 tests on emulator-5554 - 11 +2026-04-04T01:56:35.2267685Z INFO | Created VkInstance:0x55630ebe3800 for application:'maplibre-native' engine:'maplibre-native'. +2026-04-04T01:56:35.2757066Z INFO | Created VkDevice:0x5563196e4010 for application:'maplibre-native' instance:0x55630ebe3800. ASTC emulation:on CPU decoding:off. +2026-04-04T01:56:39.2549387Z INFO | Destroyed VkDevice:0x5563196e4010 +2026-04-04T01:56:39.2550015Z INFO | Destroyed VkInstance:0x55630ebe3800 for application:'maplibre-native' engine:'maplibre-native'. +2026-04-04T01:56:39.5736611Z +2026-04-04T01:56:39.5739608Z org.terst.nav.MainActivitySmokeTest > fabRecordTrack_isDisplayedWithRecordDescription[emulator-5554 - 11] FAILED  +2026-04-04T01:56:39.5741856Z +2026-04-04T01:56:52.2779158Z Tests on emulator-5554 - 11 failed: There was 1 failure(s). +2026-04-04T01:56:52.7732127Z Test run failed to complete. Instrumentation run failed due to Process crashed. +2026-04-04T01:56:53.1722719Z +2026-04-04T01:56:53.1731069Z > Task :app:connectedDebugAndroidTest FAILED +2026-04-04T01:56:53.2766820Z +2026-04-04T01:56:53.2769260Z FAILURE: Build failed with an exception. +2026-04-04T01:56:53.2769661Z +2026-04-04T01:56:53.2769810Z * What went wrong: +2026-04-04T01:56:53.2774005Z Execution failed for task ':app:connectedDebugAndroidTest'. +2026-04-04T01:56:53.2795724Z > There were failing tests. See the report at: file:///home/runner/work/nav/nav/android-app/app/build/reports/androidTests/connected/debug/index.html +2026-04-04T01:56:53.2796631Z +2026-04-04T01:56:53.2796742Z * Try: +2026-04-04T01:56:53.2797108Z > Run with --stacktrace option to get the stack trace. +2026-04-04T01:56:53.2797691Z > Run with --info or --debug option to get more log output. +2026-04-04T01:56:53.2798210Z > Run with --scan to get full insights. +2026-04-04T01:56:53.2798667Z > Get more help at https://help.gradle.org. +2026-04-04T01:56:53.2798977Z +2026-04-04T01:56:53.2799100Z BUILD FAILED in 2m 31s +2026-04-04T01:56:53.2799448Z 70 actionable tasks: 70 executed +2026-04-04T01:56:53.7622555Z ##[error]The process '/usr/bin/sh' failed with exit code 1 +2026-04-04T01:56:53.7641237Z ##[group]Terminate Emulator +2026-04-04T01:56:53.7656690Z [command]/usr/local/lib/android/sdk/platform-tools/adb -s emulator-5554 emu kill +2026-04-04T01:56:53.7745435Z OK: killing emulator, bye bye +2026-04-04T01:56:53.7746020Z OK +2026-04-04T01:56:53.7752509Z ##[endgroup] +2026-04-04T01:56:53.7756781Z INFO | Wait for emulator (pid 2406) 20 seconds to shutdown gracefully before kill;you can set environment variable ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL(in seconds) to change the default value (20 seconds) +2026-04-04T01:56:53.7758062Z +2026-04-04T01:56:53.7925089Z USER_INFO | Snapshots have been disabled by the user, save request is ignored. +2026-04-04T01:56:53.7926108Z INFO | Saving snapshot 'default_boot' using 1 ms +2026-04-04T01:56:54.7212023Z ERROR | stop: Not implemented +2026-04-04T01:56:54.7213674Z WARNING | Emulator client has not yet been configured.. Call configure me first! +2026-04-04T01:56:55.5041022Z ##[group]Run actions/upload-artifact@v4 +2026-04-04T01:56:55.5041326Z with: +2026-04-04T01:56:55.5041520Z name: smoke-test-results +2026-04-04T01:56:55.5041825Z path: android-app/app/build/outputs/androidTest-results/ +2026-04-04T01:56:55.5042165Z if-no-files-found: warn +2026-04-04T01:56:55.5042395Z compression-level: 6 +2026-04-04T01:56:55.5042605Z overwrite: false +2026-04-04T01:56:55.5042808Z include-hidden-files: false +2026-04-04T01:56:55.5043026Z env: +2026-04-04T01:56:55.5043305Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:56:55.5043777Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:56:55.5044181Z ANDROID_AVD_HOME: /home/runner/.android/avd +2026-04-04T01:56:55.5044687Z ##[endgroup] +2026-04-04T01:56:55.7771795Z With the provided path, there will be 9 files uploaded +2026-04-04T01:56:55.7773689Z Artifact name is valid! +2026-04-04T01:56:55.7774677Z Root directory input is valid! +2026-04-04T01:56:56.0006963Z Beginning upload of artifact content to blob storage +2026-04-04T01:56:56.3350542Z Uploaded bytes 14279 +2026-04-04T01:56:56.4014717Z Finished uploading artifact content to blob storage! +2026-04-04T01:56:56.4019377Z SHA256 digest of uploaded artifact zip is e6d4d38f75c9aabeaf3196574826b47be14163cf1ed2ca909e65f49c06224d6d +2026-04-04T01:56:56.4022555Z Finalizing artifact upload +2026-04-04T01:56:56.5301252Z Artifact smoke-test-results.zip successfully finalized. Artifact ID 6267886311 +2026-04-04T01:56:56.5303441Z Artifact smoke-test-results has been successfully uploaded! Final size is 14279 bytes. Artifact ID is 6267886311 +2026-04-04T01:56:56.5308997Z Artifact download URL: https://github.com/thepeterstone/nav/actions/runs/23968772481/artifacts/6267886311 +2026-04-04T01:56:56.5447233Z ##[group]Run PAYLOAD=$(jq -n \ +2026-04-04T01:56:56.5447563Z PAYLOAD=$(jq -n \ +2026-04-04T01:56:56.5447824Z  --arg action "completed" \ +2026-04-04T01:56:56.5448111Z  --arg conclusion "failure" \ +2026-04-04T01:56:56.5448426Z  --arg name "Android CI/CD / smoke-test" \ +2026-04-04T01:56:56.5448755Z  --arg repo "thepeterstone/nav" \ +2026-04-04T01:56:56.5449108Z  --arg sha "7d4e856193954a0eba8e68b3ca5aa8f6a2dbd175" \ +2026-04-04T01:56:56.5449644Z  --arg run_id "23968772481" \ +2026-04-04T01:56:56.5450250Z  '{action: $action, workflow_run: {conclusion: $conclusion, name: $name, head_sha: $sha, id: ($run_id | tonumber)}, repository: {full_name: $repo}}') +2026-04-04T01:56:56.5451106Z SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$CLAUDOMATOR_WEBHOOK_SECRET" | awk '{print $2}') +2026-04-04T01:56:56.5451729Z curl -sf -X POST \ +2026-04-04T01:56:56.5452301Z  -H "Content-Type: application/json" \ +2026-04-04T01:56:56.5452741Z  -H "X-GitHub-Event: workflow_run" \ +2026-04-04T01:56:56.5453201Z  -H "X-Hub-Signature-256: sha256=${SIG}" \ +2026-04-04T01:56:56.5453727Z  "${CLAUDOMATOR_WEBHOOK_URL}/api/webhooks/github" \ +2026-04-04T01:56:56.5454142Z  -d "$PAYLOAD" +2026-04-04T01:56:56.5749275Z shell: /usr/bin/bash -e {0} +2026-04-04T01:56:56.5749701Z env: +2026-04-04T01:56:56.5750383Z JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:56:56.5751260Z JAVA_HOME_17_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/x64 +2026-04-04T01:56:56.5764687Z ANDROID_AVD_HOME: /home/runner/.android/avd +2026-04-04T01:56:56.5765457Z CLAUDOMATOR_WEBHOOK_URL: *** +2026-04-04T01:56:56.5765894Z CLAUDOMATOR_WEBHOOK_SECRET: +2026-04-04T01:56:56.5766362Z ##[endgroup] +2026-04-04T01:56:56.9050286Z {"task_id":"02e868c0-ba22-4af8-b81e-7b62ab0ccd30"} +2026-04-04T01:56:56.9188421Z Post job cleanup. +2026-04-04T01:56:57.1172545Z Post job cleanup. +2026-04-04T01:56:57.2247411Z [command]/usr/bin/git version +2026-04-04T01:56:57.2289244Z git version 2.53.0 +2026-04-04T01:56:57.2346622Z Temporarily overriding HOME='/home/runner/work/_temp/89f9e09d-122a-4c96-8d55-306a85909284' before making global git config changes +2026-04-04T01:56:57.2350222Z Adding repository directory to the temporary git global config as a safe directory +2026-04-04T01:56:57.2353729Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/nav/nav +2026-04-04T01:56:57.2399249Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand +2026-04-04T01:56:57.2442020Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :" +2026-04-04T01:56:57.2702949Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader +2026-04-04T01:56:57.2727881Z http.https://github.com/.extraheader +2026-04-04T01:56:57.2740894Z [command]/usr/bin/git config --local --unset-all http.https://github.com/.extraheader +2026-04-04T01:56:57.2781710Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || :" +2026-04-04T01:56:57.3020839Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir: +2026-04-04T01:56:57.3055810Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url +2026-04-04T01:56:57.3409750Z Cleaning up orphan processes +2026-04-04T01:56:57.3794978Z Terminate orphan process: pid (2470) (adb) +2026-04-04T01:56:57.3931384Z Terminate orphan process: pid (2596) (java) +2026-04-04T01:56:57.4089644Z Terminate orphan process: pid (2720) (java) +2026-04-04T01:56:57.4127684Z ##[warning]Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: actions/checkout@v4, actions/download-artifact@v4, actions/setup-java@v4, actions/upload-artifact@v4. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Node.js 20 will be removed from the runner on September 16th, 2026. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ -- cgit v1.2.3 From a8d851e5bfb78b065f10d457bf3ce8f2c771bb4c Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 4 Apr 2026 04:53:50 +0000 Subject: refactor(ui): stabilize layout by moving BottomNav outside CoordinatorLayout --- .../app/src/main/res/layout/activity_main.xml | 158 +++++++++++---------- 1 file changed, 82 insertions(+), 76 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index a3d347f..b8df5c9 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -1,102 +1,108 @@ - - - + android:layout_height="0dp" + android:layout_weight="1"> - + + android:layout_height="match_parent"> - - + - + + - + - - + - + + android:layout_height="wrap_content" + app:behavior_hideable="false" + app:behavior_peekHeight="120dp" + app:cardElevation="16dp" + app:cardCornerRadius="24dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> - + + + + + + + + + + + - - - - - - - + -- cgit v1.2.3 From 9f01ddfba17dda7fb386e83f007c671fec6d5b8e Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 4 Apr 2026 07:10:41 +0000 Subject: feat(ui): surface trip planning and reports in instrument sheet --- .agent/worklog.md | 174 +++++---------------- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 14 ++ .../main/res/layout/layout_instruments_sheet.xml | 54 ++++++- .../kotlin/org/terst/nav/track/TrackRepository.kt | 14 +- 4 files changed, 118 insertions(+), 138 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/.agent/worklog.md b/.agent/worklog.md index 7a4467f..95bd266 100644 --- a/.agent/worklog.md +++ b/.agent/worklog.md @@ -1,154 +1,64 @@ # SESSION_STATE.md ## Current Task Goal -Section 7.3 AIS display — COMPLETE (2026-03-15): AIS integrated into ViewModel, MapFragment, and MainActivity +Section 7.4 Trip Reporting & Enhanced Tracks — COMPLETE (2026-04-04) -## Verified (2026-03-15) -- All 41 tests GREEN: 22 GPS/NMEA + 16 AIS + 3 ViewModel AIS (via /tmp/ais-vm-test-runner/) -- NmeaParser extended with MWV (wind), DBT (depth), HDG/HDM (heading) parsers -- Sensor data classes added: WindData, DepthData, HeadingData -- NmeaStreamManager added for TCP stream management +## Verified (2026-04-04) +- Build Successful: `android-app/gradlew assembleDebug` passed. +- UI Layout: `activity_main.xml` refactored to LinearLayout to prevent BottomNav overlap. +- Track Differentiation: Active (solid) vs Past (dotted) tracks verified in `MapHandler.kt`. +- Pre-Trip Logic: Sail suggestions reefing logic verified in `PreTripReportGenerator.kt`. ## Completed Items -### [APPROVED] GpsPosition data class +### [APPROVED] Trip Narrative Generator + Multi-Style AI (2026-04-04) +- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt`: Logic for distance, speed, and stylized narratives (Professional, Adventurous, Journal, Pirate). +- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportViewModel.kt`: State management for narrative generation. +- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt`: UI for viewing and selecting narrative styles. + +### [APPROVED] Pre-Trip Planning & Sail Suggestions (2026-04-04) +- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt`: Routing and reefing suggestions based on wind/waves. +- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt`: Domain models for boat profiles and suggestions. +- `android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt`: UI for generating and viewing pre-trip plans. + +### [APPROVED] Enhanced Map & Track Visualization (2026-04-04) +- `android-app/app/src/main/kotlin/org/terst/nav/ui/MapHandler.kt`: `updateTrackLayer` now supports active and past tracks with distinct styles. +- `android-app/app/src/main/res/layout/activity_main.xml`: Refactored to vertical LinearLayout root to stabilize BottomNav and FAB placement. +- `android-app/app/src/main/res/layout/layout_instruments_sheet.xml`: Added "TRIP REPORTS & PLANNING" section for direct access from main view. +- `android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt`: Wired Pre-Trip and Trip Report buttons from instrument sheet; added `showReport` navigation helper. +- `android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt`: Added `pastTracks` storage. +- `test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt`: Synchronized with main app implementation. + +### [APPROVED] GpsPosition data class (2026-03-15) - File: `app/src/main/kotlin/org/terst/nav/gps/GpsPosition.kt` -- Package: `org.terst.nav.gps` - Fields: latitude, longitude, sog (knots), cog (degrees true), timestampMs -### [APPROVED] GpsProvider / GpsListener interfaces -- File: `app/src/main/kotlin/org/terst/nav/gps/GpsProvider.kt` -- `GpsProvider`: start/stop, position property, addListener/removeListener -- `GpsListener`: onPositionUpdate(GpsPosition), onFixLost() - -### [APPROVED] DeviceGpsProvider -- File: `app/src/main/kotlin/org/terst/nav/gps/DeviceGpsProvider.kt` +### [APPROVED] DeviceGpsProvider (2026-03-15) - Wraps `LocationManager` with `GPS_PROVIDER` -- Default interval: 1000ms (1 Hz); configurable via constructor -- SOG: Location.speed (m/s) × 1.94384 → knots -- COG: Location.bearing (degrees true, no conversion) - Fix-lost timer: fires `onFixLost()` after 10s with no update -- Thread-safe listener list (synchronized) -- Android dependency: Context, LocationManager, Handler — device only - -### [APPROVED] FakeGpsProvider + GpsProviderTest (9 tests — all GREEN) -- File: `app/src/test/kotlin/org/terst/nav/gps/GpsProviderTest.kt` -- No Android dependencies — pure JVM -- Tests: - - `start sets started to true` - - `stop sets started to false` - - `listener receives position update` - - `listener notified of fix lost` - - `multiple listeners all receive position update` - - `multiple listeners all notified of fix lost` - - `removing listener stops notifications` - - `position property reflects last simulated position` - - `SOG conversion sanity check - 1 mps is approximately 1_94384 knots` -## Build Notes -- `app/build` and `app/.kotlin/sessions` are root-owned from a prior run. - Tests were verified via direct `kotlinc` + `JUnitCore` invocation (all 9 pass). - Full Gradle build requires fixing directory permissions: `chown -R www-data:www-data app/build app/.kotlin` -- Pre-existing XML layout error in `activity_main.xml:300` (unrelated to GPS work) - -### [APPROVED] NmeaParser — RMC parser -- File: `app/src/main/kotlin/org/terst/nav/nmea/NmeaParser.kt` +### [APPROVED] NmeaParser — RMC parser (2026-03-15) - Parses any `*RMC` sentence (GP, GN, etc.) -- Returns `null` for void status (V), malformed input, wrong sentence type -- SOG/COG default to 0.0 when fields are empty -- Latitude: positive = North, negative = South -- Longitude: positive = East, negative = West -- Timestamp: HHMMSS + DDMMYY → Unix epoch millis UTC (YY < 70 → 2000+YY) -- No Android dependencies — pure JVM - -### [APPROVED] GpsPositionTest + NmeaParserTest (22 tests — all GREEN) -- `app/src/test/kotlin/org/terst/nav/gps/GpsPositionTest.kt` (2 tests) -- `app/src/test/kotlin/org/terst/nav/nmea/NmeaParserTest.kt` (11 tests) -- `app/src/test/kotlin/org/terst/nav/gps/GpsProviderTest.kt` (9 tests, pre-existing) -- All verified via direct `kotlinc` (1.9.22) + `JUnitCore` invocation - -### [APPROVED] AisVessel data class -- File: `app/src/main/kotlin/org/terst/nav/ais/AisVessel.kt` -- Package: `org.terst.nav.ais` -- Fields: mmsi, name, callsign, lat, lon, sog, cog, heading, vesselType, timestampMs -- Note: `com/example/androidapp` tree is root-owned; files placed under `org/terst/nav/` (actual project package) - -### [APPROVED] CpaCalculator object -- File: `app/src/main/kotlin/org/terst/nav/ais/CpaCalculator.kt` -- Flat-earth CPA/TCPA algorithm; returns (cpa_nm, tcpa_min) -- Zero-relative-velocity guard: returns (currentDist, 0.0) - -### [APPROVED] AisVdmParser class -- File: `app/src/main/kotlin/org/terst/nav/nmea/AisVdmParser.kt` -- Parses !AIVDM/!AIVDO sentences; multi-part reassembly by seqId -- Type 1/2/3: MMSI, SOG, lon, lat, COG, heading decoded -- Type 5: MMSI, callsign (7 chars), name (20 chars), vesselType decoded; trailing '@'/' ' trimmed -- Not-available sentinel handling: SOG=1023→0.0, COG=3600→0.0, lon=0x6791AC0→0.0, lat=0x3412140→0.0 - -### [APPROVED] AIS Tests (16 tests — all GREEN) -- `test-runner/src/test/kotlin/org/terst/nav/ais/AisVesselTest.kt` (4 tests) -- `test-runner/src/test/kotlin/org/terst/nav/ais/CpaCalculatorTest.kt` (4 tests) -- `test-runner/src/test/kotlin/org/terst/nav/nmea/AisVdmParserTest.kt` (8 tests) -- Verification harness: `/tmp/ais-test-runner/` (JUnit5, com.example.androidapp package) -- Production test files also in `android-app/app/src/test/kotlin/org/terst/nav/ais/` - -### [APPROVED] AisRepository class -- File: `app/src/main/kotlin/org/terst/nav/ais/AisRepository.kt` -- Upserts position targets by MMSI; merges type-5 static data (name/callsign/vesselType) -- Pending static map: holds type-5 data until position arrives for same MMSI -- `evictStale(nowMs)`: removes vessels older than `staleTimeoutMs` (default 10 min) -- `AisDataSource` interface (with Flow) defined in same file - -### [APPROVED] AisHubApiService + AisHubVessel -- File: `app/src/main/kotlin/org/terst/nav/ais/AisHubApiService.kt` -- Note: placed in `ais/` directory (writable) with package `org.terst.nav.data.api` -- `AisHubVessel` data class with Moshi `@Json` and `@JsonClass(generateAdapter=true)` annotations -- `AisHubApiService` Retrofit interface: GET /0/down with lat/lon bounding box params - -### [APPROVED] AisHubSource object -- File: `app/src/main/kotlin/org/terst/nav/ais/AisHubSource.kt` -- Converts `AisHubVessel` REST response to `AisVessel` domain objects -- Returns null for non-numeric MMSI, lat, or lon; defaults sog/cog=0.0, heading=511, vesselType=0 - -### [APPROVED] AIS Repository Tests (11 tests — all GREEN) -- `app/src/test/kotlin/org/terst/nav/ais/AisRepositoryTest.kt` (7 tests) -- `app/src/test/kotlin/org/terst/nav/ais/AisHubSourceTest.kt` (4 tests) -- Harness: `/tmp/ais-repo-test-runner/` (JUnit5, org.terst.nav package, stub annotations for offline build) - -### [APPROVED] Section 7.3 AIS display — ViewModel + MapFragment integration (2026-03-15) -- `MainViewModel.processAisSentence(sentence)` — delegates to AisRepository, updates `_aisTargets` StateFlow -- `MainViewModel.refreshAisFromInternet(latMin, latMax, lonMin, lonMax, username, password)` — AISHub polling via inline Retrofit; skips if username empty -- `MainViewModel.aisTargets: StateFlow>` — exposed to observers -- `AisRepository.ingestVessel(vessel)` — direct insert for internet-sourced vessels -- `MapFragment.updateAisLayer(style, vessels)` — GeoJSON FeatureCollection, SymbolLayer "ais-vessels"; heading-based iconRotate; zoom-step text (visible at zoom ≥ 12) -- `MapFragment.addShipArrowImage(style)` — rasterises ic_ship_arrow.xml into style -- `ic_ship_arrow.xml` — pink arrow vector drawable for AIS icons -- `MainActivity.startAisHardwareFeed(host, port)` — TCP stub reads !AIVDM lines, forwards to viewModel -- `MainViewModelTest` — 3 new tests: valid type-1 adds vessel, same MMSI deduped, non-AIS stays empty -- JVM test harness: `/tmp/ais-vm-test-runner/` (3 tests — all GREEN) +- HHMMSS + DDMMYY → Unix epoch millis UTC -### [APPROVED] TrackRepository (2026-03-25) -- `android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt` -- `test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt` -- `isRecording` flag; `startTrack()` clears + starts; `stopTrack()`; `addPoint()` guards on isRecording; `getPoints()` returns snapshot -- 8 tests — all GREEN (`TrackRepositoryTest`) +### [APPROVED] AIS Integration (2026-03-15) +- `AisVdmParser.kt`: Parses !AIVDM/!AIVDO (Types 1, 2, 3, 5). +- `AisRepository.kt`: Upserts vessels and merges static data. +- `AisHubSource.kt`: Integration with AISHub REST API. +- `MapFragment`: GeoJSON layer for AIS vessels with rotation and labels. -### [APPROVED] Track ViewModel + Map overlay + Record FAB (2026-03-25) -- `MainViewModel`: TrackRepository wired in; exposes `isRecording: StateFlow`, `trackPoints: StateFlow>`; `startTrack()`, `stopTrack()`, `addGpsPoint(lat, lon, sogKnots, cogDeg)` -- `MapHandler.updateTrackLayer(style, points)`: lazy LineLayer init; red (#E53935) 3dp polyline from List -- `MainActivity`: stores `loadedStyle`; GPS flow feeds `viewModel.addGpsPoint()` (m/s→knots); observes `trackPoints` → `mapHandler.updateTrackLayer()`; observes `isRecording` → FAB icon toggle (ic_track_record / ic_close) -- `activity_main.xml`: `fab_record_track` FAB anchored top|end of bottom nav -- `drawable/ic_track_record.xml`: red dot record icon +### [APPROVED] Track Recording (2026-03-25) +- `MainViewModel`: TrackRepository wired; `addGpsPoint` enriches with environmental data. +- `MainActivity`: FAB for toggling recording; MapHandler for rendering. ## Next 3 Specific Steps -1. **Persist track to GPX/Room** — export recorded track as GPX file or store in Room DB -2. **Track stats in Log tab** — show elapsed time, distance, avg SOG while recording -3. **AnchorWatchHandler UI** — wire `AnchorWatchHandler` fully into SafetyFragment (currently stub) +1. **Unit Tests for Trip Generators** — Add JVM tests in `test-runner` for `TripReportGenerator` and `PreTripReportGenerator`. +2. **Persist track to Room** — Replace in-memory `pastTracks` with a Room database for persistence across sessions. +3. **AnchorWatchHandler UI** — Re-integrate `AnchorWatchHandler` UI into `activity_main.xml` and wire it to the Safety Dashboard. ## Scripts Added -- `test-runner/` — standalone Kotlin/JVM Gradle project; runs all 22 GPS/NMEA tests without Android SDK - - Command: `cd /workspace/nav/test-runner && GRADLE_USER_HOME=/tmp/gradle-home ./gradlew test` +- `test-runner/` — standalone Kotlin/JVM Gradle project for fast test cycles. ## Process Improvements -- Gradle builds blocked by Android SDK requirement; added `test-runner/` JVM-only subproject as reliable test runner -- All 22 tests verified GREEN via `test-runner/` JVM project (2026-03-14) \ No newline at end of file +- Stabilized UI layout by moving navigation out of the `CoordinatorLayout`. +- Duplicated core logic into `test-runner` to bypass Android SDK requirements for logic testing. diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 0f2eb91..3f09309 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -93,6 +93,14 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { setupBottomNavigation() setupHandlers() setupMap() + + findViewById(R.id.btn_nav_pretrip).setOnClickListener { + showReport(org.terst.nav.tripreport.PreTripReportFragment()) + } + + findViewById(R.id.btn_nav_tripreport).setOnClickListener { + showReport(org.terst.nav.tripreport.TripReportFragment()) + } fabRecordTrack.setOnClickListener { if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack() @@ -162,6 +170,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { fragmentContainer.visibility = View.GONE } + private fun showReport(fragment: androidx.fragment.app.Fragment) { + bottomSheetBehavior.isHideable = true + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + showOverlay(fragment) + } + override fun onActivateMob() { lifecycleScope.launch { LocationService.locationFlow.firstOrNull()?.let { gpsData -> diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml index a6b74b0..16410c0 100644 --- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml +++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml @@ -159,8 +159,7 @@ android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@id/label_conditions" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintEnd_toEndOf="parent"> + + + + + + + + + + + + + diff --git a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index 7953822..85dd2dd 100644 --- a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -5,22 +5,28 @@ class TrackRepository { var isRecording: Boolean = false private set - private val points = mutableListOf() + private val activePoints = mutableListOf() + private val pastTracks = mutableListOf>() fun startTrack() { - points.clear() + activePoints.clear() isRecording = true } fun stopTrack() { + if (isRecording && activePoints.isNotEmpty()) { + pastTracks.add(activePoints.toList()) + } isRecording = false } fun addPoint(point: TrackPoint): Boolean { if (!isRecording) return false - points.add(point) + activePoints.add(point) return true } - fun getPoints(): List = points.toList() + fun getPoints(): List = activePoints.toList() + + fun getPastTracks(): List> = pastTracks.toList() } -- cgit v1.2.3 From 2d86c0bcbc6c0f499406ef817b4bf54195756b45 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 5 Apr 2026 07:30:27 +0000 Subject: feat(ui): remove report section from instrument sheet, fix touch-through Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 8 --- .../main/res/layout/layout_instruments_sheet.xml | 57 ++-------------------- 2 files changed, 5 insertions(+), 60 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index bc9c7e5..6197475 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -92,14 +92,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { setupHandlers() setupMap() - findViewById(R.id.btn_nav_pretrip).setOnClickListener { - showReport(org.terst.nav.tripreport.PreTripReportFragment()) - } - - findViewById(R.id.btn_nav_tripreport).setOnClickListener { - showReport(org.terst.nav.tripreport.TripReportFragment()) - } - fabRecordTrack.setOnClickListener { if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack() } diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml index 16410c0..8c41ff3 100644 --- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml +++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml @@ -5,7 +5,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" - android:background="?attr/colorSurface"> + android:background="?attr/colorSurface" + android:clickable="true" + android:focusable="true"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent"> - - - - - - - - - - - - -- cgit v1.2.3 From 2cce533490baa4fe0d14f8541be1375e8069b164 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 6 Apr 2026 05:29:18 +0000 Subject: feat(ui): restructure instrument sheet layout — inline arrows, WaveView, ocean forecast section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full layout rewrite: 3×2 GridLayout instrument grid with inline DirectionArrowView for AWS/TWS/HDG/COG, depth+baro row, animated WaveView divider, and ocean-blue forecast section for Current/Waves/Swell. Stubs valueCurrDir/valueWaveDir as nullable in InstrumentHandler to compile; full handler rewrite follows in Task 6. Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 4 +- .../kotlin/org/terst/nav/ui/InstrumentHandler.kt | 8 +- .../main/res/layout/layout_instruments_sheet.xml | 328 +++++++++++++++++---- 3 files changed, 279 insertions(+), 61 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 6197475..023cb94 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -190,9 +190,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { valueDepth = findViewById(R.id.value_depth), valueBaro = findViewById(R.id.value_baro), valueCurrSpd = findViewById(R.id.value_curr_spd), - valueCurrDir = findViewById(R.id.value_curr_dir), + valueCurrDir = null, valueWaveHt = findViewById(R.id.value_wave_ht), - valueWaveDir = findViewById(R.id.value_wave_dir), + valueWaveDir = null, valueSwellHt = findViewById(R.id.value_swell_ht), valueSwellPer = findViewById(R.id.value_swell_per) ) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt index 91582c0..370d8cf 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt @@ -15,9 +15,9 @@ class InstrumentHandler( private val valueDepth: TextView, private val valueBaro: TextView, private val valueCurrSpd: TextView, - private val valueCurrDir: TextView, + private val valueCurrDir: TextView?, private val valueWaveHt: TextView, - private val valueWaveDir: TextView, + private val valueWaveDir: TextView?, private val valueSwellHt: TextView, private val valueSwellPer: TextView ) { @@ -57,9 +57,9 @@ class InstrumentHandler( swellPer: String? = null ) { currSpd?.let { valueCurrSpd.text = it } - currDir?.let { valueCurrDir.text = it } + currDir?.let { valueCurrDir?.text = it } waveHt?.let { valueWaveHt.text = it } - waveDir?.let { valueWaveDir.text = it } + waveDir?.let { valueWaveDir?.text = it } swellHt?.let { valueSwellHt.text = it } swellPer?.let { valueSwellPer.text = it } } diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml index 8c41ff3..33a7bd9 100644 --- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml +++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml @@ -1,34 +1,46 @@ - + - + + app:layout_constraintTop_toBottomOf="@id/drag_handle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> - + - + + + + + - + - + + + + - + - + + + + - + - + + + + + - + - + + + + - + - + + + + - + @@ -117,87 +221,201 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:orientation="vertical"> + android:orientation="vertical" + android:gravity="center" + android:padding="8dp"> - + + + + + + + android:orientation="vertical" + android:gravity="center" + android:padding="8dp"> - + + + + - - + - - - + + - - - + android:gravity="center" + android:padding="4dp"> + + + + + + + + + + - - - + android:gravity="center" + android:padding="4dp"> + + + + + + + + + + - - - + android:gravity="center" + android:padding="4dp"> + + + + + + + + + + - -- cgit v1.2.3 From 59d31d8d6198d5a8c2c4ba17cf9ad1b42a7e2018 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 6 Apr 2026 15:38:31 +0000 Subject: feat(tracks): show summary sheet on track stop; 2-min minimum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 7 + .../kotlin/org/terst/nav/track/TrackRepository.kt | 17 +- .../org/terst/nav/track/TrackSummarySheet.kt | 95 ++++++++++ .../main/kotlin/org/terst/nav/ui/MainViewModel.kt | 13 +- .../main/res/layout/layout_track_summary_sheet.xml | 199 +++++++++++++++++++++ 5 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/track/TrackSummarySheet.kt create mode 100644 android-app/app/src/main/res/layout/layout_track_summary_sheet.xml (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 022b748..d64ce8d 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -109,6 +109,13 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { fabRecordTrack.contentDescription = if (recording) "Stop Recording" else "Record Track" } } + lifecycleScope.launch { + viewModel.trackSummary.collect { summary -> + org.terst.nav.track.TrackSummarySheet + .from(summary, viewModel.trackStartMs) + .show(supportFragmentManager, "track_summary") + } + } } private fun setupBottomSheet() { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index ed32497..228a484 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -12,7 +12,10 @@ class TrackRepository(context: Context) { private set private val activePoints = mutableListOf() - private var trackStartMs = 0L + + /** Epoch-ms when the current (or last) track started. Exposed for the summary sheet title. */ + var trackStartMs: Long = 0L + private set // Loaded lazily from Documents/Nav/ on first access private val _pastTracks = mutableListOf>() @@ -26,18 +29,22 @@ class TrackRepository(context: Context) { /** * Stops the active track, computes a [TrackSummary], persists the track - * to shared storage, and returns the summary. Returns null if no points - * were recorded. + * to shared storage, and returns the summary. Returns null if the track + * was too short (< 2 minutes) or had no points. */ + suspend fun stopTrack(): TrackSummary? = withContext(Dispatchers.IO) { if (!isRecording) return@withContext null isRecording = false val points = activePoints.toList() activePoints.clear() - if (points.isEmpty()) return@withContext null + if (points.size < 2) return@withContext null val summary = summarise(points) - _pastTracks.add(0, points) // prepend so most recent is first + // Discard tracks shorter than 2 minutes — likely accidental taps + if (summary.durationMs < 2 * 60_000L) return@withContext null + + _pastTracks.add(0, points) storage.saveTrack(points, trackStartMs) summary } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackSummarySheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackSummarySheet.kt new file mode 100644 index 0000000..8d9d7c7 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackSummarySheet.kt @@ -0,0 +1,95 @@ +package org.terst.nav.track + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.terst.nav.R +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale + +class TrackSummarySheet : BottomSheetDialogFragment() { + + companion object { + private const val ARG_DISTANCE = "distance_nm" + private const val ARG_DURATION = "duration_ms" + private const val ARG_MAX_SOG = "max_sog" + private const val ARG_AVG_SOG = "avg_sog" + private const val ARG_AVG_WIND = "avg_wind" // -1 if absent + private const val ARG_AVG_WAVE = "avg_wave_m" // -1 if absent + private const val ARG_START_MS = "start_ms" + + fun from(summary: TrackSummary, startMs: Long) = TrackSummarySheet().apply { + arguments = Bundle().apply { + putDouble(ARG_DISTANCE, summary.distanceNm) + putLong(ARG_DURATION, summary.durationMs) + putDouble(ARG_MAX_SOG, summary.maxSogKt) + putDouble(ARG_AVG_SOG, summary.avgSogKt) + putDouble(ARG_AVG_WIND, summary.avgWindKt ?: -1.0) + putDouble(ARG_AVG_WAVE, summary.avgWaveHeightM ?: -1.0) + putLong(ARG_START_MS, startMs) + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + inflater.inflate(R.layout.layout_track_summary_sheet, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val args = requireArguments() + val distanceNm = args.getDouble(ARG_DISTANCE) + val durationMs = args.getLong(ARG_DURATION) + val maxSog = args.getDouble(ARG_MAX_SOG) + val avgSog = args.getDouble(ARG_AVG_SOG) + val avgWind = args.getDouble(ARG_AVG_WIND).takeIf { it >= 0 } + val avgWaveM = args.getDouble(ARG_AVG_WAVE).takeIf { it >= 0 } + val startMs = args.getLong(ARG_START_MS) + + val titleFmt = DateTimeFormatter.ofPattern("dd MMM HH:mm", Locale.getDefault()) + .withZone(ZoneId.systemDefault()) + view.findViewById(R.id.summary_title).text = + "Track · ${titleFmt.format(Instant.ofEpochMilli(startMs))}" + + view.findViewById(R.id.summary_distance).text = + "%.1f".format(Locale.getDefault(), distanceNm) + + view.findViewById(R.id.summary_duration).text = formatDuration(durationMs) + + view.findViewById(R.id.summary_max_sog).text = + "%.1f".format(Locale.getDefault(), maxSog) + + view.findViewById(R.id.summary_avg_sog).text = + "%.1f".format(Locale.getDefault(), avgSog) + + val conditionsRow = view.findViewById(R.id.summary_conditions_row) + val windCell = view.findViewById(R.id.summary_wind_cell) + val waveCell = view.findViewById(R.id.summary_wave_cell) + + if (avgWind != null) { + windCell.visibility = View.VISIBLE + view.findViewById(R.id.summary_avg_wind).text = + "%.0f".format(Locale.getDefault(), avgWind) + } + if (avgWaveM != null) { + waveCell.visibility = View.VISIBLE + val waveFt = avgWaveM * 3.28084 + view.findViewById(R.id.summary_avg_wave).text = + "%.1f".format(Locale.getDefault(), waveFt) + } + if (avgWind != null || avgWaveM != null) { + conditionsRow.visibility = View.VISIBLE + } + } + + private fun formatDuration(ms: Long): String { + val totalMinutes = ms / 60_000 + val hours = totalMinutes / 60 + val minutes = totalMinutes % 60 + return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m" + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt index c1707ab..5797138 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt @@ -15,10 +15,14 @@ import org.terst.nav.data.repository.WeatherRepository import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.terst.nav.track.TrackSummary import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory @@ -60,18 +64,25 @@ class MainViewModel( private val _pastTracks = MutableStateFlow>>(emptyList()) val pastTracks: StateFlow>> = _pastTracks.asStateFlow() + private val _trackSummary = MutableSharedFlow(replay = 0) + val trackSummary: SharedFlow = _trackSummary.asSharedFlow() + fun startTrack() { trackRepository.startTrack() _trackPoints.value = emptyList() _isRecording.value = true } + /** Epoch-ms when the current track started — for the summary sheet title. */ + val trackStartMs: Long get() = trackRepository.trackStartMs + fun stopTrack() { viewModelScope.launch { - trackRepository.stopTrack() + val summary = trackRepository.stopTrack() _pastTracks.value = trackRepository.getPastTracks() _trackPoints.value = emptyList() _isRecording.value = false + summary?.let { _trackSummary.emit(it) } } } diff --git a/android-app/app/src/main/res/layout/layout_track_summary_sheet.xml b/android-app/app/src/main/res/layout/layout_track_summary_sheet.xml new file mode 100644 index 0000000..a26b76e --- /dev/null +++ b/android-app/app/src/main/res/layout/layout_track_summary_sheet.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3 From 676314e3b5ad2445e64120c691fd1c2671076ebb Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 6 Apr 2026 16:22:42 +0000 Subject: feat(map): layer manager — satellite/charts/hybrid + wind toggle, long-press picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 41 +++---- .../kotlin/org/terst/nav/ui/LayerPickerSheet.kt | 48 ++++++++ .../kotlin/org/terst/nav/ui/MapLayerManager.kt | 132 +++++++++++++++++++++ .../main/res/layout/layout_layer_picker_sheet.xml | 104 ++++++++++++++++ 4 files changed, 303 insertions(+), 22 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt create mode 100644 android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 06c45ca..de1f4dd 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -30,10 +30,6 @@ import java.util.Locale import org.maplibre.android.MapLibre import org.maplibre.android.maps.MapView import org.maplibre.android.maps.Style -import org.maplibre.android.style.layers.PropertyFactory -import org.maplibre.android.style.layers.RasterLayer -import org.maplibre.android.style.sources.RasterSource -import org.maplibre.android.style.sources.TileSet import org.terst.nav.ui.* import org.terst.nav.ui.doc.DocFragment import org.terst.nav.ui.safety.SafetyFragment @@ -48,6 +44,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private var instrumentHandler: InstrumentHandler? = null private var mapHandler: MapHandler? = null private val loadedStyleFlow = MutableStateFlow(null) + private lateinit var layerManager: MapLayerManager private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var fragmentContainer: FrameLayout @@ -265,9 +262,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } private fun setupMap() { + layerManager = MapLayerManager(this) mapView = findViewById(R.id.mapView) if (NavApplication.isTesting) return - + mapView?.onCreate(null) mapView?.getMapAsync { maplibreMap -> mapHandler = MapHandler(maplibreMap) @@ -282,29 +280,28 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } } } - val style = Style.Builder() - .fromUri("https://tiles.openfreemap.org/styles/bright") // Base for labels if needed, or use satellite only - .withSource(RasterSource("satellite-source", - TileSet("2.2.0", "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}"), 256)) - .withLayer(RasterLayer("satellite-layer", "satellite-source")) - .withSource(RasterSource("wind-source", - TileSet("2.2.0", "https://tile.openweathermap.org/map/wind_new/{z}/{x}/{y}.png?appid=ae2a038149aa0900d1bc74160aa2a37e"), 256)) - .withLayer(RasterLayer("wind-layer", "wind-source").apply { - setProperties(PropertyFactory.rasterOpacity(0.6f)) - }) - .withSource(RasterSource("openseamap-source", - TileSet("2.2.0", "https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png").also { - it.setMaxZoom(18f) - }, 256)) - .withLayer(RasterLayer("openseamap-layer", "openseamap-source")) - - maplibreMap.setStyle(style) { style -> + + val styleBuilder = Style.Builder() + .fromUri("https://tiles.openfreemap.org/styles/bright") + layerManager.addToStyleBuilder(styleBuilder) + + maplibreMap.setStyle(styleBuilder) { style -> loadedStyleFlow.value = style val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor) val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow) val userBitmap = rasterizeDrawable(R.drawable.ic_ship_arrow) mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap, userBitmap) } + + maplibreMap.addOnMapLongClickListener { _ -> + val currentStyle = loadedStyleFlow.value ?: return@addOnMapLongClickListener true + LayerPickerSheet( + manager = layerManager, + onBaseChanged = { preset -> layerManager.setBasePreset(currentStyle, preset) }, + onWindChanged = { enabled -> layerManager.setWindEnabled(currentStyle, enabled) } + ).show(supportFragmentManager, "layer_picker") + true + } } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt new file mode 100644 index 0000000..48dc808 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt @@ -0,0 +1,48 @@ +package org.terst.nav.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.chip.ChipGroup +import com.google.android.material.switchmaterial.SwitchMaterial +import org.terst.nav.R + +class LayerPickerSheet( + private val manager: MapLayerManager, + private val onBaseChanged: (MapBasePreset) -> Unit, + private val onWindChanged: (Boolean) -> Unit +) : BottomSheetDialogFragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + inflater.inflate(R.layout.layout_layer_picker_sheet, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val chipGroup = view.findViewById(R.id.chip_group_base) + val windSwitch = view.findViewById(R.id.switch_wind) + + // Set initial state from manager + val chipId = when (manager.basePreset) { + MapBasePreset.SATELLITE -> R.id.chip_satellite + MapBasePreset.CHARTS -> R.id.chip_charts + MapBasePreset.HYBRID -> R.id.chip_hybrid + } + chipGroup.check(chipId) + windSwitch.isChecked = manager.windEnabled + + chipGroup.setOnCheckedStateChangeListener { _, checkedIds -> + val preset = when (checkedIds.firstOrNull()) { + R.id.chip_satellite -> MapBasePreset.SATELLITE + R.id.chip_charts -> MapBasePreset.CHARTS + R.id.chip_hybrid -> MapBasePreset.HYBRID + else -> return@setOnCheckedStateChangeListener + } + onBaseChanged(preset) + } + + windSwitch.setOnCheckedChangeListener { _, checked -> + onWindChanged(checked) + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt new file mode 100644 index 0000000..cf78ec2 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt @@ -0,0 +1,132 @@ +package org.terst.nav.ui + +import android.content.Context +import org.maplibre.android.maps.Style +import org.maplibre.android.style.layers.PropertyFactory +import org.maplibre.android.style.layers.RasterLayer +import org.maplibre.android.style.sources.RasterSource +import org.maplibre.android.style.sources.TileSet + +enum class MapBasePreset { SATELLITE, CHARTS, HYBRID } + +/** + * Manages the raster layer stack: base imagery (satellite / NOAA charts / hybrid) + * and the wind overlay. Persists selections across sessions. + * + * All sources are registered at style-build time; this class only toggles + * layer visibility — no runtime source swapping required. + */ +class MapLayerManager(context: Context) { + + private val prefs = context.getSharedPreferences("map_layers", Context.MODE_PRIVATE) + + var basePreset: MapBasePreset = loadBasePreset() + private set + + var windEnabled: Boolean = prefs.getBoolean(KEY_WIND, true) + private set + + // ── Source / Layer IDs ──────────────────────────────────────────────────── + + companion object { + const val SOURCE_SATELLITE = "satellite-source" + const val SOURCE_CHARTS = "charts-source" + const val SOURCE_WIND = "wind-source" + const val SOURCE_SEAMARKS = "openseamap-source" + + const val LAYER_SATELLITE = "satellite-layer" + const val LAYER_CHARTS = "charts-layer" + const val LAYER_WIND = "wind-layer" + const val LAYER_SEAMARKS = "openseamap-layer" + + private const val KEY_BASE = "base_preset" + private const val KEY_WIND = "wind_enabled" + + // Tile URLs + private const val URL_SATELLITE = + "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}" + private const val URL_CHARTS = + "https://tileservice.charts.noaa.gov/tiles/50000_1/{z}/{x}/{y}.png" + private const val URL_WIND = + "https://tile.openweathermap.org/map/wind_new/{z}/{x}/{y}.png?appid=ae2a038149aa0900d1bc74160aa2a37e" + private const val URL_SEAMARKS = + "https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png" + } + + /** + * Registers all raster sources and layers into the [Style.Builder]. + * Call this before [Style.Builder.build]. Layers are added in the correct + * order; MapHandler's vector layers go on top after style loads. + */ + fun addToStyleBuilder(builder: Style.Builder) { + // ── Sources ─────────────────────────────────────────────────────────── + builder.withSource(RasterSource(SOURCE_SATELLITE, + TileSet("2.2.0", URL_SATELLITE), 256)) + + builder.withSource(RasterSource(SOURCE_CHARTS, + TileSet("2.2.0", URL_CHARTS), 256)) + + builder.withSource(RasterSource(SOURCE_WIND, + TileSet("2.2.0", URL_WIND), 256)) + + builder.withSource(RasterSource(SOURCE_SEAMARKS, + TileSet("2.2.0", URL_SEAMARKS).also { it.setMaxZoom(18f) }, 256)) + + // ── Layers (bottom → top within raster stack) ───────────────────────── + builder.withLayer(RasterLayer(LAYER_SATELLITE, SOURCE_SATELLITE).apply { + setProperties(PropertyFactory.rasterOpacity(1f)) + setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_SATELLITE))) + }) + + builder.withLayer(RasterLayer(LAYER_CHARTS, SOURCE_CHARTS).apply { + setProperties(PropertyFactory.rasterOpacity(opacityFor(basePreset))) + setProperties(PropertyFactory.visibility(visibilityFor(basePreset, LAYER_CHARTS))) + }) + + builder.withLayer(RasterLayer(LAYER_WIND, SOURCE_WIND).apply { + setProperties(PropertyFactory.rasterOpacity(0.6f)) + setProperties(PropertyFactory.visibility(if (windEnabled) "visible" else "none")) + }) + + builder.withLayer(RasterLayer(LAYER_SEAMARKS, SOURCE_SEAMARKS).apply { + setProperties(PropertyFactory.visibility("visible")) + }) + } + + /** Apply a new base preset to a live style. */ + fun setBasePreset(style: Style, preset: MapBasePreset) { + basePreset = preset + prefs.edit().putString(KEY_BASE, preset.name).apply() + + style.getLayer(LAYER_SATELLITE)?.setProperties( + PropertyFactory.visibility(visibilityFor(preset, LAYER_SATELLITE))) + style.getLayer(LAYER_CHARTS)?.let { + it.setProperties(PropertyFactory.visibility(visibilityFor(preset, LAYER_CHARTS))) + (it as? RasterLayer)?.setProperties(PropertyFactory.rasterOpacity(opacityFor(preset))) + } + } + + /** Toggle wind overlay on a live style. */ + fun setWindEnabled(style: Style, enabled: Boolean) { + windEnabled = enabled + prefs.edit().putBoolean(KEY_WIND, enabled).apply() + style.getLayer(LAYER_WIND)?.setProperties( + PropertyFactory.visibility(if (enabled) "visible" else "none")) + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private fun visibilityFor(preset: MapBasePreset, layerId: String): String = when (layerId) { + LAYER_SATELLITE -> if (preset == MapBasePreset.SATELLITE || preset == MapBasePreset.HYBRID) "visible" else "none" + LAYER_CHARTS -> if (preset == MapBasePreset.CHARTS || preset == MapBasePreset.HYBRID) "visible" else "none" + else -> "visible" + } + + private fun opacityFor(preset: MapBasePreset): Float = + if (preset == MapBasePreset.HYBRID) 0.75f else 1f + + private fun loadBasePreset(): MapBasePreset = + prefs.getString(KEY_BASE, null)?.let { + runCatching { MapBasePreset.valueOf(it) }.getOrNull() + } ?: MapBasePreset.SATELLITE +} diff --git a/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml b/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml new file mode 100644 index 0000000..c424606 --- /dev/null +++ b/android-app/app/src/main/res/layout/layout_layer_picker_sheet.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3 From 5ee2dd8925afa858f466ae63db3f7df5c7516953 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Tue, 7 Apr 2026 06:42:51 +0000 Subject: fix(track): fix silent GPX save failure + add stop friction + quit button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 32 +++++++++++++++++- .../kotlin/org/terst/nav/track/TrackRepository.kt | 4 ++- .../kotlin/org/terst/nav/track/TrackStorage.kt | 39 ++++++++++++++++++++-- .../app/src/main/res/layout/activity_main.xml | 18 ++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 0309364..6263e13 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -9,6 +9,7 @@ import android.media.MediaPlayer import android.os.Build import android.os.Bundle import android.util.Log +import android.view.HapticFeedbackConstants import android.view.View import android.widget.FrameLayout import androidx.activity.result.contract.ActivityResultContracts @@ -51,6 +52,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private lateinit var fabRecordTrack: FloatingActionButton private lateinit var fabMob: FloatingActionButton private lateinit var fabRecenter: MaterialButton + private lateinit var btnQuit: MaterialButton private lateinit var bottomSheet: CardView private lateinit var bottomNav: BottomNavigationView private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) } @@ -81,6 +83,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { fabRecordTrack = findViewById(R.id.fab_record_track) fabMob = findViewById(R.id.fab_mob) fabRecenter = findViewById(R.id.fab_recenter) + btnQuit = findViewById(R.id.btn_quit) bottomSheet = findViewById(R.id.instrument_bottom_sheet) bottomNav = findViewById(R.id.bottom_navigation) @@ -89,8 +92,16 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { setupHandlers() setupMap() + // Single tap starts; long press stops (requires deliberate intent mid-sail) fabRecordTrack.setOnClickListener { - if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack() + if (!viewModel.isRecording.value) viewModel.startTrack() + } + fabRecordTrack.setOnLongClickListener { + if (viewModel.isRecording.value) { + it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + viewModel.stopTrack() + true + } else false } fabMob.setOnClickListener { onActivateMob() } @@ -98,6 +109,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { fabRecenter.setOnClickListener { mapHandler?.recenter() } + + btnQuit.setOnClickListener { onQuitRequested() } // Observe immediately — pure UI state, not gated on GPS permission lifecycleScope.launch { viewModel.isRecording.collect { recording -> @@ -177,6 +190,23 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { showOverlay(fragment) } + private fun onQuitRequested() { + if (viewModel.isRecording.value) { + androidx.appcompat.app.AlertDialog.Builder(this) + .setMessage("Recording in progress. Quit and discard the current track?") + .setPositiveButton("Quit") { _, _ -> exitApp() } + .setNegativeButton("Cancel", null) + .show() + } else { + exitApp() + } + } + + private fun exitApp() { + stopService(Intent(this, LocationService::class.java)) + finishAffinity() + } + override fun onActivateMob() { lifecycleScope.launch { LocationService.locationFlow.firstOrNull()?.let { gpsData -> diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index 228a484..7ef67af 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -1,6 +1,7 @@ package org.terst.nav.track import android.content.Context +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -45,7 +46,8 @@ class TrackRepository(context: Context) { if (summary.durationMs < 2 * 60_000L) return@withContext null _pastTracks.add(0, points) - storage.saveTrack(points, trackStartMs) + val saved = storage.saveTrack(points, trackStartMs) + if (!saved) Log.e("TrackRepository", "GPX save failed — ${points.size} points will be lost on restart") summary } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt index 620431c..08e1dc9 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackStorage.kt @@ -5,11 +5,14 @@ import android.content.Context import android.os.Build import android.os.Environment import android.provider.MediaStore +import android.util.Log import java.io.File import java.time.Instant import java.time.ZoneOffset import java.time.format.DateTimeFormatter +private const val TAG = "TrackStorage" + /** * Persists completed tracks as GPX files in the shared Documents/Nav/ folder. * @@ -56,19 +59,49 @@ class TrackStorage(private val context: Context) { // ── API 29+ ────────────────────────────────────────────────────────────── private fun saveViaMediaStore(fileName: String, gpx: String): Boolean { + // Guard: external storage must be mounted before touching MediaStore + val storageState = Environment.getExternalStorageState() + if (storageState != Environment.MEDIA_MOUNTED) { + Log.e(TAG, "External storage not mounted (state=$storageState) — cannot save $fileName") + return false + } + + // IS_PENDING marks the entry as in-progress, preventing a race condition on + // Android 10-11 where insert() succeeds but openOutputStream() returns null + // because the file hasn't been physically created on disk yet. val values = ContentValues().apply { put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName) put(MediaStore.Files.FileColumns.MIME_TYPE, "application/gpx+xml") put(MediaStore.Files.FileColumns.RELATIVE_PATH, "Documents/Nav/") + put(MediaStore.MediaColumns.IS_PENDING, 1) } val uri = context.contentResolver.insert( MediaStore.Files.getContentUri("external"), values - ) ?: return false + ) ?: run { + Log.e(TAG, "MediaStore insert returned null for $fileName") + return false + } return runCatching { - context.contentResolver.openOutputStream(uri)?.use { it.write(gpx.toByteArray()) } + val stream = context.contentResolver.openOutputStream(uri) + if (stream == null) { + context.contentResolver.delete(uri, null, null) + Log.e(TAG, "openOutputStream null for $fileName — deleted orphan entry") + return@runCatching false + } + stream.use { it.write(gpx.toByteArray()) } + + // Clear IS_PENDING so the file is visible to other apps and file managers + val update = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) } + context.contentResolver.update(uri, update, null, null) + + Log.d(TAG, "Saved $fileName (${gpx.length} bytes) → $uri") true - }.getOrDefault(false) + }.getOrElse { e -> + Log.e(TAG, "Write failed for $fileName: ${e.message}") + runCatching { context.contentResolver.delete(uri, null, null) } + false + } } private fun loadViaMediaStore(): List> { diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index b8df5c9..0734476 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -30,6 +30,24 @@ android:visibility="gone" android:background="?attr/colorSurface" /> + + + Date: Fri, 10 Apr 2026 09:30:55 +0000 Subject: Area conditions HUD redesign: map-center conditions refresh + boat HUD strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../main/kotlin/org/terst/nav/LocationService.kt | 11 ++ .../src/main/kotlin/org/terst/nav/MainActivity.kt | 86 ++++++---- .../org/terst/nav/data/model/MarineConditions.kt | 1 + .../terst/nav/data/repository/WeatherRepository.kt | 1 + .../kotlin/org/terst/nav/ui/InstrumentHandler.kt | 86 ++++------ .../app/src/main/res/drawable/ic_crosshair.xml | 28 +++ .../app/src/main/res/layout/activity_main.xml | 24 +++ .../main/res/layout/layout_instruments_sheet.xml | 187 +++------------------ .../app/src/main/res/layout/layout_nav_hud.xml | 161 ++++++++++++++++++ 9 files changed, 329 insertions(+), 256 deletions(-) create mode 100644 android-app/app/src/main/res/drawable/ic_crosshair.xml create mode 100644 android-app/app/src/main/res/layout/layout_nav_hud.xml (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt index b18db8d..41fb2ec 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.asStateFlow import org.terst.nav.nmea.NmeaParser import org.terst.nav.nmea.NmeaStreamManager +import org.terst.nav.sensors.BoatSpeedData import org.terst.nav.sensors.DepthData import org.terst.nav.sensors.HeadingData import org.terst.nav.sensors.WindData @@ -128,6 +129,13 @@ class LocationService : Service() { } } + // Collect NMEA Boat Speed Data + serviceScope.launch { + nmeaStreamManager.nmeaBoatSpeedData.collectLatest { bspData -> + _nmeaBoatSpeedDataFlow.emit(bspData) + } + } + locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { locationResult.lastLocation?.let { location -> @@ -393,6 +401,9 @@ class LocationService : Service() { private val _nmeaHeadingDataFlow = MutableSharedFlow(replay = 1) val nmeaHeadingDataFlow: SharedFlow get() = _nmeaHeadingDataFlow + private val _nmeaBoatSpeedDataFlow = MutableSharedFlow(replay = 1) + val nmeaBoatSpeedData: SharedFlow get() = _nmeaBoatSpeedDataFlow + private val _currentPowerMode = MutableStateFlow(PowerMode.FULL) val currentPowerMode: StateFlow get() = _currentPowerMode } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 6d02014..21aa55b 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -11,6 +11,7 @@ import android.os.Bundle import android.view.HapticFeedbackConstants import android.view.View import android.widget.FrameLayout +import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -57,6 +58,14 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private lateinit var btnQuit: MaterialButton private lateinit var bottomSheet: CardView private lateinit var bottomNav: BottomNavigationView + private lateinit var mapCrosshair: View + + // HUD TextViews — wired to layout_nav_hud.xml + private lateinit var hudSog: TextView + private lateinit var hudCog: TextView + private lateinit var hudBsp: TextView + private lateinit var hudDepth: TextView + private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) } private val viewModel: MainViewModel by viewModels() @@ -88,6 +97,19 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { btnQuit = findViewById(R.id.btn_quit) bottomSheet = findViewById(R.id.instrument_bottom_sheet) bottomNav = findViewById(R.id.bottom_navigation) + mapCrosshair = findViewById(R.id.map_crosshair) + + // HUD views (inside the included layout_nav_hud) + hudSog = findViewById(R.id.hud_sog) + hudCog = findViewById(R.id.hud_cog) + hudBsp = findViewById(R.id.hud_bsp) + hudDepth = findViewById(R.id.hud_depth) + + // Initialise HUD with dashes + hudSog.text = "—" + hudCog.text = "—" + hudBsp.text = "—" + hudDepth.text = "—" setupBottomSheet() setupBottomNavigation() @@ -184,12 +206,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { fragmentContainer.visibility = View.GONE } - private fun showReport(fragment: androidx.fragment.app.Fragment) { - bottomSheetBehavior.isHideable = true - bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN - showOverlay(fragment) - } - private fun onQuitRequested() { if (viewModel.isRecording.value) { androidx.appcompat.app.AlertDialog.Builder(this) @@ -222,18 +238,11 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private fun setupHandlers() { instrumentHandler = InstrumentHandler( - valueAws = findViewById(R.id.value_aws), - valueTws = findViewById(R.id.value_tws), - valueHdg = findViewById(R.id.value_hdg), - valueCog = findViewById(R.id.value_cog), - valueBsp = findViewById(R.id.value_bsp), - valueSog = findViewById(R.id.value_sog), - valueDepth = findViewById(R.id.value_depth), - valueBaro = findViewById(R.id.value_baro), - arrowAws = findViewById(R.id.arrow_aws), - arrowTws = findViewById(R.id.arrow_tws), - arrowHdg = findViewById(R.id.arrow_hdg), - arrowCog = findViewById(R.id.arrow_cog), + valueTws = findViewById(R.id.value_tws), + arrowTws = findViewById(R.id.arrow_tws), + bearingTws = findViewById(R.id.bearing_tws), + valueTemp = findViewById(R.id.value_temp), + valueBaro = findViewById(R.id.value_baro), valueCurrSpd = findViewById(R.id.value_curr_spd), valueWaveHt = findViewById(R.id.value_wave_ht), valueSwellHt = findViewById(R.id.value_swell_ht), @@ -246,12 +255,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { bearingSwell = findViewById(R.id.bearing_swell), waveView = findViewById(R.id.wave_divider) ) - instrumentHandler?.updateDisplay( - aws = "—", tws = "—", hdg = "—", - cog = "—", bsp = "—", sog = "—", - baro = "—" + instrumentHandler?.updateConditions( + tws = "—", baro = "—", currSpd = "—" ) - instrumentHandler?.updateConditions(currSpd = "—") } private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt() @@ -302,11 +308,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { lifecycleScope.launch { mapHandler!!.isFollowing.collect { following -> + mapCrosshair.visibility = if (following) View.GONE else View.VISIBLE if (following) { fadeOut(fabRecenter, gone = true) - fadeIn(bottomSheet, bottomNav, fabMob, fabRecordTrack) + fadeIn(bottomNav, fabMob, fabRecordTrack) } else { - fadeOut(bottomSheet, bottomNav, fabMob, fabRecordTrack, gone = true) + fadeOut(bottomNav, fabMob, fabRecordTrack, gone = true) fadeIn(fabRecenter) } } @@ -339,6 +346,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { corners.minOf { it.longitude }, corners.maxOf { it.longitude } ) } + // Refresh conditions for current map center (works in both follow + pan mode) + maplibreMap.cameraPosition.target?.let { center -> + viewModel.loadConditions(center.latitude, center.longitude) + } } maplibreMap.addOnMapLongClickListener { _ -> @@ -360,11 +371,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude) mapHandler?.updateUserPosition(gpsData.latitude, gpsData.longitude, gpsData.cog.toFloat()) viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, gpsData.sog, gpsData.cog) - instrumentHandler?.updateDisplay( - sog = "%.1f".format(Locale.getDefault(), gpsData.sog), - cog = "%.0f°".format(Locale.getDefault(), gpsData.cog), - cogBearingDeg = gpsData.cog.toFloat() - ) + // HUD — SOG and COG come from GPS + hudSog.text = "%.1f".format(Locale.getDefault(), gpsData.sog) + hudCog.text = "%.0f°".format(Locale.getDefault(), gpsData.cog) if (!conditionsLoaded) { conditionsLoaded = true viewModel.loadConditions(gpsData.latitude, gpsData.longitude) @@ -391,18 +400,17 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { lifecycleScope.launch { LocationService.barometerStatus.collect { status -> if (status.history.isNotEmpty()) { - instrumentHandler?.updateDisplay(baro = status.formatPressure()) + instrumentHandler?.updateConditions(baro = status.formatPressure()) } } } lifecycleScope.launch { viewModel.marineConditions.collect { c -> if (c == null) return@collect - instrumentHandler?.updateDisplay( - tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) }, - twsBearingDeg = c.windDirDeg?.toFloat() - ) instrumentHandler?.updateConditions( + tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) }, + twsBearingDeg = c.windDirDeg?.toFloat(), + tempC = c.tempC, currSpd = c.currentSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) } ?: "—", currDirDeg = c.currentDirDeg?.toFloat(), waveHeightM = c.waveHeightM, @@ -420,7 +428,13 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } lifecycleScope.launch { LocationService.nmeaDepthDataFlow.collect { depthData -> - instrumentHandler?.updateDisplay(depthM = depthData.depthMeters) + // Update HUD depth (converted to feet) + hudDepth.text = "%.1f".format(Locale.getDefault(), depthData.depthMeters * 3.28084) + } + } + lifecycleScope.launch { + LocationService.nmeaBoatSpeedData.collect { bspData -> + hudBsp.text = "%.1f".format(Locale.getDefault(), bspData.bspKnots) } } lifecycleScope.launch { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt index 3cde023..557d6a2 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/data/model/MarineConditions.kt @@ -7,6 +7,7 @@ package org.terst.nav.data.model data class MarineConditions( val windSpeedKt: Double?, val windDirDeg: Double?, + val tempC: Double?, val waveHeightM: Double?, val waveDirDeg: Double?, val swellHeightM: Double?, diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt index c79366d..dc47a20 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt @@ -59,6 +59,7 @@ class WeatherRepository( MarineConditions( windSpeedKt = w.windspeed10m.firstOrNull(), windDirDeg = w.winddirection10m.firstOrNull(), + tempC = w.temperature2m.firstOrNull(), waveHeightM = m.waveHeight.firstOrNull(), waveDirDeg = m.waveDirection.firstOrNull(), swellHeightM = m.swellWaveHeight.firstOrNull(), diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt index cb59a3a..48ebb3b 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt @@ -25,10 +25,11 @@ fun formatPeriod(sec: Double, locale: Locale = Locale.getDefault()): String = // ── InstrumentHandler ──────────────────────────────────────────────────────── /** - * Drives all text fields, direction arrows, and the wave view in the - * instrument bottom sheet. + * Drives the area-conditions bottom sheet: wind header row, wave view, + * and the forecast section (current / waves / swell). * - * Forecast [DirectionArrowView] instances are initialised with OCEAN style. + * All boat-instrument data (SOG, COG, BSP, Depth) is handled directly + * in MainActivity via the HUD strip. * * Units contract: * - Speed values: pre-formatted strings in knots (caller's responsibility) @@ -37,34 +38,26 @@ fun formatPeriod(sec: Double, locale: Locale = Locale.getDefault()): String = * into bearing TextViews by this class */ class InstrumentHandler( - // ── Instrument section TextViews ───────────────────────────────── - private val valueAws: TextView, - private val valueTws: TextView, - private val valueHdg: TextView, - private val valueCog: TextView, - private val valueBsp: TextView, - private val valueSog: TextView, - private val valueDepth: TextView, - private val valueBaro: TextView, - // ── Instrument section DirectionArrowViews ─────────────────────── - private val arrowAws: DirectionArrowView, - private val arrowTws: DirectionArrowView, - private val arrowHdg: DirectionArrowView, - private val arrowCog: DirectionArrowView, - // ── Forecast section TextViews ─────────────────────────────────── + // ── Conditions header ──────────────────────────────────────────────── + private val valueTws: TextView, + private val arrowTws: DirectionArrowView, + private val bearingTws: TextView, + private val valueTemp: TextView, + private val valueBaro: TextView, + // ── Forecast section TextViews ─────────────────────────────────────── private val valueCurrSpd: TextView, private val valueWaveHt: TextView, private val valueSwellHt: TextView, private val valueSwellPer: TextView, - // ── Forecast section DirectionArrowViews ───────────────────────── + // ── Forecast section DirectionArrowViews ───────────────────────────── private val arrowCurr: DirectionArrowView, private val arrowWaves: DirectionArrowView, private val arrowSwell: DirectionArrowView, - // ── Forecast section bearing TextViews ─────────────────────────── + // ── Forecast section bearing TextViews ─────────────────────────────── private val bearingCurr: TextView, private val bearingWaves: TextView, private val bearingSwell: TextView, - // ── Wave view ──────────────────────────────────────────────────── + // ── Wave view ──────────────────────────────────────────────────────── private val waveView: WaveView ) { init { @@ -74,48 +67,27 @@ class InstrumentHandler( } /** - * Updates instrument-section text and arrows. + * Updates all area-conditions fields. * Null arguments leave the current value unchanged. - * [depthM] is raw metres — converted to feet internally. - */ - fun updateDisplay( - aws: String? = null, awsBearingDeg: Float? = null, - tws: String? = null, twsBearingDeg: Float? = null, - hdg: String? = null, hdgBearingDeg: Float? = null, - cog: String? = null, cogBearingDeg: Float? = null, - bsp: String? = null, - sog: String? = null, - depthM: Double? = null, - baro: String? = null - ) { - aws?.let { valueAws.text = it } - tws?.let { valueTws.text = it } - hdg?.let { valueHdg.text = it } - cog?.let { valueCog.text = it } - bsp?.let { valueBsp.text = it } - sog?.let { valueSog.text = it } - baro?.let { valueBaro.text = it } - depthM?.let { valueDepth.text = formatFt(metresToFeet(it)) } - - awsBearingDeg?.let { arrowAws.bearing = it } - twsBearingDeg?.let { arrowTws.bearing = it } - hdgBearingDeg?.let { arrowHdg.bearing = it } - cogBearingDeg?.let { arrowCog.bearing = it } - } - - /** - * Updates the forecast section. * [waveHeightM] and [swellHeightM] are raw metres — converted to feet internally. */ fun updateConditions( - currSpd: String? = null, - currDirDeg: Float? = null, - waveHeightM: Double? = null, - waveDirDeg: Float? = null, - swellHeightM: Double? = null, - swellDirDeg: Float? = null, + tws: String? = null, twsBearingDeg: Float? = null, + tempC: Double? = null, + baro: String? = null, + currSpd: String? = null, currDirDeg: Float? = null, + waveHeightM: Double? = null, waveDirDeg: Float? = null, + swellHeightM: Double? = null, swellDirDeg: Float? = null, swellPeriodS: Double? = null ) { + tws?.let { valueTws.text = it } + baro?.let { valueBaro.text = it } + tempC?.let { valueTemp.text = "%.0f".format(Locale.getDefault(), it) } + twsBearingDeg?.let { + arrowTws.bearing = it + bearingTws.text = formatBearing(it.toDouble()) + } + currSpd?.let { valueCurrSpd.text = it } currDirDeg?.let { arrowCurr.bearing = it diff --git a/android-app/app/src/main/res/drawable/ic_crosshair.xml b/android-app/app/src/main/res/drawable/ic_crosshair.xml new file mode 100644 index 0000000..609538e --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_crosshair.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index 1741c62..5147506 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -29,6 +29,30 @@ android:clickable="false" android:focusable="false" /> + + + + + + - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - + + - + - + tools:text="18" /> + + android:background="@color/md_theme_outline" /> + diff --git a/android-app/app/src/main/res/layout/layout_nav_hud.xml b/android-app/app/src/main/res/layout/layout_nav_hud.xml new file mode 100644 index 0000000..4623bb1 --- /dev/null +++ b/android-app/app/src/main/res/layout/layout_nav_hud.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3 From bf57713ef0e378ebedd518f4fb243328de08179d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 16:27:42 +0000 Subject: Add unit settings, wind particles toggle, and recenter-to-top MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 190 +++++++---- .../app/src/main/kotlin/org/terst/nav/UnitPrefs.kt | 83 +++++ .../kotlin/org/terst/nav/ui/InstrumentHandler.kt | 79 ++--- .../kotlin/org/terst/nav/ui/LayerPickerSheet.kt | 95 +++++- .../kotlin/org/terst/nav/ui/MapLayerManager.kt | 14 +- .../app/src/main/res/layout/activity_main.xml | 4 +- .../main/res/layout/layout_instruments_sheet.xml | 12 +- .../main/res/layout/layout_layer_picker_sheet.xml | 352 ++++++++++++++++----- .../app/src/main/res/layout/layout_nav_hud.xml | 3 + 9 files changed, 643 insertions(+), 189 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 21aa55b..86dd531 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -37,6 +37,7 @@ import org.terst.nav.ui.map.ParticleWindView import org.terst.nav.ui.safety.SafetyFragment import org.terst.nav.ui.voicelog.VoiceLogFragment import java.util.* +import org.terst.nav.data.model.MarineConditions import org.terst.nav.safety.AnchorWatchState class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { @@ -60,12 +61,35 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private lateinit var bottomNav: BottomNavigationView private lateinit var mapCrosshair: View - // HUD TextViews — wired to layout_nav_hud.xml + // HUD value TextViews private lateinit var hudSog: TextView private lateinit var hudCog: TextView private lateinit var hudBsp: TextView private lateinit var hudDepth: TextView + // HUD unit label TextViews + private lateinit var unitHudSog: TextView + private lateinit var unitHudBsp: TextView + private lateinit var unitHudDepth: TextView + + // Instrument sheet unit label TextViews + private lateinit var unitTws: TextView + private lateinit var unitTemp: TextView + private lateinit var unitBaro: TextView + private lateinit var unitCurrSpd: TextView + private lateinit var unitWaveHt: TextView + private lateinit var unitSwellHt: TextView + + // Unit preferences + private lateinit var unitPrefs: UnitPrefs + + // Cached raw values for unit-change refresh + private var lastConditions: MarineConditions? = null + private var lastBaroHpa: Float? = null + private var lastSogKnots: Double? = null + private var lastBspKnots: Double? = null + private var lastDepthMeters: Double? = null + private val safetyFragment = SafetyFragment().apply { setSafetyListener(this@MainActivity) } private val viewModel: MainViewModel by viewModels() @@ -85,6 +109,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { MapLibre.getInstance(this) setContentView(R.layout.activity_main) + unitPrefs = UnitPrefs(this) checkForegroundPermissions() initializeUI() } @@ -99,24 +124,37 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { bottomNav = findViewById(R.id.bottom_navigation) mapCrosshair = findViewById(R.id.map_crosshair) - // HUD views (inside the included layout_nav_hud) + // HUD value views hudSog = findViewById(R.id.hud_sog) hudCog = findViewById(R.id.hud_cog) hudBsp = findViewById(R.id.hud_bsp) hudDepth = findViewById(R.id.hud_depth) - // Initialise HUD with dashes + // HUD unit label views + unitHudSog = findViewById(R.id.unit_hud_sog) + unitHudBsp = findViewById(R.id.unit_hud_bsp) + unitHudDepth = findViewById(R.id.unit_hud_depth) + + // Instrument sheet unit label views + unitTws = findViewById(R.id.unit_tws) + unitTemp = findViewById(R.id.unit_temp) + unitBaro = findViewById(R.id.unit_baro) + unitCurrSpd = findViewById(R.id.unit_curr_spd) + unitWaveHt = findViewById(R.id.unit_wave_ht) + unitSwellHt = findViewById(R.id.unit_swell_ht) + + // Init HUD with dashes; apply correct unit labels from stored prefs hudSog.text = "—" hudCog.text = "—" hudBsp.text = "—" hudDepth.text = "—" + applyUnitLabels() setupBottomSheet() setupBottomNavigation() setupHandlers() setupMap() - // Single tap starts; long press stops (requires deliberate intent mid-sail) fabRecordTrack.setOnClickListener { if (!viewModel.isRecording.value) viewModel.startTrack() } @@ -129,12 +167,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } fabMob.setOnClickListener { onActivateMob() } - - fabRecenter.setOnClickListener { - mapHandler?.recenter() - } - + fabRecenter.setOnClickListener { mapHandler?.recenter() } btnQuit.setOnClickListener { onQuitRequested() } + lifecycleScope.launch { viewModel.isRecording.collect { recording -> val icon = if (recording) R.drawable.ic_close else R.drawable.ic_track_record @@ -151,11 +186,73 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } } + /** Updates all static unit label TextViews to match current [unitPrefs]. */ + private fun applyUnitLabels() { + unitHudSog.text = unitPrefs.speedUnitLabel() + unitHudBsp.text = unitPrefs.speedUnitLabel() + unitHudDepth.text = unitPrefs.depthUnitLabel() + unitTws.text = unitPrefs.speedUnitLabel() + unitTemp.text = unitPrefs.tempUnitLabel() + unitBaro.text = unitPrefs.pressureUnitLabel() + unitCurrSpd.text = unitPrefs.speedUnitLabel() + unitWaveHt.text = unitPrefs.depthUnitLabel() + unitSwellHt.text = unitPrefs.depthUnitLabel() + } + + /** + * Re-formats every cached raw value with the current unit prefs. + * Called after the user changes a unit in the settings sheet. + */ + private fun refreshUnits() { + applyUnitLabels() + lastSogKnots?.let { hudSog.text = unitPrefs.formatSpeed(it) } + lastBspKnots?.let { hudBsp.text = unitPrefs.formatSpeed(it) } + lastDepthMeters?.let { hudDepth.text = unitPrefs.formatDepth(it) } + lastConditions?.let { applyConditions(it) } + lastBaroHpa?.let { instrumentHandler?.updateConditions(baroHpa = it) } + } + + private fun applyConditions(c: MarineConditions) { + instrumentHandler?.updateConditions( + twsKt = c.windSpeedKt, + twsBearingDeg = c.windDirDeg?.toFloat(), + tempC = c.tempC, + currSpdKt = c.currentSpeedKt, + currDirDeg = c.currentDirDeg?.toFloat(), + waveHeightM = c.waveHeightM, + waveDirDeg = c.waveDirDeg?.toFloat(), + swellHeightM = c.swellHeightM, + swellDirDeg = c.swellDirDeg?.toFloat(), + swellPeriodS = c.swellPeriodS + ) + instrumentHandler?.updateWaveState( + swellHeightM = c.swellHeightM?.toFloat() ?: 0.9f, + swellPeriodSec = c.swellPeriodS?.toFloat() ?: 10f, + windWaveHeightM = c.waveHeightM?.toFloat() ?: 0.45f, + windSpeedKt = c.windSpeedKt?.toFloat() ?: 0f + ) + } + private fun setupBottomSheet() { bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } + private fun buildLayerPickerSheet(): LayerPickerSheet { + val currentStyle = loadedStyleFlow.value + return LayerPickerSheet( + manager = layerManager, + onBaseChanged = { preset -> currentStyle?.let { layerManager.setBasePreset(it, preset) } }, + onWindChanged = { enabled -> currentStyle?.let { layerManager.setWindEnabled(it, enabled) } }, + onParticlesChanged = { enabled -> + layerManager.setParticlesEnabled(enabled) + particleWindView?.visibility = if (enabled) View.VISIBLE else View.GONE + }, + unitPrefs = unitPrefs, + onUnitsChanged = { refreshUnits() } + ) + } + private fun setupBottomNavigation() { bottomNav.setOnItemSelectedListener { item -> when (item.itemId) { @@ -167,14 +264,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { true } R.id.nav_layers -> { - val currentStyle = loadedStyleFlow.value - if (currentStyle != null) { - LayerPickerSheet( - manager = layerManager, - onBaseChanged = { preset -> layerManager.setBasePreset(currentStyle, preset) }, - onWindChanged = { enabled -> layerManager.setWindEnabled(currentStyle, enabled) } - ).show(supportFragmentManager, "layer_picker") - } + buildLayerPickerSheet().show(supportFragmentManager, "layer_picker") bottomNav.post { bottomNav.selectedItemId = R.id.nav_map } true } @@ -253,20 +343,18 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { bearingCurr = findViewById(R.id.bearing_curr), bearingWaves = findViewById(R.id.bearing_waves), bearingSwell = findViewById(R.id.bearing_swell), - waveView = findViewById(R.id.wave_divider) - ) - instrumentHandler?.updateConditions( - tws = "—", baro = "—", currSpd = "—" + waveView = findViewById(R.id.wave_divider), + unitPrefs = unitPrefs ) } private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt() private fun checkForegroundPermissions() { - val fineLocationPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) - val coarseLocationPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) + val fineLocation = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + val coarseLocation = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) - if (fineLocationPermission == PackageManager.PERMISSION_GRANTED || coarseLocationPermission == PackageManager.PERMISSION_GRANTED) { + if (fineLocation == PackageManager.PERMISSION_GRANTED || coarseLocation == PackageManager.PERMISSION_GRANTED) { startServices() } else { requestPermissionLauncher.launch(arrayOf( @@ -299,6 +387,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { layerManager = MapLayerManager(this) mapView = findViewById(R.id.mapView) particleWindView = findViewById(R.id.particle_wind_view) + + // Apply persisted particles preference immediately + if (!layerManager.particlesEnabled) { + particleWindView?.visibility = View.GONE + } + if (NavApplication.isTesting) return mapView?.onCreate(null) @@ -325,9 +419,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { maplibreMap.setStyle(styleBuilder) { style -> loadedStyleFlow.value = style - val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor) - val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow) - val userBitmap = rasterizeDrawable(R.drawable.ic_ship_arrow) + val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor) + val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow) + val userBitmap = rasterizeDrawable(R.drawable.ic_ship_arrow) val windArrowBitmap = rasterizeDrawable(R.drawable.ic_wind_arrow) mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap, userBitmap) mapHandler?.setupWindLayer(style, windArrowBitmap) @@ -346,19 +440,13 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { corners.minOf { it.longitude }, corners.maxOf { it.longitude } ) } - // Refresh conditions for current map center (works in both follow + pan mode) maplibreMap.cameraPosition.target?.let { center -> viewModel.loadConditions(center.latitude, center.longitude) } } maplibreMap.addOnMapLongClickListener { _ -> - val currentStyle = loadedStyleFlow.value ?: return@addOnMapLongClickListener true - LayerPickerSheet( - manager = layerManager, - onBaseChanged = { preset -> layerManager.setBasePreset(currentStyle, preset) }, - onWindChanged = { enabled -> layerManager.setWindEnabled(currentStyle, enabled) } - ).show(supportFragmentManager, "layer_picker") + buildLayerPickerSheet().show(supportFragmentManager, "layer_picker") true } } @@ -371,8 +459,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude) mapHandler?.updateUserPosition(gpsData.latitude, gpsData.longitude, gpsData.cog.toFloat()) viewModel.addGpsPoint(gpsData.latitude, gpsData.longitude, gpsData.sog, gpsData.cog) - // HUD — SOG and COG come from GPS - hudSog.text = "%.1f".format(Locale.getDefault(), gpsData.sog) + // HUD — SOG and COG from GPS + lastSogKnots = gpsData.sog + hudSog.text = unitPrefs.formatSpeed(gpsData.sog) hudCog.text = "%.0f°".format(Locale.getDefault(), gpsData.cog) if (!conditionsLoaded) { conditionsLoaded = true @@ -400,41 +489,28 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { lifecycleScope.launch { LocationService.barometerStatus.collect { status -> if (status.history.isNotEmpty()) { - instrumentHandler?.updateConditions(baro = status.formatPressure()) + lastBaroHpa = status.currentPressureHpa + instrumentHandler?.updateConditions(baroHpa = status.currentPressureHpa) } } } lifecycleScope.launch { viewModel.marineConditions.collect { c -> if (c == null) return@collect - instrumentHandler?.updateConditions( - tws = c.windSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) }, - twsBearingDeg = c.windDirDeg?.toFloat(), - tempC = c.tempC, - currSpd = c.currentSpeedKt?.let { "%.1f".format(Locale.getDefault(), it) } ?: "—", - currDirDeg = c.currentDirDeg?.toFloat(), - waveHeightM = c.waveHeightM, - waveDirDeg = c.waveDirDeg?.toFloat(), - swellHeightM = c.swellHeightM, - swellDirDeg = c.swellDirDeg?.toFloat(), - swellPeriodS = c.swellPeriodS - ) - val swellHtFt = c.swellHeightM?.let { (it * 3.28084f).toFloat() } ?: 3f - val windHtFt = c.waveHeightM?.let { (it * 3.28084f).toFloat() } ?: 1.5f - val swellPeriod = c.swellPeriodS?.toFloat() ?: 10f - val windKt = c.windSpeedKt?.toFloat() ?: 0f - instrumentHandler?.updateWaveState(swellHtFt, swellPeriod, windHtFt, windKt) + lastConditions = c + applyConditions(c) } } lifecycleScope.launch { LocationService.nmeaDepthDataFlow.collect { depthData -> - // Update HUD depth (converted to feet) - hudDepth.text = "%.1f".format(Locale.getDefault(), depthData.depthMeters * 3.28084) + lastDepthMeters = depthData.depthMeters + hudDepth.text = unitPrefs.formatDepth(depthData.depthMeters) } } lifecycleScope.launch { LocationService.nmeaBoatSpeedData.collect { bspData -> - hudBsp.text = "%.1f".format(Locale.getDefault(), bspData.bspKnots) + lastBspKnots = bspData.bspKnots + hudBsp.text = unitPrefs.formatSpeed(bspData.bspKnots) } } lifecycleScope.launch { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt b/android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt new file mode 100644 index 0000000..8d60ae0 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/UnitPrefs.kt @@ -0,0 +1,83 @@ +package org.terst.nav + +import android.content.Context +import java.util.Locale + +enum class TempUnit { CELSIUS, FAHRENHEIT } +enum class DepthUnit { FEET, METERS } +enum class PressureUnit { HPA, INHG } +enum class SpeedUnit { KNOTS, MPH, KPH } + +/** + * Persists and applies the user's preferred display units. + * Defaults: Fahrenheit, feet, hPa, knots. + */ +class UnitPrefs(context: Context) { + + private val prefs = context.getSharedPreferences("unit_prefs", Context.MODE_PRIVATE) + + var tempUnit: TempUnit + get() = TempUnit.valueOf(prefs.getString(KEY_TEMP, TempUnit.FAHRENHEIT.name)!!) + set(v) { prefs.edit().putString(KEY_TEMP, v.name).apply() } + + var depthUnit: DepthUnit + get() = DepthUnit.valueOf(prefs.getString(KEY_DEPTH, DepthUnit.FEET.name)!!) + set(v) { prefs.edit().putString(KEY_DEPTH, v.name).apply() } + + var pressureUnit: PressureUnit + get() = PressureUnit.valueOf(prefs.getString(KEY_PRESSURE, PressureUnit.HPA.name)!!) + set(v) { prefs.edit().putString(KEY_PRESSURE, v.name).apply() } + + var speedUnit: SpeedUnit + get() = SpeedUnit.valueOf(prefs.getString(KEY_SPEED, SpeedUnit.KNOTS.name)!!) + set(v) { prefs.edit().putString(KEY_SPEED, v.name).apply() } + + fun formatTemp(tempC: Double): String = when (tempUnit) { + TempUnit.CELSIUS -> "%.0f".format(Locale.getDefault(), tempC) + TempUnit.FAHRENHEIT -> "%.0f".format(Locale.getDefault(), tempC * 9.0 / 5.0 + 32.0) + } + + fun tempUnitLabel(): String = when (tempUnit) { + TempUnit.CELSIUS -> "°C" + TempUnit.FAHRENHEIT -> "°F" + } + + fun formatDepth(metres: Double): String = when (depthUnit) { + DepthUnit.FEET -> "%.1f".format(Locale.getDefault(), metres * 3.28084) + DepthUnit.METERS -> "%.1f".format(Locale.getDefault(), metres) + } + + fun depthUnitLabel(): String = when (depthUnit) { + DepthUnit.FEET -> "ft" + DepthUnit.METERS -> "m" + } + + fun formatPressure(hpa: Float): String = when (pressureUnit) { + PressureUnit.HPA -> "%.1f".format(Locale.getDefault(), hpa) + PressureUnit.INHG -> "%.2f".format(Locale.getDefault(), hpa * 0.02953f) + } + + fun pressureUnitLabel(): String = when (pressureUnit) { + PressureUnit.HPA -> "hPa" + PressureUnit.INHG -> "inHg" + } + + fun formatSpeed(knots: Double): String = when (speedUnit) { + SpeedUnit.KNOTS -> "%.1f".format(Locale.getDefault(), knots) + SpeedUnit.MPH -> "%.1f".format(Locale.getDefault(), knots * 1.15078) + SpeedUnit.KPH -> "%.1f".format(Locale.getDefault(), knots * 1.852) + } + + fun speedUnitLabel(): String = when (speedUnit) { + SpeedUnit.KNOTS -> "kt" + SpeedUnit.MPH -> "mph" + SpeedUnit.KPH -> "kph" + } + + companion object { + private const val KEY_TEMP = "temp_unit" + private const val KEY_DEPTH = "depth_unit" + private const val KEY_PRESSURE = "pressure_unit" + private const val KEY_SPEED = "speed_unit" + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt index 48ebb3b..ccb3a91 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt @@ -1,19 +1,11 @@ package org.terst.nav.ui import android.widget.TextView +import org.terst.nav.UnitPrefs import java.util.Locale // ── Pure formatting helpers (top-level for testability) ────────────────────── -private const val M_TO_FT = 3.28084 - -/** Converts metres to feet. */ -fun metresToFeet(metres: Double): Double = metres * M_TO_FT - -/** Formats a feet value to one decimal place. */ -fun formatFt(ft: Double, locale: Locale = Locale.getDefault()): String = - "%.1f".format(locale, ft) - /** Formats a bearing to zero decimal places with a degree symbol. */ fun formatBearing(deg: Double, locale: Locale = Locale.getDefault()): String = "%.0f°".format(locale, deg) @@ -31,11 +23,9 @@ fun formatPeriod(sec: Double, locale: Locale = Locale.getDefault()): String = * All boat-instrument data (SOG, COG, BSP, Depth) is handled directly * in MainActivity via the HUD strip. * - * Units contract: - * - Speed values: pre-formatted strings in knots (caller's responsibility) - * - Height values: caller passes raw metres; this class converts to feet - * - Bearing values: raw degrees as Float, rotated into arrows and formatted - * into bearing TextViews by this class + * All numeric values are accepted in SI / nautical base units: + * speeds in knots, heights in metres, pressure in hPa, temperature in °C. + * [unitPrefs] is consulted on each update to produce the correct formatted string. */ class InstrumentHandler( // ── Conditions header ──────────────────────────────────────────────── @@ -58,47 +48,60 @@ class InstrumentHandler( private val bearingWaves: TextView, private val bearingSwell: TextView, // ── Wave view ──────────────────────────────────────────────────────── - private val waveView: WaveView + private val waveView: WaveView, + // ── Unit preferences ───────────────────────────────────────────────── + private val unitPrefs: UnitPrefs ) { init { arrowCurr.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN arrowWaves.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN arrowSwell.arrowStyle = DirectionArrowView.ArrowStyle.OCEAN + valueTws.text = "—" + valueTemp.text = "—" + valueBaro.text = "—" + valueCurrSpd.text = "—" + valueWaveHt.text = "—" + valueSwellHt.text = "—" } /** - * Updates all area-conditions fields. - * Null arguments leave the current value unchanged. - * [waveHeightM] and [swellHeightM] are raw metres — converted to feet internally. + * Updates all area-conditions fields. Null arguments leave the current value unchanged. + * + * @param twsKt True wind speed in knots + * @param tempC Air temperature in degrees Celsius + * @param baroHpa Barometric pressure in hPa + * @param currSpdKt Ocean current speed in knots + * @param waveHeightM Wind-wave height in metres + * @param swellHeightM Swell height in metres */ fun updateConditions( - tws: String? = null, twsBearingDeg: Float? = null, + twsKt: Double? = null, twsBearingDeg: Float? = null, tempC: Double? = null, - baro: String? = null, - currSpd: String? = null, currDirDeg: Float? = null, + baroHpa: Float? = null, + currSpdKt: Double? = null, currDirDeg: Float? = null, waveHeightM: Double? = null, waveDirDeg: Float? = null, swellHeightM: Double? = null, swellDirDeg: Float? = null, swellPeriodS: Double? = null ) { - tws?.let { valueTws.text = it } - baro?.let { valueBaro.text = it } - tempC?.let { valueTemp.text = "%.0f".format(Locale.getDefault(), it) } + twsKt?.let { valueTws.text = unitPrefs.formatSpeed(it) } + tempC?.let { valueTemp.text = unitPrefs.formatTemp(it) } + baroHpa?.let { valueBaro.text = unitPrefs.formatPressure(it) } twsBearingDeg?.let { arrowTws.bearing = it bearingTws.text = formatBearing(it.toDouble()) } - currSpd?.let { valueCurrSpd.text = it } + currSpdKt?.let { valueCurrSpd.text = unitPrefs.formatSpeed(it) } currDirDeg?.let { arrowCurr.bearing = it bearingCurr.text = formatBearing(it.toDouble()) } - waveHeightM?.let { valueWaveHt.text = formatFt(metresToFeet(it)) } + waveHeightM?.let { valueWaveHt.text = unitPrefs.formatDepth(it) } waveDirDeg?.let { arrowWaves.bearing = it bearingWaves.text = formatBearing(it.toDouble()) } - swellHeightM?.let { valueSwellHt.text = formatFt(metresToFeet(it)) } + swellHeightM?.let { valueSwellHt.text = unitPrefs.formatDepth(it) } swellDirDeg?.let { arrowSwell.bearing = it bearingSwell.text = formatBearing(it.toDouble()) @@ -108,26 +111,28 @@ class InstrumentHandler( /** * Updates the WaveView with current sea state. Call once when conditions load. - * - * View height is set here to reflect swell scale: 1ft → 56dp, 8ft → 160dp. + * Heights are passed in metres; the WaveView scale is computed internally. * [windSpeedKt] gates whitecap rendering (Beaufort 4 threshold = 12 kt). */ fun updateWaveState( - swellHeightFt: Float, + swellHeightM: Float, swellPeriodSec: Float, - windWaveHeightFt: Float, + windWaveHeightM: Float, windSpeedKt: Float = 0f ) { - waveView.swellHeightFt = swellHeightFt + val swellHtFt = swellHeightM * 3.28084f + val windWaveHtFt = windWaveHeightM * 3.28084f + + waveView.swellHeightFt = swellHtFt waveView.swellPeriodSec = swellPeriodSec - waveView.windWaveHeightFt = windWaveHeightFt + waveView.windWaveHeightFt = windWaveHtFt waveView.windSpeedKt = windSpeedKt // Size the view to the swell — bigger swell = taller window - val density = waveView.resources.displayMetrics.density - val heightDp = (32f + swellHeightFt * 16f).coerceIn(56f, 160f) - val lp = waveView.layoutParams - lp.height = (heightDp * density).toInt() + val density = waveView.resources.displayMetrics.density + val heightDp = (32f + swellHtFt * 16f).coerceIn(56f, 160f) + val lp = waveView.layoutParams + lp.height = (heightDp * density).toInt() waveView.layoutParams = lp } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt index 48dc808..78fd70f 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/LayerPickerSheet.kt @@ -7,30 +7,33 @@ import android.view.ViewGroup import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.chip.ChipGroup import com.google.android.material.switchmaterial.SwitchMaterial +import org.terst.nav.DepthUnit +import org.terst.nav.PressureUnit import org.terst.nav.R +import org.terst.nav.SpeedUnit +import org.terst.nav.TempUnit +import org.terst.nav.UnitPrefs class LayerPickerSheet( private val manager: MapLayerManager, private val onBaseChanged: (MapBasePreset) -> Unit, - private val onWindChanged: (Boolean) -> Unit + private val onWindChanged: (Boolean) -> Unit, + private val onParticlesChanged: (Boolean) -> Unit, + private val unitPrefs: UnitPrefs, + private val onUnitsChanged: () -> Unit ) : BottomSheetDialogFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = inflater.inflate(R.layout.layout_layer_picker_sheet, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val chipGroup = view.findViewById(R.id.chip_group_base) - val windSwitch = view.findViewById(R.id.switch_wind) - - // Set initial state from manager - val chipId = when (manager.basePreset) { + // ── Base layer chips ────────────────────────────────────────────────── + val chipGroup = view.findViewById(R.id.chip_group_base) + chipGroup.check(when (manager.basePreset) { MapBasePreset.SATELLITE -> R.id.chip_satellite MapBasePreset.CHARTS -> R.id.chip_charts MapBasePreset.HYBRID -> R.id.chip_hybrid - } - chipGroup.check(chipId) - windSwitch.isChecked = manager.windEnabled - + }) chipGroup.setOnCheckedStateChangeListener { _, checkedIds -> val preset = when (checkedIds.firstOrNull()) { R.id.chip_satellite -> MapBasePreset.SATELLITE @@ -41,8 +44,76 @@ class LayerPickerSheet( onBaseChanged(preset) } - windSwitch.setOnCheckedChangeListener { _, checked -> - onWindChanged(checked) + // ── Wind overlay switch ─────────────────────────────────────────────── + val windSwitch = view.findViewById(R.id.switch_wind) + windSwitch.isChecked = manager.windEnabled + windSwitch.setOnCheckedChangeListener { _, checked -> onWindChanged(checked) } + + // ── Particles switch ────────────────────────────────────────────────── + val particlesSwitch = view.findViewById(R.id.switch_particles) + particlesSwitch.isChecked = manager.particlesEnabled + particlesSwitch.setOnCheckedChangeListener { _, checked -> onParticlesChanged(checked) } + + // ── Temperature chips ───────────────────────────────────────────────── + val chipTemp = view.findViewById(R.id.chip_group_temp) + chipTemp.check(when (unitPrefs.tempUnit) { + TempUnit.FAHRENHEIT -> R.id.chip_fahrenheit + TempUnit.CELSIUS -> R.id.chip_celsius + }) + chipTemp.setOnCheckedStateChangeListener { _, ids -> + unitPrefs.tempUnit = when (ids.firstOrNull()) { + R.id.chip_fahrenheit -> TempUnit.FAHRENHEIT + R.id.chip_celsius -> TempUnit.CELSIUS + else -> return@setOnCheckedStateChangeListener + } + onUnitsChanged() + } + + // ── Depth chips ─────────────────────────────────────────────────────── + val chipDepth = view.findViewById(R.id.chip_group_depth) + chipDepth.check(when (unitPrefs.depthUnit) { + DepthUnit.FEET -> R.id.chip_feet + DepthUnit.METERS -> R.id.chip_meters + }) + chipDepth.setOnCheckedStateChangeListener { _, ids -> + unitPrefs.depthUnit = when (ids.firstOrNull()) { + R.id.chip_feet -> DepthUnit.FEET + R.id.chip_meters -> DepthUnit.METERS + else -> return@setOnCheckedStateChangeListener + } + onUnitsChanged() + } + + // ── Pressure chips ──────────────────────────────────────────────────── + val chipPressure = view.findViewById(R.id.chip_group_pressure) + chipPressure.check(when (unitPrefs.pressureUnit) { + PressureUnit.HPA -> R.id.chip_hpa + PressureUnit.INHG -> R.id.chip_inhg + }) + chipPressure.setOnCheckedStateChangeListener { _, ids -> + unitPrefs.pressureUnit = when (ids.firstOrNull()) { + R.id.chip_hpa -> PressureUnit.HPA + R.id.chip_inhg -> PressureUnit.INHG + else -> return@setOnCheckedStateChangeListener + } + onUnitsChanged() + } + + // ── Speed chips ─────────────────────────────────────────────────────── + val chipSpeed = view.findViewById(R.id.chip_group_speed) + chipSpeed.check(when (unitPrefs.speedUnit) { + SpeedUnit.KNOTS -> R.id.chip_knots + SpeedUnit.MPH -> R.id.chip_mph + SpeedUnit.KPH -> R.id.chip_kph + }) + chipSpeed.setOnCheckedStateChangeListener { _, ids -> + unitPrefs.speedUnit = when (ids.firstOrNull()) { + R.id.chip_knots -> SpeedUnit.KNOTS + R.id.chip_mph -> SpeedUnit.MPH + R.id.chip_kph -> SpeedUnit.KPH + else -> return@setOnCheckedStateChangeListener + } + onUnitsChanged() } } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt index 83cfa70..855233f 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MapLayerManager.kt @@ -26,6 +26,9 @@ class MapLayerManager(context: Context) { var windEnabled: Boolean = prefs.getBoolean(KEY_WIND, true) private set + var particlesEnabled: Boolean = prefs.getBoolean(KEY_PARTICLES, true) + private set + // ── Source / Layer IDs ──────────────────────────────────────────────────── companion object { @@ -39,8 +42,9 @@ class MapLayerManager(context: Context) { const val LAYER_WIND = "wind-layer" const val LAYER_SEAMARKS = "openseamap-layer" - private const val KEY_BASE = "base_preset" - private const val KEY_WIND = "wind_enabled" + private const val KEY_BASE = "base_preset" + private const val KEY_WIND = "wind_enabled" + private const val KEY_PARTICLES = "particles_enabled" // Tile URLs private const val URL_SATELLITE = @@ -106,6 +110,12 @@ class MapLayerManager(context: Context) { } } + /** Toggle wind particles (Canvas overlay — no style interaction needed). */ + fun setParticlesEnabled(enabled: Boolean) { + particlesEnabled = enabled + prefs.edit().putBoolean(KEY_PARTICLES, enabled).apply() + } + /** Toggle wind overlay on a live style. */ fun setWindEnabled(style: Style, enabled: Boolean) { windEnabled = enabled diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index 5147506..5943949 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -90,10 +90,10 @@ android:visibility="gone" app:cornerRadius="20dp" app:elevation="20dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/nav_hud" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - android:layout_marginBottom="24dp" /> + android:layout_marginTop="8dp" /> diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml index faec826..03cefb7 100644 --- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml +++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml @@ -56,7 +56,7 @@ android:id="@+id/value_tws" style="@style/InstrumentPrimaryValue" tools:text="15.5" /> - + - + @@ -123,7 +123,7 @@ android:id="@+id/value_baro" style="@style/InstrumentPrimaryValue" tools:text="1013" /> - + @@ -175,7 +175,7 @@ android:id="@+id/value_curr_spd" style="@style/ForecastValue" tools:text="0.8" /> - + - + - + - - - - - - - - - + + + + + + + android:layout_marginBottom="12dp" + android:text="MAP LAYERS" + android:textSize="11sp" + android:textAllCaps="true" + android:letterSpacing="0.12" + android:fontFamily="sans-serif-light" + android:textColor="@color/instrument_text_secondary" /> - + + android:layout_marginBottom="20dp" + app:singleSelection="true" + app:selectionRequired="true"> - + + + + + + + + + + + + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginBottom="12dp"> - + + + + - - + - - + + + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginBottom="8dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:singleSelection="true" + app:selectionRequired="true"> + + + - + + + + + + + + + - - + diff --git a/android-app/app/src/main/res/layout/layout_nav_hud.xml b/android-app/app/src/main/res/layout/layout_nav_hud.xml index 4623bb1..90119fa 100644 --- a/android-app/app/src/main/res/layout/layout_nav_hud.xml +++ b/android-app/app/src/main/res/layout/layout_nav_hud.xml @@ -39,6 +39,7 @@ android:fontFamily="sans-serif-medium" tools:text="7.1" /> Date: Fri, 10 Apr 2026 21:08:13 +0000 Subject: Add boat profiles and overhaul pre-trip departure briefing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../main/kotlin/org/terst/nav/NavApplication.kt | 2 + .../terst/nav/tripreport/BoatProfileRepository.kt | 137 ++++++++ .../org/terst/nav/tripreport/PreTripModels.kt | 74 +++- .../terst/nav/tripreport/PreTripReportFragment.kt | 209 +++++++++--- .../terst/nav/tripreport/PreTripReportGenerator.kt | 380 ++++++++++++++++++--- .../terst/nav/tripreport/PreTripReportViewModel.kt | 56 ++- .../main/res/layout/fragment_pretrip_report.xml | 246 +++++++++---- .../src/main/res/layout/item_condition_column.xml | 44 +++ 8 files changed, 977 insertions(+), 171 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/tripreport/BoatProfileRepository.kt create mode 100644 android-app/app/src/main/res/layout/item_condition_column.xml (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt index 7c43dd5..0f1e245 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt @@ -14,6 +14,7 @@ class NavApplication : Application() { companion object { val logbookRepository = org.terst.nav.logbook.InMemoryLogbookRepository() lateinit var trackRepository: org.terst.nav.track.TrackRepository + lateinit var boatProfileRepository: org.terst.nav.tripreport.BoatProfileRepository var isTesting: Boolean = false get() { if (field) return true @@ -29,6 +30,7 @@ class NavApplication : Application() { override fun onCreate() { super.onCreate() trackRepository = org.terst.nav.track.TrackRepository(this) + boatProfileRepository = org.terst.nav.tripreport.BoatProfileRepository(this) FirebaseCrashlytics.getInstance().sendUnsentReports() installCrashLogger() } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/BoatProfileRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/BoatProfileRepository.kt new file mode 100644 index 0000000..090e928 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/BoatProfileRepository.kt @@ -0,0 +1,137 @@ +package org.terst.nav.tripreport + +import android.content.Context +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +/** + * Persists boat profiles to SharedPreferences as a JSON array. + * Pre-seeds Erickson 23 and Cal 20 on first launch. + */ +class BoatProfileRepository(context: Context) { + + private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + private val listType = Types.newParameterizedType(List::class.java, ProfileJson::class.java) + private val adapter = moshi.adapter>(listType) + + // ── Public API ──────────────────────────────────────────────────────────── + + fun loadProfiles(): List { + val json = prefs.getString(KEY_PROFILES, null) + val list = if (json != null) runCatching { adapter.fromJson(json) }.getOrNull() else null + return if (list.isNullOrEmpty()) { + val defaults = defaultProfiles() + saveProfiles(defaults) + defaults + } else { + list.map { it.toDomain() } + } + } + + fun saveProfiles(profiles: List) { + prefs.edit().putString(KEY_PROFILES, adapter.toJson(profiles.map { ProfileJson.from(it) })).apply() + } + + fun selectedId(): String? = prefs.getString(KEY_SELECTED, null) + + fun setSelectedId(id: String) = prefs.edit().putString(KEY_SELECTED, id).apply() + + fun selectedProfile(): BoatProfile? { + val id = selectedId() ?: return loadProfiles().firstOrNull() + return loadProfiles().firstOrNull { it.id == id } + } + + // ── JSON transfer object (Moshi needs simple primitives) ───────────────── + + @JsonClass(generateAdapter = true) + data class SailJson( + val name: String, + val minWindKt: Int, + val maxWindKt: Int, + val isHeadsail: Boolean + ) { + fun toDomain() = SailConfig(name, minWindKt, maxWindKt, isHeadsail) + companion object { + fun from(s: SailConfig) = SailJson(s.name, s.minWindKt, s.maxWindKt, s.isHeadsail) + } + } + + @JsonClass(generateAdapter = true) + data class ProfileJson( + val id: String, + val name: String, + val lengthFt: Double, + val type: String, // BoatType.name + val rig: String, // RigType.name + val headsails: List, + val mainReefs: Int, + val hasSpinnaker: Boolean, + val hasGennaker: Boolean, + val notes: String + ) { + fun toDomain() = BoatProfile( + id = id, + name = name, + lengthFt = lengthFt, + type = runCatching { BoatType.valueOf(type) }.getOrDefault(BoatType.MONOHULL), + rig = runCatching { RigType.valueOf(rig) }.getOrDefault(RigType.SLOOP), + headsails = headsails.map { it.toDomain() }, + mainReefs = mainReefs, + hasSpinnaker = hasSpinnaker, + hasGennaker = hasGennaker, + notes = notes + ) + companion object { + fun from(p: BoatProfile) = ProfileJson( + id = p.id, + name = p.name, + lengthFt = p.lengthFt, + type = p.type.name, + rig = p.rig.name, + headsails = p.headsails.map { SailJson.from(it) }, + mainReefs = p.mainReefs, + hasSpinnaker = p.hasSpinnaker, + hasGennaker = p.hasGennaker, + notes = p.notes + ) + } + } + + companion object { + private const val PREF_NAME = "boat_profiles" + private const val KEY_PROFILES = "profiles" + private const val KEY_SELECTED = "selected_id" + + fun defaultProfiles() = listOf( + BoatProfile( + id = "e23", + name = "Erickson 23", + lengthFt = 23.0, + type = BoatType.MONOHULL, + rig = RigType.SLOOP, + headsails = listOf( + SailConfig("155% Genoa", minWindKt = 0, maxWindKt = 13, isHeadsail = true), + SailConfig("100% Jib", minWindKt = 10, maxWindKt = 21, isHeadsail = true), + SailConfig("65% Blade", minWindKt = 18, maxWindKt = 40, isHeadsail = true) + ), + mainReefs = 2 + ), + BoatProfile( + id = "cal20", + name = "Cal 20", + lengthFt = 20.0, + type = BoatType.MONOHULL, + rig = RigType.SLOOP, + headsails = listOf( + SailConfig("150% Genoa", minWindKt = 0, maxWindKt = 13, isHeadsail = true), + SailConfig("100% Jib", minWindKt = 10, maxWindKt = 22, isHeadsail = true), + SailConfig("65% Blade", minWindKt = 19, maxWindKt = 40, isHeadsail = true) + ), + mainReefs = 1 + ) + ) + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt index 2362079..c1bc6e7 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripModels.kt @@ -1,25 +1,34 @@ package org.terst.nav.tripreport -enum class BoatType { - MONOHULL, - MULTIHULL -} +enum class BoatType { MONOHULL, MULTIHULL } +enum class RigType { SLOOP, CUTTER, KETCH } -enum class RigType { - SLOOP, - CUTTER, - KETCH -} +/** + * A single headsail (or main-sail configuration) in the boat's inventory. + * Wind ranges are approximate apparent-wind speeds at which the sail is appropriate. + */ +data class SailConfig( + val name: String, // e.g. "155% Genoa", "100% Jib", "65% Blade" + val minWindKt: Int = 0, + val maxWindKt: Int, + val isHeadsail: Boolean +) data class BoatProfile( + val id: String, val name: String, val lengthFt: Double, val type: BoatType, val rig: RigType, + val headsails: List = emptyList(), + val mainReefs: Int = 0, val hasSpinnaker: Boolean = false, - val hasGennaker: Boolean = false + val hasGennaker: Boolean = false, + val notes: String = "" ) +// ── Snapshot used to build the report ──────────────────────────────────────── + data class PreTripSummary( val timestampMs: Long, val lat: Double, @@ -31,13 +40,52 @@ data class PreTripSummary( val boatProfile: BoatProfile ) +// ── Output models ───────────────────────────────────────────────────────────── + +/** One hourly step in the trip-window forecast table. */ +data class ConditionSlice( + val label: String, // "Now", "+1 h", "+2 h", "+3 h" + val windKt: Double, + val windDirDeg: Double, + val waveHeightM: Double?, + val weatherDescription: String +) + +/** Suggested heading and distance for a round-trip sail. */ +data class RouteProjection( + val departureHeadingDeg: Double, + val outboundLegNm: Double, // nm before recommended turn-around + val returnHeadingDeg: Double, + val outboundPointOfSail: String, // "Beam Reach", "Close Hauled", etc. + val returnPointOfSail: String, + val rationale: String // human-readable description +) + +/** A single hazard or advisory notice. */ +data class WatchItem( + val severity: String, // "⚠️" warning or "ℹ️" info + val message: String +) + +/** Aggregated stats from past trips that had similar wind conditions. */ +data class SimilarTripSummary( + val count: Int, + val avgDistanceNm: Double, + val avgDurationHrs: Double, + val avgMaxSogKt: Double, + val conditionDescription: String // e.g. "10–15 kt NE" +) + data class SailSuggestion( val sailName: String, - val action: String // e.g., "Full Main", "1 Reef", "Furl" + val action: String ) data class PreTripReport( val summary: PreTripSummary, - val routingSuggestion: String, - val sailPlan: List + val conditionWindow: List, + val routeProjection: RouteProjection, + val sailPlan: List, + val watchItems: List, + val similarTrips: SimilarTripSummary? ) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt index 819485f..c88dc48 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt @@ -4,67 +4,134 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView -import kotlinx.coroutines.flow.first +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup import kotlinx.coroutines.launch +import org.terst.nav.NavApplication import org.terst.nav.R import org.terst.nav.ui.MainViewModel import java.util.Locale class PreTripReportFragment : Fragment() { - private val viewModel: PreTripReportViewModel by activityViewModels() private val mainViewModel: MainViewModel by activityViewModels() + private lateinit var viewModel: PreTripReportViewModel - private lateinit var tvWeatherSummary: TextView - private lateinit var tvRoutingContent: TextView - private lateinit var tvSailPlanContent: TextView - private lateinit var cardReport: MaterialCardView - private lateinit var btnGenerate: MaterialButton + private lateinit var chipGroupBoats: ChipGroup + private lateinit var btnGenerate: com.google.android.material.button.MaterialButton private lateinit var progress: ProgressBar + private lateinit var cardConditions: MaterialCardView + private lateinit var conditionsTable: LinearLayout + private lateinit var cardRoute: MaterialCardView + private lateinit var tvRoute: TextView + private lateinit var cardSailPlan: MaterialCardView + private lateinit var tvSailPlan: TextView + private lateinit var cardWatchlist: MaterialCardView + private lateinit var tvWatchlist: TextView + private lateinit var cardSimilar: MaterialCardView + private lateinit var tvSimilar: TextView + private lateinit var tvError: TextView override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.inflate(R.layout.fragment_pretrip_report, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - tvWeatherSummary = view.findViewById(R.id.tv_weather_summary) - tvRoutingContent = view.findViewById(R.id.tv_routing_content) - tvSailPlanContent = view.findViewById(R.id.tv_sail_plan_content) - cardReport = view.findViewById(R.id.card_report) - btnGenerate = view.findViewById(R.id.btn_generate_pretrip) - progress = view.findViewById(R.id.progress_pretrip) + // Construct ViewModel with the application-level repo + viewModel = ViewModelProvider( + requireActivity(), + PreTripReportViewModel.Factory(NavApplication.boatProfileRepository) + )[PreTripReportViewModel::class.java] - btnGenerate.setOnClickListener { - generateReport() + bindViews(view) + observeViewModel() + + btnGenerate.setOnClickListener { triggerGenerate() } + + // Auto-generate if forecast data is already available + if (mainViewModel.forecast.value.isNotEmpty()) { + triggerGenerate() } + } + private fun bindViews(view: View) { + chipGroupBoats = view.findViewById(R.id.chip_group_boats) + btnGenerate = view.findViewById(R.id.btn_generate_pretrip) + progress = view.findViewById(R.id.progress_pretrip) + cardConditions = view.findViewById(R.id.card_conditions) + conditionsTable = view.findViewById(R.id.conditions_table) + cardRoute = view.findViewById(R.id.card_route) + tvRoute = view.findViewById(R.id.tv_route) + cardSailPlan = view.findViewById(R.id.card_sail_plan) + tvSailPlan = view.findViewById(R.id.tv_sail_plan) + cardWatchlist = view.findViewById(R.id.card_watchlist) + tvWatchlist = view.findViewById(R.id.tv_watchlist) + cardSimilar = view.findViewById(R.id.card_similar) + tvSimilar = view.findViewById(R.id.tv_similar) + tvError = view.findViewById(R.id.tv_error) + } + + private fun observeViewModel() { + viewLifecycleOwner.lifecycleScope.launch { + viewModel.profiles.collect { profiles -> rebuildBoatChips(profiles) } + } + viewLifecycleOwner.lifecycleScope.launch { + viewModel.selectedProfile.collect { profile -> + if (profile != null) syncChipSelection(profile.id) + } + } viewLifecycleOwner.lifecycleScope.launch { viewModel.state.collect { renderState(it) } } } - private fun generateReport() { - viewLifecycleOwner.lifecycleScope.launch { - val forecast = mainViewModel.forecast.value.firstOrNull() - val conditions = mainViewModel.marineConditions.value - // For now, use 0,0 if no location, but ideally we'd have last known - // In a real app, we'd get this from a LocationProvider - viewModel.generate(0.0, 0.0, forecast, conditions) + private fun rebuildBoatChips(profiles: List) { + chipGroupBoats.removeAllViews() + val selectedId = viewModel.selectedProfile.value?.id + profiles.forEach { profile -> + val chip = Chip(requireContext()).apply { + text = profile.name + isCheckable = true + isChecked = (profile.id == selectedId) + tag = profile.id + } + chip.setOnCheckedChangeListener { _, checked -> + if (checked) { + viewModel.selectProfile(profile.id) + triggerGenerate() + } + } + chipGroupBoats.addView(chip) + } + } + + private fun syncChipSelection(selectedId: String) { + for (i in 0 until chipGroupBoats.childCount) { + val chip = chipGroupBoats.getChildAt(i) as? Chip ?: continue + chip.isChecked = (chip.tag == selectedId) } } + private fun triggerGenerate() { + val forecast = mainViewModel.forecast.value + val conditions = mainViewModel.marineConditions.value + viewModel.generate(forecast, conditions) + } + + // ── Rendering ───────────────────────────────────────────────────────────── + private fun renderState(state: PreTripState) { + tvError.visibility = View.GONE when (state) { is PreTripState.Loading -> { progress.visibility = View.VISIBLE @@ -73,30 +140,82 @@ class PreTripReportFragment : Fragment() { is PreTripState.Success -> { progress.visibility = View.GONE btnGenerate.isEnabled = true - cardReport.visibility = View.VISIBLE - - val r = state.report - tvWeatherSummary.text = "Wind: %.1f kts from %.0f°\nWaves: %s\nSky: %s".format( - Locale.getDefault(), - r.summary.windSpeedKt, - r.summary.windDirDeg, - r.summary.waveHeightM?.let { "%.1fm".format(it) } ?: "N/A", - r.summary.weatherDescription - ) - - tvRoutingContent.text = r.routingSuggestion - - val sailPlanText = r.sailPlan.joinToString("\n") { - "• ${it.sailName}: ${it.action}" - } - tvSailPlanContent.text = sailPlanText + renderReport(state.report) } is PreTripState.Error -> { progress.visibility = View.GONE btnGenerate.isEnabled = true - // Show toast or error message + tvError.text = state.message + tvError.visibility = View.VISIBLE } - else -> {} + else -> { + progress.visibility = View.GONE + btnGenerate.isEnabled = true + } + } + } + + private fun renderReport(r: PreTripReport) { + renderConditionsWindow(r.conditionWindow) + renderRoute(r.routeProjection) + renderSailPlan(r.sailPlan) + renderWatchlist(r.watchItems) + renderSimilarTrips(r.similarTrips) + } + + private fun renderConditionsWindow(slices: List) { + conditionsTable.removeAllViews() + slices.forEach { slice -> + val col = LayoutInflater.from(requireContext()) + .inflate(R.layout.item_condition_column, conditionsTable, false) + col.findViewById(R.id.col_label).text = slice.label + col.findViewById(R.id.col_wind).text = "%.0f kt".format(slice.windKt) + col.findViewById(R.id.col_wind_dir).text = cardinalDir(slice.windDirDeg) + col.findViewById(R.id.col_sky).text = slice.weatherDescription.take(12) + conditionsTable.addView(col) + } + cardConditions.visibility = if (slices.isNotEmpty()) View.VISIBLE else View.GONE + } + + private fun renderRoute(proj: RouteProjection) { + tvRoute.text = proj.rationale + cardRoute.visibility = View.VISIBLE + } + + private fun renderSailPlan(plan: List) { + tvSailPlan.text = plan.joinToString("\n") { "• ${it.sailName}: ${it.action}" } + cardSailPlan.visibility = View.VISIBLE + } + + private fun renderWatchlist(items: List) { + if (items.isEmpty()) { + cardWatchlist.visibility = View.GONE + return } + tvWatchlist.text = items.joinToString("\n") { "${it.severity} ${it.message}" } + cardWatchlist.visibility = View.VISIBLE + } + + private fun renderSimilarTrips(similar: SimilarTripSummary?) { + if (similar == null) { + cardSimilar.visibility = View.GONE + return + } + tvSimilar.text = "%d trip%s in %s\n• Avg distance %.1f nm\n• Avg duration %.1f h\n• Avg max SOG %.1f kt".format( + Locale.getDefault(), + similar.count, + if (similar.count == 1) "" else "s", + similar.conditionDescription, + similar.avgDistanceNm, + similar.avgDurationHrs, + similar.avgMaxSogKt + ) + cardSimilar.visibility = View.VISIBLE + } + + private fun cardinalDir(deg: Double): String { + val dirs = listOf("N","NNE","NE","ENE","E","ESE","SE","SSE", + "S","SSW","SW","WSW","W","WNW","NW","NNW") + return dirs[((deg + 11.25) / 22.5).toInt() % 16] } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt index 2ccabfb..2508d44 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt @@ -2,65 +2,367 @@ package org.terst.nav.tripreport import org.terst.nav.data.model.ForecastItem import org.terst.nav.data.model.MarineConditions +import org.terst.nav.track.TrackPoint +import org.terst.nav.track.summarise +import java.util.Calendar +import kotlin.math.* class PreTripReportGenerator { + /** + * Builds a full pre-trip briefing. + * + * @param lat Current position latitude + * @param lon Current position longitude + * @param forecastItems Hourly forecast list; first item = current hour + * @param conditions Current marine snapshot (waves, current, swell) + * @param boatProfile Selected vessel with sail inventory + * @param pastTracks All saved tracks for similar-conditions comparison + * @param durationHrs Planned trip duration in hours (default 3) + */ fun generateReport( lat: Double, lon: Double, - forecast: ForecastItem?, + forecastItems: List, conditions: MarineConditions?, - boatProfile: BoatProfile + boatProfile: BoatProfile, + pastTracks: List> = emptyList(), + durationHrs: Double = 3.0 ): PreTripReport { + val current = forecastItems.firstOrNull() + val windKt = current?.windKt ?: 0.0 + val windDir = current?.windDirDeg ?: 0.0 + val summary = PreTripSummary( - timestampMs = System.currentTimeMillis(), - lat = lat, - lon = lon, - windSpeedKt = forecast?.windKt ?: 0.0, - windDirDeg = forecast?.windDirDeg ?: 0.0, - waveHeightM = conditions?.waveHeightM, - weatherDescription = forecast?.weatherDescription() ?: "Unknown", - boatProfile = boatProfile + timestampMs = System.currentTimeMillis(), + lat = lat, + lon = lon, + windSpeedKt = windKt, + windDirDeg = windDir, + waveHeightM = conditions?.waveHeightM, + weatherDescription = current?.weatherDescription() ?: "Unknown", + boatProfile = boatProfile ) - val routing = suggestRouting(summary) - val sailPlan = suggestSailPlan(summary) + return PreTripReport( + summary = summary, + conditionWindow = buildConditionWindow(forecastItems), + routeProjection = projectRoute(windDir, windKt, + conditions?.currentSpeedKt ?: 0.0, + conditions?.currentDirDeg ?: 0.0, + durationHrs, boatProfile), + sailPlan = suggestSailPlan(windKt, forecastItems, boatProfile), + watchItems = buildWatchList(conditions, forecastItems, boatProfile), + similarTrips = findSimilarTrips(pastTracks, windKt, windDir) + ) + } + + // ── Condition window ────────────────────────────────────────────────────── - return PreTripReport(summary, routing, sailPlan) + private fun buildConditionWindow(items: List): List { + val labels = listOf("Now", "+1 h", "+2 h", "+3 h") + return items.take(4).mapIndexed { i, item -> + ConditionSlice( + label = labels.getOrElse(i) { "+${i} h" }, + windKt = item.windKt, + windDirDeg = item.windDirDeg, + waveHeightM = null, // marine API doesn't give hourly per slot here; use conditions + weatherDescription = item.weatherDescription() + ) + } } - private fun suggestRouting(summary: PreTripSummary): String { - val wind = summary.windSpeedKt - val waves = summary.waveHeightM ?: 0.0 - - return when { - wind > 35.0 -> "STORM WARNING: Winds exceed 35kts. Consider remaining in port or seeking shelter immediately." - wind > 25.0 && waves > 2.5 -> "HEAVY WEATHER: Expect challenging conditions. Coastal routing advised to minimize fetch." - wind < 5.0 -> "LIGHT WINDS: Motor-sailing likely required for efficient passage." - else -> "FAVORABLE CONDITIONS: Standard routing based on destination bearing should be effective." + // ── Route projection ────────────────────────────────────────────────────── + + /** + * Candidate headings from Honokohau — each carries local context. + * Format: (headingDeg, label, localNote) + */ + private val candidates = listOf( + Triple(350.0, "N", "Kohala coast — upwind grind out on NE trades, fast run home"), + Triple(305.0, "NW", "Offshore — watch for trade acceleration past Upolu Point"), + Triple(270.0, "W", "Open water — comfortable in most trade conditions"), + Triple(225.0, "SW", "Toward Keauhou — broad reach out, may be a beat home if trades build"), + Triple(180.0, "S", "Keauhou Bay — note Alenuihāhā Channel shipping traffic") + ) + + private fun projectRoute( + windDirDeg: Double, + twsKt: Double, + currentSpeedKt: Double, + currentDirDeg: Double, + durationHrs: Double, + boatProfile: BoatProfile + ): RouteProjection { + // Score each candidate heading + data class Candidate( + val heading: Double, val label: String, val note: String, + val twa: Double, val sog: Double, val score: Double + ) + + val scored = candidates.map { (hdg, lbl, note) -> + val twa = twa(windDirDeg, hdg) + val sog = estimatedSogKt(boatProfile.lengthFt, twsKt, twa) + val score = headingScore(twa) + Candidate(hdg, lbl, note, twa, sog, score) + } + + val best = scored.maxByOrNull { it.score }!! + + // Return leg is the reciprocal heading + val returnHdg = (best.heading + 180.0) % 360.0 + val returnTwa = twa(windDirDeg, returnHdg) + val returnSog = estimatedSogKt(boatProfile.lengthFt, twsKt, returnTwa) + + // Outbound leg distance (half the trip duration at outbound SOG) + val outboundHrs = durationHrs / 2.0 + val outboundNm = best.sog * outboundHrs + + // Current component along outbound heading + val currentComponent = currentSpeedKt * + cos(Math.toRadians(currentDirDeg - best.heading)) + val currentNote = when { + currentComponent > 0.15 -> "Current adds %.1f kt outbound.".format(currentComponent) + currentComponent < -0.15 -> "Current opposes %.1f kt outbound.".format(-currentComponent) + else -> "" + } + + val outboundPos = pointOfSailName(best.twa) + val returnPos = pointOfSailName(returnTwa) + + val rationale = buildString { + append("Head ${best.label} (${best.heading.toInt()}°) — $outboundPos ") + append("at ~%.1f kt. ".format(best.sog)) + append("Est. turn-around %.1f nm out".format(outboundNm)) + append(", ~%.0f min. ".format(outboundHrs * 60)) + append("Return (${returnHdg.toInt()}°): $returnPos at ~%.1f kt.".format(returnSog)) + if (currentNote.isNotEmpty()) append(" $currentNote") + append("\n${best.note}") + } + + return RouteProjection( + departureHeadingDeg = best.heading, + outboundLegNm = outboundNm, + returnHeadingDeg = returnHdg, + outboundPointOfSail = outboundPos, + returnPointOfSail = returnPos, + rationale = rationale + ) + } + + /** True wind angle in degrees (0–180) for a given course relative to wind direction. */ + private fun twa(windDirFrom: Double, boatHdg: Double): Double { + var angle = abs(windDirFrom - boatHdg) % 360.0 + if (angle > 180.0) angle = 360.0 - angle + return angle + } + + /** Simplified polar SOG estimate based on hull speed limit and point of sail. */ + fun estimatedSogKt(lengthFt: Double, twsKt: Double, twaDegs: Double): Double { + val hullSpeed = 1.34 * sqrt(lengthFt) + val efficiency = when { + twaDegs < 40 -> 0.45 + twaDegs < 70 -> 0.70 + twaDegs < 120 -> 0.85 // beam reach — optimum + twaDegs < 150 -> 0.78 + else -> 0.60 // dead run } + val windFactor = when { + twsKt < 5 -> 0.30 + twsKt < 10 -> 0.65 + twsKt < 15 -> 0.85 + twsKt < 22 -> 1.00 + else -> 0.88 // reefed, slightly slower + } + return (hullSpeed * efficiency * windFactor).coerceAtMost(hullSpeed) } - private fun suggestSailPlan(summary: PreTripSummary): List { - val wind = summary.windSpeedKt + /** Higher = better heading; prefer beam reach, penalise hard beat or dead run. */ + private fun headingScore(twa: Double): Double = when { + twa < 35 -> 0.2 + twa < 55 -> 0.5 + twa < 125 -> 1.0 // broad beam reach window — ideal + twa < 150 -> 0.75 + else -> 0.4 + } + + private fun pointOfSailName(twa: Double): String = when { + twa < 45 -> "Close Hauled" + twa < 70 -> "Close Reach" + twa < 115 -> "Beam Reach" + twa < 150 -> "Broad Reach" + else -> "Run" + } + + // ── Sail plan ───────────────────────────────────────────────────────────── + + private fun suggestSailPlan( + windKt: Double, + forecastItems: List, + boatProfile: BoatProfile + ): List { val suggestions = mutableListOf() - // Main sail - suggestions.add(when { - wind > 30.0 -> SailSuggestion("Main", "Deep Reef / Trysail") - wind > 22.0 -> SailSuggestion("Main", "2nd Reef") - wind > 16.0 -> SailSuggestion("Main", "1st Reef") - else -> SailSuggestion("Main", "Full Main") - }) - - // Headsail - suggestions.add(when { - wind > 25.0 -> SailSuggestion("Headsail", "Storm Jib / Furl 50%") - wind > 18.0 -> SailSuggestion("Headsail", "Working Jib / Furl 30%") - wind < 10.0 && summary.boatProfile.hasGennaker -> SailSuggestion("Gennaker", "Deploy for light air reach") - else -> SailSuggestion("Headsail", "Full Genoa") - }) + // Pick best-fit headsail from inventory (highest maxWindKt that still covers windKt, + // falling back to smallest sail if wind exceeds all) + val headsail = boatProfile.headsails + .sortedBy { it.maxWindKt } + .firstOrNull { windKt <= it.maxWindKt } + ?: boatProfile.headsails.minByOrNull { it.maxWindKt } + + if (headsail != null) { + // Build a rationale note for this headsail choice + val note = headsailNote(headsail, windKt, forecastItems, boatProfile) + suggestions.add(SailSuggestion(headsail.name, note)) + } + + // Main reef recommendation + val mainAction = when { + windKt > 21 && boatProfile.mainReefs >= 2 -> "2nd Reef" + windKt > 15 && boatProfile.mainReefs >= 1 -> "1st Reef" + windKt > 22 && boatProfile.mainReefs == 1 -> "Full Reef" + else -> "Full Main" + } + suggestions.add(SailSuggestion("Main", mainAction)) + + // If wind is likely to build into a higher band during the trip, add a heads-up + val maxForecastWind = forecastItems.take(4).maxOfOrNull { it.windKt } ?: windKt + if (maxForecastWind > windKt + 4) { + val nextSail = boatProfile.headsails + .sortedBy { it.maxWindKt } + .firstOrNull { maxForecastWind <= it.maxWindKt && it != headsail } + if (nextSail != null) { + suggestions.add(SailSuggestion( + "On deck", + "${nextSail.name} — winds forecast to reach %.0f kt".format(maxForecastWind) + )) + } + } return suggestions } + + private fun headsailNote( + sail: SailConfig, + windKt: Double, + forecastItems: List, + profile: BoatProfile + ): String { + val maxForecast = forecastItems.take(4).maxOfOrNull { it.windKt } ?: windKt + + // E23-specific 155% overlap note + if (sail.name.contains("155%") && windKt > 10) { + return if (maxForecast > sail.maxWindKt) + "Flying now, but winds forecast to ${maxForecast.toInt()} kt — swap to 100% before departure" + else + "Good in current conditions; 155% sheets outside shrouds, plan tacks early" + } + + return when { + windKt <= sail.maxWindKt * 0.6 -> "Light for this sail — ${sail.name} is correct choice" + windKt <= sail.maxWindKt -> "In the sweet spot for ${sail.name}" + else -> "Approaching upper limit — consider smaller headsail" + } + } + + // ── Watch list ──────────────────────────────────────────────────────────── + + private fun buildWatchList( + conditions: MarineConditions?, + forecastItems: List, + boatProfile: BoatProfile + ): List { + val items = mutableListOf() + val windKt = forecastItems.firstOrNull()?.windKt ?: 0.0 + + // Swell hazards + conditions?.swellHeightM?.let { swell -> + if (swell > 2.0) + items += WatchItem("⚠️", "Significant swell %.1fm — expect motion".format(swell)) + } + conditions?.swellPeriodS?.let { period -> + if (period < 8.0) + items += WatchItem("⚠️", "Short swell period %.0fs — choppy, uncomfortable".format(period)) + } + + // Building wind in the trip window + val peakForecastWind = forecastItems.take(4).maxOfOrNull { it.windKt } ?: windKt + val peakHour = forecastItems.take(4).indexOfFirst { it.windKt == peakForecastWind } + if (peakForecastWind > windKt + 5) { + items += WatchItem("⚠️", + "Wind building to ${peakForecastWind.toInt()} kt in ~${peakHour} h — plan to be back before then") + } + + // Rain + val maxPrecip = forecastItems.take(4).maxOfOrNull { it.precipProbabilityPct } ?: 0 + if (maxPrecip > 30) + items += WatchItem("⚠️", "Rain likely ($maxPrecip% chance) — visibility may reduce") + + // Time-of-day trade build + val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) + if (hour in 11..14) + items += WatchItem("ℹ️", "Departing midday — Kona trades typically build 5–8 kt through afternoon") + else if (hour >= 15) + items += WatchItem("ℹ️", "Afternoon departure — trades may be at their strongest; build extra time margin") + + // E23 155% genoa overpower warning + val largeGenoa = boatProfile.headsails.firstOrNull { it.name.contains("155%") } + if (largeGenoa != null && windKt > largeGenoa.maxWindKt) { + items += WatchItem("⚠️", "155% Genoa will overpower ${boatProfile.name} at ${windKt.toInt()} kt — rig 100% Jib instead") + } + + // General storm check + if (windKt > 30) + items += WatchItem("⚠️", "Winds ${windKt.toInt()} kt — consider staying in port") + else if (windKt > 22) + items += WatchItem("⚠️", "Strong winds — reef before casting off") + + return items + } + + // ── Similar trips ───────────────────────────────────────────────────────── + + private fun findSimilarTrips( + pastTracks: List>, + windKt: Double, + windDirDeg: Double + ): SimilarTripSummary? { + if (pastTracks.isEmpty()) return null + + val matched = pastTracks.filter { points -> + if (points.size < 5) return@filter false + val winds = points.mapNotNull { it.windSpeedKnots } + if (winds.isEmpty()) return@filter false + val avgWind = winds.average() + val avgDir = points.mapNotNull { it.windAngleDeg }.average().takeIf { !it.isNaN() } ?: return@filter false + abs(avgWind - windKt) <= 5.0 && angleDiff(avgDir, windDirDeg) <= 30.0 + } + + if (matched.isEmpty()) return null + + val summaries = matched.map { summarise(it) } + val condLo = (windKt - 5).coerceAtLeast(0.0).toInt() + val condHi = (windKt + 5).toInt() + val dirLabel = cardinalDir(windDirDeg) + + return SimilarTripSummary( + count = matched.size, + avgDistanceNm = summaries.map { it.distanceNm }.average(), + avgDurationHrs = summaries.map { it.durationMs / 3_600_000.0 }.average(), + avgMaxSogKt = summaries.map { it.maxSogKt }.average(), + conditionDescription = "$condLo–$condHi kt $dirLabel" + ) + } + + private fun angleDiff(a: Double, b: Double): Double { + var d = abs(a - b) % 360.0 + if (d > 180.0) d = 360.0 - d + return d + } + + private fun cardinalDir(deg: Double): String { + val dirs = listOf("N","NNE","NE","ENE","E","ESE","SE","SSE", + "S","SSW","SW","WSW","W","WNW","NW","NNW") + return dirs[((deg + 11.25) / 22.5).toInt() % 16] + } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt index 9fd32c7..5d4cd14 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt @@ -1,51 +1,77 @@ package org.terst.nav.tripreport import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.terst.nav.LocationService +import org.terst.nav.NavApplication import org.terst.nav.data.model.ForecastItem import org.terst.nav.data.model.MarineConditions sealed class PreTripState { - object Idle : PreTripState() + object Idle : PreTripState() object Loading : PreTripState() data class Success(val report: PreTripReport) : PreTripState() data class Error(val message: String) : PreTripState() } class PreTripReportViewModel( + private val boatRepo: BoatProfileRepository, private val generator: PreTripReportGenerator = PreTripReportGenerator() ) : ViewModel() { private val _state = MutableStateFlow(PreTripState.Idle) val state: StateFlow = _state.asStateFlow() - private val _boatProfile = MutableStateFlow( - BoatProfile("Default Sloop", 35.0, BoatType.MONOHULL, RigType.SLOOP) - ) - val boatProfile: StateFlow = _boatProfile.asStateFlow() + private val _profiles = MutableStateFlow>(emptyList()) + val profiles: StateFlow> = _profiles.asStateFlow() - fun updateBoatProfile(profile: BoatProfile) { - _boatProfile.value = profile + private val _selectedProfile = MutableStateFlow(null) + val selectedProfile: StateFlow = _selectedProfile.asStateFlow() + + init { + val profiles = boatRepo.loadProfiles() + _profiles.value = profiles + _selectedProfile.value = boatRepo.selectedProfile() ?: profiles.firstOrNull() + } + + fun selectProfile(id: String) { + boatRepo.setSelectedId(id) + _selectedProfile.value = _profiles.value.firstOrNull { it.id == id } } - fun generate( - lat: Double, - lon: Double, - forecast: ForecastItem?, - conditions: MarineConditions? - ) { + fun generate(forecastItems: List, conditions: MarineConditions?) { + val profile = _selectedProfile.value ?: return + val pos = LocationService.bestPosition.value + viewModelScope.launch { _state.value = PreTripState.Loading try { - val report = generator.generateReport(lat, lon, forecast, conditions, _boatProfile.value) + val pastTracks = NavApplication.trackRepository.getPastTracks() + val report = generator.generateReport( + lat = pos?.latitude ?: 19.664, // Honokohau fallback + lon = pos?.longitude ?: -156.024, + forecastItems = forecastItems, + conditions = conditions, + boatProfile = profile, + pastTracks = pastTracks + ) _state.value = PreTripState.Success(report) } catch (e: Exception) { - _state.value = PreTripState.Error(e.message ?: "Failed to generate pre-trip report") + _state.value = PreTripState.Error(e.message ?: "Failed to generate report") } } } + + // ── Factory ─────────────────────────────────────────────────────────────── + + class Factory(private val boatRepo: BoatProfileRepository) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + PreTripReportViewModel(boatRepo) as T + } } diff --git a/android-app/app/src/main/res/layout/fragment_pretrip_report.xml b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml index d7ede49..5f3fb67 100644 --- a/android-app/app/src/main/res/layout/fragment_pretrip_report.xml +++ b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml @@ -1,32 +1,32 @@ - + android:background="?attr/colorBackground"> + android:padding="16dp"> + - + + android:layout_marginBottom="12dp" + app:cardElevation="2dp" + app:cardCornerRadius="12dp"> + + + + + + + + + + + + + + + + + + + android:text="CONDITIONS WINDOW" + android:textSize="11sp" + android:textAllCaps="true" + android:letterSpacing="0.12" + android:textColor="?attr/colorOnSurfaceVariant" + android:layout_marginBottom="12dp" /> + + + - + + app:cardElevation="2dp" + app:cardCornerRadius="12dp"> + android:padding="16dp"> + android:textAppearance="?attr/textAppearanceBodyMedium" + android:lineSpacingMultiplier="1.3" /> - + + + + + + + + android:textAppearance="?attr/textAppearanceBodyMedium" + android:lineSpacingMultiplier="1.4" /> - + + + + + + + + android:textAppearance="?attr/textAppearanceBodyMedium" + android:lineSpacingMultiplier="1.5" /> - + + android:layout_height="wrap_content" + android:layout_marginBottom="12dp" + android:visibility="gone" + app:cardElevation="2dp" + app:cardCornerRadius="12dp"> - + + + + + + + + + + + android:visibility="gone" + android:textColor="?attr/colorError" + android:textAppearance="?attr/textAppearanceBodyMedium" /> diff --git a/android-app/app/src/main/res/layout/item_condition_column.xml b/android-app/app/src/main/res/layout/item_condition_column.xml new file mode 100644 index 0000000..1ffcd55 --- /dev/null +++ b/android-app/app/src/main/res/layout/item_condition_column.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + -- cgit v1.2.3 From 35b214094a5563ae5b6cbd7cdff1e1617ca33184 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 21:19:43 +0000 Subject: Fix invalid colorBackground theme attr in pretrip layout colorBackground is not a Material theme attribute; replace with colorSurface which is the correct equivalent. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX --- android-app/app/src/main/res/layout/fragment_pretrip_report.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/res/layout/fragment_pretrip_report.xml b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml index 5f3fb67..510411b 100644 --- a/android-app/app/src/main/res/layout/fragment_pretrip_report.xml +++ b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/colorBackground"> + android:background="?attr/colorSurface"> Date: Fri, 10 Apr 2026 22:47:27 +0000 Subject: Add Learn tab; move quit to Safety; remove persistent MOB FAB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- android-app/.gitignore | 4 +- .../app/src/main/assets/docs/migrate_navionics.md | 56 +++++ .../app/src/main/assets/docs/migrate_sea_people.md | 59 ++++++ .../src/main/kotlin/org/terst/nav/MainActivity.kt | 20 +- .../kotlin/org/terst/nav/ui/learn/LearnFragment.kt | 56 +++++ .../org/terst/nav/ui/safety/SafetyFragment.kt | 5 + android-app/app/src/main/res/drawable/ic_learn.xml | 9 + .../app/src/main/res/layout/activity_main.xml | 35 +-- .../app/src/main/res/layout/fragment_learn.xml | 235 +++++++++++++++++++++ .../app/src/main/res/layout/fragment_safety.xml | 11 + .../app/src/main/res/menu/bottom_nav_menu.xml | 4 + 11 files changed, 450 insertions(+), 44 deletions(-) create mode 100644 android-app/app/src/main/assets/docs/migrate_navionics.md create mode 100644 android-app/app/src/main/assets/docs/migrate_sea_people.md create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/ui/learn/LearnFragment.kt create mode 100644 android-app/app/src/main/res/drawable/ic_learn.xml create mode 100644 android-app/app/src/main/res/layout/fragment_learn.xml (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/.gitignore b/android-app/.gitignore index 7db1cd2..acd3de6 100644 --- a/android-app/.gitignore +++ b/android-app/.gitignore @@ -11,8 +11,8 @@ build/ *.iws local.properties -# Generated files -app/src/main/assets/ +# Generated assets (add specific patterns here if needed) +# app/src/main/assets/ — removed blanket ignore; docs are tracked # Keystore *.jks diff --git a/android-app/app/src/main/assets/docs/migrate_navionics.md b/android-app/app/src/main/assets/docs/migrate_navionics.md new file mode 100644 index 0000000..83b28fa --- /dev/null +++ b/android-app/app/src/main/assets/docs/migrate_navionics.md @@ -0,0 +1,56 @@ +# Migrating from Navionics + +Welcome to Nav. This guide covers the key differences and how to move your data over. + +--- + +## What's Different + +| Feature | Navionics | Nav | +|---|---|---| +| Charts | Navionics SonarChart + Raster | OpenFreeMap vector tiles | +| Track recording | In-app GPX | GPX saved to Documents/Nav/ | +| Trip log | Voyage log | Voice log (transcribed) | +| Departure planning | None | Pre-trip briefing with sail plan | +| Wind overlay | Basic | Wind particles + hourly forecast | + +--- + +## Exporting Your Tracks from Navionics + +1. Open Navionics on your phone or tablet +2. Go to **My Charts → Tracks** +3. Tap a track → **Share → Export GPX** +4. Save or AirDrop the `.gpx` file to your device + +--- + +## Importing Tracks into Nav + +Nav automatically reads GPX files from the **Documents/Nav/** folder on your device. + +1. Move your exported `.gpx` files to `Documents/Nav/` using the Files app +2. Re-open Nav — tracks are loaded on startup +3. Similar-conditions trip comparison in the Pre-Trip Briefing will use these tracks + +--- + +## Waypoints + +Nav does not currently manage waypoints. Use Navionics or a dedicated chart plotter for waypoint routing. Nav focuses on departure briefing, track recording, and the sail log. + +--- + +## Charts + +Nav uses **OpenFreeMap** vector tiles. These are free, fast, and work well for coastal cruising. They do not include depth contours or hazard overlays — continue using Navionics or a NOAA chart app for navigation in unfamiliar waters. + +--- + +## Daily Workflow + +**Before you sail:** Open the **Safety → Plan Trip** screen for your departure briefing — sail plan, heading recommendation, watch items, and a look at how conditions will change over your trip window. + +**On the water:** Tap **Log** to dictate voice notes. The GPS track records automatically when you tap the record button. + +**After you return:** The post-trip report summarises distance, duration, and max SOG. diff --git a/android-app/app/src/main/assets/docs/migrate_sea_people.md b/android-app/app/src/main/assets/docs/migrate_sea_people.md new file mode 100644 index 0000000..b1920b4 --- /dev/null +++ b/android-app/app/src/main/assets/docs/migrate_sea_people.md @@ -0,0 +1,59 @@ +# Migrating from Sea People + +Welcome to Nav. This guide covers what transfers over and what works differently. + +--- + +## What's Different + +| Feature | Sea People | Nav | +|---|---|---| +| Social feed | Community posts | Not applicable — Nav is single-user | +| Trip log | Text entries + photos | Voice log (transcribed audio) | +| Boat profiles | Basic boat card | Detailed sail inventory with reef points | +| Weather | Integrated forecast | Open-Meteo hourly + NOAA marine | +| Departure planning | Manual notes | Pre-trip briefing with sail plan & route | + +--- + +## Exporting Your Log from Sea People + +1. Open Sea People → **Profile → My Logs** +2. Use the **Export** option (CSV or PDF depending on your version) +3. Save entries you want to keep for reference + +Nav's voice log works differently — entries are dictated on the water and transcribed. You won't be able to import Sea People entries directly, but you can review them alongside Nav going forward. + +--- + +## Boat Profiles + +Nav stores a detailed boat profile with your sail inventory: + +- **Headsails** with wind range for each (e.g. 155% Genoa ≤ 13 kt, 100% Jib 10–21 kt, 65% Blade 18+ kt) +- **Main reef points** +- **Hull length** (used for hull-speed estimates in the departure briefing) + +Your boats are pre-seeded on first launch. Profiles are stored locally and used in the **Pre-Trip Briefing** to give sail recommendations specific to your boat. + +--- + +## Trip Log Workflow + +**On the water:** Tap the **Log** tab and use the microphone button to dictate a note. Notes are timestamped with your GPS position. + +**Voice notes work well for:** +- Sea state observations ("1.5m swell, 10s period, comfortable") +- Traffic ("fishing vessel on starboard, gave way") +- Sail changes ("reefed main at 18 kt") +- Time stamps ("rounded the point, heading west") + +--- + +## Daily Workflow + +**Before you sail:** Safety → Plan Trip for your departure briefing. + +**On the water:** Log tab for voice notes; tap the record FAB to start the GPS track. + +**After you return:** The post-trip report generates a narrative summary you can save or share. diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 86dd531..996892e 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -54,9 +54,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var fragmentContainer: FrameLayout private lateinit var fabRecordTrack: FloatingActionButton - private lateinit var fabMob: FloatingActionButton private lateinit var fabRecenter: MaterialButton - private lateinit var btnQuit: MaterialButton private lateinit var bottomSheet: CardView private lateinit var bottomNav: BottomNavigationView private lateinit var mapCrosshair: View @@ -117,9 +115,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private fun initializeUI() { fragmentContainer = findViewById(R.id.fragment_container) fabRecordTrack = findViewById(R.id.fab_record_track) - fabMob = findViewById(R.id.fab_mob) fabRecenter = findViewById(R.id.fab_recenter) - btnQuit = findViewById(R.id.btn_quit) bottomSheet = findViewById(R.id.instrument_bottom_sheet) bottomNav = findViewById(R.id.bottom_navigation) mapCrosshair = findViewById(R.id.map_crosshair) @@ -166,9 +162,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } else false } - fabMob.setOnClickListener { onActivateMob() } fabRecenter.setOnClickListener { mapHandler?.recenter() } - btnQuit.setOnClickListener { onQuitRequested() } lifecycleScope.launch { viewModel.isRecording.collect { recording -> @@ -280,6 +274,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN true } + R.id.nav_learn -> { + showOverlay(org.terst.nav.ui.learn.LearnFragment()) + bottomSheetBehavior.isHideable = true + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + true + } else -> false } } @@ -287,6 +287,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private fun showOverlay(fragment: androidx.fragment.app.Fragment) { fragmentContainer.visibility = View.VISIBLE + fabRecordTrack.visibility = View.GONE supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, fragment) .commit() @@ -294,9 +295,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private fun hideOverlays() { fragmentContainer.visibility = View.GONE + fabRecordTrack.visibility = View.VISIBLE } - private fun onQuitRequested() { + override fun onQuitRequested() { if (viewModel.isRecording.value) { androidx.appcompat.app.AlertDialog.Builder(this) .setMessage("Recording in progress. Quit and discard the current track?") @@ -405,9 +407,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { mapCrosshair.visibility = if (following) View.GONE else View.VISIBLE if (following) { fadeOut(fabRecenter, gone = true) - fadeIn(bottomNav, fabMob, fabRecordTrack) + fadeIn(bottomNav, fabRecordTrack) } else { - fadeOut(bottomNav, fabMob, fabRecordTrack, gone = true) + fadeOut(bottomNav, fabRecordTrack, gone = true) fadeIn(fabRecenter) } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/learn/LearnFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/learn/LearnFragment.kt new file mode 100644 index 0000000..8440edb --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/learn/LearnFragment.kt @@ -0,0 +1,56 @@ +package org.terst.nav.ui.learn + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.google.android.material.card.MaterialCardView +import org.terst.nav.R +import org.terst.nav.ui.doc.DocFragment + +class LearnFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_learn, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Migration guides — open local markdown docs + view.findViewById(R.id.card_migrate_navionics).setOnClickListener { + openDoc("docs/migrate_navionics.md") + } + view.findViewById(R.id.card_migrate_seapeople).setOnClickListener { + openDoc("docs/migrate_sea_people.md") + } + + // ASA / external links — open in browser + view.findViewById(R.id.card_asa_courses).setOnClickListener { + openUrl("https://www.asa.com/courses/") + } + view.findViewById(R.id.card_asa_online).setOnClickListener { + openUrl("https://www.asa.com/online-courses/") + } + view.findViewById(R.id.card_colregs).setOnClickListener { + openUrl("https://www.navcen.uscg.gov/international-regulations-for-preventing-collisions-at-sea") + } + view.findViewById(R.id.card_flashcards).setOnClickListener { + openUrl("https://quizlet.com/subject/sailing/") + } + } + + private fun openDoc(path: String) { + parentFragmentManager.beginTransaction() + .replace(R.id.fragment_container, DocFragment.newInstance(path)) + .addToBackStack(null) + .commit() + } + + private fun openUrl(url: String) { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt index 4bc0c7a..6dd7411 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt @@ -15,6 +15,7 @@ class SafetyFragment : Fragment() { interface SafetyListener { fun onActivateMob() fun onConfigureAnchor() + fun onQuitRequested() } private var listener: SafetyListener? = null @@ -55,6 +56,10 @@ class SafetyFragment : Fragment() { .addToBackStack(null) .commit() } + + view.findViewById(R.id.button_quit).setOnClickListener { + listener?.onQuitRequested() + } } fun updateAnchorStatus(statusText: String) { diff --git a/android-app/app/src/main/res/drawable/ic_learn.xml b/android-app/app/src/main/res/drawable/ic_learn.xml new file mode 100644 index 0000000..1574693 --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_learn.xml @@ -0,0 +1,9 @@ + + + diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index 5943949..1bb88b3 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -59,26 +59,10 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" + android:clickable="true" + android:focusable="true" android:background="?attr/colorSurface" /> - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/fragment_safety.xml b/android-app/app/src/main/res/layout/fragment_safety.xml index f90420e..60ea1aa 100644 --- a/android-app/app/src/main/res/layout/fragment_safety.xml +++ b/android-app/app/src/main/res/layout/fragment_safety.xml @@ -113,4 +113,15 @@ android:text="PLAN TRIP (PRE-TRIP REPORT)" app:layout_constraintTop_toBottomOf="@id/card_anchor" /> + + diff --git a/android-app/app/src/main/res/menu/bottom_nav_menu.xml b/android-app/app/src/main/res/menu/bottom_nav_menu.xml index e7fc15d..3037b7e 100644 --- a/android-app/app/src/main/res/menu/bottom_nav_menu.xml +++ b/android-app/app/src/main/res/menu/bottom_nav_menu.xml @@ -16,4 +16,8 @@ android:id="@+id/nav_safety" android:icon="@drawable/ic_safety" android:title="Safety" /> + -- cgit v1.2.3 From 4a2d0298ab2caa3d62cfbd54c0071ae47eb89ccf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 02:27:51 +0000 Subject: Four features: outbound link markers, offline content, log text/photo, departure picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- android-app/app/src/main/AndroidManifest.xml | 1 + .../app/src/main/assets/docs/colregs_reference.md | 230 +++++++++++++++++++++ .../app/src/main/assets/docs/sailing_reference.md | 206 ++++++++++++++++++ .../main/kotlin/org/terst/nav/logbook/LogEntry.kt | 3 +- .../org/terst/nav/logbook/VoiceLogViewModel.kt | 14 +- .../terst/nav/tripreport/PreTripReportFragment.kt | 122 ++++++++--- .../terst/nav/tripreport/PreTripReportGenerator.kt | 116 +++++++---- .../terst/nav/tripreport/PreTripReportViewModel.kt | 20 +- .../kotlin/org/terst/nav/ui/learn/LearnFragment.kt | 11 +- .../org/terst/nav/ui/voicelog/VoiceLogFragment.kt | 162 +++++++++++---- .../app/src/main/res/drawable/ic_calendar.xml | 44 ++++ .../app/src/main/res/drawable/ic_open_in_new.xml | 30 +++ .../app/src/main/res/layout/fragment_learn.xml | 226 +++++++++++++++----- .../main/res/layout/fragment_pretrip_report.xml | 52 +++++ .../app/src/main/res/layout/fragment_voice_log.xml | 142 ++++++++++--- 15 files changed, 1176 insertions(+), 203 deletions(-) create mode 100644 android-app/app/src/main/assets/docs/colregs_reference.md create mode 100644 android-app/app/src/main/assets/docs/sailing_reference.md create mode 100644 android-app/app/src/main/res/drawable/ic_calendar.xml create mode 100644 android-app/app/src/main/res/drawable/ic_open_in_new.xml (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index bcab20c..7cbafc7 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + diff --git a/android-app/app/src/main/assets/docs/colregs_reference.md b/android-app/app/src/main/assets/docs/colregs_reference.md new file mode 100644 index 0000000..6e5c39a --- /dev/null +++ b/android-app/app/src/main/assets/docs/colregs_reference.md @@ -0,0 +1,230 @@ +# ColRegs — Rules of the Road + +International Regulations for Preventing Collisions at Sea (COLREGS), 1972 with amendments. This is a summary reference; consult the official text for legal purposes. + +--- + +## Part A — General (Rules 1–3) + +**Rule 1 — Application** +Applies to all vessels on the high seas and connected navigable waters. + +**Rule 2 — Responsibility** +Nothing in these rules exonerates a vessel, owner, master, or crew from the consequences of neglect. Good seamanship always applies. + +**Rule 3 — Definitions** +- *Vessel* — any watercraft, including seaplanes and WIG craft +- *Power-driven vessel* — any vessel propelled by machinery +- *Sailing vessel* — under sail only; if engine is running, she is power-driven +- *Vessel engaged in fishing* — using nets, lines, trawls that restrict maneuverability +- *Underway* — not at anchor, aground, or made fast to shore +- *Restricted visibility* — fog, mist, falling snow, heavy rain, sandstorm, or similar conditions + +--- + +## Part B — Steering and Sailing Rules + +### Section I — Conduct in Any Visibility (Rules 4–10) + +**Rule 4** — Applies in any condition of visibility. + +**Rule 5 — Look-out** +Every vessel shall maintain a proper look-out at all times by sight, hearing, and all available means. + +**Rule 6 — Safe Speed** +Every vessel shall proceed at a safe speed. Factors: visibility, traffic density, vessel maneuverability, background lights at night, radar state, sea state. + +**Rule 7 — Risk of Collision** +Risk exists if the compass bearing of an approaching vessel does not appreciably change. When in doubt, assume risk exists. + +**Rule 8 — Action to Avoid Collision** +- Action must be positive, made in ample time, and large enough to be readily apparent +- Course or speed changes should be large enough to be noticed +- If necessary, stop or reverse + +**Rule 9 — Narrow Channels** +- Keep to the starboard side of a narrow channel +- Vessels under 20 m or sailing vessels shall not impede vessels that can safely navigate only in the channel +- Overtaking only when safe and the overtaken vessel signals agreement +- Do not cross a narrow channel if it impedes a through-traffic vessel + +**Rule 10 — Traffic Separation Schemes** +- Join/leave at end; if joining from side, at acute angle +- Keep out of separation zones +- Crossing traffic does so at right angles where practicable +- Inshore traffic zones: use only if < 20 m, or sailing, or fishing + +--- + +### Section II — Conduct in Sight of One Another (Rules 11–18) + +**Rule 11** — Applies to vessels in sight of one another. + +**Rule 12 — Sailing Vessels** +- Vessel on port tack gives way to vessel on starboard tack +- Both on same tack: windward vessel gives way to leeward vessel +- Port tack vessel cannot determine which tack the other is on: gives way + +**Rule 13 — Overtaking** +Any vessel overtaking gives way. Overtaking means coming up from more than 22.5° abaft the other's beam. Overtaking status persists until clear and past. + +**Rule 14 — Head-on Situation** +Both vessels altering course to starboard so each passes on the port side of the other. Applies when risk of collision exists and vessels are nearly end-on. + +**Rule 15 — Crossing Situation** +The vessel that has the other on its own starboard side gives way (the "burdened" or give-way vessel). The stand-on vessel is on the right. + +**Rule 16 — Action by Give-way Vessel** +Take early and substantial action to keep well clear. + +**Rule 17 — Action by Stand-on Vessel** +- May take action to avoid collision by own maneuver alone when it becomes apparent the give-way vessel is not taking sufficient action +- Must take action when collision cannot be avoided by give-way vessel alone +- Course change to port for a vessel on your port side is avoided if possible + +**Rule 18 — Responsibilities Between Vessels** + +Hierarchy (higher number gives way to all above): +1. Vessel not under command (NUC) +2. Vessel restricted in ability to maneuver (RAM) +3. Vessel constrained by draft +4. Vessel engaged in fishing +5. Sailing vessel +6. Power-driven vessel underway + +*Note:* Sailing and power vessels give way to NUC, RAM, constrained, and fishing vessels. A power vessel gives way to a sailing vessel. + +--- + +### Section III — Conduct in Restricted Visibility (Rule 19) + +**Rule 19 — Restricted Visibility** +- Proceed at safe speed adapted to conditions +- Have engines ready for immediate maneuver +- On hearing fog signal apparently forward of beam: reduce to bare steerage or stop +- Avoid alteration of course to port for a vessel forward of beam (except overtaking) +- Avoid alteration toward a vessel abeam or abaft beam + +--- + +## Part C — Lights and Shapes (Rules 20–31) + +### Lights (Rules 20–22) + +**Rule 20 — Application** +Lights required from sunset to sunrise and in restricted visibility. + +**Rule 21 — Definitions** +- *Masthead light* — white forward light, 225° arc +- *Side lights* — red (port) and green (starboard), 112.5° each +- *Stern light* — white aft, 135° arc +- *Towing light* — yellow, same arc as stern light +- *All-round light* — 360° arc +- *Flashing light* — 120+ flashes/minute + +**Rule 22 — Visibility of Lights** + +| Vessel size | Masthead | Side | Stern | All-round | +|---|---|---|---|---| +| ≥ 50 m | 6 nm | 3 nm | 3 nm | 3 nm | +| 12–50 m | 5 nm | 2 nm | 2 nm | 2 nm | +| 7–12 m | 3 nm | 1 nm | 2 nm | 2 nm | +| < 7 m | — | — | — | 2 nm | + +--- + +### Light Combinations to Know + +**Under power (≥ 50 m):** Two masthead lights (forward lower, aft higher) + sidelights + stern light + +**Under power (< 50 m):** One masthead light + sidelights + stern light + +**Under sail (underway):** Sidelights + stern light only. *No masthead light when under sail.* + +**Sail + engine:** Power-driven vessel rules apply — show cone (point down) by day. + +**At anchor (< 50 m):** One white all-round light forward. +**At anchor (≥ 50 m):** White all-round forward + aft. + +**Not under command:** Two red all-round lights (vertical). If making way: add sidelights + stern light. + +**Restricted in ability to maneuver:** Red-white-red all-round lights (vertical). If making way: add masthead + sidelights + stern. + +**Vessel aground:** Anchor lights + two red all-round lights (vertical). + +**Towing vessel:** Extra masthead light(s) + yellow towing light instead of (or in addition to) stern light. + +**Fishing (trawling):** Green over white all-round (vertical) + sidelights + stern if making way. +**Fishing (other):** Red over white all-round (vertical) + sidelights + stern if making way + white toward gear if gear > 150 m. + +**Pilot vessel on duty:** White over red all-round lights. + +--- + +### Day Shapes (Rule 28) + +| Shape | Vessel Type | +|---|---| +| Black ball | At anchor | +| Black cone (apex down) | Sailing vessel with engine | +| Two black balls (vertical) | Not under command | +| Ball-diamond-ball (vertical) | Restricted in ability to maneuver | +| Black cylinder | Constrained by draft | +| Basket | Engaged in fishing | +| Cone (apex up) | Vessel being towed (if requested) | + +--- + +## Part D — Sound and Light Signals (Rules 32–37) + +**Rule 32 — Definitions** +- *Short blast* — about 1 second +- *Prolonged blast* — 4–6 seconds + +**Rule 33 — Equipment** +- ≥ 12 m: whistle + bell +- ≥ 100 m: also gong + +**Rule 34 — Maneuvering and Warning Signals** + +| Signal | Meaning | +|---|---| +| 1 short | I am altering course to starboard | +| 2 shorts | I am altering course to port | +| 3 shorts | I am operating astern propulsion | +| 5+ shorts (rapid) | Danger / doubt signal | +| 1 prolonged | Vessel leaving berth | + +**Rule 35 — Sound Signals in Restricted Visibility** + +| Signal | Vessel | +|---|---| +| 1 prolonged (≤ 2 min) | Power-driven vessel making way | +| 2 prolonged (≤ 2 min) | Power-driven vessel underway but stopped | +| 1 long + 2 short (≤ 2 min) | NUC, RAM, sailing, fishing, towing | +| 1 long + 3 short | Vessel being towed (last vessel) | +| Rapid bell (5 sec, ≤ 1 min) | At anchor (< 100 m) | +| Bell + gong (≤ 1 min) | At anchor (≥ 100 m) | +| 3 strokes + rapid bell + 3 strokes | Vessel aground | + +**Rule 36 — Attention Signal** +Five or more short and rapid blasts. Also a light signal of the same pattern. + +**Rule 37 — Distress Signals** +Gun fired at ~1 min intervals; continuous foghorn; SOS (···−−−···); MAYDAY by voice; orange smoke; flames; parachute flare; dye; square flag + ball; high-intensity white light flashing; radio alarm signal. + +--- + +## Part E — Exemptions (Rule 38) + +Older vessels may be exempt from some lighting requirements for a period of years after the rules came into force. + +--- + +## Quick Memory Aids + +**Starboard right-of-way:** When another vessel is on your starboard side in a crossing situation, YOU give way. + +**Lights mnemonic — red over green, sailing machine:** A sailing vessel shows red (port side) and green (starboard) sidelights plus a white stern light. No masthead light while under sail alone. + +**The hierarchy:** NUC → RAM → Constrained → Fishing → Sail → Power diff --git a/android-app/app/src/main/assets/docs/sailing_reference.md b/android-app/app/src/main/assets/docs/sailing_reference.md new file mode 100644 index 0000000..7fc7bdb --- /dev/null +++ b/android-app/app/src/main/assets/docs/sailing_reference.md @@ -0,0 +1,206 @@ +# Sailing Quick Reference + +--- + +## Points of Sail + +The point of sail describes the angle between the boat's heading and the true wind direction. + +| Point of Sail | True Wind Angle | Description | +|---|---|---| +| In irons | 0–30° | Head-to-wind, sails luffing, no drive | +| Close hauled | ~30–45° | Sailing as close to the wind as possible | +| Close reach | ~45–60° | Between close hauled and beam reach | +| Beam reach | ~90° | Wind directly abeam — often fastest point | +| Broad reach | ~120–150° | Wind on the quarter — comfortable, fast | +| Run | ~150–180° | Wind from directly behind | + +**No-go zone:** ~0–30° on either side of the wind — the boat cannot make progress sailing directly into the wind. + +--- + +## Tacking vs. Gybing + +**Tacking** — turning the bow through the wind (bow crosses the wind). The boom swings across from one side to the other. Used to head upwind. + +**Gybing** — turning the stern through the wind (stern crosses the wind). The boom can swing violently — always control the mainsheet. Used to change direction downwind. + +--- + +## Sail Trim Basics + +**Telltales** — strips of yarn or fabric on the sail. +- Both telltales streaming aft → sail trimmed correctly +- Windward telltale lifting → sheet in (trim), or bear away +- Leeward telltale lifting → sheet out (ease), or head up + +**In irons fix:** Let sails luff, push boom to one side, fall off onto a tack. + +**Reef** — reducing sail area by partially lowering the mainsail and tying off the excess. Reef before you think you need to. Typical thresholds: first reef ~15–18 kt, second reef ~21–25 kt. + +--- + +## Hull Speed + +The theoretical maximum displacement hull speed: + +**Hull speed (kt) ≈ 1.34 × √(waterline length in feet)** + +| LOA | Hull Speed | +|---|---| +| 20 ft | ~6.0 kt | +| 23 ft | ~6.4 kt | +| 30 ft | ~7.3 kt | +| 40 ft | ~8.5 kt | + +A modern fin-keel boat can exceed hull speed in planing conditions (surfing downwind in big waves). + +--- + +## Navigation Lights — Quick Reference + +| Situation | What You See | What It Is | +|---|---|---| +| Red + green + white | Two side lights + stern | Head-on approach | +| Red only | Port sidelight | Vessel crossing left-to-right in front of you | +| Green only | Starboard sidelight | Vessel crossing right-to-left — you are give-way | +| White only (masthead) + green | Overtaking from starboard | Vessel overtaking you on starboard | +| Two white (stacked) + red/green | Two masthead lights | Large ship (≥50 m) underway under power | +| Red + white (all-round, vertical) | Not under command | Give way — vessel cannot maneuver | +| Green + white (all-round, vertical) | Trawler | Give way — engaged in fishing | +| White all-round only | At anchor | Avoid — vessel at anchor | +| White + red all-round (vertical) | Pilot vessel | Pilot boat on duty | + +--- + +## Day Shapes + +| Shape | Meaning | +|---|---| +| ⚫ Black ball | Vessel at anchor | +| 🔻 Black cone (apex down) | Sailing vessel motorsailing | +| ⚫ ⚫ Two balls (vertical) | Not under command | +| ⚫ ◆ ⚫ Ball-diamond-ball | Restricted in ability to maneuver | +| ▬ Black cylinder | Constrained by draft | + +--- + +## Beaufort Wind Scale + +| Force | kt | Description | Sea State | +|---|---|---|---| +| 0 | < 1 | Calm | Mirror smooth | +| 1 | 1–3 | Light air | Ripples | +| 2 | 4–6 | Light breeze | Small wavelets | +| 3 | 7–10 | Gentle breeze | Scattered whitecaps | +| 4 | 11–16 | Moderate breeze | Moderate waves, frequent whitecaps | +| 5 | 17–21 | Fresh breeze | Long waves, many whitecaps, spray | +| 6 | 22–27 | Strong breeze | Large waves, spray, whitecaps everywhere | +| 7 | 28–33 | Near gale | Sea heaping up, foam streaks | +| 8 | 34–40 | Gale | Moderately high waves, edges blowing | +| 9 | 41–47 | Strong gale | High waves, dense foam, visibility affected | +| 10 | 48–55 | Storm | Very high waves, sea white, heavy sea roll | +| 11 | 56–63 | Violent storm | Exceptionally high waves | +| 12 | 64+ | Hurricane force | Air filled with foam, visibility nil | + +--- + +## Common Knots + +**Bowline** — fixed loop that won't slip. The classic sailing knot. "The rabbit comes out of the hole, round the tree, and back down the hole." + +**Cleat hitch** — securing a line to a cleat. Take a round turn around the base, then two figure-8 turns, then one locking hitch over the horn. + +**Clove hitch** — temporary attachment to a post or rail. Two half hitches; easy to adjust and release. + +**Figure-eight** — stopper knot. Prevents a line from running through a block or fairlead. + +**Round turn and two half hitches** — secure, adjustable attachment to a ring or rail. + +**Reef knot** — joining two lines of similar diameter. Right over left, left over right. Not for critical loads — use a sheet bend for mismatched diameters. + +**Sheet bend** — joining two lines of different diameter. The thicker line forms the loop. + +**Rolling hitch** — attaching to another line or spar under load. Grips when pulled along the spar. + +**Anchor hitch (fisherman's bend)** — the correct knot for attaching a line to an anchor. + +--- + +## Buoyage — IALA System B (Americas, Japan, Philippines, Korea) + +**Red right returning** — red buoys on the starboard side when returning from sea. + +| Mark | Shape | Color | Top Mark | Meaning | +|---|---|---|---|---| +| Port lateral | Can / pillar | Red | None | Keep to starboard (IALA-B: keep red to starboard) | +| Starboard lateral | Nun / cone | Green | Cone | Keep to port | +| Safe water | Sphere | Red + white vertical stripes | Sphere | Safe water on all sides | +| Isolated danger | Pillar / spar | Black + red bands | Two black balls | Isolated danger, safe water around it | +| Special mark | Any | Yellow | Yellow X | Special purpose (mooring, racing, TSS) | +| Cardinal (N) | Pillar / spar | Black over yellow | Two cones pointing up | Pass to the north | +| Cardinal (S) | Pillar / spar | Yellow over black | Two cones pointing down | Pass to the south | +| Cardinal (E) | Pillar / spar | Black-yellow-black bands | Cones base-to-base | Pass to the east | +| Cardinal (W) | Pillar / spar | Yellow-black-yellow bands | Cones point-to-point | Pass to the west | + +*IALA-A (Europe, Africa, most of Asia):* Red/green assignments are reversed — "red left returning." + +--- + +## VHF Radio Channels + +| Channel | Use | +|---|---| +| 16 | **International distress, safety, and calling** — always monitor | +| 22A | US Coast Guard working channel | +| 9 | Boater calling channel (US) | +| 6 | Ship-to-ship safety communications | +| 13 | Bridge-to-bridge (1 watt) | +| 70 | DSC digital selective calling — do not use for voice | +| 24, 25, 26, 27, 28 | Public correspondence (marine operator) | + +**MAYDAY procedure:** +1. MAYDAY MAYDAY MAYDAY +2. This is [vessel name × 3] +3. MAYDAY [vessel name] +4. Position +5. Nature of distress +6. Number of persons aboard +7. Any other information +8. Over + +--- + +## Tide and Current Basics + +**Flood** — tide coming in (rising sea level). +**Ebb** — tide going out (falling sea level). +**Slack** — the period of minimal current around high and low water. + +Rule of twelfths — tide rises/falls unevenly: +- Hour 1: 1/12 of range +- Hour 2: 2/12 of range +- Hour 3: 3/12 of range ← fastest +- Hour 4: 3/12 of range ← fastest +- Hour 5: 2/12 of range +- Hour 6: 1/12 of range + +**Spring tides** — larger range; occur near new and full moon. +**Neap tides** — smaller range; occur near quarter moons. + +--- + +## Distress Signals (Rule 37 / SOLAS) + +Any of these signals indicate distress and request assistance: +- Red parachute flare or red hand flare +- Orange smoke signal +- MAYDAY spoken over radio (Ch 16) +- SOS (···−−−···) by any signaling method +- Continuous foghorn sound +- Gun fired at approximately 1-minute intervals +- Flames on the vessel +- Slowly and repeatedly raising and lowering both arms +- Square flag with ball above or below it +- Orange dye in water +- Satellite EPIRB signal diff --git a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogEntry.kt b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogEntry.kt index 17cebfb..c038547 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogEntry.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogEntry.kt @@ -8,5 +8,6 @@ data class LogEntry( val text: String, val entryType: EntryType, val lat: Double? = null, - val lon: Double? = null + val lon: Double? = null, + val photoPath: String? = null // absolute file path or content URI string ) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/logbook/VoiceLogViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/logbook/VoiceLogViewModel.kt index 067cbaf..0a68ba8 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/logbook/VoiceLogViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/logbook/VoiceLogViewModel.kt @@ -20,12 +20,18 @@ class VoiceLogViewModel(private val repository: InMemoryLogbookRepository) { _state.value = VoiceLogState.Error(message) } - fun confirmAndSave() { - val current = _state.value as? VoiceLogState.Result ?: return + /** + * Save an entry with the given text and optional photo path. + * Called directly by the fragment from the Save button so the user + * can have edited the recognized text in the EditText beforehand. + */ + fun save(text: String, photoPath: String? = null) { + if (text.isBlank() && photoPath == null) return val entry = LogEntry( timestampMs = System.currentTimeMillis(), - text = current.recognized, - entryType = EntryType.GENERAL + text = text.ifBlank { "(photo only)" }, + entryType = EntryType.GENERAL, + photoPath = photoPath ) val saved = repository.save(entry) _state.value = VoiceLogState.Saved(saved) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt index c88dc48..ac1350d 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt @@ -11,13 +11,21 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.DateValidatorPointForward +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat import kotlinx.coroutines.launch import org.terst.nav.NavApplication import org.terst.nav.R import org.terst.nav.ui.MainViewModel +import java.text.SimpleDateFormat +import java.util.Calendar import java.util.Locale class PreTripReportFragment : Fragment() { @@ -26,7 +34,9 @@ class PreTripReportFragment : Fragment() { private lateinit var viewModel: PreTripReportViewModel private lateinit var chipGroupBoats: ChipGroup - private lateinit var btnGenerate: com.google.android.material.button.MaterialButton + private lateinit var tvDepartureTime: TextView + private lateinit var btnPickDeparture: MaterialButton + private lateinit var btnGenerate: MaterialButton private lateinit var progress: ProgressBar private lateinit var cardConditions: MaterialCardView private lateinit var conditionsTable: LinearLayout @@ -40,6 +50,8 @@ class PreTripReportFragment : Fragment() { private lateinit var tvSimilar: TextView private lateinit var tvError: TextView + private val departureSdf = SimpleDateFormat("EEE MMM d, h:mm a", Locale.getDefault()) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.inflate(R.layout.fragment_pretrip_report, container, false) @@ -47,7 +59,6 @@ class PreTripReportFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Construct ViewModel with the application-level repo viewModel = ViewModelProvider( requireActivity(), PreTripReportViewModel.Factory(NavApplication.boatProfileRepository) @@ -57,28 +68,30 @@ class PreTripReportFragment : Fragment() { observeViewModel() btnGenerate.setOnClickListener { triggerGenerate() } + btnPickDeparture.setOnClickListener { showDeparturePicker() } - // Auto-generate if forecast data is already available if (mainViewModel.forecast.value.isNotEmpty()) { triggerGenerate() } } private fun bindViews(view: View) { - chipGroupBoats = view.findViewById(R.id.chip_group_boats) - btnGenerate = view.findViewById(R.id.btn_generate_pretrip) - progress = view.findViewById(R.id.progress_pretrip) - cardConditions = view.findViewById(R.id.card_conditions) - conditionsTable = view.findViewById(R.id.conditions_table) - cardRoute = view.findViewById(R.id.card_route) - tvRoute = view.findViewById(R.id.tv_route) - cardSailPlan = view.findViewById(R.id.card_sail_plan) - tvSailPlan = view.findViewById(R.id.tv_sail_plan) - cardWatchlist = view.findViewById(R.id.card_watchlist) - tvWatchlist = view.findViewById(R.id.tv_watchlist) - cardSimilar = view.findViewById(R.id.card_similar) - tvSimilar = view.findViewById(R.id.tv_similar) - tvError = view.findViewById(R.id.tv_error) + chipGroupBoats = view.findViewById(R.id.chip_group_boats) + tvDepartureTime = view.findViewById(R.id.tv_departure_time) + btnPickDeparture = view.findViewById(R.id.btn_pick_departure) + btnGenerate = view.findViewById(R.id.btn_generate_pretrip) + progress = view.findViewById(R.id.progress_pretrip) + cardConditions = view.findViewById(R.id.card_conditions) + conditionsTable = view.findViewById(R.id.conditions_table) + cardRoute = view.findViewById(R.id.card_route) + tvRoute = view.findViewById(R.id.tv_route) + cardSailPlan = view.findViewById(R.id.card_sail_plan) + tvSailPlan = view.findViewById(R.id.tv_sail_plan) + cardWatchlist = view.findViewById(R.id.card_watchlist) + tvWatchlist = view.findViewById(R.id.tv_watchlist) + cardSimilar = view.findViewById(R.id.card_similar) + tvSimilar = view.findViewById(R.id.tv_similar) + tvError = view.findViewById(R.id.tv_error) } private fun observeViewModel() { @@ -90,20 +103,81 @@ class PreTripReportFragment : Fragment() { if (profile != null) syncChipSelection(profile.id) } } + viewLifecycleOwner.lifecycleScope.launch { + viewModel.departureMs.collect { ms -> renderDepartureLabel(ms) } + } viewLifecycleOwner.lifecycleScope.launch { viewModel.state.collect { renderState(it) } } } + // ── Departure picker ────────────────────────────────────────────────────── + + private fun showDeparturePicker() { + val currentMs = viewModel.departureMs.value + + // Constrain to today or future + val constraints = CalendarConstraints.Builder() + .setValidator(DateValidatorPointForward.now()) + .build() + + val datePicker = MaterialDatePicker.Builder.datePicker() + .setTitleText("Departure date") + .setSelection(currentMs) + .setCalendarConstraints(constraints) + .build() + + datePicker.addOnPositiveButtonClickListener { dateMs -> + // dateMs is midnight UTC of the selected date; add current time-of-day offset + val cal = Calendar.getInstance().apply { timeInMillis = dateMs } + // Default to current hour of day, rounded to nearest half-hour + val now = Calendar.getInstance() + cal.set(Calendar.HOUR_OF_DAY, now.get(Calendar.HOUR_OF_DAY)) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + cal.set(Calendar.MILLISECOND, 0) + + val timePicker = MaterialTimePicker.Builder() + .setTitleText("Departure time") + .setTimeFormat(TimeFormat.CLOCK_12H) + .setHour(cal.get(Calendar.HOUR_OF_DAY)) + .setMinute(0) + .build() + + timePicker.addOnPositiveButtonClickListener { + cal.set(Calendar.HOUR_OF_DAY, timePicker.hour) + cal.set(Calendar.MINUTE, timePicker.minute) + viewModel.setDeparture(cal.timeInMillis) + // Re-generate with new departure time + if (mainViewModel.forecast.value.isNotEmpty()) triggerGenerate() + } + + timePicker.show(childFragmentManager, "time_picker") + } + + datePicker.show(childFragmentManager, "date_picker") + } + + private fun renderDepartureLabel(ms: Long) { + val now = System.currentTimeMillis() + tvDepartureTime.text = if (abs(ms - now) < 3_600_000L) { + "Now" + } else { + departureSdf.format(ms) + } + } + + // ── Boat chips ──────────────────────────────────────────────────────────── + private fun rebuildBoatChips(profiles: List) { chipGroupBoats.removeAllViews() val selectedId = viewModel.selectedProfile.value?.id profiles.forEach { profile -> val chip = Chip(requireContext()).apply { - text = profile.name + text = profile.name isCheckable = true isChecked = (profile.id == selectedId) - tag = profile.id + tag = profile.id } chip.setOnCheckedChangeListener { _, checked -> if (checked) { @@ -168,10 +242,10 @@ class PreTripReportFragment : Fragment() { slices.forEach { slice -> val col = LayoutInflater.from(requireContext()) .inflate(R.layout.item_condition_column, conditionsTable, false) - col.findViewById(R.id.col_label).text = slice.label - col.findViewById(R.id.col_wind).text = "%.0f kt".format(slice.windKt) - col.findViewById(R.id.col_wind_dir).text = cardinalDir(slice.windDirDeg) - col.findViewById(R.id.col_sky).text = slice.weatherDescription.take(12) + col.findViewById(R.id.col_label).text = slice.label + col.findViewById(R.id.col_wind).text = "%.0f kt".format(slice.windKt) + col.findViewById(R.id.col_wind_dir).text = cardinalDir(slice.windDirDeg) + col.findViewById(R.id.col_sky).text = slice.weatherDescription.take(12) conditionsTable.addView(col) } cardConditions.visibility = if (slices.isNotEmpty()) View.VISIBLE else View.GONE @@ -218,4 +292,6 @@ class PreTripReportFragment : Fragment() { "S","SSW","SW","WSW","W","WNW","NW","NNW") return dirs[((deg + 11.25) / 22.5).toInt() % 16] } + + private fun abs(x: Long) = if (x < 0) -x else x } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt index 2508d44..2840d76 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportGenerator.kt @@ -4,7 +4,10 @@ import org.terst.nav.data.model.ForecastItem import org.terst.nav.data.model.MarineConditions import org.terst.nav.track.TrackPoint import org.terst.nav.track.summarise +import java.text.SimpleDateFormat import java.util.Calendar +import java.util.Locale +import java.util.TimeZone import kotlin.math.* class PreTripReportGenerator { @@ -12,13 +15,14 @@ class PreTripReportGenerator { /** * Builds a full pre-trip briefing. * - * @param lat Current position latitude - * @param lon Current position longitude - * @param forecastItems Hourly forecast list; first item = current hour - * @param conditions Current marine snapshot (waves, current, swell) - * @param boatProfile Selected vessel with sail inventory - * @param pastTracks All saved tracks for similar-conditions comparison - * @param durationHrs Planned trip duration in hours (default 3) + * @param lat Current position latitude + * @param lon Current position longitude + * @param forecastItems Hourly forecast list (168 slots) + * @param conditions Current marine snapshot (waves, current, swell) + * @param boatProfile Selected vessel with sail inventory + * @param pastTracks All saved tracks for similar-conditions comparison + * @param durationHrs Planned trip duration in hours (default 3) + * @param departureDateTimeMs Unix millis for planned departure (default = now) */ fun generateReport( lat: Double, @@ -27,14 +31,19 @@ class PreTripReportGenerator { conditions: MarineConditions?, boatProfile: BoatProfile, pastTracks: List> = emptyList(), - durationHrs: Double = 3.0 + durationHrs: Double = 3.0, + departureDateTimeMs: Long = System.currentTimeMillis() ): PreTripReport { - val current = forecastItems.firstOrNull() + // Find the forecast slot nearest to the planned departure time + val startIndex = findDepartureSlot(forecastItems, departureDateTimeMs) + val window = forecastItems.drop(startIndex).take(4) + + val current = window.firstOrNull() val windKt = current?.windKt ?: 0.0 val windDir = current?.windDirDeg ?: 0.0 val summary = PreTripSummary( - timestampMs = System.currentTimeMillis(), + timestampMs = departureDateTimeMs, lat = lat, lon = lon, windSpeedKt = windKt, @@ -46,27 +55,71 @@ class PreTripReportGenerator { return PreTripReport( summary = summary, - conditionWindow = buildConditionWindow(forecastItems), + conditionWindow = buildConditionWindow(window, departureDateTimeMs), routeProjection = projectRoute(windDir, windKt, conditions?.currentSpeedKt ?: 0.0, conditions?.currentDirDeg ?: 0.0, durationHrs, boatProfile), - sailPlan = suggestSailPlan(windKt, forecastItems, boatProfile), - watchItems = buildWatchList(conditions, forecastItems, boatProfile), + sailPlan = suggestSailPlan(windKt, window, boatProfile), + watchItems = buildWatchList(conditions, window, boatProfile, departureDateTimeMs), similarTrips = findSimilarTrips(pastTracks, windKt, windDir) ) } + // ── Departure slot lookup ───────────────────────────────────────────────── + + /** + * Finds the index of the forecast item closest to [departureMs]. + * Open-Meteo returns times as UTC ISO strings ("yyyy-MM-dd'T'HH:mm"). + */ + private fun findDepartureSlot(items: List, departureMs: Long): Int { + if (items.isEmpty()) return 0 + val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.US) + sdf.timeZone = TimeZone.getTimeZone("UTC") + + var bestIdx = 0 + var bestDiff = Long.MAX_VALUE + items.forEachIndexed { idx, item -> + try { + val itemMs = sdf.parse(item.timeIso)?.time ?: return@forEachIndexed + val diff = abs(itemMs - departureMs) + if (diff < bestDiff) { + bestDiff = diff + bestIdx = idx + } + } catch (_: Exception) {} + } + // Leave at least 4 items for the condition window + return bestIdx.coerceAtMost((items.size - 4).coerceAtLeast(0)) + } + // ── Condition window ────────────────────────────────────────────────────── - private fun buildConditionWindow(items: List): List { - val labels = listOf("Now", "+1 h", "+2 h", "+3 h") + private fun buildConditionWindow( + items: List, + departureDateTimeMs: Long + ): List { + val isNearNow = abs(departureDateTimeMs - System.currentTimeMillis()) < 3_600_000L + val utcSdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.US).also { + it.timeZone = TimeZone.getTimeZone("UTC") + } + // Display times in device local time (e.g. "2 PM") + val displaySdf = SimpleDateFormat("h a", Locale.getDefault()) + return items.take(4).mapIndexed { i, item -> + val label = when { + isNearNow && i == 0 -> "Now" + isNearNow -> "+${i}h" + else -> try { + val t = utcSdf.parse(item.timeIso) + if (t != null) displaySdf.format(t) else "+${i}h" + } catch (_: Exception) { "+${i}h" } + } ConditionSlice( - label = labels.getOrElse(i) { "+${i} h" }, + label = label, windKt = item.windKt, windDirDeg = item.windDirDeg, - waveHeightM = null, // marine API doesn't give hourly per slot here; use conditions + waveHeightM = null, weatherDescription = item.weatherDescription() ) } @@ -94,7 +147,6 @@ class PreTripReportGenerator { durationHrs: Double, boatProfile: BoatProfile ): RouteProjection { - // Score each candidate heading data class Candidate( val heading: Double, val label: String, val note: String, val twa: Double, val sog: Double, val score: Double @@ -109,20 +161,17 @@ class PreTripReportGenerator { val best = scored.maxByOrNull { it.score }!! - // Return leg is the reciprocal heading val returnHdg = (best.heading + 180.0) % 360.0 val returnTwa = twa(windDirDeg, returnHdg) val returnSog = estimatedSogKt(boatProfile.lengthFt, twsKt, returnTwa) - // Outbound leg distance (half the trip duration at outbound SOG) val outboundHrs = durationHrs / 2.0 val outboundNm = best.sog * outboundHrs - // Current component along outbound heading val currentComponent = currentSpeedKt * cos(Math.toRadians(currentDirDeg - best.heading)) val currentNote = when { - currentComponent > 0.15 -> "Current adds %.1f kt outbound.".format(currentComponent) + currentComponent > 0.15 -> "Current adds %.1f kt outbound.".format(currentComponent) currentComponent < -0.15 -> "Current opposes %.1f kt outbound.".format(-currentComponent) else -> "" } @@ -203,20 +252,16 @@ class PreTripReportGenerator { ): List { val suggestions = mutableListOf() - // Pick best-fit headsail from inventory (highest maxWindKt that still covers windKt, - // falling back to smallest sail if wind exceeds all) val headsail = boatProfile.headsails .sortedBy { it.maxWindKt } .firstOrNull { windKt <= it.maxWindKt } ?: boatProfile.headsails.minByOrNull { it.maxWindKt } if (headsail != null) { - // Build a rationale note for this headsail choice val note = headsailNote(headsail, windKt, forecastItems, boatProfile) suggestions.add(SailSuggestion(headsail.name, note)) } - // Main reef recommendation val mainAction = when { windKt > 21 && boatProfile.mainReefs >= 2 -> "2nd Reef" windKt > 15 && boatProfile.mainReefs >= 1 -> "1st Reef" @@ -225,7 +270,6 @@ class PreTripReportGenerator { } suggestions.add(SailSuggestion("Main", mainAction)) - // If wind is likely to build into a higher band during the trip, add a heads-up val maxForecastWind = forecastItems.take(4).maxOfOrNull { it.windKt } ?: windKt if (maxForecastWind > windKt + 4) { val nextSail = boatProfile.headsails @@ -250,7 +294,6 @@ class PreTripReportGenerator { ): String { val maxForecast = forecastItems.take(4).maxOfOrNull { it.windKt } ?: windKt - // E23-specific 155% overlap note if (sail.name.contains("155%") && windKt > 10) { return if (maxForecast > sail.maxWindKt) "Flying now, but winds forecast to ${maxForecast.toInt()} kt — swap to 100% before departure" @@ -270,12 +313,12 @@ class PreTripReportGenerator { private fun buildWatchList( conditions: MarineConditions?, forecastItems: List, - boatProfile: BoatProfile + boatProfile: BoatProfile, + departureDateTimeMs: Long = System.currentTimeMillis() ): List { val items = mutableListOf() val windKt = forecastItems.firstOrNull()?.windKt ?: 0.0 - // Swell hazards conditions?.swellHeightM?.let { swell -> if (swell > 2.0) items += WatchItem("⚠️", "Significant swell %.1fm — expect motion".format(swell)) @@ -285,7 +328,6 @@ class PreTripReportGenerator { items += WatchItem("⚠️", "Short swell period %.0fs — choppy, uncomfortable".format(period)) } - // Building wind in the trip window val peakForecastWind = forecastItems.take(4).maxOfOrNull { it.windKt } ?: windKt val peakHour = forecastItems.take(4).indexOfFirst { it.windKt == peakForecastWind } if (peakForecastWind > windKt + 5) { @@ -293,25 +335,23 @@ class PreTripReportGenerator { "Wind building to ${peakForecastWind.toInt()} kt in ~${peakHour} h — plan to be back before then") } - // Rain val maxPrecip = forecastItems.take(4).maxOfOrNull { it.precipProbabilityPct } ?: 0 if (maxPrecip > 30) items += WatchItem("⚠️", "Rain likely ($maxPrecip% chance) — visibility may reduce") - // Time-of-day trade build - val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) - if (hour in 11..14) + // Use departure hour for time-of-day trade warnings + val departureHour = Calendar.getInstance().also { it.timeInMillis = departureDateTimeMs } + .get(Calendar.HOUR_OF_DAY) + if (departureHour in 11..14) items += WatchItem("ℹ️", "Departing midday — Kona trades typically build 5–8 kt through afternoon") - else if (hour >= 15) + else if (departureHour >= 15) items += WatchItem("ℹ️", "Afternoon departure — trades may be at their strongest; build extra time margin") - // E23 155% genoa overpower warning val largeGenoa = boatProfile.headsails.firstOrNull { it.name.contains("155%") } if (largeGenoa != null && windKt > largeGenoa.maxWindKt) { items += WatchItem("⚠️", "155% Genoa will overpower ${boatProfile.name} at ${windKt.toInt()} kt — rig 100% Jib instead") } - // General storm check if (windKt > 30) items += WatchItem("⚠️", "Winds ${windKt.toInt()} kt — consider staying in port") else if (windKt > 22) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt index 5d4cd14..64a739a 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportViewModel.kt @@ -33,6 +33,9 @@ class PreTripReportViewModel( private val _selectedProfile = MutableStateFlow(null) val selectedProfile: StateFlow = _selectedProfile.asStateFlow() + private val _departureMs = MutableStateFlow(System.currentTimeMillis()) + val departureMs: StateFlow = _departureMs.asStateFlow() + init { val profiles = boatRepo.loadProfiles() _profiles.value = profiles @@ -44,6 +47,10 @@ class PreTripReportViewModel( _selectedProfile.value = _profiles.value.firstOrNull { it.id == id } } + fun setDeparture(ms: Long) { + _departureMs.value = ms + } + fun generate(forecastItems: List, conditions: MarineConditions?) { val profile = _selectedProfile.value ?: return val pos = LocationService.bestPosition.value @@ -53,12 +60,13 @@ class PreTripReportViewModel( try { val pastTracks = NavApplication.trackRepository.getPastTracks() val report = generator.generateReport( - lat = pos?.latitude ?: 19.664, // Honokohau fallback - lon = pos?.longitude ?: -156.024, - forecastItems = forecastItems, - conditions = conditions, - boatProfile = profile, - pastTracks = pastTracks + lat = pos?.latitude ?: 19.664, // Honokohau fallback + lon = pos?.longitude ?: -156.024, + forecastItems = forecastItems, + conditions = conditions, + boatProfile = profile, + pastTracks = pastTracks, + departureDateTimeMs = _departureMs.value ) _state.value = PreTripState.Success(report) } catch (e: Exception) { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/learn/LearnFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/learn/LearnFragment.kt index 8440edb..da3f020 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/learn/LearnFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/learn/LearnFragment.kt @@ -28,6 +28,14 @@ class LearnFragment : Fragment() { openDoc("docs/migrate_sea_people.md") } + // Reference — offline docs + view.findViewById(R.id.card_colregs).setOnClickListener { + openDoc("docs/colregs_reference.md") + } + view.findViewById(R.id.card_sailing_reference).setOnClickListener { + openDoc("docs/sailing_reference.md") + } + // ASA / external links — open in browser view.findViewById(R.id.card_asa_courses).setOnClickListener { openUrl("https://www.asa.com/courses/") @@ -35,9 +43,6 @@ class LearnFragment : Fragment() { view.findViewById(R.id.card_asa_online).setOnClickListener { openUrl("https://www.asa.com/online-courses/") } - view.findViewById(R.id.card_colregs).setOnClickListener { - openUrl("https://www.navcen.uscg.gov/international-regulations-for-preventing-collisions-at-sea") - } view.findViewById(R.id.card_flashcards).setOnClickListener { openUrl("https://quizlet.com/subject/sailing/") } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt index 1c797d5..ab30d96 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt @@ -3,6 +3,8 @@ package org.terst.nav.ui.voicelog import android.Manifest import android.content.Intent import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.net.Uri import android.os.Bundle import android.speech.RecognitionListener import android.speech.RecognizerIntent @@ -10,37 +12,75 @@ import android.speech.SpeechRecognizer import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button -import android.widget.LinearLayout +import android.widget.FrameLayout +import android.widget.ImageView import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.google.android.material.button.MaterialButton import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.textfield.TextInputEditText import kotlinx.coroutines.launch import org.terst.nav.R -import org.terst.nav.logbook.InMemoryLogbookRepository import org.terst.nav.logbook.VoiceLogState import org.terst.nav.logbook.VoiceLogViewModel +import java.io.File +import java.io.FileOutputStream import java.util.Locale class VoiceLogFragment : Fragment() { - private lateinit var speechRecognizer: SpeechRecognizer private val viewModel by lazy { VoiceLogViewModel(repository = org.terst.nav.NavApplication.logbookRepository) } - private lateinit var tvStatus: TextView - private lateinit var tvRecognized: TextView + private lateinit var speechRecognizer: SpeechRecognizer + private lateinit var etNote: TextInputEditText private lateinit var fabMic: FloatingActionButton - private lateinit var llConfirm: LinearLayout - private lateinit var btnSave: Button - private lateinit var btnRetry: Button + private lateinit var btnCamera: MaterialButton + private lateinit var btnGallery: MaterialButton + private lateinit var tvStatus: TextView + private lateinit var framePhoto: FrameLayout + private lateinit var ivPhoto: ImageView + private lateinit var btnRemovePhoto: MaterialButton + private lateinit var btnSave: MaterialButton + private lateinit var btnClear: MaterialButton private lateinit var tvSavedConfirmation: TextView private lateinit var btnGenerateReport: MaterialButton + /** Path or URI string for the currently attached photo, null if none. */ + private var pendingPhotoPath: String? = null + + // ── Camera (TakePicturePreview returns a thumbnail bitmap — no FileProvider needed) ── + + private val cameraLauncher = registerForActivityResult( + ActivityResultContracts.TakePicturePreview() + ) { bitmap: Bitmap? -> + if (bitmap != null) { + val file = File(requireContext().cacheDir, "log_${System.currentTimeMillis()}.jpg") + try { + FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 85, it) } + setPhoto(file.absolutePath) { ivPhoto.setImageBitmap(bitmap) } + } catch (_: Exception) { + tvStatus.text = "Couldn't save photo" + } + } + } + + // ── Gallery (GetContent — no storage permission needed; URI valid for this session) ── + + private val galleryLauncher = registerForActivityResult( + ActivityResultContracts.GetContent() + ) { uri: Uri? -> + if (uri != null) { + setPhoto(uri.toString()) { ivPhoto.setImageURI(uri) } + } + } + + // ── Lifecycle ───────────────────────────────────────────────────────────── + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -50,20 +90,33 @@ class VoiceLogFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - tvStatus = view.findViewById(R.id.tv_status) - tvRecognized = view.findViewById(R.id.tv_recognized) - fabMic = view.findViewById(R.id.fab_mic) - llConfirm = view.findViewById(R.id.ll_confirm_buttons) - btnSave = view.findViewById(R.id.btn_save) - btnRetry = view.findViewById(R.id.btn_retry) + etNote = view.findViewById(R.id.et_note) + fabMic = view.findViewById(R.id.fab_mic) + btnCamera = view.findViewById(R.id.btn_camera) + btnGallery = view.findViewById(R.id.btn_gallery) + tvStatus = view.findViewById(R.id.tv_status) + framePhoto = view.findViewById(R.id.frame_photo) + ivPhoto = view.findViewById(R.id.iv_photo) + btnRemovePhoto = view.findViewById(R.id.btn_remove_photo) + btnSave = view.findViewById(R.id.btn_save) + btnClear = view.findViewById(R.id.btn_clear) tvSavedConfirmation = view.findViewById(R.id.tv_saved_confirmation) - btnGenerateReport = view.findViewById(R.id.btn_generate_report) + btnGenerateReport = view.findViewById(R.id.btn_generate_report) setupSpeechRecognizer() fabMic.setOnClickListener { startListening() } - btnSave.setOnClickListener { viewModel.confirmAndSave() } - btnRetry.setOnClickListener { viewModel.retry() } + btnCamera.setOnClickListener { cameraLauncher.launch(null) } + btnGallery.setOnClickListener { galleryLauncher.launch("image/*") } + btnRemovePhoto.setOnClickListener { clearPhoto() } + + btnSave.setOnClickListener { + val text = etNote.text?.toString()?.trim() ?: "" + viewModel.save(text, pendingPhotoPath) + } + + btnClear.setOnClickListener { clearEntry() } + btnGenerateReport.setOnClickListener { parentFragmentManager.beginTransaction() .replace(R.id.fragment_container, org.terst.nav.tripreport.TripReportFragment()) @@ -76,18 +129,20 @@ class VoiceLogFragment : Fragment() { } } + // ── Speech ──────────────────────────────────────────────────────────────── + private fun setupSpeechRecognizer() { if (!SpeechRecognizer.isRecognitionAvailable(requireContext())) { - tvStatus.text = "Speech recognition not available" fabMic.isEnabled = false + tvStatus.text = "Speech recognition unavailable" return } speechRecognizer = SpeechRecognizer.createSpeechRecognizer(requireContext()) speechRecognizer.setRecognitionListener(object : RecognitionListener { override fun onReadyForSpeech(params: Bundle?) { viewModel.onListeningStarted() } override fun onResults(results: Bundle?) { - val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) - val text = matches?.firstOrNull() ?: "" + val text = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) + ?.firstOrNull() ?: "" if (text.isNotBlank()) viewModel.onSpeechRecognized(text) else viewModel.onRecognitionError("Could not understand speech") } @@ -95,11 +150,11 @@ class VoiceLogFragment : Fragment() { viewModel.onRecognitionError("Recognition error: $error") } override fun onBeginningOfSpeech() {} - override fun onBufferReceived(buffer: ByteArray?) {} + override fun onBufferReceived(b: ByteArray?) {} override fun onEndOfSpeech() {} - override fun onEvent(eventType: Int, params: Bundle?) {} - override fun onPartialResults(partialResults: Bundle?) {} - override fun onRmsChanged(rmsdB: Float) {} + override fun onEvent(t: Int, p: Bundle?) {} + override fun onPartialResults(p: Bundle?) {} + override fun onRmsChanged(r: Float) {} }) } @@ -119,38 +174,57 @@ class VoiceLogFragment : Fragment() { speechRecognizer.startListening(intent) } + // ── Photo helpers ───────────────────────────────────────────────────────── + + private fun setPhoto(path: String, applyImage: () -> Unit) { + pendingPhotoPath = path + applyImage() + framePhoto.visibility = View.VISIBLE + } + + private fun clearPhoto() { + pendingPhotoPath = null + ivPhoto.setImageDrawable(null) + framePhoto.visibility = View.GONE + } + + private fun clearEntry() { + etNote.setText("") + clearPhoto() + viewModel.retry() + tvSavedConfirmation.text = "" + tvStatus.text = "" + } + + // ── State rendering ─────────────────────────────────────────────────────── + private fun renderState(state: VoiceLogState) { when (state) { is VoiceLogState.Idle -> { - tvStatus.text = "Tap microphone to log" - tvRecognized.text = "" - llConfirm.visibility = View.GONE - tvSavedConfirmation.text = "" + tvStatus.text = "" fabMic.isEnabled = true } is VoiceLogState.Listening -> { tvStatus.text = "Listening…" - tvRecognized.text = "" - llConfirm.visibility = View.GONE fabMic.isEnabled = false } is VoiceLogState.Result -> { - tvStatus.text = "Recognized:" - tvRecognized.text = state.recognized - llConfirm.visibility = View.VISIBLE - fabMic.isEnabled = false + // Fill the text field; user can edit before saving + etNote.setText(state.recognized) + etNote.setSelection(state.recognized.length) + tvStatus.text = "" + fabMic.isEnabled = true } is VoiceLogState.Saved -> { - tvStatus.text = "Saved!" - tvRecognized.text = state.entry.text - tvSavedConfirmation.text = "[${state.entry.entryType}] entry saved" - llConfirm.visibility = View.GONE + val photoNote = if (state.entry.photoPath != null) " + photo" else "" + tvSavedConfirmation.text = "Saved: ${state.entry.text.take(60)}$photoNote" + etNote.setText("") + clearPhoto() fabMic.isEnabled = true + tvStatus.text = "" } is VoiceLogState.Error -> { - tvStatus.text = "Error: ${state.message}" - tvRecognized.text = "" - llConfirm.visibility = View.GONE + tvStatus.text = state.message fabMic.isEnabled = true } } @@ -162,7 +236,9 @@ class VoiceLogFragment : Fragment() { permissions: Array, grantResults: IntArray ) { - if (requestCode == RC_AUDIO && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) { + if (requestCode == RC_AUDIO + && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED + ) { startListening() } } diff --git a/android-app/app/src/main/res/drawable/ic_calendar.xml b/android-app/app/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 0000000..dd8030c --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/drawable/ic_open_in_new.xml b/android-app/app/src/main/res/drawable/ic_open_in_new.xml new file mode 100644 index 0000000..4645522 --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_open_in_new.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/android-app/app/src/main/res/layout/fragment_learn.xml b/android-app/app/src/main/res/layout/fragment_learn.xml index f41e577..8813ba2 100644 --- a/android-app/app/src/main/res/layout/fragment_learn.xml +++ b/android-app/app/src/main/res/layout/fragment_learn.xml @@ -96,11 +96,11 @@ - + + android:orientation="horizontal" + android:padding="16dp" + android:gravity="center_vertical"> - + android:layout_weight="1" + android:orientation="vertical"> + + + + + + + android:text="›" + android:textSize="20sp" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:orientation="horizontal" + android:padding="16dp" + android:gravity="center_vertical"> - + android:layout_weight="1" + android:orientation="vertical"> + + + + + + + android:text="›" + android:textSize="20sp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + + + android:orientation="horizontal" + android:padding="16dp" + android:gravity="center_vertical"> - + android:layout_weight="1" + android:orientation="vertical"> + + + + + + + + - + + + + + + + + android:layout_weight="1" + android:orientation="vertical"> + + + + + + + + @@ -211,22 +317,38 @@ - - + android:orientation="horizontal" + android:padding="16dp" + android:gravity="center_vertical"> - + android:layout_weight="1" + android:orientation="vertical"> + + + + + + + + diff --git a/android-app/app/src/main/res/layout/fragment_pretrip_report.xml b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml index 510411b..8cb094a 100644 --- a/android-app/app/src/main/res/layout/fragment_pretrip_report.xml +++ b/android-app/app/src/main/res/layout/fragment_pretrip_report.xml @@ -54,6 +54,58 @@ + + + + + + + + + + + + + + + + + + + android:padding="20dp"> - + + android:layout_marginBottom="12dp" + style="@style/Widget.Material3.TextInputLayout.OutlinedBox"> + + + + + + + + + + + + + + + + + -