app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 6a306a7b0c65a9eb5d76d0e1661e12f9a973468f
parent 04d8cf8038c9da70152fab240eada9a7724fc397
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 21:22:45 +0000

network: distinguish relay configuration from observation

- Model relay read and write capabilities as configured or not configured.
- Keep aggregate connection observation independent from endpoint setup.
- Rename overview evidence to configured endpoint counts.
- Remove invented pending-operation evidence and expose empty configuration.

Diffstat:
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreen.kt | 39+++++++++++++++++++++++++++------------
Mapp/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkModelTest.kt | 26+++++++++++++++++++++++---
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreenTest.kt | 10+++++-----
3 files changed, 55 insertions(+), 20 deletions(-)

diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreen.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreen.kt @@ -22,11 +22,13 @@ enum class NetworkIdentityState { ReadOnly, Active, CredentialUnavailable, Avail enum class RelayObservationState { NotYetObserved, Available, Degraded, Unavailable } +enum class RelayCapability { Configured, NotConfigured } + data class NetworkRelayModel( val url: String, val destination: RelayDestination, - val read: Boolean, - val write: Boolean, + val readCapability: RelayCapability, + val writeCapability: RelayCapability, ) data class FoundationNetworkModel( @@ -37,7 +39,6 @@ data class FoundationNetworkModel( val relays: List<NetworkRelayModel>, val runtimeState: ApplicationLifecycle, val runtimeProblem: String?, - val pendingOperations: Int, ) fun foundationNetworkModel(state: HarvestCircleShellState): FoundationNetworkModel { @@ -61,8 +62,8 @@ fun foundationNetworkModel(state: HarvestCircleShellState): FoundationNetworkMod NetworkRelayModel( url = relay.url, destination = relay.destination, - read = relay.read, - write = relay.write, + readCapability = relay.read.toCapability(), + writeCapability = relay.write.toCapability(), ) }, runtimeState = snapshot.lifecycle, @@ -71,7 +72,6 @@ fun foundationNetworkModel(state: HarvestCircleShellState): FoundationNetworkMod ?: snapshot.sessionProblem?.safeMessage ?: snapshot.recoverableProblem?.safeMessage ?: state.identity.problem, - pendingOperations = if (state.identity.busy) 1 else 0, ) } @@ -122,10 +122,15 @@ private fun NetworkDetail( when (selection.value) { "overview" -> { Fact("Signer", model.identityState.label()) - Fact("Public relay reads", model.relays.count(NetworkRelayModel::read).toString()) - Fact("Public relay writes", model.relays.count(NetworkRelayModel::write).toString()) + Fact( + "Configured read endpoints", + model.relays.count { it.readCapability == RelayCapability.Configured }.toString(), + ) + Fact( + "Configured write endpoints", + model.relays.count { it.writeCapability == RelayCapability.Configured }.toString(), + ) Fact("Local runtime", model.runtimeState.label()) - Fact("Pending operations", model.pendingOperations.toString()) BasicText("No managed HarvestCircle service is configured.") } "identity" -> { @@ -139,18 +144,20 @@ private fun NetworkDetail( } "public_relays" -> { BasicText(model.relayState.label(), Modifier.testTag("network-relay-state")) + if (model.relays.isEmpty()) { + BasicText("No public relay endpoints are configured.", Modifier.testTag("network-relays-empty")) + } model.relays.forEach { relay -> Column(Modifier.testTag("network-relay:${relay.url}")) { BasicText(relay.url) BasicText(relay.destination.label()) - BasicText(if (relay.read) "Read available" else "Read unavailable") - BasicText(if (relay.write) "Write available" else "Write unavailable") + BasicText(relay.readCapability.label("Read")) + BasicText(relay.writeCapability.label("Write")) } } } "runtime" -> { Fact("Local runtime", model.runtimeState.label()) - Fact("Pending operations", model.pendingOperations.toString()) model.runtimeProblem?.let { BasicText(it, Modifier.testTag("network-runtime-problem")) } } } @@ -195,6 +202,14 @@ private fun RelayObservationState.label(): String = private fun RelayDestination.label(): String = name +private fun Boolean.toCapability(): RelayCapability = if (this) RelayCapability.Configured else RelayCapability.NotConfigured + +private fun RelayCapability.label(direction: String): String = + when (this) { + RelayCapability.Configured -> "$direction configured" + RelayCapability.NotConfigured -> "$direction not configured" + } + private fun ApplicationLifecycle.label(): String = when (this) { ApplicationLifecycle.Ready -> "Available" diff --git a/app/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkModelTest.kt b/app/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkModelTest.kt @@ -37,11 +37,10 @@ class FoundationNetworkModelTest { assertEquals(null, model.profileLabel) assertEquals(RelayObservationState.Degraded, model.relayState) assertEquals(RelayDestination.Public, model.relays.single().destination) - assertEquals(true, model.relays.single().read) - assertEquals(false, model.relays.single().write) + assertEquals(RelayCapability.Configured, model.relays.single().readCapability) + assertEquals(RelayCapability.NotConfigured, model.relays.single().writeCapability) assertEquals(ApplicationLifecycle.Degraded, model.runtimeState) assertEquals("Runtime degraded.", model.runtimeProblem) - assertEquals(1, model.pendingOperations) } @Test @@ -50,6 +49,27 @@ class FoundationNetworkModelTest { assertEquals(NetworkIdentityState.ReadOnly, model.identityState) assertEquals(RelayObservationState.NotYetObserved, model.relayState) } + + @Test + fun unavailableObservationPreservesConfiguredCapabilities() { + val state = + shellState().let { shell -> + val active = requireNotNull(shell.identity.snapshot.activeIdentity) + shell.copy( + identity = + shell.identity.copy( + snapshot = + shell.identity.snapshot.copy( + activeIdentity = active.copy(relays = active.relays.copy(state = RelayConnectionState.Error)), + ), + ), + ) + } + val model = foundationNetworkModel(state) + assertEquals(RelayObservationState.Unavailable, model.relayState) + assertEquals(RelayCapability.Configured, model.relays.single().readCapability) + assertEquals(RelayCapability.NotConfigured, model.relays.single().writeCapability) + } } private fun shellState( diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreenTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreenTest.kt @@ -45,8 +45,8 @@ class FoundationNetworkScreenTest { onNodeWithTag("network-tab-public_relays").performClick() onNodeWithText("wss://relay.example").assertExists() onNodeWithText("Public").assertExists() - onNodeWithText("Read available").assertExists() - onNodeWithText("Write unavailable").assertExists() + onNodeWithText("Read configured").assertExists() + onNodeWithText("Write not configured").assertExists() onNodeWithText("Degraded").assertExists() onNodeWithTag("network-tab-runtime").performClick() onNodeWithText("Storage is temporarily unavailable.").assertExists() @@ -59,6 +59,7 @@ class FoundationNetworkScreenTest { onNodeWithText("Read-only").assertExists() onNodeWithTag("network-tab-public_relays").performClick() onNodeWithText("Not yet observed").assertExists() + onNodeWithText("No public relay endpoints are configured.").assertExists() } } @@ -69,8 +70,8 @@ private fun model( NetworkRelayModel( url = "wss://relay.example", destination = RelayDestination.Public, - read = true, - write = false, + readCapability = RelayCapability.Configured, + writeCapability = RelayCapability.NotConfigured, ), ), ) = FoundationNetworkModel( @@ -81,5 +82,4 @@ private fun model( relays = relays, runtimeState = ApplicationLifecycle.Degraded, runtimeProblem = "Storage is temporarily unavailable.", - pendingOperations = 1, )