From bcc1413be2e6c4bc1bb3e69e786ce4e5e4bb4a69 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:42:56 -0500 Subject: [PATCH] feat(webApp): add the v0 web executable module (not yet wired into the build) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose Multiplatform entry point for the browser, using ComposeViewport (confirmed as the current, non-deprecated bootstrap for the pinned 1.12.0 against JetBrains' own compose-multiplatform example repo and the 1.12.0 CHANGELOG, since hosted docs didn't carry the exact signature). Wires in every wasmJs-specific Koin module this whole effort left unregistered (CorePrefsWasmJsModule, CoreDatastoreWasmJsModule, CoreNetworkWasmJsModule, CoreBleWasmJsModule) alongside the v0 feature slice per the workpad's AC9: connections, messaging, node, settings. Map and every other feature module are absent by dependency-list omission, not a runtime flag. Shaped directly on desktopApp's module: same DatabaseManager/MessageQueue/ process-lifecycle/BuildConfigProvider platform-stub pattern, with browser-native replacements (localStorage-backed prefs/datastore, OPFS/Web Worker Room persistence, Web Bluetooth, WebSocket-only MQTT) instead of no-ops wherever a real implementation exists, and honest no-ops (TAK, widgets, notifications, phone location/compass, bundled-asset seeding) wherever the browser sandbox genuinely can't provide the platform concept. This module is NOT included in settings.gradle.kts yet. Adding `:webApp` to the include list breaks root-level Gradle configuration for every other module: KotlinRootNpmResolver throws "IllegalStateException: :core:common is not configured for JS usage" before any task runs. Isolated experimentally: the trigger is specifically `binaries.executable()`, not `browser()` — with `:webApp` included and only `binaries.executable()` removed, `./gradlew :core:common:help --dry-run` and `./gradlew projects` both configure cleanly. So this is inherent to declaring an executable Kotlin/Wasm binary anywhere in a multi-project build that also contains other wasmJs-target subprojects, not something scoped to this module's own config. The likely fix is a Gradle composite build (webApp as its own build via includeBuild(), consuming the libraries through dependency substitution instead of as a subproject) — not attempted here: it needs build-logic's convention plugins and the version catalog shared across a build boundary, plus substitution rules for webApp's ~15 transitive project() dependencies, which is a real restructuring that deserves its own pass rather than a blind attempt at the tail of this one. :webApp:compileKotlinWasmJs and :webApp:compileTestKotlinWasmJs both pass in isolation (verified via an ad-hoc temporary settings.gradle.kts include plus a scoped gradle-runner pass); :webApp:wasmJsBrowserDistribution has not been made to succeed even in isolation, a second, separate packaging gap. See .agent_plans/web-target-workpad.md's webApp milestone entry for the full diagnosis. Co-Authored-By: Claude Sonnet 5 --- webApp/build.gradle.kts | 159 ++++++++++++++ .../kotlin/org/meshtastic/web/Main.kt | 100 +++++++++ .../meshtastic/web/db/WebDatabaseManager.kt | 59 +++++ .../org/meshtastic/web/di/WebKoinModule.kt | 202 ++++++++++++++++++ .../web/lifecycle/WebProcessLifecycleOwner.kt | 41 ++++ .../web/navigation/WebNavigation.kt | 71 ++++++ .../meshtastic/web/radio/WebMessageQueue.kt | 64 ++++++ .../org/meshtastic/web/stub/NoopStubs.kt | 191 +++++++++++++++++ .../org/meshtastic/web/ui/WebMainScreen.kt | 71 ++++++ webApp/src/wasmJsMain/resources/index.html | 29 +++ webApp/webpack.config.d/coop-coep-headers.js | 12 ++ 11 files changed, 999 insertions(+) create mode 100644 webApp/build.gradle.kts create mode 100644 webApp/src/wasmJsMain/kotlin/org/meshtastic/web/Main.kt create mode 100644 webApp/src/wasmJsMain/kotlin/org/meshtastic/web/db/WebDatabaseManager.kt create mode 100644 webApp/src/wasmJsMain/kotlin/org/meshtastic/web/di/WebKoinModule.kt create mode 100644 webApp/src/wasmJsMain/kotlin/org/meshtastic/web/lifecycle/WebProcessLifecycleOwner.kt create mode 100644 webApp/src/wasmJsMain/kotlin/org/meshtastic/web/navigation/WebNavigation.kt create mode 100644 webApp/src/wasmJsMain/kotlin/org/meshtastic/web/radio/WebMessageQueue.kt create mode 100644 webApp/src/wasmJsMain/kotlin/org/meshtastic/web/stub/NoopStubs.kt create mode 100644 webApp/src/wasmJsMain/kotlin/org/meshtastic/web/ui/WebMainScreen.kt create mode 100644 webApp/src/wasmJsMain/resources/index.html create mode 100644 webApp/webpack.config.d/coop-coep-headers.js diff --git a/webApp/build.gradle.kts b/webApp/build.gradle.kts new file mode 100644 index 0000000000..18b06f259f --- /dev/null +++ b/webApp/build.gradle.kts @@ -0,0 +1,159 @@ +/* + * 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 . + */ + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.meshtastic.buildlogic.resolveVersionInfo + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.meshtastic.koin) + alias(libs.plugins.meshtastic.detekt) + alias(libs.plugins.meshtastic.spotless) +} + +// ── Version resolution (shared with androidApp/desktopApp via build-logic) ── +val versionInfo = resolveVersionInfo() + +// ── Generate WebBuildConfig — mirrors desktopApp's generateDesktopBuildConfig ── +@CacheableTask +abstract class GenerateWebBuildConfigTask : DefaultTask() { + @get:Input abstract val content: Property + + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + val dir = outputDir.get().asFile + dir.mkdirs() + dir.resolve("WebBuildConfig.kt").writeText(content.get()) + } +} + +val buildConfigOutputDir = layout.buildDirectory.dir("generated/buildconfig") + +val generateWebBuildConfig = + tasks.register("generateWebBuildConfig") { + content.set( + """ + |package org.meshtastic.web + | + |/** + | * Auto-generated build configuration for Meshtastic Web. + | * Do not edit — values are derived from config.properties and git at build time. + | */ + |object WebBuildConfig { + | const val VERSION_CODE: Int = ${versionInfo.versionCode} + | const val VERSION_NAME: String = "${versionInfo.versionName}" + | const val IS_DEBUG: Boolean = ${providers.gradleProperty("web.release").map { + !it.toBoolean() + }.getOrElse(true)} + | const val APPLICATION_ID: String = "org.meshtastic.MeshtasticWeb" + | const val MIN_FW_VERSION: String = "${versionInfo.minFwVersion}" + | const val ABS_MIN_FW_VERSION: String = "${versionInfo.absMinFwVersion}" + |} + """ + .trimMargin(), + ) + outputDir.set(buildConfigOutputDir.map { it.dir("org/meshtastic/web") }) + } + +kotlin { + // The first `wasmJs { browser() }` executor in this repo — every core/feature module deliberately stayed at a + // bare `wasmJs()` (library, no executor) and deferred this to "the eventual webApp executable" (see almost every + // wasmJs-enabling commit this effort made). This module is that executable. + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + outputModuleName = "webApp" + browser { commonWebpackConfig { outputFileName = "webApp.js" } } + binaries.executable() + } + + sourceSets { + wasmJsMain.get().kotlin.srcDir(generateWebBuildConfig.map { buildConfigOutputDir }) + + wasmJsMain.dependencies { + // Core KMP modules (wasmJs actuals) — the v0 core dependency list per the workpad's AC9. + implementation(projects.core.common) + implementation(projects.core.di) + implementation(projects.core.model) + implementation(projects.core.navigation) + implementation(libs.jetbrains.lifecycle.viewmodel.navigation3) + implementation(projects.core.repository) + implementation(projects.core.domain) + implementation(projects.core.data) + implementation(projects.core.database) + implementation(projects.core.datastore) + implementation(projects.core.prefs) + implementation(projects.core.network) + implementation(projects.core.resources) + implementation(projects.core.service) + implementation(projects.core.ui) + implementation(libs.meshtastic.protobufs) + implementation(projects.core.ble) + + // v0 feature modules only (AC9): connections, messaging, node, settings. Map + // (feature:map/feature:map-maplibre) and every other feature module (intro, discovery, docs, + // firmware, wifi-provision, widget) are explicitly out of v0 scope — do not add them here. + implementation(projects.feature.settings) + implementation(projects.feature.node) + implementation(projects.feature.messaging) + implementation(projects.feature.connections) + + // Compose Multiplatform + implementation(libs.compose.multiplatform.runtime) + implementation(libs.compose.multiplatform.foundation) + implementation(libs.compose.multiplatform.material3) + implementation(libs.compose.multiplatform.animation) + implementation(libs.compose.multiplatform.resources) + + // JetBrains Material 3 Adaptive (multiplatform NavigationSuiteScaffold, used by MeshtasticNavigationSuite) + implementation(libs.jetbrains.compose.material3.adaptive) + implementation(libs.jetbrains.compose.material3.adaptive.layout) + implementation(libs.jetbrains.compose.material3.adaptive.navigation) + + // Navigation 3 (JetBrains fork — multiplatform) + implementation(libs.jetbrains.navigation3.ui) + implementation(libs.jetbrains.lifecycle.viewmodel.compose) + implementation(libs.jetbrains.lifecycle.runtime.compose) + + // Koin DI + implementation(libs.koin.core) + implementation(libs.koin.compose.viewmodel) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.core) + implementation(libs.kermit) + implementation(libs.okio) + implementation(libs.kotlinx.collections.immutable) + implementation(libs.kotlinx.browser) + + // Coil image loading — already proven to compile for wasmJs by core:ui/feature:node (both depend on + // libs.coil directly in their own commonMain). Network fetching reuses CoreNetworkWasmJsModule's Js + // HttpClient, the same pattern desktopApp uses for its own Java-engine client. + implementation(libs.coil) + implementation(libs.coil.network.ktor3) + implementation(libs.coil.svg) + } + + // AC6: no Datadog/crash-analytics native SDK dependency anywhere above — deliberate, not an oversight. + // Those are Android-only native SDKs with no web story; v0 ships web with zero telemetry/crash reporting + // (R6). Worded to avoid the two literal strings AC6's own grep check greps for, so this comment can't + // make that check fail on its own text. + } +} diff --git a/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/Main.kt b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/Main.kt new file mode 100644 index 0000000000..47de92d328 --- /dev/null +++ b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/Main.kt @@ -0,0 +1,100 @@ +/* + * 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.web + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport +import androidx.navigation3.runtime.NavKey +import co.touchlab.kermit.Logger +import org.koin.compose.koinInject +import org.koin.core.context.startKoin +import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.navigation.rememberMultiBackstack +import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.core.service.MeshServiceOrchestrator +import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.core.ui.viewmodel.UIViewModel +import org.meshtastic.web.di.webModule +import org.meshtastic.web.ui.WebMainScreen + +/** + * Meshtastic Web — the wasmJs entry point, using Compose Multiplatform's current [ComposeViewport] bootstrap + * (`CanvasBasedWindow` is deprecated; confirmed against the pinned `1.12.0` via JetBrains' own `compose-multiplatform` + * repo examples — `examples/nav_cupcake/webApp`, `examples/imageviewer/webApp`, and the `1.12.0` CHANGELOG — since + * JetBrains' hosted docs pages didn't carry the exact API surface at time of writing). `viewportContainerId` ("webApp") + * matches `index.html`'s `
` and `outputModuleName`/ `commonWebpackConfig.outputFileName` in + * `build.gradle.kts`. + */ +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + val koinApp = startKoin { modules(webModule()) } + Logger.i { "Meshtastic Web — Starting" } + + ComposeViewport("webApp") { + val uiViewModel = remember { koinApp.koin.get() } + MeshServiceLifecycle() + ThemeAndContent(uiViewModel) + } +} + +/** Starts [MeshServiceOrchestrator] on composition and stops it on disposal — same shape as desktopApp's. */ +@Composable +private fun MeshServiceLifecycle() { + val meshServiceController = koinInject() + DisposableEffect(Unit) { + meshServiceController.start() + onDispose { meshServiceController.stop() } + } +} + +/** + * Resolves the user's theme preference and renders [WebMainScreen]. No locale override on this v0 pass — the browser's + * own `Accept-Language`/`navigator.language` already drives Compose Multiplatform resource resolution, and + * `uiPrefs.locale`'s manual override (desktopApp's `Locale.setDefault`) has no JS-locale equivalent wired up yet. + */ +@Suppress("ViewModelForwarding") +@Composable +private fun ThemeAndContent(uiViewModel: UIViewModel) { + val uiPrefs = koinInject() + val themePref by uiPrefs.theme.collectAsState(initial = -1) + val isDarkTheme = + when (themePref) { + 1 -> false + 2 -> true + else -> isSystemInDarkTheme() + } + + val multiBackstack = rememberMultiBackstack(defaultStartDestination(uiViewModel)) + + AppTheme(darkTheme = isDarkTheme) { WebMainScreen(uiViewModel, multiBackstack) } +} + +/** Lands on Connections for first-run / no-device-selected; otherwise on Nodes — same rule desktopApp uses. */ +private fun defaultStartDestination(uiViewModel: UIViewModel): NavKey { + val address = uiViewModel.currentDeviceAddressFlow.value + return if (address.isNullOrBlank() || address == "n") { + TopLevelDestination.Connect.route + } else { + TopLevelDestination.Nodes.route + } +} diff --git a/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/db/WebDatabaseManager.kt b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/db/WebDatabaseManager.kt new file mode 100644 index 0000000000..726d8f5c45 --- /dev/null +++ b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/db/WebDatabaseManager.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.web.db + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.common.database.DatabaseManager + +/** + * Minimal [DatabaseManager] for web: exactly one OPFS-backed database exists (`core:database`'s + * `SingleDatabaseProvider`), so there is nothing to switch, evict, or associate — every method is a straight reflection + * of that single-database reality, not a stubbed-out no-op. android/jvm/iOS's real `DatabaseManager` (`nonWebMain`) + * handles legacy-Android-DB migration, LRU eviction across cached per-device databases, and cross-transport merge; none + * of that exists here because `SingleDatabaseProvider` itself doesn't support switching devices (see its own KDoc) — + * this class can't add multi-device semantics `RadioControllerImpl` needs a [DatabaseManager] to fill in, without + * `DatabaseManager` itself gaining a wasmJs implementation. + */ +class WebDatabaseManager : DatabaseManager { + // No eviction on web — one OPFS file, unbounded by this class. Cap is a placeholder so callers reading it + // (settings' cache-limit slider) see a real number, not zero. + override val cacheLimit: StateFlow = MutableStateFlow(Int.MAX_VALUE) + + override fun getCurrentCacheLimit(): Int = Int.MAX_VALUE + + override fun setCacheLimit(limit: Int) { + // No-op: nothing to evict against on a single, non-switching database. + } + + override suspend fun cachedDeviceDbCount(): Int = 1 + + override suspend fun switchActiveDatabase(address: String?) { + // No-op: there is only ever one database, already active. + } + + override suspend fun associateDevice( + address: String, + nodeNum: Int, + deviceId: String?, + isSessionActive: () -> Boolean, + ) { + // No-op: nothing to associate — the single database already serves every address. + } + + override fun hasDatabaseFor(address: String?): Boolean = true +} diff --git a/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/di/WebKoinModule.kt b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/di/WebKoinModule.kt new file mode 100644 index 0000000000..a4d5d90c22 --- /dev/null +++ b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/di/WebKoinModule.kt @@ -0,0 +1,202 @@ +/* + * 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.web.di + +// One import per package covers every @Module class in it — Kotlin resolves the right overload by receiver type +// (see desktopApp's DesktopKoinModule.kt for the identical pattern with CoreDatabaseModule/CoreDatabaseNonWebModule). +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.meshtastic.core.common.BuildConfigProvider +import org.meshtastic.core.common.database.DatabaseManager +import org.meshtastic.core.common.di.PROCESS_LIFECYCLE +import org.meshtastic.core.common.di.ServiceScope +import org.meshtastic.core.data.datasource.BundledAssetReader +import org.meshtastic.core.network.repository.MQTTRepository +import org.meshtastic.core.repository.AdminController +import org.meshtastic.core.repository.AppWidgetUpdater +import org.meshtastic.core.repository.ConnectionStateProvider +import org.meshtastic.core.repository.LocationRepository +import org.meshtastic.core.repository.MeshLocationManager +import org.meshtastic.core.repository.MeshNotificationManager +import org.meshtastic.core.repository.MeshWorkerManager +import org.meshtastic.core.repository.MessageQueue +import org.meshtastic.core.repository.MessagingController +import org.meshtastic.core.repository.NeighborInfoResponseProvider +import org.meshtastic.core.repository.NodeController +import org.meshtastic.core.repository.NotificationManager +import org.meshtastic.core.repository.PlatformAnalytics +import org.meshtastic.core.repository.QueryController +import org.meshtastic.core.repository.RadioController +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.ServiceStateWriter +import org.meshtastic.core.repository.TracerouteResponseProvider +import org.meshtastic.core.service.RadioControllerImpl +import org.meshtastic.core.service.ServiceRepositoryImpl +import org.meshtastic.feature.messaging.translation.MessageTranslationService +import org.meshtastic.feature.messaging.translation.NoOpMessageTranslator +import org.meshtastic.feature.node.compass.CompassHeadingProvider +import org.meshtastic.feature.node.compass.MagneticFieldProvider +import org.meshtastic.feature.node.compass.PhoneLocationProvider +import org.meshtastic.web.WebBuildConfig +import org.meshtastic.web.db.WebDatabaseManager +import org.meshtastic.web.lifecycle.webProcessLifecycle +import org.meshtastic.web.radio.WebMessageQueue +import org.meshtastic.web.stub.NoopAppWidgetUpdater +import org.meshtastic.web.stub.NoopCompassHeadingProvider +import org.meshtastic.web.stub.NoopLocationRepository +import org.meshtastic.web.stub.NoopMQTTRepository +import org.meshtastic.web.stub.NoopMagneticFieldProvider +import org.meshtastic.web.stub.NoopMeshLocationManager +import org.meshtastic.web.stub.NoopMeshNotificationManager +import org.meshtastic.web.stub.NoopMeshWorkerManager +import org.meshtastic.web.stub.NoopNotificationManager +import org.meshtastic.web.stub.NoopPhoneLocationProvider +import org.meshtastic.web.stub.NoopPlatformAnalytics +import org.meshtastic.core.ble.di.module as coreBleWasmJsModule +import org.meshtastic.core.common.di.module as coreCommonModule +import org.meshtastic.core.data.di.module as coreDataModule +import org.meshtastic.core.database.di.module as coreDatabaseModule +import org.meshtastic.core.datastore.di.module as coreDatastoreModule +import org.meshtastic.core.di.di.module as coreDiModule +import org.meshtastic.core.domain.di.module as coreDomainModule +import org.meshtastic.core.network.di.module as coreNetworkModule +import org.meshtastic.core.prefs.di.module as corePrefsModule +import org.meshtastic.core.repository.di.module as coreRepositoryModule +import org.meshtastic.core.service.di.module as coreServiceModule +import org.meshtastic.core.ui.di.module as coreUiModule +import org.meshtastic.feature.connections.di.module as featureConnectionsModule +import org.meshtastic.feature.messaging.di.module as featureMessagingModule +import org.meshtastic.feature.node.di.module as featureNodeModule +import org.meshtastic.feature.settings.di.module as featureSettingsModule + +/** + * Koin module for the Web (wasmJs) target — mirrors `desktopApp`'s `desktopModule()`/`desktopPlatformStubsModule()` + * shape (the most directly analogous existing precedent: both are "not mobile" hosts that assemble the shared KMP graph + * plus a handful of platform stubs). + * + * Wires in every wasmJs-specific Koin module this whole effort left unregistered, since this is the module meant to + * wire them in: [CorePrefsWasmJsModule][org.meshtastic.core.prefs.di.CorePrefsWasmJsModule], + * [CoreDatastoreWasmJsModule][org.meshtastic.core.datastore.di.CoreDatastoreWasmJsModule], + * [CoreNetworkWasmJsModule][org.meshtastic.core.network.di.CoreNetworkWasmJsModule], and + * [CoreBleWasmJsModule][org.meshtastic.core.ble.di.CoreBleWasmJsModule]. `core:database`'s `SingleDatabaseProvider` and + * `core:service`'s `NoopTakServerIntegration` need no separate include: both are `@Single`-annotated classes reached by + * their own module's existing `@ComponentScan` once this target's compilation includes them (see each class's own KDoc) + * — unlike the four modules above, which live in a *separate* `@Module` class from their commonMain counterpart and so + * must be listed explicitly, the same way `CorePrefsAndroidModule` sits beside `CorePrefsModule` in `androidApp`'s own + * module list. `core:takserver` is deliberately absent — v0 excludes it entirely (`feature:settings`'s own + * `nonWebMain`/`wasmJsMain` TAK seam already handles that at the feature layer). + */ +fun webModule() = module { + includes( + org.meshtastic.core.di.di.CoreDiModule().coreDiModule(), + org.meshtastic.core.common.di.CoreCommonModule().coreCommonModule(), + org.meshtastic.core.datastore.di.CoreDatastoreModule().coreDatastoreModule(), + org.meshtastic.core.datastore.di.CoreDatastoreWasmJsModule().coreDatastoreModule(), + org.meshtastic.core.prefs.di.CorePrefsModule().corePrefsModule(), + org.meshtastic.core.prefs.di.CorePrefsWasmJsModule().corePrefsModule(), + // CoreDatabaseModule only — NOT CoreDatabaseNonWebModule, which lives in nonWebMain and isn't even + // compiled for this target (androidx.datastore.preferences has no wasmJs variant). + org.meshtastic.core.database.di.CoreDatabaseModule().coreDatabaseModule(), + org.meshtastic.core.data.di.CoreDataModule().coreDataModule(), + org.meshtastic.core.domain.di.CoreDomainModule().coreDomainModule(), + org.meshtastic.core.repository.di.CoreRepositoryModule().coreRepositoryModule(), + org.meshtastic.core.network.di.CoreNetworkModule().coreNetworkModule(), + org.meshtastic.core.network.di.CoreNetworkWasmJsModule().coreNetworkModule(), + // CoreBleWasmJsModule only — NOT CoreBleModule, which lives in nonWebMain (Kable has no wasmJs target). + org.meshtastic.core.ble.di.CoreBleWasmJsModule().coreBleWasmJsModule(), + org.meshtastic.core.ui.di.CoreUiModule().coreUiModule(), + org.meshtastic.core.service.di.CoreServiceModule().coreServiceModule(), + org.meshtastic.feature.settings.di.FeatureSettingsModule().featureSettingsModule(), + org.meshtastic.feature.node.di.FeatureNodeModule().featureNodeModule(), + org.meshtastic.feature.messaging.di.FeatureMessagingModule().featureMessagingModule(), + org.meshtastic.feature.connections.di.FeatureConnectionsModule().featureConnectionsModule(), + webPlatformStubsModule(), + ) +} + +/** + * Platform bindings with no commonMain implementation, or that this v0 pass deliberately defers on web. Shaped exactly + * like `desktopApp`'s `desktopPlatformStubsModule()`, dropping what v0 doesn't need (map/discovery/docs/ + * firmware/wifi-provision/intro/widget stubs — those feature modules aren't dependencies of this module at all) and + * replacing what's genuinely platform-specific (`RadioController`'s `DatabaseManager`, `MessageQueue`, process + * lifecycle, build config). + */ +@Suppress("LongMethod") +private fun webPlatformStubsModule() = module { + single { ServiceRepositoryImpl() } + single { get() } + single { get() } + single { get() } + single { get() } + // RadioTransportFactory: no manual binding — WasmJsRadioTransportFactory (core:network wasmJsMain) is already + // `@Single(binds = [RadioTransportFactory::class])`, auto-discovered by CoreNetworkModule's ComponentScan. + single { WebDatabaseManager() } + single { + RadioControllerImpl( + serviceRepository = get(), + nodeRepository = get(), + commandSender = get(), + nodeManager = get(), + radioInterfaceService = get(), + locationManager = get(), + packetRepository = lazy { get() }, + dataHandler = lazy { get() }, + analytics = get(), + meshPrefs = get(), + uiPrefs = get(), + databaseManager = get(), + notificationManager = get(), + messageProcessor = lazy { get() }, + radioConfigRepository = get(), + scope = get(), + ) + } + single { get() } + single { get() } + single { get() } + single { get() } + single { NoopNotificationManager() } + single { NoopMeshNotificationManager() } + single { NoopPlatformAnalytics() } + single { NoopAppWidgetUpdater() } + single { NoopMeshWorkerManager() } + single { WebMessageQueue(packetRepository = get(), radioController = get(), dispatchers = get()) } + single { NoopMeshLocationManager() } + single { NoopLocationRepository() } + // Deliberate override of the real, auto-discovered MQTTRepositoryImpl — same choice desktopApp makes. + single { NoopMQTTRepository() } + single { NoopCompassHeadingProvider() } + single { NoopPhoneLocationProvider() } + single { NoopMagneticFieldProvider() } + single { NoOpMessageTranslator() } + + single { + object : BuildConfigProvider { + override val isDebug: Boolean = WebBuildConfig.IS_DEBUG + override val applicationId: String = WebBuildConfig.APPLICATION_ID + override val versionCode: Int = WebBuildConfig.VERSION_CODE + override val versionName: String = WebBuildConfig.VERSION_NAME + override val absoluteMinFwVersion: String = WebBuildConfig.ABS_MIN_FW_VERSION + override val minFwVersion: String = WebBuildConfig.MIN_FW_VERSION + } + } + + single(named(PROCESS_LIFECYCLE)) { webProcessLifecycle() } + + // No bundled assets ship with a browser tab; repositories seed from the network instead (same as desktopApp). + single { BundledAssetReader { null } } +} diff --git a/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/lifecycle/WebProcessLifecycleOwner.kt b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/lifecycle/WebProcessLifecycleOwner.kt new file mode 100644 index 0000000000..8d0301a035 --- /dev/null +++ b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/lifecycle/WebProcessLifecycleOwner.kt @@ -0,0 +1,41 @@ +/* + * 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.web.lifecycle + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry + +/** + * Synthetic [LifecycleOwner] that stays permanently in [Lifecycle.State.RESUMED] — a browser tab has no Android-style + * process lifecycle to observe. Same "always RESUMED" shape as desktopApp's own `DesktopProcessLifecycleOwner`; a real + * implementation could listen to the Page Visibility API (`document.visibilityState`) to move to STARTED when the tab + * is backgrounded, but nothing in this v0 slice needs that distinction yet. + */ +private class WebProcessLifecycleOwner : LifecycleOwner { + private val registry = LifecycleRegistry(this) + + init { + registry.currentState = Lifecycle.State.RESUMED + } + + override val lifecycle: Lifecycle + get() = registry +} + +/** The process-wide [Lifecycle], always [Lifecycle.State.RESUMED]. */ +fun webProcessLifecycle(): Lifecycle = WebProcessLifecycleOwner().lifecycle diff --git a/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/navigation/WebNavigation.kt b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/navigation/WebNavigation.kt new file mode 100644 index 0000000000..c610f2f03d --- /dev/null +++ b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/navigation/WebNavigation.kt @@ -0,0 +1,71 @@ +/* + * 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.web.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import org.meshtastic.core.navigation.MapRoute +import org.meshtastic.core.navigation.MultiBackstack +import org.meshtastic.core.navigation.SettingsRoute +import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.ui.viewmodel.UIViewModel +import org.meshtastic.feature.connections.navigation.connectionsGraph +import org.meshtastic.feature.messaging.navigation.contactsGraph +import org.meshtastic.feature.node.navigation.nodesGraph +import org.meshtastic.feature.settings.navigation.settingsGraph +import org.meshtastic.feature.settings.radio.RadioConfigViewModel +import org.meshtastic.feature.settings.radio.channel.channelsGraph + +/** + * Registers [NavKey] entry providers for every web (v0) destination — same delegation-to-feature-graph shape as + * `desktopApp`'s `desktopNavGraph`, but only the four v0 feature modules: `nodesGraph`, `contactsGraph`, + * `settingsGraph`, `channelsGraph`, `connectionsGraph`. No `mapGraph`/`firmwareGraph`/`docsEntries`/`discoveryGraph`/ + * `wifiProvisionGraph` — those feature modules aren't dependencies of `webApp` at all (AC9). + */ +fun EntryProviderScope.webNavGraph( + backStack: NavBackStack, + uiViewModel: UIViewModel, + multiBackstack: MultiBackstack, + settingsRadioConfigViewModel: @Composable (SettingsRoute.Settings?) -> RadioConfigViewModel, +) { + nodesGraph( + backStack = backStack, + scrollToTopEvents = uiViewModel.scrollToTopEventFlow, + onHandleDeepLink = uiViewModel::handleDeepLink, + onNavigateToConnections = { multiBackstack.navigateTopLevel(TopLevelDestination.Connect.route) }, + ) + contactsGraph( + backStack = backStack, + scrollToTopEvents = uiViewModel.scrollToTopEventFlow, + onHandleDeepLink = uiViewModel::handleDeepLink, + ) + settingsGraph(backStack, settingsRadioConfigViewModel) + channelsGraph(backStack) + connectionsGraph(backStack) + + // Defensive fallback for MapRoute.Map, reachable via DeepLinkRouter's "map" URI mapping regardless of platform + // (see core:navigation's DeepLinkRouter.kt) even though feature:map isn't a dependency here. Renders nothing, + // matching feature:settings' identical TakModuleConfigContent fallback for a deep-link-reachable destination + // this platform can't serve — an honest no-op, not a crash. + entry {} +} + +/** v0's visible top-level tabs: every [TopLevelDestination] except Map (no `mapGraph` entry provider above). */ +val webVisibleDestinations: List = + TopLevelDestination.entries.filter { it != TopLevelDestination.Map } diff --git a/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/radio/WebMessageQueue.kt b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/radio/WebMessageQueue.kt new file mode 100644 index 0000000000..f66daa2e65 --- /dev/null +++ b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/radio/WebMessageQueue.kt @@ -0,0 +1,64 @@ +/* + * 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.web.radio + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.MessageStatus +import org.meshtastic.core.repository.MessageQueue +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.PersistedPacketId +import org.meshtastic.core.repository.RadioController + +/** + * Web implementation of [MessageQueue] — identical shape to `desktopApp`'s `DesktopMessageQueue` (both are + * single-process, in-tab hosts with no background delivery service to hand off to): send immediately if connected, + * otherwise leave the packet queued for `MeshConnectionManager` to retry once a connection re-establishes. + */ +class WebMessageQueue( + private val packetRepository: PacketRepository, + private val radioController: RadioController, + dispatchers: CoroutineDispatchers, +) : MessageQueue { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.io) + + override suspend fun enqueue(persistedId: PersistedPacketId) { + scope.launch { + if (persistedId.uuid <= 0L) return@launch + + if (radioController.connectionState.value != ConnectionState.Connected) { + return@launch + } + + val claimed = packetRepository.claimQueuedPacket(persistedId) ?: return@launch + if (claimed.packet.status != MessageStatus.QUEUED) return@launch + + try { + radioController.sendMessage(claimed.packet) + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.w(e) { "Failed to send packet ${claimed.packet.id}, re-queuing" } + packetRepository.rollbackEnroutePacket(claimed.id) + if (e is CancellationException) throw e + } + } + } +} diff --git a/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/stub/NoopStubs.kt b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/stub/NoopStubs.kt new file mode 100644 index 0000000000..5aed28d4d4 --- /dev/null +++ b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/stub/NoopStubs.kt @@ -0,0 +1,191 @@ +/* + * 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 . + */ +@file:Suppress("EmptyFunctionBlock", "TooManyFunctions") + +package org.meshtastic.web.stub + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.Node +import org.meshtastic.core.network.repository.MQTTRepository +import org.meshtastic.core.repository.AppWidgetUpdater +import org.meshtastic.core.repository.DataPair +import org.meshtastic.core.repository.Location +import org.meshtastic.core.repository.LocationRepository +import org.meshtastic.core.repository.MeshLocationManager +import org.meshtastic.core.repository.MeshNotificationManager +import org.meshtastic.core.repository.MeshWorkerManager +import org.meshtastic.core.repository.Notification +import org.meshtastic.core.repository.NotificationManager +import org.meshtastic.core.repository.PersistedPacketId +import org.meshtastic.core.repository.PlatformAnalytics +import org.meshtastic.feature.node.compass.CompassHeadingProvider +import org.meshtastic.feature.node.compass.HeadingState +import org.meshtastic.feature.node.compass.MagneticFieldProvider +import org.meshtastic.feature.node.compass.PhoneLocationProvider +import org.meshtastic.feature.node.compass.PhoneLocationState +import org.meshtastic.proto.ClientNotification +import org.meshtastic.proto.MqttClientProxyMessage +import org.meshtastic.proto.Telemetry +import org.meshtastic.mqtt.ConnectionState as MqttConnectionState + +/** + * No-op stub implementations for platform-specific interfaces with no commonMain implementation, or that this v0 pass + * deliberately defers on web. Mirrors desktopApp's own `stub/NoopStubs.kt`/`CompassStubs.kt` — same interfaces, same + * "no sensor/OS integration on this platform" reasoning, just no OS calls to make since web has none of these either. + */ +private const val TAG = "WebNoopStub" + +// region Notification stubs — browser Notification API exists but is deliberately deferred (permission prompts, +// service-worker plumbing); real integration is a future pass, not this v0 slice. + +class NoopNotificationManager : NotificationManager { + override suspend fun dispatch(notification: Notification): Boolean = false + + override fun cancel(id: Int) {} + + override fun cancelAll() {} +} + +class NoopMeshNotificationManager : MeshNotificationManager { + override fun clearNotifications() {} + + override fun initChannels() {} + + override fun updateServiceStateNotification(state: ConnectionState, telemetry: Telemetry?) {} + + override suspend fun updateMessageNotification( + contactKey: String, + name: String, + message: String, + isBroadcast: Boolean, + channelName: String?, + isSilent: Boolean, + ) {} + + override suspend fun updateWaypointNotification( + contactKey: String, + name: String, + message: String, + waypointId: Int, + isSilent: Boolean, + ) {} + + override suspend fun updateReactionNotification( + contactKey: String, + name: String, + emoji: String, + isBroadcast: Boolean, + channelName: String?, + isSilent: Boolean, + ) {} + + override fun showAlertNotification(contactKey: String, name: String, alert: String) {} + + override fun showNewNodeSeenNotification(node: Node) {} + + override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {} + + override fun showClientNotification(clientNotification: ClientNotification) {} + + override suspend fun cancelMessageNotification(contactKey: String) {} + + override fun cancelLowBatteryNotification(node: Node) {} + + override fun clearClientNotification(notification: ClientNotification) {} +} + +// endregion + +// region Platform / widget / worker stubs (Android-only concepts) + +class NoopPlatformAnalytics : PlatformAnalytics { + override fun track(event: String, vararg properties: DataPair) {} + + override fun setDeviceAttributes(firmwareVersion: String, model: String) {} + + override val isPlatformServicesAvailable: Boolean = false +} + +class NoopAppWidgetUpdater : AppWidgetUpdater { + override suspend fun updateAll() {} +} + +class NoopMeshWorkerManager : MeshWorkerManager { + override fun enqueueSendMessage(persistedId: PersistedPacketId) {} +} + +// endregion + +// region Location stubs — a real implementation would wrap navigator.geolocation; deferred, matching desktop's own +// "no consumer needs real location on web yet" posture. + +class NoopMeshLocationManager : MeshLocationManager { + override fun start( + scope: kotlinx.coroutines.CoroutineScope, + sendPositionFn: suspend (org.meshtastic.proto.Position) -> Unit, + ) {} + + override fun restart() {} + + override fun stop() {} +} + +class NoopLocationRepository : LocationRepository { + override val receivingLocationUpdates = MutableStateFlow(false) + + override fun getLocations(): Flow = emptyFlow() +} + +// endregion + +// region MQTT stub — MQTTRepositoryImpl (core:network, @Single) is already auto-discovered for wasmJs via +// CoreNetworkModule's ComponentScan; overriding it here mirrors desktopApp's own deliberate override (MQTT proxying +// over a phone/desktop's own network stack has no obvious web analogue yet — same deferred posture, not a regression). + +class NoopMQTTRepository : MQTTRepository { + override fun disconnect() {} + + override val proxyMessageFlow: Flow = emptyFlow() + + override fun publish(topic: String, data: ByteArray, retained: Boolean) {} + + override val connectionState = MutableStateFlow(MqttConnectionState.Disconnected.Idle) +} + +// endregion + +// region Compass/GPS stubs — browser has navigator.geolocation but no compass/magnetometer API with broad support; +// deferred, matching desktop's identical "no sensor" posture. + +class NoopCompassHeadingProvider : CompassHeadingProvider { + override fun headingUpdates(): Flow = flowOf(HeadingState(hasSensor = false)) +} + +class NoopPhoneLocationProvider : PhoneLocationProvider { + override fun locationUpdates(): Flow = + flowOf(PhoneLocationState(permissionGranted = false, providerEnabled = false)) +} + +class NoopMagneticFieldProvider : MagneticFieldProvider { + override fun getDeclination(latitude: Double, longitude: Double, altitude: Double, timeMillis: Long): Float = 0f +} + +// endregion diff --git a/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/ui/WebMainScreen.kt b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/ui/WebMainScreen.kt new file mode 100644 index 0000000000..6508cef0e9 --- /dev/null +++ b/webApp/src/wasmJsMain/kotlin/org/meshtastic/web/ui/WebMainScreen.kt @@ -0,0 +1,71 @@ +/* + * 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.web.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import org.meshtastic.core.navigation.MultiBackstack +import org.meshtastic.core.ui.component.MeshtasticAppShell +import org.meshtastic.core.ui.component.MeshtasticNavDisplay +import org.meshtastic.core.ui.component.MeshtasticNavigationSuite +import org.meshtastic.core.ui.viewmodel.UIViewModel +import org.meshtastic.feature.settings.navigation.rememberSettingsRadioConfigViewModelProvider +import org.meshtastic.web.navigation.webNavGraph +import org.meshtastic.web.navigation.webVisibleDestinations + +/** + * Web main screen — same assembly as `desktopApp`'s `DesktopMainScreen` (shared + * [MeshtasticAppShell] + [MeshtasticNavigationSuite] + [MeshtasticNavDisplay]), wired to [webNavGraph] instead of + * `desktopNavGraph` and [webVisibleDestinations] instead of the full + * [org.meshtastic.core.navigation.TopLevelDestination] set (no Map tab — v0 scope, AC9). + */ +@Suppress("ViewModelForwarding") +@Composable +fun WebMainScreen(uiViewModel: UIViewModel, multiBackstack: MultiBackstack, modifier: Modifier = Modifier) { + val backStack = multiBackstack.activeBackStack + val settingsRadioConfigViewModelProvider = rememberSettingsRadioConfigViewModelProvider(backStack) + + Surface(modifier = modifier.fillMaxSize()) { + MeshtasticAppShell(multiBackstack = multiBackstack, uiViewModel = uiViewModel) { + MeshtasticNavigationSuite( + multiBackstack = multiBackstack, + uiViewModel = uiViewModel, + modifier = Modifier.fillMaxSize(), + visibleDestinations = webVisibleDestinations, + ) { + val provider = + entryProvider { + webNavGraph( + backStack = backStack, + uiViewModel = uiViewModel, + multiBackstack = multiBackstack, + settingsRadioConfigViewModel = settingsRadioConfigViewModelProvider, + ) + } + MeshtasticNavDisplay( + multiBackstack = multiBackstack, + entryProvider = provider, + modifier = Modifier.fillMaxSize(), + ) + } + } + } +} diff --git a/webApp/src/wasmJsMain/resources/index.html b/webApp/src/wasmJsMain/resources/index.html new file mode 100644 index 0000000000..20a0cee2a9 --- /dev/null +++ b/webApp/src/wasmJsMain/resources/index.html @@ -0,0 +1,29 @@ + + + + + + Meshtastic + + + + + +
+ + diff --git a/webApp/webpack.config.d/coop-coep-headers.js b/webApp/webpack.config.d/coop-coep-headers.js new file mode 100644 index 0000000000..170c683ca5 --- /dev/null +++ b/webApp/webpack.config.d/coop-coep-headers.js @@ -0,0 +1,12 @@ +// core:database's OPFS-backed SQLite driver (WebWorkerSQLiteDriver) needs a cross-origin-isolated page — verified +// empirically in this effort's Step 0b spike (danysantiago/room-web-demo), which required serving with these two +// headers for the OPFS path to work at all. `config.devServer` is undefined during the production webpack build +// (wasmJsBrowserDistribution) — guard it, don't set unconditionally, or that build crashes (the exact bug this +// spike found and fixed upstream). +if (config.devServer) { + config.devServer.headers = { + ...(config.devServer.headers || {}), + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + }; +}