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:
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,
)