diff --git a/conductor/archive/android_kable_migration_20260314/metadata.json b/conductor/archive/android_kable_migration_20260314/metadata.json index 8b975774b..8dd4dc82b 100644 --- a/conductor/archive/android_kable_migration_20260314/metadata.json +++ b/conductor/archive/android_kable_migration_20260314/metadata.json @@ -1,7 +1,7 @@ { "track_id": "android_kable_migration_20260314", "type": "feature", - "status": "new", + "status": "completed", "created_at": "2026-03-14T17:15:00Z", "updated_at": "2026-03-14T17:15:00Z", "description": "Replace Nordic with Kable on Android" diff --git a/conductor/archive/deep_dive_docs_20260316/metadata.json b/conductor/archive/deep_dive_docs_20260316/metadata.json index 919480970..4851ae35a 100644 --- a/conductor/archive/deep_dive_docs_20260316/metadata.json +++ b/conductor/archive/deep_dive_docs_20260316/metadata.json @@ -1,7 +1,7 @@ { "track_id": "deep_dive_docs_20260316", "type": "chore", - "status": "new", + "status": "completed", "created_at": "2026-03-16T12:00:00Z", "updated_at": "2026-03-16T12:00:00Z", "description": "do a deep dive of project docs and plans in /docs - verify against actual project/codebase state, then validate against modern best practices for android, kotlin, kmp, and the dependencies used. be thorough - check all the major dependencies. Update docs and plans accordingly." diff --git a/conductor/archive/desktop_ble_kable_20260314/metadata.json b/conductor/archive/desktop_ble_kable_20260314/metadata.json index 6c738ab4b..813ef1cab 100644 --- a/conductor/archive/desktop_ble_kable_20260314/metadata.json +++ b/conductor/archive/desktop_ble_kable_20260314/metadata.json @@ -1,7 +1,7 @@ { "track_id": "desktop_ble_kable_20260314", "type": "feature", - "status": "new", + "status": "completed", "created_at": "2026-03-14T12:00:00Z", "updated_at": "2026-03-14T12:00:00Z", "description": "Kable swap Keep Nordic on Android short-term. Add Kable backend only for jvmMain in core:ble first (desktop BLE enablement). Introduce a MeshtasticRadioProfile abstraction in core:ble/commonMain so NordicBleInterface no longer depends on Android/Nordic classes. Once that seam is clean, decide whether Android should stay Nordic or move to Kable." diff --git a/conductor/archive/desktop_di_autowiring_20260313/metadata.json b/conductor/archive/desktop_di_autowiring_20260313/metadata.json index 7ea36cf65..940262ddd 100644 --- a/conductor/archive/desktop_di_autowiring_20260313/metadata.json +++ b/conductor/archive/desktop_di_autowiring_20260313/metadata.json @@ -1,7 +1,7 @@ { "track_id": "desktop_di_autowiring_20260313", "type": "chore", - "status": "new", + "status": "completed", "created_at": "2026-03-13T12:00:00Z", "updated_at": "2026-03-13T12:00:00Z", "description": "Architecture Health & DI (Immediate Priority) * Desktop Koin checkModules() test: Add a test to ensure Desktop DI bindings are validated at compile-time/test-time so we catch missing interfaces early. * Auto-wire Desktop ViewModels: Configure KSP so we can eliminate the manual ViewModel wiring in DesktopKoinModule and rely on @KoinViewModel annotations like Android does." diff --git a/conductor/archive/desktop_parity_20260311/metadata.json b/conductor/archive/desktop_parity_20260311/metadata.json index 1eda225dc..d4c5031c4 100644 --- a/conductor/archive/desktop_parity_20260311/metadata.json +++ b/conductor/archive/desktop_parity_20260311/metadata.json @@ -1,7 +1,7 @@ { "track_id": "desktop_parity_20260311", "type": "feature", - "status": "new", + "status": "completed", "created_at": "2026-03-11T12:00:00Z", "updated_at": "2026-03-11T12:00:00Z", "description": "continue bringing desktop up to parity with android" diff --git a/conductor/archive/desktop_serial_transport_20260317/metadata.json b/conductor/archive/desktop_serial_transport_20260317/metadata.json index 3d1257289..0d31a3eb1 100644 --- a/conductor/archive/desktop_serial_transport_20260317/metadata.json +++ b/conductor/archive/desktop_serial_transport_20260317/metadata.json @@ -1,7 +1,7 @@ { "track_id": "desktop_serial_transport_20260317", "type": "feature", - "status": "new", + "status": "completed", "created_at": "2026-03-17T12:00:00Z", "updated_at": "2026-03-17T12:00:00Z", "description": "Implement Serial/USB transport for the Desktop target using jSerialComm. This fulfills the medium-term priority for direct radio connections on JVM and uses the shared RadioTransport interface." diff --git a/conductor/archive/desktop_ux_enhancements_20260316/metadata.json b/conductor/archive/desktop_ux_enhancements_20260316/metadata.json index 2adf241f1..826d38551 100644 --- a/conductor/archive/desktop_ux_enhancements_20260316/metadata.json +++ b/conductor/archive/desktop_ux_enhancements_20260316/metadata.json @@ -1,7 +1,7 @@ { "id": "desktop_ux_enhancements_20260316", "name": "Desktop UX Enhancements", - "status": "in-progress", + "status": "completed", "priority": "medium", "tags": ["desktop", "ux", "compose"] } \ No newline at end of file diff --git a/conductor/archive/doc_consolidation_20260311/metadata.json b/conductor/archive/doc_consolidation_20260311/metadata.json index 97337ceaf..5720a7d88 100644 --- a/conductor/archive/doc_consolidation_20260311/metadata.json +++ b/conductor/archive/doc_consolidation_20260311/metadata.json @@ -1,7 +1,7 @@ { "track_id": "doc_consolidation_20260311", "type": "feature", - "status": "new", + "status": "completed", "created_at": "2026-03-11T00:00:00Z", "updated_at": "2026-03-11T00:00:00Z", "description": "Implement document consolidation plan" diff --git a/conductor/archive/expand_testing_20260318/metadata.json b/conductor/archive/expand_testing_20260318/metadata.json index 462e52236..36897869c 100644 --- a/conductor/archive/expand_testing_20260318/metadata.json +++ b/conductor/archive/expand_testing_20260318/metadata.json @@ -1,7 +1,7 @@ { "track_id": "expand_testing_20260318", "type": "chore", - "status": "new", + "status": "completed", "created_at": "2026-03-18T10:00:00Z", "updated_at": "2026-03-18T10:00:00Z", "description": "Expand Testing Coverage" diff --git a/conductor/archive/extract_android_navigation_20260318/metadata.json b/conductor/archive/extract_android_navigation_20260318/metadata.json index 706b78f08..ac855c487 100644 --- a/conductor/archive/extract_android_navigation_20260318/metadata.json +++ b/conductor/archive/extract_android_navigation_20260318/metadata.json @@ -1,7 +1,7 @@ { "track_id": "extract_android_navigation_20260318", "type": "refactor", - "status": "new", + "status": "completed", "created_at": "2026-03-18T00:00:00Z", "updated_at": "2026-03-18T00:00:00Z", "description": "Extract Android Navigation graphs to feature modules for app thinning" diff --git a/conductor/archive/extract_hardware_transport_20260311/metadata.json b/conductor/archive/extract_hardware_transport_20260311/metadata.json index 2d9cc643e..a9dc547bf 100644 --- a/conductor/archive/extract_hardware_transport_20260311/metadata.json +++ b/conductor/archive/extract_hardware_transport_20260311/metadata.json @@ -1,7 +1,7 @@ { "track_id": "extract_hardware_transport_20260311", "type": "feature", - "status": "new", + "status": "completed", "created_at": "2026-03-11T00:00:00Z", "updated_at": "2026-03-11T00:00:00Z", "description": "extract hardware/transport layers out of :app into dedicated :core modules" diff --git a/conductor/archive/extract_remaining_background_20260318/metadata.json b/conductor/archive/extract_remaining_background_20260318/metadata.json index d16cfd870..52498f9fc 100644 --- a/conductor/archive/extract_remaining_background_20260318/metadata.json +++ b/conductor/archive/extract_remaining_background_20260318/metadata.json @@ -1,7 +1,7 @@ { "track_id": "extract_remaining_background_20260318", "type": "refactor", - "status": "new", + "status": "completed", "created_at": "2026-03-18T14:55:00Z", "updated_at": "2026-03-18T14:55:00Z", "description": "Extract remaining background services and workers from app module" diff --git a/conductor/archive/extract_services_20260317/metadata.json b/conductor/archive/extract_services_20260317/metadata.json index d40405670..adf7d650c 100644 --- a/conductor/archive/extract_services_20260317/metadata.json +++ b/conductor/archive/extract_services_20260317/metadata.json @@ -1,7 +1,7 @@ { "track_id": "extract_services_20260317", "type": "refactor", - "status": "new", + "status": "completed", "created_at": "2026-03-17T00:00:00Z", "updated_at": "2026-03-17T00:00:00Z", "description": "Extract service/worker/radio files from `app` to `core:service/androidMain` and `core:network/androidMain`" diff --git a/conductor/archive/extract_viewmodels_20260316/metadata.json b/conductor/archive/extract_viewmodels_20260316/metadata.json index 3ac6e636e..5b56ec476 100644 --- a/conductor/archive/extract_viewmodels_20260316/metadata.json +++ b/conductor/archive/extract_viewmodels_20260316/metadata.json @@ -1,7 +1,7 @@ { "track_id": "extract_viewmodels_20260316", "type": "refactor", - "status": "new", + "status": "completed", "created_at": "2026-03-16T12:00:00Z", "updated_at": "2026-03-16T12:00:00Z", "description": "Extract remaining 5 App-Only ViewModels (AndroidSettingsViewModel, AndroidRadioConfigViewModel, AndroidDebugViewModel, AndroidMetricsViewModel, UIViewModel) to shared KMP feature/core modules by isolating Android-specific dependencies (Uri, Location, Locale) behind abstractions." diff --git a/conductor/archive/fix_android_animations_20260313/metadata.json b/conductor/archive/fix_android_animations_20260313/metadata.json index 6add289e4..987eb12b7 100644 --- a/conductor/archive/fix_android_animations_20260313/metadata.json +++ b/conductor/archive/fix_android_animations_20260313/metadata.json @@ -1,7 +1,7 @@ { "track_id": "fix_android_animations_20260313", "type": "bug", - "status": "new", + "status": "completed", "created_at": "2026-03-13T12:00:00Z", "updated_at": "2026-03-13T12:00:00Z", "description": "Android animations broken - mainly noticeable on Connections screen, the indescriminate circular and linear progress bars don't move, and the MeshActivity animation is not firing, investigate recomposition and threading strangely enough they're working on Desktop" diff --git a/conductor/archive/kmp_doc_review_20260313/metadata.json b/conductor/archive/kmp_doc_review_20260313/metadata.json index fcd5405ec..25a90e45b 100644 --- a/conductor/archive/kmp_doc_review_20260313/metadata.json +++ b/conductor/archive/kmp_doc_review_20260313/metadata.json @@ -1,7 +1,7 @@ { "track_id": "kmp_doc_review_20260313", "type": "chore", - "status": "new", + "status": "completed", "created_at": "2026-03-13T12:00:00Z", "updated_at": "2026-03-13T12:00:00Z", "description": "do a thorough review of the project docs for quality and veracity against the current codebase and recent changes - use tooling as needed. Evaluate updating project documentation for clarity and context. Synthesize and condense documentation and plans as needed. Be sure to thoroughly investigate the current state of the codebase and it's migration to kmp." diff --git a/conductor/archive/kmp_test_migration_20260318/metadata.json b/conductor/archive/kmp_test_migration_20260318/metadata.json index 4dd477a02..73b8373cc 100644 --- a/conductor/archive/kmp_test_migration_20260318/metadata.json +++ b/conductor/archive/kmp_test_migration_20260318/metadata.json @@ -1,7 +1,7 @@ { "track_id": "kmp_test_migration_20260318", "type": "chore", - "status": "new", + "status": "completed", "created_at": "2026-03-18T10:00:00Z", "updated_at": "2026-03-18T10:00:00Z", "description": "Migrate tests to KMP best practices and expand coverage" diff --git a/conductor/archive/migrate_debug_panel_20260319/index.md b/conductor/archive/migrate_debug_panel_20260319/index.md new file mode 100644 index 000000000..30a087a64 --- /dev/null +++ b/conductor/archive/migrate_debug_panel_20260319/index.md @@ -0,0 +1,5 @@ +# Track migrate_debug_panel_20260319 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/migrate_debug_panel_20260319/metadata.json b/conductor/archive/migrate_debug_panel_20260319/metadata.json new file mode 100644 index 000000000..0e0ab5b5d --- /dev/null +++ b/conductor/archive/migrate_debug_panel_20260319/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "migrate_debug_panel_20260319", + "type": "feature", + "status": "completed", + "created_at": "2026-03-19T00:00:00Z", + "updated_at": "2026-03-19T10:00:00Z", + "description": "migrate the fully featured debug panel to common source for use in other targets, wire it up in desktop" +} \ No newline at end of file diff --git a/conductor/archive/migrate_debug_panel_20260319/plan.md b/conductor/archive/migrate_debug_panel_20260319/plan.md new file mode 100644 index 000000000..f1e15d3a7 --- /dev/null +++ b/conductor/archive/migrate_debug_panel_20260319/plan.md @@ -0,0 +1,23 @@ +# Implementation Plan: Debug Panel KMP Migration + +## Phase 1: Analysis and Relocation [checkpoint: a2e83eb] +- [x] Task: Locate all source files for the Android Debug Panel (UI, ViewModels, States). +- [x] Task: Move these files from the Android-specific source sets (e.g., `feature/settings/src/androidMain`) into `feature/settings/src/commonMain`. +- [x] Task: Conductor - User Manual Verification 'Phase 1: Analysis and Relocation' (Protocol in workflow.md) + +## Phase 2: Adaptation to KMP [checkpoint: 834f42c] +- [x] Task: Resolve compilation errors by removing Android-specific imports (`android.*`, `java.*`). +- [x] Task: Migrate Android Jetpack Compose imports (`androidx.compose`) to Compose Multiplatform equivalents (`org.jetbrains.compose.*` or ensuring the standard Multiplatform aliases are used). +- [x] Task: Ensure the Debug Panel ViewModel uses the multiplatform `androidx.lifecycle.ViewModel`. +- [x] Task: Abstract any necessary platform-specific logging or hardware interactions using `expect`/`actual` or KMP interfaces. +- [x] Task: Write or migrate corresponding unit tests to `commonTest`. +- [x] Task: Conductor - User Manual Verification 'Phase 2: Adaptation to KMP' (Protocol in workflow.md) + +## Phase 3: Desktop Integration [checkpoint: de2ae06] +- [x] Task: Wire the Debug Panel into the Desktop target's settings menu (`DesktopSettingsNavigation.kt`). +- [x] Task: Add DI bindings for the Desktop module if the Debug Panel requires specific dependencies. +- [x] Task: Verify the Debug Panel screen can be opened and navigated to from the Desktop app. +- [x] Task: Conductor - User Manual Verification 'Phase 3: Desktop Integration' (Protocol in workflow.md) + +## Phase: Review Fixes +- [x] Task: Apply review suggestions ac69e73 \ No newline at end of file diff --git a/conductor/archive/migrate_debug_panel_20260319/spec.md b/conductor/archive/migrate_debug_panel_20260319/spec.md new file mode 100644 index 000000000..526d336d4 --- /dev/null +++ b/conductor/archive/migrate_debug_panel_20260319/spec.md @@ -0,0 +1,24 @@ +# Specification: Debug Panel KMP Migration + +## Overview +Migrate the existing Android-specific Debug Panel to `commonMain` to enable its use across all Kotlin Multiplatform targets, specifically wiring it up for the Desktop target. + +## Functional Requirements +- The complete Android debug panel implementation will be moved and adapted to `commonMain`. +- All capabilities from the existing Android debug panel should be preserved and made functional on the Desktop target if possible. +- The Debug Panel will be accessible within the Desktop Settings menu, mirroring the Android application's navigation structure. +- Any platform-specific system logging (e.g., Logcat) that cannot be migrated will be appropriately abstracted or gracefully degraded. + +## Non-Functional Requirements +- **Architecture:** Follow the project's MVI/UDF architecture. +- **UI:** Leverage Compose Multiplatform for the shared UI, removing any Android-specific Jetpack Compose dependencies from the core shared UI logic. +- **Testing:** Add `commonTest` coverage for the migrated ViewModels and presentation logic. + +## Acceptance Criteria +- [ ] The Debug Panel source code resides in a `commonMain` module (e.g., `feature/settings/src/commonMain`). +- [ ] The Debug Panel compiles and runs successfully on both the Android and Desktop targets. +- [ ] The Desktop application can navigate to the Debug Panel from the Settings menu. +- [ ] Essential debug features (transport logs, packet inspection, etc.) function on the Desktop. + +## Out of Scope +- Creating new debug capabilities that do not already exist in the Android implementation. \ No newline at end of file diff --git a/conductor/archive/mqtt_transport_20260318/metadata.json b/conductor/archive/mqtt_transport_20260318/metadata.json index bd7d32747..f2ac1628d 100644 --- a/conductor/archive/mqtt_transport_20260318/metadata.json +++ b/conductor/archive/mqtt_transport_20260318/metadata.json @@ -1,7 +1,7 @@ { "track_id": "mqtt_transport_20260318", "type": "feature", - "status": "new", + "status": "completed", "created_at": "2026-03-18T00:00:00Z", "updated_at": "2026-03-18T00:00:00Z", "description": "MQTT transport" diff --git a/conductor/archive/wire_up_notifs_20260316/metadata.json b/conductor/archive/wire_up_notifs_20260316/metadata.json index e37b2b1ba..c0a345cb9 100644 --- a/conductor/archive/wire_up_notifs_20260316/metadata.json +++ b/conductor/archive/wire_up_notifs_20260316/metadata.json @@ -1,7 +1,7 @@ { "track_id": "wire_up_notifs_20260316", "type": "feature", - "status": "new", + "status": "completed", "created_at": "2026-03-16T00:00:00Z", "updated_at": "2026-03-16T00:00:00Z", "description": "wire up notifs" diff --git a/conductor/product.md b/conductor/product.md index 036b95200..3a4b02629 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -18,6 +18,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application designed to facil - Adaptive node and contact management - Offline map rendering and device positioning - Device configuration and firmware updates +- Unified cross-platform debugging and packet inspection ## Key Architecture Goals - Provide a robust, shared KMP core (`core:model`, `core:ble`, `core:repository`, `core:domain`, `core:data`, `core:network`, `core:service`) to support multiple platforms (Android, Desktop, iOS) diff --git a/conductor/tracks.md b/conductor/tracks.md index 07ad7c20d..22d3d6494 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,4 +1,3 @@ # Project Tracks This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder. - diff --git a/desktop/README.md b/desktop/README.md index 14a66457f..a86553993 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -59,7 +59,6 @@ The module depends on the JVM variants of KMP modules: | `ui/settings/DesktopNetworkConfigScreen.kt` | Network config without QR/NFC scanning | | `ui/settings/DesktopSecurityConfigScreen.kt` | Security config with JVM `SecureRandom` (omits file export) | | `ui/settings/DesktopExternalNotificationConfigScreen.kt` | External notification config without MediaPlayer/file import | -| `ui/settings/DesktopDebugScreen.kt` | Desktop-specific debug info screen | | `ui/nodes/DesktopAdaptiveNodeListScreen.kt` | Adaptive node list-detail using JetBrains `ListDetailPaneScaffold` | | `ui/messaging/DesktopAdaptiveContactsScreen.kt` | Adaptive contacts list-detail using JetBrains `ListDetailPaneScaffold` | | `ui/messaging/DesktopMessageContent.kt` | Desktop message content with send, reactions, and selection | diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt index 46e6fdb4c..aab9ea8e5 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt @@ -139,10 +139,10 @@ fun EntryProviderScope.desktopSettingsGraph(backStack: NavBackStack { val viewModel: org.meshtastic.feature.settings.debugging.DebugViewModel = koinViewModel() - org.meshtastic.desktop.ui.settings.DesktopDebugScreen( + org.meshtastic.feature.settings.debugging.DebugScreen( viewModel = viewModel, onNavigateUp = { backStack.removeLastOrNull() }, ) diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/settings/DesktopDebugScreen.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/settings/DesktopDebugScreen.kt deleted file mode 100644 index 69a849620..000000000 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/settings/DesktopDebugScreen.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.desktop.ui.settings - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.debug_panel -import org.meshtastic.core.ui.component.MainAppBar -import org.meshtastic.feature.settings.debugging.DebugViewModel - -/** - * A basic Desktop implementation of the Debug Panel. Allows viewing the raw mesh logs without the Android-specific - * export/sharing intents. - */ -@Composable -fun DesktopDebugScreen(viewModel: DebugViewModel, onNavigateUp: () -> Unit) { - val logs by viewModel.meshLog.collectAsStateWithLifecycle() - - Scaffold( - topBar = { - MainAppBar( - title = stringResource(Res.string.debug_panel), - ourNode = null, - showNodeChip = false, - canNavigateUp = true, - onNavigateUp = onNavigateUp, - actions = {}, - onClickChip = {}, - ) - }, - ) { paddingValues -> - LazyColumn(modifier = Modifier.fillMaxSize().padding(paddingValues)) { - items(logs, key = { it.uuid }) { log -> - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "${log.formattedReceivedDate} - ${log.messageType}", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.primary, - ) - Text( - text = log.logMessage, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 4.dp), - ) - } - HorizontalDivider() - } - } - } -} diff --git a/docs/kmp-status.md b/docs/kmp-status.md index 63df6b274..f09196acd 100644 --- a/docs/kmp-status.md +++ b/docs/kmp-status.md @@ -63,7 +63,7 @@ Working Compose Desktop application with: - **Desktop language picker** backed by `UiPreferencesDataSource.locale`, with immediate Compose Multiplatform resource updates - **Navigation-preserving locale switching** via `Main.kt` `staticCompositionLocalOf` recomposition instead of recreating the Nav3 backstack - Node detail metrics screens (Device, Environment, Signal, Power, Pax) wired with shared KMP + Vico charts -- 7 desktop-specific screens (Settings, Device, Position, Network, Security, ExternalNotification, Debug) +- 6 desktop-specific screens (Settings, Device, Position, Network, Security, ExternalNotification) - **Native notifications and system tray icon** wired via `DesktopNotificationManager` - **Native release pipeline** generating `.dmg` (macOS), `.msi` (Windows), and `.deb` (Linux) installers in CI diff --git a/docs/roadmap.md b/docs/roadmap.md index 430f19fef..72ae20665 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -63,7 +63,7 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | | Feature | Status | |---|---| -| Settings | ✅ ~35 real screens (7 desktop-specific) + desktop locale picker with in-place recomposition | +| Settings | ✅ ~35 real screens (6 desktop-specific) + desktop locale picker with in-place recomposition | | Node list | ✅ Adaptive list-detail with real `NodeDetailContent` | | Messaging | ✅ Adaptive contacts with real message view + send | | Connections | ✅ Unified shared UI with dynamic transport detection | diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index b8bcc2ca0..df6267427 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -76,6 +76,17 @@ kotlin { implementation(project(":core:datastore")) } - val androidHostTest by getting { dependencies { implementation(project(":core:datastore")) } } + val androidHostTest by getting { + dependencies { + implementation(project(":core:datastore")) + implementation(libs.junit) + implementation(libs.robolectric) + implementation(libs.turbine) + implementation(libs.kotlinx.coroutines.test) + implementation(libs.androidx.compose.ui.test.junit4) + implementation(libs.androidx.compose.ui.test.manifest) + implementation(libs.androidx.test.ext.junit) + } + } } } diff --git a/feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/debugging/DebugFiltersTest.kt b/feature/settings/src/androidHostTest/kotlin/org/meshtastic/feature/settings/debugging/DebugFiltersTest.kt similarity index 98% rename from feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/debugging/DebugFiltersTest.kt rename to feature/settings/src/androidHostTest/kotlin/org/meshtastic/feature/settings/debugging/DebugFiltersTest.kt index 5b31f7012..aeef9129d 100644 --- a/feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/debugging/DebugFiltersTest.kt +++ b/feature/settings/src/androidHostTest/kotlin/org/meshtastic/feature/settings/debugging/DebugFiltersTest.kt @@ -40,8 +40,10 @@ import org.meshtastic.core.resources.debug_active_filters import org.meshtastic.core.resources.debug_filters import org.meshtastic.core.resources.getString import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog +import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) +@Config(sdk = [34]) class DebugFiltersTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTest.kt b/feature/settings/src/androidHostTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTest.kt similarity index 99% rename from feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTest.kt rename to feature/settings/src/androidHostTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTest.kt index ed1e6a7f5..b768528e9 100644 --- a/feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTest.kt +++ b/feature/settings/src/androidHostTest/kotlin/org/meshtastic/feature/settings/debugging/DebugSearchTest.kt @@ -42,8 +42,10 @@ import org.meshtastic.core.resources.getString import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchMatch import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchState +import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) +@Config(sdk = [34]) class DebugSearchTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt new file mode 100644 index 000000000..9b894f0ad --- /dev/null +++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.debugging + +import android.content.Context +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import co.touchlab.kermit.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.debug_export_failed +import org.meshtastic.core.resources.debug_export_success +import org.meshtastic.core.ui.util.showToast +import java.io.OutputStreamWriter +import java.nio.charset.StandardCharsets + +@Composable +actual fun rememberLogExporter(logsProvider: suspend () -> List): (fileName: String) -> Unit { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val exportLogsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { createdUri -> + if (createdUri != null) { + scope.launch { exportAllLogsToUri(context, createdUri, logsProvider()) } + } + } + return { fileName -> exportLogsLauncher.launch(fileName) } +} + +private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List) = + withContext(Dispatchers.IO) { + try { + if (logs.isEmpty()) { + withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, "No logs to export") } + Logger.w { "MeshLog export aborted: no logs available" } + return@withContext + } + + context.contentResolver.openOutputStream(targetUri)?.use { os -> + OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer -> + logs.forEach { log -> + writer.write("${log.formattedReceivedDate} [${log.messageType}]\n") + writer.write(log.logMessage) + log.decodedPayload?.let { decodedPayload -> + if (decodedPayload.isNotBlank()) { + writer.write("\n\nDecoded Payload:\n{\n") + // Redact Decoded keys. + decodedPayload.lineSequence().forEach { line -> + var outputLine = line + val redacted = redactedKeys.firstOrNull { line.contains(it) } + if (redacted != null) { + val idx = line.indexOf(':') + if (idx != -1) { + outputLine = line.take(idx + 1) + outputLine += "" + } + } + writer.write(outputLine) + writer.write("\n") + } + writer.write("}\n\n") + } + } + } + } + } + Logger.i { "MeshLog exported successfully to $targetUri" } + withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_success, logs.size) } + } catch (e: java.io.IOException) { + Logger.e(e) { "Failed to export logs to URI: $targetUri" } + withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, e.message ?: "") } + } + } + +private val redactedKeys = listOf("session_passkey", "private_key", "admin_key") diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt similarity index 55% rename from feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt rename to feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt index d0328e23c..84e279814 100644 --- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt @@ -16,16 +16,11 @@ */ package org.meshtastic.feature.settings.debugging -import android.content.Context -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -36,34 +31,26 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Settings -import androidx.compose.material.icons.twotone.FilterAltOff import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ColorScheme import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle @@ -71,15 +58,10 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import co.touchlab.kermit.Logger import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.format @@ -91,11 +73,6 @@ import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.debug_clear import org.meshtastic.core.resources.debug_decoded_payload -import org.meshtastic.core.resources.debug_default_search -import org.meshtastic.core.resources.debug_export_failed -import org.meshtastic.core.resources.debug_export_success -import org.meshtastic.core.resources.debug_filters -import org.meshtastic.core.resources.debug_logs_export import org.meshtastic.core.resources.debug_panel import org.meshtastic.core.resources.debug_store_logs_summary import org.meshtastic.core.resources.debug_store_logs_title @@ -109,19 +86,11 @@ import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.theme.AnnotationColor -import org.meshtastic.core.ui.theme.AppTheme -import org.meshtastic.core.ui.util.showToast import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog -import java.io.IOException -import java.io.OutputStreamWriter -import java.nio.charset.StandardCharsets import kotlin.time.Instant.Companion.fromEpochMilliseconds private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE) -// list of dict keys to redact when exporting logs. These are evaluated as line.contains, so partials are fine. -private var redactedKeys: List = listOf("session_passkey", "private_key", "admin_key") - @Suppress("LongMethod") @Composable fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) { @@ -130,8 +99,6 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) { val searchState by viewModel.searchState.collectAsStateWithLifecycle() val filterTexts by viewModel.filterTexts.collectAsStateWithLifecycle() val selectedLogId by viewModel.selectedLogId.collectAsStateWithLifecycle() - val context = LocalContext.current - val scope = rememberCoroutineScope() var filterMode by remember { mutableStateOf(FilterMode.OR) } @@ -157,13 +124,8 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) { listState.requestScrollToItem(searchState.allMatches[searchState.currentMatchIndex].logIndex) } } - // Prepare a document creator for exporting logs via SAF - val exportLogsLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { createdUri -> - if (createdUri != null) { - scope.launch { exportAllLogsToUri(context, createdUri, viewModel.loadLogsForExport()) } - } - } + // Prepare a document creator for exporting logs + val exportLogsLauncher = rememberLogExporter { viewModel.loadLogsForExport() } var showSettings by remember { mutableStateOf(false) } @@ -216,7 +178,7 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) { val timestamp = fromEpochMilliseconds(nowMillis).toLocalDateTime(TimeZone.UTC).format(format) val fileName = "meshtastic_debug_$timestamp.txt" - exportLogsLauncher.launch(fileName) + exportLogsLauncher(fileName) }, ) if (showSettings) { @@ -431,53 +393,6 @@ fun DebugMenuActions(deleteLogs: () -> Unit, modifier: Modifier = Modifier) { } } -private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List) = - withContext(Dispatchers.IO) { - try { - if (logs.isEmpty()) { - withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, "No logs to export") } - Logger.w { "MeshLog export aborted: no logs available" } - return@withContext - } - - context.contentResolver.openOutputStream(targetUri)?.use { os -> - OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer -> - logs.forEach { log -> - writer.write("${log.formattedReceivedDate} [${log.messageType}]\n") - writer.write(log.logMessage) - log.decodedPayload?.let { decodedPayload -> - if (decodedPayload.isNotBlank()) { - writer.write("\n\nDecoded Payload:\n{") - writer.write("\n") - // Redact Decoded keys. - decodedPayload.lineSequence().forEach { line -> - var outputLine = line - val redacted = redactedKeys.firstOrNull { line.contains(it) } - if (redacted != null) { - val idx = line.indexOf(':') - if (idx != -1) { - outputLine = line.take(idx + 1) - outputLine += "" - } - } - writer.write(outputLine) - writer.write("\n") - } - writer.write("\n}") - } - } - writer.write("\n\n") - } - } - } ?: run { throw IOException("Unable to open output stream for URI: $targetUri") } - - withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_success, logs.size) } - } catch (e: IOException) { - withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, e.message ?: "") } - Logger.w(e) { "MeshLog export failed" } - } - } - @Composable private fun DecodedPayloadBlock( decodedPayload: String, @@ -541,281 +456,3 @@ private fun rememberAnnotatedDecodedPayload( } } } - -@PreviewLightDark -@Composable -private fun DebugPacketPreview() { - AppTheme { - DebugItem( - UiMeshLog( - uuid = "", - messageType = "NodeInfo", - formattedReceivedDate = "9/27/20, 8:00:58 PM", - logMessage = - "from: 2885173132\n" + - "decoded {\n" + - " position {\n" + - " altitude: 60\n" + - " battery_level: 81\n" + - " latitude_i: 411111136\n" + - " longitude_i: -711111805\n" + - " time: 1600390966\n" + - " }\n" + - "}\n" + - "hop_limit: 3\n" + - "id: 1737414295\n" + - "rx_snr: 9.5\n" + - "rx_time: 316400569\n" + - "to: -1409790708", - ), - ) - } -} - -@PreviewLightDark -@Composable -private fun DebugItemWithSearchHighlightPreview() { - AppTheme { - DebugItem( - UiMeshLog( - uuid = "1", - messageType = "TextMessage", - formattedReceivedDate = "9/27/20, 8:00:58 PM", - logMessage = "Hello world! This is a test message with some keywords to search for.", - ), - searchText = "test message", - ) - } -} - -@PreviewLightDark -@Composable -private fun DebugItemPositionPreview() { - AppTheme { - DebugItem( - UiMeshLog( - uuid = "2", - messageType = "Position", - formattedReceivedDate = "9/27/20, 8:01:15 PM", - logMessage = "Position update from node (!a1b2c3d4) at coordinates 40.7128, -74.0060", - ), - ) - } -} - -@PreviewLightDark -@Composable -private fun DebugItemErrorPreview() { - AppTheme { - DebugItem( - UiMeshLog( - uuid = "3", - messageType = "Error", - formattedReceivedDate = "9/27/20, 8:02:30 PM", - logMessage = - "Connection failed: timeout after 30 seconds\n" + - "Retry attempt: 3/5\n" + - "Last known position: 40.7128, -74.0060", - ), - ) - } -} - -@PreviewLightDark -@Composable -private fun DebugItemLongMessagePreview() { - AppTheme { - DebugItem( - UiMeshLog( - uuid = "4", - messageType = "Waypoint", - formattedReceivedDate = "9/27/20, 8:03:45 PM", - logMessage = - "Waypoint created:\n" + - " Name: Home Base\n" + - " Description: Primary meeting location\n" + - " Latitude: 40.7128\n" + - " Longitude: -74.0060\n" + - " Altitude: 100m\n" + - " Icon: 🏠\n" + - " Created by: (!a1b2c3d4)\n" + - " Expires: 2025-12-31 23:59:59", - ), - ) - } -} - -@PreviewLightDark -@Composable -private fun DebugItemSelectedPreview() { - AppTheme { - DebugItem( - UiMeshLog( - uuid = "5", - messageType = "TextMessage", - formattedReceivedDate = "9/27/20, 8:04:20 PM", - logMessage = "This is a selected log item with larger font sizes for better readability.", - ), - isSelected = true, - ) - } -} - -@PreviewLightDark -@Composable -private fun DebugMenuActionsPreview() { - AppTheme { - Row(modifier = Modifier.padding(16.dp)) { - IconButton(onClick = { /* Preview only */ }, modifier = Modifier.padding(4.dp)) { - Icon( - imageVector = Icons.Outlined.FileDownload, - contentDescription = stringResource(Res.string.debug_logs_export), - ) - } - IconButton(onClick = { /* Preview only */ }, modifier = Modifier.padding(4.dp)) { - Icon(imageVector = Icons.Rounded.Delete, contentDescription = stringResource(Res.string.debug_clear)) - } - } - } -} - -@PreviewLightDark -@Composable -@Suppress("detekt:LongMethod") // big preview -private fun DebugScreenEmptyPreview() { - AppTheme { - Surface { - LazyColumn(modifier = Modifier.fillMaxSize()) { - stickyHeader { - Surface(modifier = Modifier.fillMaxWidth().padding(8.dp)) { - Column(modifier = Modifier.padding(8.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - modifier = Modifier.weight(1f), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - value = "", - onValueChange = {}, - modifier = Modifier.weight(1f).padding(end = 8.dp), - placeholder = { Text(stringResource(Res.string.debug_default_search)) }, - singleLine = true, - ) - TextButton(onClick = {}) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = "Filters", style = TextStyle(fontWeight = FontWeight.Bold)) - Icon( - imageVector = Icons.TwoTone.FilterAltOff, - contentDescription = stringResource(Res.string.debug_filters), - ) - } - } - } - } - } - } - } - // Empty state - item { - Box(modifier = Modifier.fillMaxWidth().padding(32.dp), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = "No Debug Logs", - style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold), - ) - Text( - text = "Debug logs will appear here when available", - style = TextStyle(fontSize = 14.sp, color = Color.Gray), - modifier = Modifier.padding(top = 8.dp), - ) - } - } - } - } - } - } -} - -@PreviewLightDark -@Composable -@Suppress("detekt:LongMethod") // big preview -private fun DebugScreenWithSampleDataPreview() { - AppTheme { - val sampleLogs = - listOf( - UiMeshLog( - uuid = "1", - messageType = "NodeInfo", - formattedReceivedDate = "9/27/20, 8:00:58 PM", - logMessage = - "from: 2885173132\n" + - "decoded {\n" + - " position {\n" + - " altitude: 60\n" + - " battery_level: 81\n" + - " latitude_i: 411111136\n" + - " longitude_i: -711111805\n" + - " time: 1600390966\n" + - " }\n" + - "}\n" + - "hop_limit: 3\n" + - "id: 1737414295\n" + - "rx_snr: 9.5\n" + - "rx_time: 316400569\n" + - "to: -1409790708", - ), - UiMeshLog( - uuid = "2", - messageType = "TextMessage", - formattedReceivedDate = "9/27/20, 8:01:15 PM", - logMessage = "Hello from node (!a1b2c3d4)! How's the weather today?", - ), - UiMeshLog( - uuid = "3", - messageType = "Position", - formattedReceivedDate = "9/27/20, 8:02:30 PM", - logMessage = "Position update: 40.7128, -74.0060, altitude: 100m, battery: 85%", - ), - UiMeshLog( - uuid = "4", - messageType = "Waypoint", - formattedReceivedDate = "9/27/20, 8:03:45 PM", - logMessage = "New waypoint created: 'Meeting Point' at 40.7589, -73.9851", - ), - UiMeshLog( - uuid = "5", - messageType = "Error", - formattedReceivedDate = "9/27/20, 8:04:20 PM", - logMessage = "Connection timeout - retrying in 5 seconds...", - ), - ) - - // Note: This preview shows the UI structure but won't have actual data - // since the ViewModel isn't injected in previews - Surface { - LazyColumn(modifier = Modifier.fillMaxSize()) { - stickyHeader { - Surface(modifier = Modifier.fillMaxWidth().padding(8.dp)) { - Column(modifier = Modifier.padding(8.dp)) { - Text( - text = "Debug Screen Preview", - style = TextStyle(fontWeight = FontWeight.Bold), - modifier = Modifier.padding(bottom = 8.dp), - ) - Text( - text = "Search and filter controls would appear here", - style = TextStyle(fontSize = 12.sp, color = Color.Gray), - ) - } - } - } - items(sampleLogs) { log -> DebugItem(log = log) } - } - } - } -} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt new file mode 100644 index 000000000..a23859bd6 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.debugging + +import androidx.compose.runtime.Composable + +@Composable +expect fun rememberLogExporter(logsProvider: suspend () -> List): (fileName: String) -> Unit diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt new file mode 100644 index 000000000..bbe7962f3 --- /dev/null +++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.settings.debugging + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import co.touchlab.kermit.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.awt.FileDialog +import java.awt.Frame +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStreamWriter +import java.nio.charset.StandardCharsets + +@Composable +actual fun rememberLogExporter(logsProvider: suspend () -> List): (fileName: String) -> Unit { + val scope = rememberCoroutineScope() + + return { fileName -> + scope.launch { + val logs = logsProvider() + if (logs.isEmpty()) { + Logger.w { "MeshLog export aborted: no logs available" } + return@launch + } + + withContext(Dispatchers.IO) { + // Run file dialog to ask user where to save + val fileDialog = FileDialog(null as Frame?, "Export Logs", FileDialog.SAVE) + fileDialog.file = fileName + fileDialog.isVisible = true + + val directory = fileDialog.directory + val selectedFile = fileDialog.file + + if (directory != null && selectedFile != null) { + val exportFile = File(directory, selectedFile) + try { + FileOutputStream(exportFile).use { fos -> + OutputStreamWriter(fos, StandardCharsets.UTF_8).use { writer -> + logs.forEach { log -> + writer.write("${log.formattedReceivedDate} [${log.messageType}]\n") + writer.write(log.logMessage) + log.decodedPayload?.let { decodedPayload -> + if (decodedPayload.isNotBlank()) { + writer.write("\n\nDecoded Payload:\n{\n") + // Redact Decoded keys. + decodedPayload.lineSequence().forEach { line -> + var outputLine = line + val redacted = redactedKeys.firstOrNull { line.contains(it) } + if (redacted != null) { + val idx = line.indexOf(':') + if (idx != -1) { + outputLine = line.take(idx + 1) + outputLine += "" + } + } + writer.write(outputLine) + writer.write("\n") + } + writer.write("}\n\n") + } + } + } + } + } + Logger.i { "MeshLog exported successfully to ${exportFile.absolutePath}" } + } catch (e: java.io.IOException) { + Logger.e(e) { "Failed to export logs to file: ${exportFile.absolutePath}" } + } + } else { + Logger.w { "MeshLog export aborted: user canceled file dialog" } + } + } + } + } +} + +private val redactedKeys = listOf("session_passkey", "private_key", "admin_key")