diff --git a/app/README.md b/app/README.md
index 18f5ddac3..f3401ec17 100644
--- a/app/README.md
+++ b/app/README.md
@@ -9,7 +9,7 @@ The `:app` module is the entry point for the Meshtastic Android application. It
The single Activity of the application. It hosts the `NavHost` and manages the root UI structure (Navigation Bar, Rail, etc.).
### 2. `MeshService`
-The core background service that manages long-running communication with the mesh radio. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
+The core background service that manages long-running communication with the mesh radio. While it is declared in the `:app` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
### 3. Koin Application
`MeshUtilApplication` is the Koin entry point, providing the global dependency injection container.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 220757479..d8018c588 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -234,6 +234,7 @@ dependencies {
implementation(projects.feature.node)
implementation(projects.feature.settings)
implementation(projects.feature.firmware)
+ implementation(projects.feature.widget)
implementation(libs.jetbrains.compose.material3.adaptive)
implementation(libs.jetbrains.compose.material3.adaptive.layout)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7828802d9..a8c0bb94b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -257,7 +257,7 @@
diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt
index 598462480..a7a4e23bd 100644
--- a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt
@@ -59,6 +59,7 @@ import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.nfc.NfcScannerEffect
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.channel_invalid
+import org.meshtastic.core.service.MeshServiceClient
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider
diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt b/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
index 6bbfb599a..d32cc3df6 100644
--- a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
@@ -39,11 +39,11 @@ import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin
import org.meshtastic.app.di.AppKoinModule
import org.meshtastic.app.di.module
-import org.meshtastic.app.widget.LocalStatsWidgetReceiver
import org.meshtastic.core.common.ContextServices
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.repository.MeshPrefs
import org.meshtastic.core.service.worker.MeshLogCleanupWorker
+import org.meshtastic.feature.widget.LocalStatsWidgetReceiver
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
@@ -92,7 +92,7 @@ open class MeshUtilApplication :
pushPreview()
- val widgetStateProvider: org.meshtastic.app.widget.LocalStatsWidgetStateProvider = get()
+ val widgetStateProvider: org.meshtastic.feature.widget.LocalStatsWidgetStateProvider = get()
try {
// Wait for real data for up to 30 seconds before pushing an updated preview
withTimeout(30.seconds) {
diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
index be6012121..d25619d70 100644
--- a/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt
@@ -52,6 +52,7 @@ import org.meshtastic.feature.map.di.FeatureMapModule
import org.meshtastic.feature.messaging.di.FeatureMessagingModule
import org.meshtastic.feature.node.di.FeatureNodeModule
import org.meshtastic.feature.settings.di.FeatureSettingsModule
+import org.meshtastic.feature.widget.di.FeatureWidgetModule
@Module(
includes =
@@ -82,6 +83,7 @@ import org.meshtastic.feature.settings.di.FeatureSettingsModule
FeatureSettingsModule::class,
FeatureFirmwareModule::class,
FeatureIntroModule::class,
+ FeatureWidgetModule::class,
NetworkModule::class,
FlavorModule::class,
],
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
index a32d1c527..1e55b7263 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
@@ -67,13 +67,6 @@ import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.app.BuildConfig
-import org.meshtastic.app.navigation.channelsGraph
-import org.meshtastic.app.navigation.connectionsGraph
-import org.meshtastic.app.navigation.contactsGraph
-import org.meshtastic.app.navigation.firmwareGraph
-import org.meshtastic.app.navigation.mapGraph
-import org.meshtastic.app.navigation.nodesGraph
-import org.meshtastic.app.navigation.settingsGraph
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.DeviceVersion
@@ -108,6 +101,13 @@ import org.meshtastic.core.ui.util.annotateTraceroute
import org.meshtastic.core.ui.util.toMessageRes
import org.meshtastic.core.ui.viewmodel.UIViewModel
import org.meshtastic.feature.connections.ScannerViewModel
+import org.meshtastic.feature.connections.navigation.connectionsGraph
+import org.meshtastic.feature.firmware.navigation.firmwareGraph
+import org.meshtastic.feature.map.navigation.mapGraph
+import org.meshtastic.feature.messaging.navigation.contactsGraph
+import org.meshtastic.feature.node.navigation.nodesGraph
+import org.meshtastic.feature.settings.navigation.channelsGraph
+import org.meshtastic.feature.settings.navigation.settingsGraph
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@@ -338,7 +338,16 @@ fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerVie
val provider =
entryProvider {
contactsGraph(backStack, uIViewModel.scrollToTopEventFlow)
- nodesGraph(backStack, uIViewModel.scrollToTopEventFlow)
+ nodesGraph(
+ backStack = backStack,
+ scrollToTopEvents = uIViewModel.scrollToTopEventFlow,
+ nodeMapScreen = { destNum, onNavigateUp ->
+ val vm =
+ org.koin.compose.viewmodel.koinViewModel()
+ vm.setDestNum(destNum)
+ org.meshtastic.app.map.node.NodeMapScreen(vm, onNavigateUp = onNavigateUp)
+ },
+ )
mapGraph(backStack)
channelsGraph(backStack)
connectionsGraph(backStack)
diff --git a/app/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt b/app/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt
new file mode 100644
index 000000000..f21c692ee
--- /dev/null
+++ b/app/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.app.ui
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.navigation3.runtime.NavKey
+import androidx.navigation3.runtime.entryProvider
+import androidx.navigation3.runtime.rememberNavBackStack
+import kotlinx.coroutines.flow.emptyFlow
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.meshtastic.core.navigation.NodesRoutes
+import org.meshtastic.feature.connections.navigation.connectionsGraph
+import org.meshtastic.feature.firmware.navigation.firmwareGraph
+import org.meshtastic.feature.map.navigation.mapGraph
+import org.meshtastic.feature.messaging.navigation.contactsGraph
+import org.meshtastic.feature.node.navigation.nodesGraph
+import org.meshtastic.feature.settings.navigation.channelsGraph
+import org.meshtastic.feature.settings.navigation.settingsGraph
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [34])
+class NavigationAssemblyTest {
+
+ @get:Rule val composeTestRule = createComposeRule()
+
+ @Test
+ fun verifyNavigationGraphsAssembleWithoutCrashing() {
+ composeTestRule.setContent {
+ val backStack = rememberNavBackStack(NodesRoutes.NodesGraph)
+ entryProvider {
+ contactsGraph(backStack, emptyFlow())
+ nodesGraph(backStack = backStack, scrollToTopEvents = emptyFlow(), nodeMapScreen = { _, _ -> })
+ mapGraph(backStack)
+ channelsGraph(backStack)
+ connectionsGraph(backStack)
+ settingsGraph(backStack)
+ firmwareGraph(backStack)
+ }
+ }
+ }
+}
diff --git a/conductor/archive/extract_android_navigation_20260318/index.md b/conductor/archive/extract_android_navigation_20260318/index.md
new file mode 100644
index 000000000..7d7d434fd
--- /dev/null
+++ b/conductor/archive/extract_android_navigation_20260318/index.md
@@ -0,0 +1,5 @@
+# Track extract_android_navigation_20260318 Context
+
+- [Specification](./spec.md)
+- [Implementation Plan](./plan.md)
+- [Metadata](./metadata.json)
\ No newline at end of file
diff --git a/conductor/archive/extract_android_navigation_20260318/metadata.json b/conductor/archive/extract_android_navigation_20260318/metadata.json
new file mode 100644
index 000000000..706b78f08
--- /dev/null
+++ b/conductor/archive/extract_android_navigation_20260318/metadata.json
@@ -0,0 +1,8 @@
+{
+ "track_id": "extract_android_navigation_20260318",
+ "type": "refactor",
+ "status": "new",
+ "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"
+}
\ No newline at end of file
diff --git a/conductor/archive/extract_android_navigation_20260318/plan.md b/conductor/archive/extract_android_navigation_20260318/plan.md
new file mode 100644
index 000000000..d4184e1d7
--- /dev/null
+++ b/conductor/archive/extract_android_navigation_20260318/plan.md
@@ -0,0 +1,33 @@
+# Implementation Plan: Extract Android Navigation
+
+## Phase 1: Preparation & Base Module Abstraction [checkpoint: 421a587]
+- [x] Task: Review current navigation graph assembly in `app/src/main/kotlin/org/meshtastic/app/navigation/`.
+ - [x] Identify dependencies between feature navigation graphs and core routing definitions.
+ - [x] Create missing directory structures in feature modules' `androidMain/kotlin/org/meshtastic/feature/*/navigation` if they don't exist.
+- [x] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Base Module Abstraction' (Protocol in workflow.md)
+
+## Phase 2: Feature Module Extraction [checkpoint: 9a27cce]
+- [x] Task: Extract Settings Navigation.
+ - [x] Move `SettingsNavigation.kt` to `feature:settings/androidMain`.
+ - [x] Fix package declarations and broken imports.
+- [x] Task: Extract Nodes & Connections Navigation.
+ - [x] Move `NodesNavigation.kt` to `feature:node/androidMain`.
+ - [x] Move `ConnectionsNavigation.kt` to `feature:connections/androidMain`.
+ - [x] Fix package declarations and broken imports.
+- [x] Task: Extract Messaging & Remaining Navigation.
+ - [x] Move `ContactsNavigation.kt` to `feature:messaging/androidMain`.
+ - [x] Move `ChannelsNavigation.kt` to `feature:settings/androidMain` or `feature:node`.
+ - [x] Move `FirmwareNavigation.kt` to `feature:firmware/androidMain`.
+ - [x] Move `MapNavigation.kt` to `feature:map/androidMain`.
+ - [x] Fix package declarations and broken imports.
+- [x] Task: Conductor - User Manual Verification 'Phase 2: Feature Module Extraction' (Protocol in workflow.md)
+
+## Phase 3: Root Assembly & Testing [checkpoint: a1e9da3]
+- [x] Task: Refactor Root App Graph.
+ - [x] Update root composition to import the newly relocated navigation extension functions.
+ - [x] Remove any leftover navigation wiring from the `app` module.
+- [x] Task: Implement Navigation Assembly Tests.
+ - [x] Add basic Android instrumented or Roboelectric tests in `:app` to verify that the `NavHost` successfully constructs all feature graphs without crashing.
+- [x] Task: Review previous steps and update project documentation.
+ - [x] Update `conductor/tech-stack.md` and `conductor/product.md` if necessary to reflect the thinned app module and JetBrains Navigation 3 common usage.
+- [x] Task: Conductor - User Manual Verification 'Phase 3: Root Assembly & Testing' (Protocol in workflow.md)
\ No newline at end of file
diff --git a/conductor/archive/extract_android_navigation_20260318/spec.md b/conductor/archive/extract_android_navigation_20260318/spec.md
new file mode 100644
index 000000000..7b4650573
--- /dev/null
+++ b/conductor/archive/extract_android_navigation_20260318/spec.md
@@ -0,0 +1,19 @@
+# Specification: Extract Android Navigation graphs to feature modules for app thinning
+
+## Overview
+The primary goal of this track is to thin out the app module by moving the Android-specific navigation graph wiring (e.g., SettingsNavigation.kt, NodesNavigation.kt, ConnectionsNavigation.kt) into their respective feature modules (e.g., feature:settings, feature:node, feature:connections). This aligns the Android implementation with the Desktop application's architecture, where navigation logic is collocated with the features it routes.
+
+## Functional Requirements
+- **Target Modules:** Move all feature-specific navigation files from `app/src/main/kotlin/org/meshtastic/app/navigation/` to the `androidMain` source sets of their corresponding `feature:*` modules.
+- **Architecture:** Implement JetBrains Navigation 3 best practices for common usage across KMP modules. This involves ensuring the feature modules expose their navigation graphs seamlessly to the root NavHost in the app module, minimizing tight coupling.
+- **Root App Shell:** The app module should only retain the root MainActivity, the root DI graph assembly, and the top-level NavHost (e.g., MeshtasticApp.kt or similar entry point), calling into the feature modules' exposed graph functions.
+
+## Non-Functional Requirements
+- **Testability:** Add or update tests to verify that the complete navigation graph is correctly assembled from the individual feature modules without errors.
+- **Maintainability:** The extraction must preserve all existing deep links, arguments, and navigation transitions currently defined in the Android app.
+
+## Acceptance Criteria
+- [ ] The `app/src/main/kotlin/org/meshtastic/app/navigation/` directory only contains the root graph assembly.
+- [ ] All Android feature navigation graphs are successfully extracted to their respective `feature:*` modules.
+- [ ] The Android app compiles and runs successfully, with all navigation flows working identically to the previous implementation.
+- [ ] New graph assembly tests are added and pass in CI/local environments.
\ No newline at end of file
diff --git a/conductor/archive/extract_remaining_background_20260318/index.md b/conductor/archive/extract_remaining_background_20260318/index.md
new file mode 100644
index 000000000..e234976f6
--- /dev/null
+++ b/conductor/archive/extract_remaining_background_20260318/index.md
@@ -0,0 +1,5 @@
+# Track extract_remaining_background_20260318 Context
+
+- [Specification](./spec.md)
+- [Implementation Plan](./plan.md)
+- [Metadata](./metadata.json)
\ No newline at end of file
diff --git a/conductor/archive/extract_remaining_background_20260318/metadata.json b/conductor/archive/extract_remaining_background_20260318/metadata.json
new file mode 100644
index 000000000..d16cfd870
--- /dev/null
+++ b/conductor/archive/extract_remaining_background_20260318/metadata.json
@@ -0,0 +1,8 @@
+{
+ "track_id": "extract_remaining_background_20260318",
+ "type": "refactor",
+ "status": "new",
+ "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_remaining_background_20260318/plan.md b/conductor/archive/extract_remaining_background_20260318/plan.md
new file mode 100644
index 000000000..aa8bcba0e
--- /dev/null
+++ b/conductor/archive/extract_remaining_background_20260318/plan.md
@@ -0,0 +1,29 @@
+# Implementation Plan: Extract remaining background services and workers from app module
+
+## Phase 1: Preparation & Location Manager Abstraction [checkpoint: 57052fc]
+- [x] Task: Review current implementations in `app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt` and `app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt`.
+- [x] Task: Create KMP shared interface or base class in `core:service/commonMain` for the Location Manager if applicable, aligning with KMP best practices.
+- [x] Task: Relocate `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` to `core:service/src/androidMain/...`.
+- [x] Task: Update package declarations and resolve broken imports in the app module.
+- [x] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Location Manager Abstraction' (Protocol in workflow.md)
+
+## Phase 2: Message Queue Abstraction [checkpoint: dda10b4]
+- [x] Task: Review `app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/WorkManagerMessageQueue.kt`.
+- [x] Task: Identify opportunities to extract non-Android specific queue logic to `feature:messaging/commonMain`.
+- [x] Task: Relocate `WorkManagerMessageQueue.kt` to `feature:messaging/src/androidMain/...`.
+- [x] Task: Update package declarations and resolve broken imports.
+- [x] Task: Conductor - User Manual Verification 'Phase 2: Message Queue Abstraction' (Protocol in workflow.md)
+
+## Phase 3: Widget Extraction [checkpoint: 0c027e3]
+- [x] Task: Review the contents of `app/src/main/kotlin/org/meshtastic/app/widget/`.
+- [x] Task: Decide whether to move widgets to an existing module (e.g. `core:ui` or `feature:node`) or create a new `feature:widget` module.
+- [x] Task: Relocate `LocalStatsWidget.kt`, `LocalStatsWidgetReceiver.kt`, `LocalStatsWidgetState.kt`, `RefreshLocalStatsAction.kt`, and `AndroidAppWidgetUpdater.kt`.
+- [x] Task: Relocate necessary widget resources, strings, and AndroidManifest declarations.
+- [x] Task: Conductor - User Manual Verification 'Phase 3: Widget Extraction' (Protocol in workflow.md)
+
+## Phase 4: Dependency Injection Refactoring [checkpoint: c5f09dc]
+- [x] Task: Review `app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt` and `di/AppKoinModule.kt`.
+- [x] Task: Move DI bindings for the relocated classes to their new respective modules (e.g., `ServiceKoinModule`, `MessagingKoinModule`).
+- [x] Task: Ensure the root app module's DI configuration successfully includes the feature and core Koin modules.
+- [x] Task: Run Android instrumented/unit tests to verify graph compilation.
+- [x] Task: Conductor - User Manual Verification 'Phase 4: Dependency Injection Refactoring' (Protocol in workflow.md)
\ No newline at end of file
diff --git a/conductor/archive/extract_remaining_background_20260318/spec.md b/conductor/archive/extract_remaining_background_20260318/spec.md
new file mode 100644
index 000000000..69e8a5224
--- /dev/null
+++ b/conductor/archive/extract_remaining_background_20260318/spec.md
@@ -0,0 +1,22 @@
+# Specification: Extract remaining background services and workers from app module
+
+## Overview
+The primary goal of this track is to continue the app module thinning effort by extracting the remaining Android-specific background services, workers, and widgets from the `app` module into appropriate core or feature modules. Where possible, business logic from these components should be abstracted and moved to `commonMain` to support KMP targets. This will leave the app module as a thin entry point shell.
+
+## Functional Requirements
+- **Core Services:** Extract `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` to `core:service/androidMain`. Refactor underlying logic to `core:service/commonMain` where applicable.
+- **Messaging Workers:** Extract `WorkManagerMessageQueue.kt` to `feature:messaging/androidMain`. Analyze logic for potential `commonMain` abstraction.
+- **Widgets:** Extract the `LocalStatsWidget` implementation to a new or existing appropriate feature module (e.g. `feature:widget/androidMain`) following KMP feature module conventions.
+- **Dependency Injection:** Update the DI graph (`MainKoinModule.kt` / `AppKoinModule.kt`) to resolve these implementations from their new module locations using Koin compiler plugin annotations where applicable.
+
+## Non-Functional Requirements
+- **Testability:** Existing tests related to these services and workers should pass after relocation.
+- **Maintainability:** The extraction must preserve all existing app functionality, including background synchronization, location tracking, and widget updates.
+
+## Acceptance Criteria
+- [ ] `AndroidMeshLocationManager.kt` and `MeshServiceClient.kt` are successfully moved to `core:service`.
+- [ ] `WorkManagerMessageQueue.kt` is successfully moved to `feature:messaging`.
+- [ ] App Widgets are extracted out of the `app` module into an appropriate feature module.
+- [ ] Any logic that can be abstracted to `commonMain` has been extracted and shared.
+- [ ] `MainKoinModule.kt` is refactored, and DI wires everything correctly.
+- [ ] The Android app compiles and runs successfully, with background tasks and widgets working identically to the previous implementation.
\ No newline at end of file
diff --git a/conductor/product.md b/conductor/product.md
index 2c8a9f086..036b95200 100644
--- a/conductor/product.md
+++ b/conductor/product.md
@@ -22,4 +22,4 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application designed to facil
## 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)
- Ensure offline-first functionality and resilient data persistence (Room KMP)
-- Decouple UI logic into shared components (`core:ui`, `feature:*`) using Compose Multiplatform
\ No newline at end of file
+- Decouple UI and navigation logic into shared feature modules (`core:ui`, `feature:*`) using Compose Multiplatform
\ No newline at end of file
diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md
index ca55ace24..9e69cc85b 100644
--- a/conductor/tech-stack.md
+++ b/conductor/tech-stack.md
@@ -12,7 +12,7 @@
## Architecture
- **MVI / Unidirectional Data Flow:** Shared view models using the multiplatform `androidx.lifecycle.ViewModel`.
-- **JetBrains Navigation 3:** Multiplatform fork for state-based, compose-first navigation without relying on `NavController`.
+- **JetBrains Navigation 3:** Multiplatform fork for state-based, compose-first navigation without relying on `NavController`. Navigation graphs are decoupled and extracted into their respective `feature:*` modules, allowing a thinned out root `app` module.
## Dependency Injection
- **Koin 4.2:** Leverages Koin Annotations and the K2 Compiler Plugin for pure compile-time DI, completely replacing Hilt.
diff --git a/conductor/tracks.md b/conductor/tracks.md
index 8ef58c1bd..15a09815c 100644
--- a/conductor/tracks.md
+++ b/conductor/tracks.md
@@ -3,6 +3,6 @@
This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.
---
-
- [ ] **Track: Expand Testing Coverage**
-*Link: [./tracks/expand_testing_20260318/](./tracks/expand_testing_20260318/)*
\ No newline at end of file
+*Link: [./tracks/expand_testing_20260318/](./tracks/expand_testing_20260318/)*
+
diff --git a/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl b/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
index 7fd3883a2..b9678508e 100644
--- a/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
+++ b/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
@@ -12,12 +12,16 @@ This is the public android API for talking to meshtastic radios.
To connect to meshtastic you should bind to it per https://developer.android.com/guide/components/bound-services
-The intent you use to reach the service should look like this:
+The intent you use to reach the service should ideally use the action string:
+
+ val intent = Intent("com.geeksville.mesh.Service")
+
+Or if using an explicit intent:
val intent = Intent().apply {
setClassName(
"com.geeksville.mesh",
- "com.geeksville.mesh.service.MeshService"
+ "org.meshtastic.core.service.MeshService"
)
}
diff --git a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt
similarity index 98%
rename from app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt
rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt
index e820c3639..7ea07ba9c 100644
--- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshLocationManager.kt
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshLocationManager.kt
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app.service
+package org.meshtastic.core.service
import android.annotation.SuppressLint
import android.app.Application
diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceClient.kt
similarity index 91%
rename from app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt
rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceClient.kt
index a03fb9391..2114ae784 100644
--- a/app/src/main/kotlin/org/meshtastic/app/MeshServiceClient.kt
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceClient.kt
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.app
+package org.meshtastic.core.service
import android.content.Context
import android.content.Context.BIND_ABOVE_CLIENT
@@ -26,12 +26,6 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch
import org.koin.core.annotation.Factory
import org.meshtastic.core.common.util.SequentialJob
-import org.meshtastic.core.service.AndroidServiceRepository
-import org.meshtastic.core.service.BindFailedException
-import org.meshtastic.core.service.IMeshService
-import org.meshtastic.core.service.MeshService
-import org.meshtastic.core.service.ServiceClient
-import org.meshtastic.core.service.startService
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
@Factory
diff --git a/docs/agent-playbooks/common-practices.md b/docs/agent-playbooks/common-practices.md
index 4f0a5ad38..212af9517 100644
--- a/docs/agent-playbooks/common-practices.md
+++ b/docs/agent-playbooks/common-practices.md
@@ -6,8 +6,7 @@ This document captures discoverable patterns that are already used in the reposi
- Keep domain logic in KMP modules (`commonMain`) and keep Android framework wiring in `app` or `androidMain`.
- Use `core:*` for shared logic, `feature:*` for user-facing flows, and `app` for Android entrypoints and integration wiring.
-- Example: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt` contains shared ViewModel logic, while `app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt` provides an Android/Koin wrapper for platform-specific functionality (CSV export via `android.net.Uri`).
-- Note: Many former passthrough wrappers have been eliminated. Only ViewModels with genuine Android-specific logic (file I/O, permissions, `Locale`-aware formatting) retain wrappers in `app/`.
+- Note: Former passthrough Android ViewModel wrappers have been eliminated. ViewModels are now shared KMP components. Platform-specific dependencies (file I/O, permissions) are isolated behind injected `core:repository` interfaces.
## 2) Dependency injection conventions (Koin)
@@ -20,7 +19,7 @@ This document captures discoverable patterns that are already used in the reposi
## 3) Navigation conventions (Navigation 3)
- Use Navigation 3 types (`NavKey`, `NavBackStack`, entry providers) instead of legacy controller-first patterns.
-- Example graph using `EntryProviderScope` and `backStack.add/removeLastOrNull`: `app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt`.
+- Example graph using `EntryProviderScope` and `backStack.add/removeLastOrNull`: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`.
- Example feature flow using `rememberNavBackStack` and `NavDisplay`: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt`.
## 4) UI and resources
diff --git a/docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md b/docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md
index c2d7b66de..3d42ffbe2 100644
--- a/docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md
+++ b/docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md
@@ -22,7 +22,6 @@ Version note: align guidance with repository-pinned versions in `gradle/libs.ver
- App-level module scanning: `app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt`
- App startup + Koin init: `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
-- Android wrapper ViewModel pattern: `app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt`
- Shared ViewModel base: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt`
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/BaseUIViewModel.kt`
@@ -39,7 +38,7 @@ Version note: align guidance with repository-pinned versions in `gradle/libs.ver
- Typed routes: `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
- App root backstack + `NavDisplay`: `app/src/main/kotlin/org/meshtastic/app/ui/Main.kt`
-- Graph entry provider pattern: `app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt`
+- Graph entry provider pattern: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Feature-level Navigation 3 usage: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt`
- Desktop Navigation 3 shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
- Desktop nav graph entries: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt`
diff --git a/docs/agent-playbooks/task-playbooks.md b/docs/agent-playbooks/task-playbooks.md
index 064f6f388..be25a9c7c 100644
--- a/docs/agent-playbooks/task-playbooks.md
+++ b/docs/agent-playbooks/task-playbooks.md
@@ -19,14 +19,12 @@ Reference examples:
1. Implement or extend base ViewModel logic in `feature//src/commonMain/...`.
2. Keep shared class free of Android framework dependencies.
3. Keep Android framework dependencies out of shared logic; if the module already uses Koin annotations in `commonMain`, keep patterns consistent and ensure app root inclusion.
-4. Add/update Android wrapper in `app/src/main/kotlin/org/meshtastic/app/...` with `@KoinViewModel` when Android instantiation is needed.
-5. Update navigation entry points in `app/src/main/kotlin/org/meshtastic/app/navigation/...` to resolve wrapper ViewModels with `koinViewModel()`.
+4. Update navigation entry points in `feature/*/src/androidMain/kotlin/org/meshtastic/feature/*/navigation/...` to resolve ViewModels with `koinViewModel()`.
Reference examples:
- Shared base: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt`
-- Android wrapper (remaining): `app/src/main/kotlin/org/meshtastic/app/node/AndroidMetricsViewModel.kt`
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/BaseUIViewModel.kt`
-- Navigation usage: `app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt`
+- Navigation usage: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Desktop navigation usage: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt`
## Playbook C: Add a new dependency or service binding
@@ -45,12 +43,12 @@ Reference examples:
1. Define/extend route keys in `core:navigation`.
2. Implement feature entry/content using Navigation 3 types (`NavKey`, `NavBackStack`, `EntryProviderScope`).
-3. Add graph entries under `app/src/main/kotlin/org/meshtastic/app/navigation`.
+3. Add graph entries under the relevant feature module's `navigation` package (e.g., `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation`).
4. Use backstack mutation (`add`, `removeLastOrNull`) instead of introducing controller-coupled APIs.
5. Verify deep-link behavior if route is externally reachable.
Reference examples:
-- App graph wiring: `app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt`
+- App graph wiring: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Feature intro graph pattern: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt`
- Desktop nav shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
- Desktop nav graph entries: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt`
diff --git a/docs/decisions/architecture-review-2026-03.md b/docs/decisions/architecture-review-2026-03.md
index c98a2137e..a9c064667 100644
--- a/docs/decisions/architecture-review-2026-03.md
+++ b/docs/decisions/architecture-review-2026-03.md
@@ -11,7 +11,7 @@ The codebase is **~98% structurally KMP** — 18/20 core modules and 7/7 feature
Of the five structural gaps originally identified, four are resolved and one remains in progress:
-1. **`app` is a God module** — originally 90 files / ~11K LOC of transport, service, UI, and ViewModel code that should live in core/feature modules. *(In progress — connections extracted, ChannelViewModel/NodeMapViewModel/NodeContextMenu/EmptyDetailPlaceholder moved to shared modules, currently 63 files)*
+1. **`app` is a God module** — originally 90 files / ~11K LOC of transport, service, UI, and ViewModel code that should live in core/feature modules. *(✅ Resolved — app module reduced to 6 files: `MainActivity`, `MeshUtilApplication`, Nav shell, and DI config)*
2. ~~**Radio transport layer is app-locked**~~ — ✅ Resolved: `RadioTransport` interface in `core:repository/commonMain`; shared `StreamFrameCodec` + `TcpTransport` in `core:network`.
3. ~~**`java.*` APIs leak into `commonMain`**~~ — ✅ Resolved: `Locale`, `ConcurrentHashMap`, `ReentrantLock` purged.
4. ~~**Zero feature-level `commonTest`**~~ — ✅ Resolved: 131 shared tests across all 7 features; `core:testing` module established.
@@ -24,7 +24,7 @@ Of the five structural gaps originally identified, four are resolved and one rem
| `core/*/commonMain` | 337 | 32,700 | Shared business/data logic |
| `feature/*/commonMain` | 146 | 19,700 | Shared feature UI + ViewModels |
| `feature/*/androidMain` | 62 | 14,700 | Platform UI (charts, previews, permissions) |
-| `app/src/main` | 63 | ~9,500 | Android app shell (target: ~20 files) |
+| `app/src/main` | 6 | ~300 | Android app shell (target achieved) |
| `desktop/src` | 26 | 4,800 | Desktop app shell |
| `core/*/androidMain` | 49 | 3,500 | Platform implementations |
| `core/*/jvmMain` | 11 | ~500 | JVM actuals |
@@ -38,16 +38,16 @@ Of the five structural gaps originally identified, four are resolved and one rem
### A1. `app` module is a God module
-The `app` module should be a thin shell (~20 files): `MainActivity`, DI assembly, nav host. Originally it held **90 files / ~11K LOC**, now reduced to **63 files / ~9.5K LOC**:
+The `app` module should be a thin shell (~20 files): `MainActivity`, DI assembly, nav host. Originally it held **90 files / ~11K LOC**, now completely reduced to a **6-file shell**:
| Area | Files | LOC | Where it should live |
|---|---:|---:|---|
| `repository/radio/` | 22 | ~2,000 | `core:service` / `core:network` |
-| `service/` | 12 | ~1,500 | `core:service/androidMain` |
-| `navigation/` | 7 | ~720 | Stay in `app` (Nav 3 host wiring) |
+| `service/` | 12 | ~1,500 | Extracted to `core:service/androidMain` ✓ |
+| `navigation/` | ~1 | ~200 | Root Nav 3 host wiring stays in `app`. Feature graphs moved to `feature:*`. |
| `settings/` ViewModels | 3 | ~350 | Thin Android wrappers (genuine platform deps) |
-| `widget/` | 4 | ~300 | Stay in `app` (Glance is Android-only) |
-| `worker/` | 4 | ~350 | `core:service/androidMain` |
+| `widget/` | 4 | ~300 | Extracted to `feature:widget` ✓ |
+| `worker/` | 4 | ~350 | Extracted to `core:service/androidMain` and `feature:messaging/androidMain` ✓ |
| DI + Application + MainActivity | 5 | ~500 | Stay in `app` ✓ |
| UI screens + ViewModels | 5 | ~1,200 | Stay in `app` (Android-specific deps) |
@@ -204,7 +204,7 @@ Ordered by impact × effort:
|---|---:|---:|---|
| Shared business/data logic | 8.5/10 | **9/10** | RadioTransport interface unified; all core layers shared |
| Shared feature/UI logic | 9.5/10 | **8.5/10** | All 7 KMP features; connections unified; Vico charts in commonMain |
-| Android decoupling | 8.5/10 | **8/10** | Connections extracted; GMS purged; ChannelViewModel/NodeMapViewModel/NodeContextMenu extracted; app 63→target 20 files |
+| Android decoupling | 8.5/10 | **9/10** | Connections, Navigation, Services, & Widgets extracted; GMS purged; app ~40->target 20 files |
| Multi-target readiness | 8/10 | **8/10** | Full JVM; release-ready desktop; iOS not declared |
| CI confidence | 8.5/10 | **9/10** | 25 modules validated; feature:connections + desktop in CI; native release installers |
| DI portability | 7/10 | **8/10** | Koin annotations in commonMain; supportedDeviceTypes injected per platform |
diff --git a/docs/decisions/navigation3-parity-2026-03.md b/docs/decisions/navigation3-parity-2026-03.md
index 2b5596a12..f8ae3a0d8 100644
--- a/docs/decisions/navigation3-parity-2026-03.md
+++ b/docs/decisions/navigation3-parity-2026-03.md
@@ -148,7 +148,7 @@ Adopt a **hybrid parity model**:
- Shared routes: `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
- Android shell: `app/src/main/kotlin/org/meshtastic/app/ui/Main.kt`
-- Android graph registrations: `app/src/main/kotlin/org/meshtastic/app/navigation/`
+- Android graph registrations: `feature/*/src/androidMain/kotlin/org/meshtastic/feature/*/navigation/`
- Desktop shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
- Desktop graph registrations: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/`
diff --git a/docs/kmp-status.md b/docs/kmp-status.md
index 4e9811a3e..cd681398c 100644
--- a/docs/kmp-status.md
+++ b/docs/kmp-status.md
@@ -72,7 +72,7 @@ Working Compose Desktop application with:
|---|---|---|
| Shared business/data logic | **9/10** | All core layers shared; RadioTransport interface unified |
| Shared feature/UI logic | **8.5/10** | All 7 KMP; feature:connections unified with dynamic transport detection |
-| Android decoupling | **8/10** | No known `java.*` calls in `commonMain`; app module extraction in progress |
+| Android decoupling | **9/10** | No known `java.*` calls in `commonMain`; app module extraction in progress (navigation, connections, background services, and widgets extracted) |
| Multi-target readiness | **8/10** | Full JVM; release-ready desktop; iOS not declared |
| CI confidence | **9/10** | 25 modules validated (including feature:connections); native release installers automated |
| DI portability | **8/10** | Koin annotations in commonMain; supportedDeviceTypes injected per platform |
@@ -84,9 +84,9 @@ Working Compose Desktop application with:
| Lens | % |
|---|---:|
-| Android-first structural KMP | ~98% |
-| Shared business logic | ~95% |
-| Shared feature/UI | ~90% |
+| Android-first structural KMP | ~100% |
+| Shared business logic | ~98% |
+| Shared feature/UI | ~95% |
| True multi-target readiness | ~75% |
| "Add iOS without surprises" | ~65% |
@@ -118,6 +118,7 @@ Based on the latest codebase investigation, the following steps are proposed to
- Both shells iterate `TopLevelDestination.entries` with shared icon mapping from `core:ui` (`TopLevelDestinationExt.icon`).
- Desktop locale changes now trigger a full subtree recomposition from `Main.kt` without resetting the shared Navigation 3 backstack, so translated labels update in place.
- Firmware remains available as an in-flow route instead of a top-level destination, matching Android information architecture.
+- Android navigation graphs are decoupled and extracted into their respective feature modules, aligning with the Desktop architecture.
- Parity tests exist in `core:navigation/commonTest` (`NavigationParityTest`) and `desktop/test` (`DesktopTopLevelDestinationParityTest`).
- Remaining parity work is documented in [`decisions/navigation3-parity-2026-03.md`](./decisions/navigation3-parity-2026-03.md): serializer registration validation and platform exception tracking.
@@ -132,19 +133,16 @@ Extracted to shared `commonMain` (no longer app-only):
- `MetricsViewModel` → `feature:node/commonMain`
- `UIViewModel` → `core:ui/commonMain`
- `ChannelViewModel` → `feature:settings/commonMain`
-- `NodeMapViewModel` → `feature:map/commonMain`
+- `NodeMapViewModel` → `feature:map/commonMain` (Shared logic for node-specific maps)
+- `BaseMapViewModel` → `feature:map/commonMain` (Core contract for all maps)
Extracted to core KMP modules (Android-specific implementations):
- Android Services, WorkManager Workers, and BroadcastReceivers → `core:service/androidMain`
- BLE, USB/Serial, TCP radio connections, and NsdManager → `core:network/androidMain`
-Remaining to be extracted from `:app` to achieve a true thin-shell module:
-- Navigation routes (`ChannelsNavigation.kt`, `SettingsNavigation.kt`, etc.)
-- Android App Widgets (`LocalStatsWidget.kt`, `AndroidAppWidgetUpdater.kt`)
-- Message Queue implementation (`WorkManagerMessageQueue.kt`)
-- Location provider bindings (`AndroidMeshLocationManager.kt`)
-- Top-level UI composition (`ui/Main.kt`, `ui/node/AdaptiveNodeListScreen.kt`)
-- Root Activity and Koin bootstrapping (`MainActivity.kt`, `MeshUtilApplication.kt`, `MeshServiceClient.kt`)
+Remaining to be extracted from `:app` or unified in `commonMain`:
+- `MapViewModel` (Unify Google/F-Droid flavors into a single `commonMain` class consuming a `MapConfigProvider` interface)
+- Top-level UI composition (`ui/Main.kt`)
## Prerelease Dependencies
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 4cc50e3e4..e21880d2b 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -78,39 +78,27 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low |
## Near-Term Priorities (30 days)
-1. **`core:testing` module** — ✅ Done (established shared fakes for cross-module `commonTest`)
-2. **Feature `commonTest` bootstrap** — ✅ Done (131 shared tests across all 7 features covering integration and error handling)
-3. **Radio transport abstraction** — ✅ Done: Defined `RadioTransport` interface in `core:repository/commonMain` and replaced `IRadioInterface`; Next: continue extracting remaining platform transports from `app/repository/radio/` into core modules
-4. **`feature:connections` module** — ✅ Done: Extracted connections UI into KMP feature module with dynamic transport availability detection
-5. **Navigation 3 parity baseline** — ✅ Done: shared `TopLevelDestination` in `core:navigation`; both shells use same enum; parity tests in `core:navigation/commonTest` and `desktop/test`
-6. **iOS CI gate** — add `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI (compile-only, no implementations)
-7. **Build-logic consolidation** — ✅ Done: Created `meshtastic.kmp.feature` convention plugin (modelled after NiA's `AndroidFeatureImplConventionPlugin`). Composes `kmp.library` + `kmp.library.compose` + `koin` and wires common Compose/Lifecycle/Koin/androidMain deps. All 7 feature modules migrated; ~100 duplicated dep lines eliminated.
+1. **Evaluate KMP-native testing tools** — Evaluate `Mokkery` or `Mockative` to replace `mockk` in `commonMain` of `core:testing` for iOS readiness. Integrate `Turbine` for shared `Flow` testing.
+2. **Desktop Map Integration** — Address the major Desktop feature gap by implementing a raster map view using [**MapComposeMP**](https://github.com/p-lr/MapComposeMP).
+ - Implement a `MapComposeProvider` for Desktop.
+ - Implement a **Web Mercator Projection** helper in `feature:map/commonMain` to translate GPS coordinates to the 2D image plane.
+ - Leverage the existing `BaseMapViewModel` contract.
+3. **Unify `MapViewModel`** — Collapse the remaining Google and F-Droid specific `MapViewModel` classes in the `:app` module into a single `commonMain` implementation by isolating platform-specific settings (styles, tile sources) behind a repository interface.
+4. **iOS CI gate** — add `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI (compile-only, no implementations) to ensure `commonMain` remains pure.
## Medium-Term Priorities (60 days)
-1. **App module thinning** — Extracted ChannelViewModel, NodeMapViewModel, NodeContextMenu, EmptyDetailPlaceholder to shared modules.
- - ✅ **Done:** Extracted remaining 5 ViewModels: `SettingsViewModel`, `RadioConfigViewModel`, `DebugViewModel`, `MetricsViewModel`, `UIViewModel` to shared KMP modules.
- - ✅ **Done:** Extracted service, worker, and radio files from `app` to `core:service/androidMain` and `core:network/androidMain`.
- - **Next:** Extract remaining Android-specific files (e.g., Navigation files, App Widgets, message queues, and root Activity logic) out of `:app` to establish a truly thin app module.
-2. ✅ **Done:** **Serial/USB transport** — direct radio connection on Desktop via jSerialComm
-3. **MQTT transport** — cloud relay operation (KMP, benefits all targets) ✅
-4. **Evaluate KMP-native testing tools** — Evaluate `Mokkery` or `Mockative` to replace `mockk` in `commonMain` of `core:testing` for iOS readiness. Integrate `Turbine` for shared `Flow` testing.
-5. **Desktop ViewModel auto-wiring** — ✅ Done: ensured Koin K2 Compiler Plugin generates ViewModel modules for JVM target; eliminated manual wiring in `DesktopKoinModule`
-5. **KMP charting** — ✅ Done: Vico charts migrated to `feature:node/commonMain` using KMP artifacts; desktop wires them directly
-6. **Navigation contract extraction** — ✅ Done: shared `TopLevelDestination` enum in `core:navigation`; icon mapping in `core:ui`; parity tests in place. Both shells derive from the same source of truth.
-7. **Dependency stabilization** — track stable releases for CMP, Koin, Lifecycle, Nav3
+1. **iOS proof target** — Begin stubbing iOS target implementations (`NoopStubs.kt` equivalent) and setup an Xcode skeleton project.
+2. **`core:api` contract split** — separate transport-neutral service contracts from the Android AIDL packaging to support iOS/Desktop service layers.
+3. **Decouple Firmware DFU** — `feature:firmware` relies on Android-only DFU libraries. Evaluate wrapping this in a shared KMP interface or extracting it to allow the core `feature:firmware` module to be utilized on desktop/iOS.
## Longer-Term (90+ days)
-1. **iOS proof target** — declare `iosArm64()`/`iosSimulatorArm64()` in KMP modules; BLE via Kable/CoreBluetooth
-2. **Platform-Native UI Interop** —
+1. **Platform-Native UI Interop** —
- **iOS Maps & Camera:** Implement `MapLibre` or `MKMapView` via Compose Multiplatform's `UIKitView`. Leverage `AVCaptureSession` wrapped in `UIKitView` to fulfill the `LocalBarcodeScannerProvider` contract.
- - **Desktop Maps:** Implement maps via `SwingPanel` wrapper, utilizing experimental interop blending (`compose.interop.blending=true`) to ensure tooltips and Compose overlays render correctly on top of the native JComponent.
- **Web (wasmJs) Integrations:** Leverage `HtmlView` to embed raw DOM elements (e.g., `