diff --git a/containers/libreportal/frontend/components/apps/core/js/apps-manager.js b/containers/libreportal/frontend/components/apps/core/js/apps-manager.js index a897ac7..806187b 100755 --- a/containers/libreportal/frontend/components/apps/core/js/apps-manager.js +++ b/containers/libreportal/frontend/components/apps/core/js/apps-manager.js @@ -155,6 +155,22 @@ class AppsManager { } catch (e) { console.error('post-uninstall task cleanup failed:', e); } } + // Instance family membership, read BEFORE the reload. `libreportal + // instance remove` deletes the instance's definition outright, so once + // apps.json is refreshed there is nothing left to say which type it + // belonged to — and the page on screen may be a SIBLING whose Instances + // bar lists the app that just came or went. + const _currentAppPreReload = decodeURIComponent((window.location.pathname.match(/^\/app\/([^/?]+)/) || [])[1] || '') + || new URL(window.location.href).searchParams.get('app') || ''; + const familyOf = (slug) => { + if (!slug) return ''; + const a = (window.apps || []).find(x => (x.command || '').split(' ').pop() === slug); + const cfg = (a && a.config) || {}; + return cfg[`CFG_${String(slug).toUpperCase()}_INSTANCE_OF`] || slug; + }; + const completedFamilyPre = familyOf(appName); + const currentFamily = familyOf(_currentAppPreReload); + this.clearCache(); await this.reloadAppsData(); if (window.serviceButtons) { @@ -166,11 +182,36 @@ class AppsManager { const pathname = window.location.pathname; const isAppsPage = pathname === '/apps' || pathname.startsWith('/apps/'); const isAppDetailPage = pathname === '/app' || pathname.startsWith('/app/'); + const stillListed = (slug) => !!slug && (window.apps || []).some(a => (a.command || '').split(' ').pop() === slug); + // A CREATED instance only gains its INSTANCE_OF after the reload, a + // REMOVED one only had it before — take whichever read still has data. + const completedFamily = stillListed(appName) ? familyOf(appName) : completedFamilyPre; + + // The app this page is showing can be GONE: an instance removal deletes + // it, where a plain uninstall only flips it to not-installed. Rendering + // it would print "App not found", so land on the type it was an instance + // of — its Instances bar is where the removal reads as a result. + if (isAppDetailPage && currentAppFromUrl === appName && !stillListed(appName)) { + const dest = (completedFamily !== appName && stillListed(completedFamily)) + ? (typeof window.appPath === 'function' ? window.appPath(completedFamily, 'config') : `/app/${completedFamily}/config`) + : '/apps'; + if (window.librePortalSPA?.navigateTo) window.librePortalSPA.navigateTo(dest); + else if (window.navigateToRoute) window.navigateToRoute(dest.replace(/^\//, '')); + else window.location.href = dest; + if (typeof window.renderInstalledApps === 'function') window.renderInstalledApps(); + return; + } if (isAppsPage && !isAppDetailPage) { const category = window.appsCategory || 'all'; this.renderApps(category); - } else if (isAppDetailPage && currentAppFromUrl === appName) { + } else if (isAppDetailPage && currentAppFromUrl + && (currentAppFromUrl === appName + || (currentFamily && currentFamily === completedFamily))) { + // Repaint the app ON SCREEN, which is not always the one the task ran + // for: a sibling instance created or removed changes this page's + // Instances bar and nothing else would redraw it. + // // Defer + isolate the heavy re-render so a throw inside // displayConfigForm / port-manager init can't lock up the // post-task UI cleanup. Fires on the next tick — gives the @@ -178,7 +219,7 @@ class AppsManager { setTimeout(() => { // _skipReload flag tells renderAppDetail not to re-fetch // apps.json again (we already just did, line above). - this.renderAppDetail(appName, null, true, { skipReload: true }) + this.renderAppDetail(currentAppFromUrl, null, true, { skipReload: true }) .catch(err => console.error('renderAppDetail failed:', err)); }, 0); }