diff --git a/server/shell.ts b/server/shell.ts index ac28b36d..58acb41d 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -871,6 +871,9 @@ export function renderShell(opts: ShellOptions): string { }; } + // Valid module IDs for this space (used to filter out stale/phantom tabs) + const validModuleIds = new Set(moduleList.map(m => m.id)); + // ── Restore tabs from localStorage ── let layers; try { @@ -879,12 +882,15 @@ export function renderShell(opts: ShellOptions): string { if (!Array.isArray(layers)) layers = []; } catch(e) { layers = []; } + // Filter out stale tabs whose moduleId no longer exists + layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId))); + // Ensure the current module is in the tab list if (!layers.find(l => l.moduleId === currentModuleId)) { layers.push(makeLayer(currentModuleId, layers.length)); } - // Persist immediately (includes the newly-added tab) + // Persist immediately (cleaned list) localStorage.setItem(TABS_KEY, JSON.stringify(layers)); // Render all tabs with the current one active @@ -954,7 +960,7 @@ export function renderShell(opts: ShellOptions): string { if (!next.has(mid) && tabCache) tabCache.removePane(mid); } - layers = deduplicateLayers(remoteLayers); + layers = deduplicateLayers(remoteLayers.filter(l => validModuleIds.has(l.moduleId))); // Always ensure the current module stays in the tab list if (currentModuleId && !layers.find(l => l.moduleId === currentModuleId)) { @@ -1001,7 +1007,8 @@ export function renderShell(opts: ShellOptions): string { // Server-authoritative: adopt server tabs exactly. // Only skip tabs the user closed in THIS session (prevents // close-then-immediate-resurrect before save propagates). - var serverTabs = data.tabs.filter(t => !_closedModuleIds.has(t.moduleId)); + var serverTabs = data.tabs.filter(t => validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId)); + serverTabs = deduplicateLayers(serverTabs); serverTabs.forEach((l, i) => { l.order = i; }); layers = serverTabs; // Ensure the currently navigated module is present @@ -1711,6 +1718,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string { if (tabBar) { tabBar.setModules(moduleList); + const validModuleIds = new Set(moduleList.map(m => m.id)); function getModuleLabel(id) { const m = moduleList.find(mod => mod.id === id); return m ? m.name : id; @@ -1718,13 +1726,20 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string { function makeLayer(id, order) { return { id: 'layer-' + id, moduleId: id, label: getModuleLabel(id), order, color: '', visible: true, createdAt: Date.now() }; } + function deduplicateLayers(list) { + const seen = new Set(); + return list.filter(l => { if (seen.has(l.moduleId)) return false; seen.add(l.moduleId); return true; }); + } let layers; try { const saved = localStorage.getItem(TABS_KEY); layers = saved ? JSON.parse(saved) : []; if (!Array.isArray(layers)) layers = []; } catch(e) { layers = []; } + // Filter out stale/phantom tabs and deduplicate + layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId))); if (!layers.find(l => l.moduleId === currentModuleId)) layers.push(makeLayer(currentModuleId, layers.length)); localStorage.setItem(TABS_KEY, JSON.stringify(layers)); tabBar.setLayers(layers); tabBar.setAttribute('active', 'layer-' + currentModuleId); if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId); + const _closedModuleIds = new Set(); function saveTabs() { localStorage.setItem(TABS_KEY, JSON.stringify(layers)); try { @@ -1754,7 +1769,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string { .then(function(r) { return r.ok ? r.json() : null; }) .then(function(data) { if (!data?.tabs || !Array.isArray(data.tabs) || data.tabs.length === 0) { saveTabs(); return; } - layers = data.tabs; + // Filter stale moduleIds and tabs closed this session + layers = deduplicateLayers(data.tabs.filter(function(t) { return validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId); })); layers.forEach(function(l, i) { l.order = i; }); if (!layers.find(function(l) { return l.moduleId === currentModuleId; })) layers.push(makeLayer(currentModuleId, layers.length)); tabBar.setLayers(layers); @@ -1765,8 +1781,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string { })(); tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); }); tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); }); - tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); }); - tabBar.addEventListener('layers-close-all', () => { layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; }); + tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; const closedMid = layerId.replace('layer-', ''); _closedModuleIds.add(closedMid); tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); }); + tabBar.addEventListener('layers-close-all', () => { layers.forEach(l => _closedModuleIds.add(l.moduleId)); layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; }); tabBar.addEventListener('layer-reorder', (e) => { const { layerId, newIndex } = e.detail; const oldIdx = layers.findIndex(l => l.id === layerId); if (oldIdx === -1 || oldIdx === newIndex) return; const [moved] = layers.splice(oldIdx, 1); layers.splice(newIndex, 0, moved); layers.forEach((l, i) => l.order = i); saveTabs(); tabBar.setLayers(layers); }); }