From b7aadf66cd776366f8ecd8f77e34a91239e76a32 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 21 Mar 2026 16:22:37 -0700 Subject: [PATCH] feat(sync): proxy /api/user/* to EncryptID for cross-session tab state Adds catch-all proxy route so client tab sync requests (saveTabs/syncTabsFromServer) reach the EncryptID container. Also fixes rstack-mi panel positioning and Shadow DOM click-outside handling. Co-Authored-By: Claude Opus 4.6 --- server/index.ts | 19 +++++++++++++++++++ shared/components/rstack-mi.ts | 16 +++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/server/index.ts b/server/index.ts index f2e825c..8e36977 100644 --- a/server/index.ts +++ b/server/index.ts @@ -452,6 +452,25 @@ app.all("/encryptid/*", async (c) => { } }); +// ── User API proxy (forward /api/user/* to EncryptID for tab state, prefs) ── +app.all("/api/user/*", async (c) => { + const targetUrl = `${ENCRYPTID_INTERNAL}${c.req.path}${new URL(c.req.url).search}`; + const headers = new Headers(c.req.raw.headers); + headers.delete("host"); + try { + const res = await fetch(targetUrl, { + method: c.req.method, + headers, + body: c.req.method !== "GET" && c.req.method !== "HEAD" ? c.req.raw.body : undefined, + // @ts-ignore duplex needed for streaming request bodies + duplex: "half", + }); + return new Response(res.body, { status: res.status, headers: res.headers }); + } catch (e: any) { + return c.json({ error: "EncryptID service unavailable" }, 502); + } +}); + // ── Existing /api/communities/* routes (backward compatible) ── /** Resolve a community slug to SpaceAuthConfig for the SDK guard */ diff --git a/shared/components/rstack-mi.ts b/shared/components/rstack-mi.ts index 10f648d..0d46a8d 100644 --- a/shared/components/rstack-mi.ts +++ b/shared/components/rstack-mi.ts @@ -236,17 +236,18 @@ export class RStackMi extends HTMLElement { } }); - // Close panel on outside click + // Close panel on outside click — use composedPath to pierce Shadow DOM document.addEventListener("click", (e) => { - if (!this.contains(e.target as Node) && !pill.contains(e.target as Node)) { + const path = e.composedPath(); + if (!path.includes(this)) { panel.classList.remove("open"); bar.classList.remove("focused"); } }); // Prevent internal clicks from closing - panel.addEventListener("click", (e) => e.stopPropagation()); - bar.addEventListener("click", (e) => e.stopPropagation()); + panel.addEventListener("mousedown", (e) => e.stopPropagation()); + bar.addEventListener("mousedown", (e) => e.stopPropagation()); // Minimize button this.#shadow.getElementById("mi-minimize")!.addEventListener("click", () => this.#minimize()); @@ -661,7 +662,7 @@ export class RStackMi extends HTMLElement { const STYLES = ` :host { display: contents; } -.mi { position: relative; flex: 1; max-width: 480px; min-width: 0; } +.mi { position: relative; flex: 1; max-width: 480px; min-width: 0; z-index: 300; } /* ── Search bar in header ── */ .mi-bar { @@ -703,12 +704,13 @@ const STYLES = ` /* ── Rich panel ── */ .mi-panel { - position: fixed; top: 56px; right: 16px; + position: absolute; top: calc(100% + 8px); left: 50%; + transform: translateX(-50%); width: 520px; max-width: calc(100vw - 32px); height: 65vh; max-height: calc(100vh - 72px); min-height: 300px; border-radius: 14px; overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,0.3); - display: none; z-index: 300; + display: none; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); resize: vertical; }