feat(apps): backend-only services classify as services with no Launch button
Demo images / Build & push demo images (push) Has been cancelled
Demo images / Build & push demo images (push) Has been cancelled
A published port no longer implies a web UI. The package scanner used to synthesize interfaces.main.ui="true" for any container with a port or onion address, so headless backends — including self-deployed compose stacks like podsteadr — showed up as launchable apps. New ui_detection module decides instead: a manifest interfaces declaration (catalog overlay first, disk second) is definitive; undeclared apps get a short HTTP probe of the launch port (HTML page, redirect, or browser auth wall = UI; JSON APIs, raw TCP, dead ports = service), with cached verdicts and probes gated on running containers. Frontend canLaunch now refuses curated services outright and only treats a bare runtime address as launchable for curated known apps. Works identically for manifest apps and containers deployed by hand outside the orchestrator. ui_detection tests 6/6, frontend suite 696/696. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d7c5d39747
commit
da14c135e4
@@ -98,6 +98,36 @@ describe('appsConfig service filtering', () => {
|
||||
expect(isWebsitePackage('some-ui-app', uiApp)).toBe(false)
|
||||
})
|
||||
|
||||
it('never offers Launch for an unknown container with a bare exposed port', () => {
|
||||
// A self-deployed compose stack (e.g. podsteadr) publishes a port, so it
|
||||
// has a runtime lan-address — but no manifest-declared or probed UI. It
|
||||
// must classify as a service and must NOT get a Launch button.
|
||||
const selfDeployed = makePkg('podsteadr', 'podsteadr', 'other')
|
||||
selfDeployed.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:8095' } }, status: 'running' } as unknown as PackageDataEntry['installed']
|
||||
expect(isWebsitePackage('podsteadr', selfDeployed)).toBe(true)
|
||||
expect(canLaunch(selfDeployed)).toBe(false)
|
||||
})
|
||||
|
||||
it('offers Launch for an unknown container once the backend confirms a UI', () => {
|
||||
const confirmedUi = makePkg('podsteadr', 'podsteadr', 'other')
|
||||
;(confirmedUi.manifest as unknown as Record<string, unknown>).interfaces = { main: { ui: 'true' } }
|
||||
confirmedUi.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:8095' } }, status: 'running' } as unknown as PackageDataEntry['installed']
|
||||
expect(canLaunch(confirmedUi)).toBe(true)
|
||||
})
|
||||
|
||||
it('never offers Launch for curated service containers even with a UI flag', () => {
|
||||
const service = makePkg('indeedhub-api', 'IndeeHub API', 'media')
|
||||
;(service.manifest as unknown as Record<string, unknown>).interfaces = { main: { ui: 'true' } }
|
||||
service.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:9100' } }, status: 'running' } as unknown as PackageDataEntry['installed']
|
||||
expect(canLaunch(service)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps Launch for curated apps that rely on a runtime address alone', () => {
|
||||
const known = makePkg('jellyfin', 'Jellyfin', 'media')
|
||||
known.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:8096' } }, status: 'running' } as unknown as PackageDataEntry['installed']
|
||||
expect(canLaunch(known)).toBe(true)
|
||||
})
|
||||
|
||||
it('explains that Fedimint waits for Bitcoin sync before Guardian starts', () => {
|
||||
const pkg = makePkg('fedimint', 'Fedimint', 'money')
|
||||
pkg.state = PackageState.Starting
|
||||
|
||||
@@ -255,9 +255,17 @@ export function resolveAppIcon(id: string, pkg: PackageDataEntry, curatedIcon?:
|
||||
|
||||
export function canLaunch(pkg: PackageDataEntry): boolean {
|
||||
if (isWebOnlyApp(pkg.manifest.id)) return true
|
||||
// Headless backends never get a Launch button, even with a published port.
|
||||
if (isServicePackage(pkg.manifest.id, pkg)) return false
|
||||
const hasRuntimeAddress = !!pkg.installed?.['interface-addresses']?.main?.['lan-address']
|
||||
const hasKnownLaunchUrl = typeof window !== 'undefined' && !!resolveAppUrl(pkg.manifest.id)
|
||||
const hasUI = pkg.manifest.interfaces?.main?.ui || hasRuntimeAddress || hasKnownLaunchUrl
|
||||
// A bare runtime address is only a launch signal for curated apps: the
|
||||
// backend now sets interfaces.main.ui strictly for confirmed web UIs
|
||||
// (manifest declaration or HTTP probe), so an unknown container with an
|
||||
// exposed non-UI port must not become launchable just for having one.
|
||||
const hasUI = pkg.manifest.interfaces?.main?.ui
|
||||
|| hasKnownLaunchUrl
|
||||
|| (hasRuntimeAddress && isKnownApp(pkg.manifest.id, pkg))
|
||||
if ((pkg.manifest.id === 'fedimint' || pkg.manifest.id === 'fedimintd') && hasUI) {
|
||||
return pkg.state === PackageState.Running || pkg.state === PackageState.Starting
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user