diff --git a/modules/rspace/mod.ts b/modules/rspace/mod.ts index 5e33444..36c30c6 100644 --- a/modules/rspace/mod.ts +++ b/modules/rspace/mod.ts @@ -11,11 +11,25 @@ import { resolve } from "node:path"; import { renderShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; +import { loadCommunity, getDocumentData } from "../../server/community-store"; const DIST_DIR = resolve(import.meta.dir, "../../dist"); const routes = new Hono(); +// GET /api/meta — space metadata (owner, members) for space-settings fallback +routes.get("/api/meta", async (c) => { + const space = c.req.param("space") || "demo"; + try { + await loadCommunity(space); + const doc = getDocumentData(space); + if (!doc) return c.json({ meta: {} }); + return c.json({ meta: { ownerDID: doc.meta?.ownerDID, members: doc.members } }); + } catch { + return c.json({ meta: {} }); + } +}); + /** * Extract body content and scripts from the full canvas.html page. * Strips the shell chrome (header, tab-bar, welcome overlay) that renderShell provides, diff --git a/shared/components/rstack-notification-bell.ts b/shared/components/rstack-notification-bell.ts index 5614b1b..6a9d0e3 100644 --- a/shared/components/rstack-notification-bell.ts +++ b/shared/components/rstack-notification-bell.ts @@ -60,6 +60,10 @@ export class RStackNotificationBell extends HTMLElement { this.#open = false; this.#render(); this.#fetchCount(); + // Restart poll timer if it was stopped by a 401 + if (!this.#pollTimer) { + this.#pollTimer = setInterval(() => this.#fetchCount(), POLL_INTERVAL); + } }; #onWsNotification = (e: CustomEvent) => { @@ -96,6 +100,11 @@ export class RStackNotificationBell extends HTMLElement { const data = await res.json(); this.#unreadCount = data.unreadCount || 0; this.#render(); + } else if (res.status === 401) { + // Token rejected — stop polling until auth changes + this.#unreadCount = 0; + if (this.#pollTimer) { clearInterval(this.#pollTimer); this.#pollTimer = null; } + this.#render(); } } catch { // Silently fail diff --git a/shared/components/rstack-space-settings.ts b/shared/components/rstack-space-settings.ts index 2294aa3..af00f98 100644 --- a/shared/components/rstack-space-settings.ts +++ b/shared/components/rstack-space-settings.ts @@ -91,8 +91,12 @@ export class RStackSpaceSettings extends HTMLElement { } } else { // Fallback: fetch from API + // On subdomain routing (jeff.rspace.online), omit space prefix to avoid double-prefix + const host = window.location.hostname; + const isSubdomain = host.split(".").length >= 3 && !host.startsWith("www."); + const metaUrl = isSubdomain ? "/rspace/api/meta" : `/${this._space}/rspace/api/meta`; try { - const res = await fetch(`/${this._space}/rspace/api/meta`, { + const res = await fetch(metaUrl, { headers: token ? { "Authorization": `Bearer ${token}` } : {}, }); if (res.ok) { diff --git a/shared/components/rstack-tab-bar.ts b/shared/components/rstack-tab-bar.ts index e44c908..926e697 100644 --- a/shared/components/rstack-tab-bar.ts +++ b/shared/components/rstack-tab-bar.ts @@ -110,6 +110,8 @@ export class RStackTabBar extends HTMLElement { #wiringSourceFeedId = ""; #wiringSourceKind: FlowKind | null = null; #escHandler: ((e: KeyboardEvent) => void) | null = null; + // Cleanup for document-level listeners (prevent leak on re-render) + #docCleanup: (() => void) | null = null; // Recent apps: moduleId → last-used timestamp #recentApps: Map = new Map(); @@ -789,6 +791,9 @@ export class RStackTabBar extends HTMLElement { // ── Events ── #attachEvents() { + // Clean up previous document-level listeners to prevent leak + if (this.#docCleanup) { this.#docCleanup(); this.#docCleanup = null; } + // Tab clicks this.#shadow.querySelectorAll(".tab").forEach(tab => { tab.addEventListener("click", (e) => { @@ -945,20 +950,6 @@ export class RStackTabBar extends HTMLElement { this.#flowDragSource = layerId; plane.classList.add("flow-drag-source"); }); - - plane.addEventListener("mouseenter", () => { - if (this.#flowDragSource && this.#flowDragSource !== layerId) { - this.#flowDragTarget = layerId; - plane.classList.add("flow-drag-target"); - } - }); - - plane.addEventListener("mouseleave", () => { - if (this.#flowDragTarget === layerId) { - this.#flowDragTarget = null; - plane.classList.remove("flow-drag-target"); - } - }); }); // 3D scene: orbit controls (drag on empty space to rotate) @@ -974,6 +965,9 @@ export class RStackTabBar extends HTMLElement { sceneContainer.style.cursor = "grabbing"; }); + // Collect all layer planes + their rects for drag target detection + const layerPlanes = [...this.#shadow.querySelectorAll(".layer-plane")]; + const onMouseMove = (e: MouseEvent) => { if (this.#orbitDragging) { const dx = e.clientX - this.#orbitLastX; @@ -985,6 +979,29 @@ export class RStackTabBar extends HTMLElement { const scene = this.#shadow.getElementById("stack-scene"); if (scene) scene.style.transform = `rotateX(${this.#sceneRotX}deg) rotateZ(${this.#sceneRotZ}deg)`; } + + // Drag-to-connect: track target via bounding rects (robust for 3D) + if (this.#flowDragSource) { + let newTarget: string | null = null; + for (const p of layerPlanes) { + const r = p.getBoundingClientRect(); + if (e.clientX >= r.left && e.clientX <= r.right && + e.clientY >= r.top && e.clientY <= r.bottom && + p.dataset.layerId !== this.#flowDragSource) { + newTarget = p.dataset.layerId!; + break; + } + } + if (newTarget !== this.#flowDragTarget) { + this.#shadow.querySelectorAll(".flow-drag-target") + .forEach(el => el.classList.remove("flow-drag-target")); + this.#flowDragTarget = newTarget; + if (newTarget) { + this.#shadow.querySelector(`.layer-plane[data-layer-id="${newTarget}"]`) + ?.classList.add("flow-drag-target"); + } + } + } }; const onMouseUp = () => { @@ -1003,9 +1020,13 @@ export class RStackTabBar extends HTMLElement { }); }; - // Attach to document for drag continuity + // Attach to document for drag continuity (cleaned up on re-render) document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); + this.#docCleanup = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + }; // Scroll to zoom sceneContainer.addEventListener("wheel", (e) => {