From 96943fb0d1bdb6da57e7e8c740f3513b1d00416b Mon Sep 17 00:00:00 2001 From: mstrofnone Date: Sun, 14 Jun 2026 08:01:59 +1000 Subject: [PATCH] desktop(namecoin): port DiagnosticCard from Android settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the Namecoin diagnostics card that Android renders below the per-server test results to the desktop settings panel, so support requests carry the same information on both platforms. - last test timestamp + pass/fail tally - host OS (name/version/arch) — desktop equivalent of Android's Build.MANUFACTURER/MODEL row - JVM name + version — desktop equivalent of Android's API level row - distinct TLS versions observed during the test run Also adds a 'Testing next server...' inline progress row matching the Android section's behaviour when the test loop is mid-run. Pure UI addition; no model, preference, or callback changes. --- .../ui/settings/NamecoinSettingsSection.kt | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/settings/NamecoinSettingsSection.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/settings/NamecoinSettingsSection.kt index ab82d59f3a..d9acd9c073 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/settings/NamecoinSettingsSection.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/settings/NamecoinSettingsSection.kt @@ -77,6 +77,10 @@ import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinCoreRpcConf import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.RpcProbeResult import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ServerTestResult import kotlinx.coroutines.launch +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle import java.util.Locale /** @@ -480,6 +484,7 @@ private fun NamecoinTestConnectionSection( val scope = rememberCoroutineScope() var isTesting by remember { mutableStateOf(false) } var testResults by remember { mutableStateOf>(emptyList()) } + var lastTestTimestamp by remember { mutableStateOf(null) } var pendingCerts by remember { mutableStateOf>(emptyList()) } var confirmingCert by remember { mutableStateOf(null) } @@ -561,6 +566,7 @@ private fun NamecoinTestConnectionSection( ) } } + lastTestTimestamp = System.currentTimeMillis() isTesting = false if (newCerts.isNotEmpty()) { pendingCerts = newCerts @@ -596,7 +602,156 @@ private fun NamecoinTestConnectionSection( testResults.forEach { result -> NamecoinServerTestResultRow(result) } + + if (isTesting && testResults.size < servers.size) { + Row( + modifier = Modifier.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 2.dp, + ) + Spacer(Modifier.width(8.dp)) + Text( + "Testing next server\u2026", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } } + + // ── Diagnostic card ──────────────────────────────────── + // Mirrors the Android section's diagnostic block so support + // requests carry the same fields on both platforms. + if (testResults.isNotEmpty() || lastTestTimestamp != null) { + Spacer(Modifier.height(16.dp)) + NamecoinDiagnosticCard( + testResults = testResults, + lastTestTimestamp = lastTestTimestamp, + ) + } + } +} + +// ── Diagnostic Card ───────────────────────────────────────────────── + +private val DIAGNOSTIC_TIMESTAMP_FORMATTER = + DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.MEDIUM) + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()) + +@Composable +private fun NamecoinDiagnosticCard( + testResults: List, + lastTestTimestamp: Long?, +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + ), + shape = RoundedCornerShape(8.dp), + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text( + "Diagnostics", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(8.dp)) + + // Last test timestamp + pass/fail tally + if (lastTestTimestamp != null) { + val formatted = + remember(lastTestTimestamp) { + DIAGNOSTIC_TIMESTAMP_FORMATTER.format( + Instant.ofEpochMilli(lastTestTimestamp), + ) + } + val successCount = testResults.count { it.success } + val totalCount = testResults.size + NamecoinDiagnosticRow( + label = "Last test", + value = "$formatted ($successCount/$totalCount OK)", + ) + } else { + NamecoinDiagnosticRow( + label = "Last test", + value = "\u2014", + ) + } + + Spacer(Modifier.height(4.dp)) + + // Host OS (desktop equivalent of Android's Build.MANUFACTURER/MODEL row) + val osName = System.getProperty("os.name") ?: "unknown" + val osVersion = System.getProperty("os.version") ?: "" + val osArch = System.getProperty("os.arch") ?: "" + NamecoinDiagnosticRow( + label = "Host", + value = + listOf(osName, osVersion, osArch) + .filter { it.isNotBlank() } + .joinToString(" "), + ) + + Spacer(Modifier.height(4.dp)) + + // JVM info (desktop equivalent of Android's API-level row) + val jvmName = System.getProperty("java.vm.name") ?: "JVM" + val jvmVersion = System.getProperty("java.version") ?: "unknown" + NamecoinDiagnosticRow( + label = "Runtime", + value = "$jvmName $jvmVersion", + ) + + Spacer(Modifier.height(4.dp)) + + // TLS info from test results (identical to Android) + val tlsVersions = + testResults + .mapNotNull { it.tlsVersion } + .distinct() + val tlsDisplay = + if (tlsVersions.isNotEmpty()) { + tlsVersions.joinToString(", ") + } else { + "\u2014" + } + NamecoinDiagnosticRow( + label = "TLS", + value = tlsDisplay, + ) + } + } +} + +@Composable +private fun NamecoinDiagnosticRow( + label: String, + value: String, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(0.35f), + ) + Text( + text = value, + style = MaterialTheme.typography.labelSmall, + fontFamily = FontFamily.Monospace, + modifier = Modifier.weight(0.65f), + ) } }