diff --git a/server/shell.ts b/server/shell.ts index 4c38a7c..d50b62f 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -844,10 +844,12 @@ export function renderShell(opts: ShellOptions): string { // Reconcile remote layer changes (shared by BroadcastChannel + Automerge) function reconcileRemoteLayers(remoteLayers) { + console.log('[shell] reconcileRemoteLayers: remote=' + remoteLayers.length + ', local=' + layers.length + ', current=' + currentModuleId); // Guard: never let remote sync wipe all tabs when we have an active module. // Empty remote layers indicate a CRDT initial state or sync race, not // an intentional "close everything" action. if (remoteLayers.length === 0 && currentModuleId) { + console.log('[shell] BLOCKED empty remote layers (keeping local)'); // Keep local layers intact — remote has nothing useful tabBar.setLayers(layers); return; @@ -936,6 +938,7 @@ export function renderShell(opts: ShellOptions): string { // This prevents the visual desync where the tab highlights before content loads. tabBar.addEventListener('layer-switch', (e) => { const { layerId, moduleId } = e.detail; + console.log('[shell] layer-switch:', moduleId, 'layerId:', layerId, 'tabCache:', !!tabCache, 'layers:', layers.length); currentModuleId = moduleId; saveTabs(); // Update settings panel to show config for the newly active module @@ -943,15 +946,20 @@ export function renderShell(opts: ShellOptions): string { if (sp) sp.setAttribute('module-id', moduleId); if (tabCache) { tabCache.switchTo(moduleId).then(ok => { + console.log('[shell] switchTo result:', ok, 'for', moduleId); if (ok) { tabBar.setAttribute('active', layerId); } else { - window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); + const url = window.__rspaceNavUrl(spaceSlug, moduleId); + console.log('[shell] switchTo failed, navigating to:', url); + window.location.href = url; } - }).catch(() => { + }).catch((err) => { + console.error('[shell] switchTo error:', err); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); }); } else { + console.log('[shell] no tabCache, navigating to:', window.__rspaceNavUrl(spaceSlug, moduleId)); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); } }); diff --git a/shared/tab-cache.ts b/shared/tab-cache.ts index 71353e0..799073d 100644 --- a/shared/tab-cache.ts +++ b/shared/tab-cache.ts @@ -101,14 +101,19 @@ export class TabCache { /** Switch to a module tab within the current space. Returns true if handled client-side. */ async switchTo(moduleId: string): Promise { const key = this.paneKey(this.spaceSlug, moduleId); - if (moduleId === this.currentModuleId && this.panes.has(key)) return true; + if (moduleId === this.currentModuleId && this.panes.has(key)) { + console.log("[TabCache] switchTo", moduleId, "→ already current + cached"); + return true; + } if (this.panes.has(key)) { + console.log("[TabCache] switchTo", moduleId, "→ cached, showing pane"); this.showPane(this.spaceSlug, moduleId); this.updateUrl(this.spaceSlug, moduleId); return true; } + console.log("[TabCache] switchTo", moduleId, "→ NOT cached, fetching…"); return this.fetchAndInject(this.spaceSlug, moduleId); } @@ -165,7 +170,7 @@ export class TabCache { /** Fetch a module page, extract content, and inject into a new pane */ private async fetchAndInject(space: string, moduleId: string): Promise { const navUrl = (window as any).__rspaceNavUrl; - if (!navUrl) return false; + if (!navUrl) { console.warn("[TabCache] fetchAndInject: no __rspaceNavUrl"); return false; } const url: string = navUrl(space, moduleId); // For cross-space fetches, use the path-based API to stay same-origin @@ -177,11 +182,14 @@ export class TabCache { fetchUrl = `/${space}/${moduleId}`; } } catch { + console.warn("[TabCache] fetchAndInject: URL resolve failed for", url); return false; } const app = document.getElementById("app"); - if (!app) return false; + if (!app) { console.warn("[TabCache] fetchAndInject: no #app"); return false; } + + console.log("[TabCache] fetchAndInject:", fetchUrl, "for", moduleId); // Show loading spinner const loadingPane = document.createElement("div"); @@ -197,13 +205,16 @@ export class TabCache { signal: AbortSignal.timeout(10_000), }); if (!resp.ok) { + console.warn("[TabCache] fetchAndInject: HTTP", resp.status, "for", fetchUrl); loadingPane.remove(); return false; } const html = await resp.text(); + console.log("[TabCache] fetchAndInject: got", html.length, "bytes for", moduleId); const content = this.extractContent(html); if (!content) { + console.warn("[TabCache] fetchAndInject: extractContent returned null for", moduleId, "— HTML has #app:", html.includes('id="app"')); loadingPane.remove(); return false; } @@ -233,8 +244,10 @@ export class TabCache { this.updateUrl(space, moduleId); this.updateCanvasLayout(moduleId); + console.log("[TabCache] fetchAndInject: SUCCESS for", moduleId); return true; - } catch { + } catch (err) { + console.error("[TabCache] fetchAndInject: CATCH for", moduleId, err); loadingPane.remove(); return false; }