From ec2bc313ba88477aeefcb5c943ed387de2883ee8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 18:05:59 +0000 Subject: Add hidden dev log (long-press drag handle) NavLogger is a 200-entry in-memory ring buffer with format: HH:mm:ss.SSS LEVEL tag: message Entries are written at: - App start - First GPS fix (lat/lon/sog/cog) - fetchCurrentConditions: request params + full success summary (wind, temp, waves, swell, current) or exception class+message - fetchForecastItems: request params + item count or error DevLogSheet (BottomSheetDialogFragment) shows the log in a monospace ScrollView with Copy and Clear buttons. Trigger: long-press the drag handle bar on the instrument bottom sheet. Primary use: copy the log and paste it to Claude to diagnose conditions API failures without needing adb. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 8 +++ .../terst/nav/data/repository/WeatherRepository.kt | 68 +++++++++++++------- .../main/kotlin/org/terst/nav/ui/DevLogSheet.kt | 58 +++++++++++++++++ .../src/main/kotlin/org/terst/nav/ui/NavLogger.kt | 42 ++++++++++++ .../app/src/main/res/layout/sheet_dev_log.xml | 74 ++++++++++++++++++++++ 5 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/ui/DevLogSheet.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/ui/NavLogger.kt create mode 100644 android-app/app/src/main/res/layout/sheet_dev_log.xml 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 7a00748..35a0e91 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 @@ -146,10 +146,16 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { hudDepth.text = "—" applyUnitLabels() + NavLogger.i("app", "started") setupBottomSheet() setupBottomNavigation() setupHandlers() setupMap() + // Hidden dev log — long-press the drag handle on the instrument sheet + findViewById(R.id.drag_handle).setOnLongClickListener { + DevLogSheet().show(supportFragmentManager, "dev_log") + true + } fabRecordTrack.setOnClickListener { if (!viewModel.isRecording.value) viewModel.startTrack() @@ -482,6 +488,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { hudCog.text = "%.0f°".format(Locale.getDefault(), gpsData.cog) if (!conditionsLoaded) { conditionsLoaded = true + NavLogger.i("gps", "first fix lat=%.4f lon=%.4f sog=%.1fkt cog=%.0f°" + .format(gpsData.latitude, gpsData.longitude, gpsData.sog, gpsData.cog)) viewModel.loadConditions(gpsData.latitude, gpsData.longitude) } if (!weatherLoaded) { 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 26c5da7..a716051 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 @@ -7,6 +7,7 @@ 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 +import org.terst.nav.ui.NavLogger class WeatherRepository( private val weatherApi: WeatherApiService, @@ -16,8 +17,9 @@ class WeatherRepository( /** * Fetch 7-day hourly forecast items for the given position. */ - suspend fun fetchForecastItems(lat: Double, lon: Double): Result> = - runCatching { + suspend fun fetchForecastItems(lat: Double, lon: Double): Result> { + NavLogger.i("forecast", "lat=%.4f lon=%.4f".format(lat, lon)) + return runCatching { val weather = weatherApi.getWeatherForecast(lat, lon) val h = weather.hourly h.time.indices.map { i -> @@ -30,7 +32,15 @@ class WeatherRepository( weatherCode = h.weathercode[i] ) } + }.also { result -> + result.onSuccess { items -> + val first = items.firstOrNull() + NavLogger.i("forecast", "OK ${items.size} items wind[0]=${first?.windKt?.let { "%.1f".format(it) } ?: "—"}kt@${first?.windDirDeg?.toInt() ?: "—"}°") + }.onFailure { e -> + NavLogger.e("forecast", "${e.javaClass.simpleName}: ${e.message}") + } } + } /** * Fetch a single WindArrow representing the current conditions at [lat]/[lon]. @@ -52,29 +62,43 @@ 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 { + suspend fun fetchCurrentConditions(lat: Double, lon: Double): Result { + NavLogger.i("conditions", "lat=%.4f lon=%.4f".format(lat, lon)) + return runCatching { coroutineScope { - val weatherDeferred = async { weatherApi.getWeatherForecast(lat, lon, forecastDays = 1) } - val marineDeferred = async { marineApi.getMarineForecast(lat, lon) } - val weather = weatherDeferred.await() - val marine = marineDeferred.await() - val w = weather.hourly - val m = marine.hourly - 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(), - swellDirDeg = m.swellWaveDirection.firstOrNull(), - swellPeriodS = m.swellWavePeriod.firstOrNull(), - currentSpeedKt = m.oceanCurrentVelocity.firstOrNull()?.let { it * 1.94384 }, - currentDirDeg = m.oceanCurrentDirection.firstOrNull() - ) + val weatherDeferred = async { weatherApi.getWeatherForecast(lat, lon, forecastDays = 1) } + val marineDeferred = async { marineApi.getMarineForecast(lat, lon) } + val weather = weatherDeferred.await() + val marine = marineDeferred.await() + val w = weather.hourly + val m = marine.hourly + 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(), + swellDirDeg = m.swellWaveDirection.firstOrNull(), + swellPeriodS = m.swellWavePeriod.firstOrNull(), + currentSpeedKt = m.oceanCurrentVelocity.firstOrNull()?.let { it * 1.94384 }, + currentDirDeg = m.oceanCurrentDirection.firstOrNull() + ) + } + }.also { result -> + result.onSuccess { c -> + NavLogger.i("conditions", "OK" + + " wind=${c.windSpeedKt?.let { "%.1f".format(it) } ?: "—"}kt@${c.windDirDeg?.toInt() ?: "—"}°" + + " temp=${c.tempC?.let { "%.1f".format(it) } ?: "—"}°C" + + " waves=${c.waveHeightM?.let { "%.2f".format(it) } ?: "—"}m" + + " swell=${c.swellHeightM?.let { "%.2f".format(it) } ?: "—"}m@${c.swellDirDeg?.toInt() ?: "—"}°/${c.swellPeriodS?.toInt() ?: "—"}s" + + " curr=${c.currentSpeedKt?.let { "%.2f".format(it) } ?: "—"}kt@${c.currentDirDeg?.toInt() ?: "—"}°" + ) + }.onFailure { e -> + NavLogger.e("conditions", "${e.javaClass.simpleName}: ${e.message}") } } + } /** * Fetch a current wind arrow for each point in [points] using Open-Meteo's batch endpoint. diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/DevLogSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/DevLogSheet.kt new file mode 100644 index 0000000..99e6d5f --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/DevLogSheet.kt @@ -0,0 +1,58 @@ +package org.terst.nav.ui + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ScrollView +import android.widget.TextView +import android.widget.Toast +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.button.MaterialButton +import org.terst.nav.R + +class DevLogSheet : BottomSheetDialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.sheet_dev_log, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Expand to full height + (dialog as? BottomSheetDialog)?.behavior?.apply { + state = BottomSheetBehavior.STATE_EXPANDED + skipCollapsed = true + } + + val logText = view.findViewById(R.id.log_text) + val scrollView = view.findViewById(R.id.scroll_view) + + fun refresh() { + val text = NavLogger.getLog() + logText.text = text.ifEmpty { "(no log entries yet)" } + scrollView.post { scrollView.fullScroll(View.FOCUS_DOWN) } + } + + refresh() + + view.findViewById(R.id.btn_copy).setOnClickListener { + val cm = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText("nav_dev_log", NavLogger.getLog())) + Toast.makeText(requireContext(), "Copied to clipboard", Toast.LENGTH_SHORT).show() + } + + view.findViewById(R.id.btn_clear).setOnClickListener { + NavLogger.clear() + refresh() + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/NavLogger.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/NavLogger.kt new file mode 100644 index 0000000..c638113 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/NavLogger.kt @@ -0,0 +1,42 @@ +package org.terst.nav.ui + +import java.util.Calendar + +/** + * Lightweight in-memory log ring-buffer. + * + * Format: `HH:mm:ss.SSS LEVEL tag: message` + * + * Designed so both humans and Claude can read the output at a glance. + * Entries are captured from API calls, GPS fixes, and key state changes. + * Retrieve with [getLog] and copy to clipboard via DevLogSheet (long-press + * the drag handle on the instrument sheet). + */ +object NavLogger { + + private const val MAX_ENTRIES = 200 + private val entries = ArrayDeque() + + fun i(tag: String, msg: String) = append("INFO ", tag, msg) + fun w(tag: String, msg: String) = append("WARN ", tag, msg) + fun e(tag: String, msg: String) = append("ERROR", tag, msg) + + private fun append(level: String, tag: String, msg: String) { + val cal = Calendar.getInstance() + val time = "%02d:%02d:%02d.%03d".format( + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND), + cal.get(Calendar.MILLISECOND) + ) + val entry = "$time $level $tag: $msg" + synchronized(entries) { + entries.addLast(entry) + while (entries.size > MAX_ENTRIES) entries.removeFirst() + } + } + + fun getLog(): String = synchronized(entries) { entries.joinToString("\n") } + + fun clear() = synchronized(entries) { entries.clear() } +} diff --git a/android-app/app/src/main/res/layout/sheet_dev_log.xml b/android-app/app/src/main/res/layout/sheet_dev_log.xml new file mode 100644 index 0000000..5464242 --- /dev/null +++ b/android-app/app/src/main/res/layout/sheet_dev_log.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3