From 8f79d429dee9a6d4910f7ec3f67527d90e0c51f8 Mon Sep 17 00:00:00 2001 From: John Onysko Date: Tue, 9 Jun 2026 17:50:21 -0400 Subject: [PATCH] fix(seeder): skip catalog sync for installed apps on diverged host ports The catalog sync added in the supply depot work overwrites container_config and ui_location for any curated service not flagged is_user_modified. Apps installed on an alternate host port before that flag existed (e.g. because the catalog default port was already taken on the host) have their record reset to the catalog port on the next boot, desyncing it from the running container and breaking the app's open link. Compare the published host ports of the existing record against the catalog and leave the row alone when an installed app diverges. Only host ports are compared, so catalog corrections to internal container ports (Meshtastic 80->8080) and ui_location scheme changes (Vaultwarden https:8480) still reach existing installs. --- admin/database/seeders/service_seeder.ts | 53 ++++++++++++++++++++---- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/admin/database/seeders/service_seeder.ts b/admin/database/seeders/service_seeder.ts index ae7eac89..2ae33a1a 100644 --- a/admin/database/seeders/service_seeder.ts +++ b/admin/database/seeders/service_seeder.ts @@ -517,6 +517,8 @@ export default class ServiceSeeder extends BaseSeeder { 'service_name', 'is_custom', 'is_user_modified', + 'installed', + 'container_config', ]) const existingServiceMap = new Map(existingServices.map((s) => [s.service_name, s])) @@ -536,14 +538,51 @@ export default class ServiceSeeder extends BaseSeeder { for (const service of ServiceSeeder.DEFAULT_SERVICES) { const existing = existingServiceMap.get(service.service_name) if (existing && !existing.is_custom && !existing.is_user_modified) { - await Service.query().where('service_name', service.service_name).update({ - container_config: service.container_config, - container_command: service.container_command ?? null, - metadata: (service as any).metadata ?? null, - category: service.category, - ui_location: service.ui_location, - }) + // An installed app whose published host ports differ from the catalog was deployed on + // an alternate port (e.g. the default was already taken on that host) before the + // is_user_modified flag could record it. Adopting the catalog values would desync the + // record from the running container and break the app's link, so leave the row alone. + // Only host ports are compared: catalog corrections to internal container ports or the + // ui_location scheme still sync. + if ( + existing.installed && + hostPortsDiffer(existing.container_config, service.container_config) + ) { + continue + } + await Service.query() + .where('service_name', service.service_name) + .update({ + container_config: service.container_config, + container_command: service.container_command ?? null, + metadata: (service as any).metadata ?? null, + category: service.category, + ui_location: service.ui_location, + }) } } } } + +/** Whether two serialized container configs publish a different set of host ports. */ +function hostPortsDiffer(existingConfig: string | null, catalogConfig: string | null): boolean { + const hostPorts = (raw: string | null): string | null => { + if (!raw) return null + try { + const bindings = JSON.parse(raw)?.HostConfig?.PortBindings ?? {} + return Object.values(bindings) + .flat() + .map((b: any) => b?.HostPort) + .filter(Boolean) + .sort() + .join(',') + } catch { + return null + } + } + const existing = hostPorts(existingConfig) + const catalog = hostPorts(catalogConfig) + // If either side is absent or unparseable, divergence can't be established — let the sync run. + if (existing === null || catalog === null) return false + return existing !== catalog +}