From 09d5639ab994186d2bc00ece86122db5ab67cb1a Mon Sep 17 00:00:00 2001 From: 1dabread Date: Tue, 11 Aug 2026 13:13:23 -0500 Subject: [PATCH] fixed app location --- admin/app/controllers/system_controller.ts | 8 ++- admin/app/services/docker_service.ts | 20 ++++++++ admin/app/services/system_service.ts | 50 +++++++++++++++++++ admin/inertia/components/ExistingAppModal.tsx | 4 +- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/admin/app/controllers/system_controller.ts b/admin/app/controllers/system_controller.ts index 9b211cf..a06cdd1 100644 --- a/admin/app/controllers/system_controller.ts +++ b/admin/app/controllers/system_controller.ts @@ -470,19 +470,23 @@ export default class SystemController { message: `Docker container ${payload.container_name} not found.`, }) } + const publishedHostPort = DockerService.getFirstPublishedHostPort(inspect) await Service.create({ service_name: payload.container_name, friendly_name: payload.friendly_name, container_image: inspect.Config?.Image || '', container_config: null, - ui_location: payload.container_name, + // Published existing apps are launchable from the Command Center. Containers without + // a published host port remain manageable in Supply Depot but do not get a dead tile. + ui_location: publishedHostPort, icon: payload.icon || 'IconBrandDocker', installed: true, installation_status: 'idle', is_dependency_service: false, is_custom: true, category: payload.category ?? 'custom', + display_order: publishedHostPort ? 49 : null, depends_on: null, }) @@ -845,4 +849,4 @@ export default class SystemController { cpus: hostConfig.NanoCpus ? hostConfig.NanoCpus / 1e9 : undefined, } } -} \ No newline at end of file +} diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index 52ad918..7f59f09 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -2040,6 +2040,26 @@ export class DockerService { return container.inspect() } + /** + * Return the first host port published by a Docker container inspect payload. + * Existing apps are already running, so their launch target comes from Docker's + * active port bindings rather than NOMAD's generated container config. + */ + static getFirstPublishedHostPort(inspect: any): string | null { + const ports = inspect?.NetworkSettings?.Ports ?? {} + const bindings = Object.values(ports).flat() as Array<{ + HostIp?: string + HostPort?: string + } | null> + const published = bindings + .filter((binding): binding is { HostIp?: string; HostPort: string } => + Boolean(binding?.HostPort) + ) + .sort((a, b) => Number.parseInt(a.HostPort, 10) - Number.parseInt(b.HostPort, 10)) + + return published[0]?.HostPort ?? null + } + /** * Decode the multiplexed stream Docker returns for non-TTY container logs. Each frame is an * 8-byte header ([streamType, 0,0,0, big-endian payloadSize]) followed by the payload. diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index 47a950f..d01f27a 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -319,6 +319,7 @@ export class SystemService { async getServices({ installedOnly = true }: { installedOnly?: boolean }): Promise { const statuses = await this._syncContainersWithDatabase() // Sync and reuse the fetched status list + await this._syncExistingPublishedAppLinks() const query = Service.query() .orderBy('display_order', 'asc') @@ -388,6 +389,55 @@ export class SystemService { return toReturn } + /** + * Backfill launch metadata for existing Docker containers added before published ports were + * detected. A published existing app gets a Command Center link and a pre-system sort order; + * unpublished containers stay manageable in Supply Depot without a dead dashboard tile. + */ + private async _syncExistingPublishedAppLinks(): Promise { + try { + const existingApps = await Service.query() + .where('installed', true) + .where('is_custom', true) + .where('is_dependency_service', false) + .whereNull('container_config') + + for (const service of existingApps) { + const inspect = await this.dockerService.inspectContainerByName(service.service_name) + if (!inspect) continue + + const publishedHostPort = DockerService.getFirstPublishedHostPort(inspect) + let changed = false + + if (publishedHostPort && service.ui_location !== publishedHostPort) { + service.ui_location = publishedHostPort + changed = true + } + if ( + publishedHostPort && + (service.display_order === null || service.display_order >= 50) + ) { + service.display_order = 49 + changed = true + } + if (!publishedHostPort && service.ui_location === service.service_name) { + service.ui_location = null + changed = true + } + + if (changed) { + await service.save() + } + } + } catch (error) { + logger.warn( + `[SystemService] Existing app launch metadata sync failed: ${ + error instanceof Error ? error.message : error + }` + ) + } + } + static getAppVersion(): string { try { if (this.appVersion) { diff --git a/admin/inertia/components/ExistingAppModal.tsx b/admin/inertia/components/ExistingAppModal.tsx index 3757030..a50d9cc 100644 --- a/admin/inertia/components/ExistingAppModal.tsx +++ b/admin/inertia/components/ExistingAppModal.tsx @@ -153,8 +153,8 @@ export default function ExistingAppModal({

- Add an existing Docker container by its name so it appears in the Supply Depot and - on the home dashboard. + Add an existing Docker container by its name so it appears in the Supply Depot. + Published containers also appear on the home dashboard.