Check companion dashboards and omit headless UI waiting messages
Demo images / Build & push demo images (push) Failing after 37s

This commit is contained in:
archipelago
2026-09-30 13:28:37 -04:00
parent bf56956790
commit c1e20a71ae
5 changed files with 102 additions and 12 deletions
+3
View File
@@ -2,6 +2,9 @@
## Unreleased ## 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. - 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. - Named the app in compact readiness messages and kept app-card actions aligned at the bottom.
@@ -172,13 +172,11 @@ impl DockerPackageScanner {
} else { } else {
// Prefer the known web UI port over arbitrary first binding // Prefer the known web UI port over arbitrary first binding
// (for example Gitea exposes SSH on 2222 before web on 3001). // (for example Gitea exposes SSH on 2222 before web on 3001).
let candidate = if uses_allocated_launch_port(&app_id) { let candidate = package_launch_candidate(
extract_lan_address(&container.ports) &app_id,
.or_else(|| PodmanClient::lan_address_for(&app_id)) &container.ports,
} else { PodmanClient::lan_address_for(&app_id),
PodmanClient::lan_address_for(&app_id) );
.or_else(|| extract_lan_address(&container.ports))
};
reachable_lan_address(&app_id, candidate).await reachable_lan_address(&app_id, candidate).await
}; };
@@ -1105,6 +1103,23 @@ fn companion_lan_address(app_id: &str) -> Option<String> {
} }
} }
/// 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<String>,
) -> Option<String> {
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 { fn uses_allocated_launch_port(app_id: &str) -> bool {
matches!( matches!(
app_id, app_id,
@@ -1189,7 +1204,42 @@ mod tor_service_name_tests {
#[cfg(test)] #[cfg(test)]
mod extract_lan_address_tests { 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] #[test]
fn skips_ssh_port_when_web_port_is_published() { fn skips_ssh_port_when_web_port_is_published() {
+22
View File
@@ -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 `/tmp/archy-runtime-order-live-check.py` to deploy with rollback, remove the
candidate manifest override, and verify five normal management startups and candidate manifest override, and verify five normal management startups and
managed Angor restarts. No new release version/tag, OTA or ISO has been created. 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.
@@ -167,7 +167,7 @@ describe('appsConfig service filtering', () => {
pkg.health = null pkg.health = null
expect(isAppReadyForLaunch(pkg)).toBe(false) expect(isAppReadyForLaunch(pkg)).toBe(false)
expect(canLaunch(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' pkg.health = 'healthy'
expect(canLaunch(pkg)).toBe(true) 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', () => { describe('HTTP readiness independent of container health', () => {
it('blocks fixed launch URLs while the HTTP upstream is unavailable', () => { it('blocks fixed launch URLs while the HTTP upstream is unavailable', () => {
for (const id of ['gitea', 'filebrowser', 'fedimint', 'lnd']) { for (const id of ['gitea', 'filebrowser', 'fedimint', 'lnd']) {
@@ -202,7 +214,7 @@ describe('HTTP readiness independent of container health', () => {
pkg.health = 'healthy' pkg.health = 'healthy'
expect(canLaunch(pkg)).toBe(false) expect(canLaunch(pkg)).toBe(false)
expect(isAppReadyForLaunch(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 pkg['ui-ready'] = true
expect(isAppReadyForLaunch(pkg)).toBe(true) expect(isAppReadyForLaunch(pkg)).toBe(true)
} }
+5 -2
View File
@@ -303,9 +303,12 @@ export function isAppReadyForLaunch(pkg: PackageDataEntry): boolean {
export function launchBlockedReason(id: string, pkg?: PackageDataEntry | null): string { export function launchBlockedReason(id: string, pkg?: PackageDataEntry | null): string {
const appId = pkg?.manifest?.id || id const appId = pkg?.manifest?.id || id
const title = pkg ? resolveAppTitle(appId, pkg) : 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?.['ui-ready'] === false && !isServicePackage(appId, pkg)) {
if (pkg.state === PackageState.Stopped || pkg.state === PackageState.Exited) return 'App is stopped. Start it to open it.' 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 ( if (
(appId === 'fedimint' || appId === 'fedimintd') && (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.' return 'Guardian opens a wait page until Bitcoin finishes initial sync.'
} }
if (pkg && pkg.state === PackageState.Running && !isAppReadyForLaunch(pkg)) { if (pkg && pkg.state === PackageState.Running && !isAppReadyForLaunch(pkg)) {
return `Waiting for ${title}…` return `Waiting for ${title}’s web interface…`
} }
return '' return ''
} }