diff --git a/CHANGELOG.md b/CHANGELOG.md index a64d3da8..baedea20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Checked Bitcoin and Electrum companion dashboards instead of backend protocol ports, preserving dashboard access during initial sync. +- Removed web-interface waiting messages from headless services such as Phoenixd and clarified which interface is unavailable for launchable apps. + - Finished runtime app-file promotion before manifest loading, preventing startup catalog refresh from forgetting disk-only apps. - Named the app in compact readiness messages and kept app-card actions aligned at the bottom. diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index d7bf8894..7422c6d1 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -172,13 +172,11 @@ impl DockerPackageScanner { } else { // Prefer the known web UI port over arbitrary first binding // (for example Gitea exposes SSH on 2222 before web on 3001). - let candidate = if uses_allocated_launch_port(&app_id) { - extract_lan_address(&container.ports) - .or_else(|| PodmanClient::lan_address_for(&app_id)) - } else { - PodmanClient::lan_address_for(&app_id) - .or_else(|| extract_lan_address(&container.ports)) - }; + let candidate = package_launch_candidate( + &app_id, + &container.ports, + PodmanClient::lan_address_for(&app_id), + ); reachable_lan_address(&app_id, candidate).await }; @@ -1105,6 +1103,23 @@ fn companion_lan_address(app_id: &str) -> Option { } } +/// Companion dashboards remain usable while their backend is syncing. Never +/// probe a Bitcoin RPC or Electrum protocol socket as dashboard readiness. +fn package_launch_candidate( + app_id: &str, + ports: &[String], + known: Option, +) -> Option { + if let Some(companion) = companion_lan_address(app_id) { + return Some(companion); + } + if uses_allocated_launch_port(app_id) { + extract_lan_address(ports).or(known) + } else { + known.or_else(|| extract_lan_address(ports)) + } +} + fn uses_allocated_launch_port(app_id: &str) -> bool { matches!( app_id, @@ -1189,7 +1204,42 @@ mod tor_service_name_tests { #[cfg(test)] mod extract_lan_address_tests { - use super::extract_lan_address; + use super::{extract_lan_address, package_launch_candidate}; + + #[test] + fn companion_dashboard_wins_over_backend_protocol_ports() { + for id in ["bitcoin", "bitcoin-core", "bitcoin-knots"] { + assert_eq!( + package_launch_candidate( + id, + &["127.0.0.1:8332->8332/tcp".into()], + Some("http://localhost:8332".into()) + ) + .as_deref(), + Some("http://localhost:8334") + ); + } + for id in ["electrumx", "electrs", "mempool-electrs"] { + assert_eq!( + package_launch_candidate( + id, + &["127.0.0.1:50001->50001/tcp".into()], + Some("http://localhost:50001".into()) + ) + .as_deref(), + Some("http://localhost:50002") + ); + } + assert_eq!( + package_launch_candidate( + "filebrowser", + &["127.0.0.1:19080->80/tcp".into()], + Some("http://localhost:8080".into()) + ) + .as_deref(), + Some("http://localhost:19080") + ); + } #[test] fn skips_ssh_port_when_web_port_is_published() { diff --git a/docs/next-release-20260930.md b/docs/next-release-20260930.md index ae79a744..27eab43d 100644 --- a/docs/next-release-20260930.md +++ b/docs/next-release-20260930.md @@ -174,3 +174,25 @@ The startup-order optimized build is logged in `/tmp/archy-runtime-order-live-check.py` to deploy with rollback, remove the candidate manifest override, and verify five normal management startups and managed Angor restarts. No new release version/tag, OTA or ISO has been created. + +## Mempool and dashboard follow-up + +- Deployed the Mempool alias and runtime-promotion-order backend to dev. Real + server state contains one healthy `mempool`; the stale `mempool-web` record + is gone. Real-data browser checks at 1440/390 pixels found exactly one tile + before and after hard refresh. Bitcoin/LND identities/start times unchanged. +- Removed the candidate manifest override. Normal management startup passed + two full cycles with 62 manifests retained through both initial catalog + refreshes. The next cycle hit a single readiness assertion; a subsequent + read-only check found Angor healthy and the manifest count intact. Remaining + repeat coverage should use bounded polling to distinguish transient request + failures from loss of app definitions; do not report five cycles passed yet. +- Bitcoin Core's dashboard was serving HTTP 200 on 8334 while readiness checked + RPC 8332. Companion URL selection now takes priority over protocol sockets + for Core/Knots and Electrum aliases, with a regression preserving allocated + UI ports for other apps. Backend suite: 1,609 passed, four opt-in ignored. + Optimized build is `/tmp/archy-dashboard-address-build.log`; deployment and + live IBD verification helper: `/tmp/archy-dashboard-address-deploy.py`. +- Phoenixd has no browser UI. Headless services now omit web-readiness messages; + actual browser apps name their web interface rather than waiting for + themselves. Focused 23 UI tests and production build passed; deployed to dev. diff --git a/neode-ui/src/views/apps/__tests__/appsConfig.test.ts b/neode-ui/src/views/apps/__tests__/appsConfig.test.ts index 76489275..f05e1194 100644 --- a/neode-ui/src/views/apps/__tests__/appsConfig.test.ts +++ b/neode-ui/src/views/apps/__tests__/appsConfig.test.ts @@ -167,7 +167,7 @@ describe('appsConfig service filtering', () => { pkg.health = null expect(isAppReadyForLaunch(pkg)).toBe(false) expect(canLaunch(pkg)).toBe(false) - expect(launchBlockedReason(pkg.manifest.id, pkg)).toBe('Waiting for GitWorkshop…') + expect(launchBlockedReason(pkg.manifest.id, pkg)).toBe('Waiting for GitWorkshop’s web interface…') pkg.health = 'healthy' expect(canLaunch(pkg)).toBe(true) }) @@ -194,6 +194,18 @@ describe('appsConfig service filtering', () => { }) }) +describe('headless service readiness messages', () => { + it('does not treat an API-only service as waiting for its own nonexistent UI', () => { + for (const id of ['phoenixd', 'angor-indexer', 'angor-relay', 'custom-api']) { + const pkg = makePkg(id, id, 'other') + pkg['ui-ready'] = false + pkg.health = 'healthy' + expect(canLaunch(pkg)).toBe(false) + expect(launchBlockedReason(id, pkg)).toBe('') + } + }) +}) + describe('HTTP readiness independent of container health', () => { it('blocks fixed launch URLs while the HTTP upstream is unavailable', () => { for (const id of ['gitea', 'filebrowser', 'fedimint', 'lnd']) { @@ -202,7 +214,7 @@ describe('HTTP readiness independent of container health', () => { pkg.health = 'healthy' expect(canLaunch(pkg)).toBe(false) expect(isAppReadyForLaunch(pkg)).toBe(false) - expect(launchBlockedReason(id, pkg)).toBe(`Waiting for ${resolveAppTitle(id, pkg)}…`) + expect(launchBlockedReason(id, pkg)).toBe(`Waiting for ${resolveAppTitle(id, pkg)}’s web interface…`) pkg['ui-ready'] = true expect(isAppReadyForLaunch(pkg)).toBe(true) } diff --git a/neode-ui/src/views/apps/appsConfig.ts b/neode-ui/src/views/apps/appsConfig.ts index eb4e089c..73975d6f 100644 --- a/neode-ui/src/views/apps/appsConfig.ts +++ b/neode-ui/src/views/apps/appsConfig.ts @@ -303,9 +303,12 @@ export function isAppReadyForLaunch(pkg: PackageDataEntry): boolean { export function launchBlockedReason(id: string, pkg?: PackageDataEntry | null): string { const appId = pkg?.manifest?.id || id const title = pkg ? resolveAppTitle(appId, pkg) : id + // API-only services have no web interface to wait for. Their health badge + // describes service availability; a failed HTML probe is not a dependency. + if (pkg && isWebsitePackage(appId, pkg)) return '' if (pkg?.['ui-ready'] === false && !isServicePackage(appId, pkg)) { if (pkg.state === PackageState.Stopped || pkg.state === PackageState.Exited) return 'App is stopped. Start it to open it.' - return `Waiting for ${title}…` + return `Waiting for ${title}’s web interface…` } if ( (appId === 'fedimint' || appId === 'fedimintd') && @@ -314,7 +317,7 @@ export function launchBlockedReason(id: string, pkg?: PackageDataEntry | null): return 'Guardian opens a wait page until Bitcoin finishes initial sync.' } if (pkg && pkg.state === PackageState.Running && !isAppReadyForLaunch(pkg)) { - return `Waiting for ${title}…` + return `Waiting for ${title}’s web interface…` } return '' }