From aa6f04e45e7b67d4daae4362a0a6383c5ae1f605 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 12 Mar 2026 20:25:29 -0700 Subject: [PATCH] fix: resolve all 127 TypeScript errors with Hono type augmentation Add types/hono.d.ts declaring effectiveSpace, spaceRole, and isOwner on Hono's ContextVariableMap. Remove 127 "as any" casts across 18 files. Fix ParticipantStatus type in rmaps SyncMessage union. Co-Authored-By: Claude Opus 4.6 --- modules/rbooks/mod.ts | 12 +- modules/rcal/mod.ts | 26 +- modules/rdata/mod.ts | 2 +- modules/rdesign/mod.ts | 2 +- modules/rdocs/mod.ts | 2 +- modules/rmaps/components/folk-map-viewer.ts | 958 ++++++++++++++++++-- modules/rmaps/components/map-sync.ts | 24 +- modules/rmaps/mod.ts | 101 ++- modules/rnetwork/mod.ts | 12 +- modules/rnotes/mod.ts | 44 +- modules/rphotos/mod.ts | 4 +- modules/rpubs/mod.ts | 2 +- modules/rschedule/mod.ts | 48 +- modules/rsocials/mod.ts | 42 +- modules/rsplat/mod.ts | 16 +- modules/rswag/mod.ts | 2 +- modules/rtasks/mod.ts | 2 +- modules/rtrips/mod.ts | 26 +- modules/rtube/mod.ts | 2 +- server/index.ts | 6 +- types/hono.d.ts | 9 + 21 files changed, 1150 insertions(+), 192 deletions(-) create mode 100644 types/hono.d.ts diff --git a/modules/rbooks/mod.ts b/modules/rbooks/mod.ts index aa4d348..0e4cdfb 100644 --- a/modules/rbooks/mod.ts +++ b/modules/rbooks/mod.ts @@ -104,7 +104,7 @@ const routes = new Hono(); // ── API: List books ── routes.get("/api/books", async (c) => { const space = c.req.param("space") || "global"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const search = c.req.query("search")?.toLowerCase(); const tag = c.req.query("tag"); const limit = Math.min(parseInt(c.req.query("limit") || "50"), 100); @@ -157,7 +157,7 @@ routes.get("/api/books", async (c) => { // ── API: Upload book ── routes.post("/api/books", async (c) => { const space = c.req.param("space") || "global"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); @@ -244,7 +244,7 @@ routes.post("/api/books", async (c) => { // ── API: Get book details ── routes.get("/api/books/:id", async (c) => { const space = c.req.param("space") || "global"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -269,7 +269,7 @@ routes.get("/api/books/:id", async (c) => { // ── API: Serve PDF ── routes.get("/api/books/:id/pdf", async (c) => { const space = c.req.param("space") || "global"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -307,7 +307,7 @@ routes.get("/api/books/:id/pdf", async (c) => { // ── Page: Library ── routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "personal"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; return c.html(renderShell({ title: `${spaceSlug} — Library | rSpace`, moduleId: "rbooks", @@ -323,7 +323,7 @@ routes.get("/", (c) => { // ── Page: Book reader ── routes.get("/read/:id", async (c) => { const spaceSlug = c.req.param("space") || "personal"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); diff --git a/modules/rcal/mod.ts b/modules/rcal/mod.ts index caccf51..96c127e 100644 --- a/modules/rcal/mod.ts +++ b/modules/rcal/mod.ts @@ -260,7 +260,7 @@ function seedDemoIfEmpty(space: string) { // GET /api/events — query events with filters routes.get("/api/events", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const { start, end, source, search, rTool, rEntityId, upcoming } = c.req.query(); const doc = ensureDoc(dataSpace); @@ -313,7 +313,7 @@ routes.post("/api/events", async (c) => { try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const body = await c.req.json(); const { title, description, start_time, end_time, all_day, timezone, source_id, location_id, location_name, is_virtual, virtual_url, virtual_platform, r_tool_source, r_tool_entity_id, @@ -398,7 +398,7 @@ routes.post("/api/events", async (c) => { // GET /api/events/scheduled — query only scheduled knowledge items routes.get("/api/events/scheduled", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const { date, upcoming, pending_only } = c.req.query(); const doc = ensureDoc(dataSpace); @@ -434,7 +434,7 @@ routes.get("/api/events/scheduled", async (c) => { // GET /api/events/:id routes.get("/api/events/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -451,7 +451,7 @@ routes.patch("/api/events/:id", async (c) => { try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const body = await c.req.json(); @@ -504,7 +504,7 @@ routes.patch("/api/events/:id", async (c) => { // DELETE /api/events/:id routes.delete("/api/events/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = calendarDocId(dataSpace); @@ -521,7 +521,7 @@ routes.delete("/api/events/:id", async (c) => { routes.get("/api/sources", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const { is_active, is_visible, source_type } = c.req.query(); const doc = ensureDoc(dataSpace); @@ -550,7 +550,7 @@ routes.post("/api/sources", async (c) => { try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const body = await c.req.json(); const docId = calendarDocId(dataSpace); ensureDoc(dataSpace); @@ -611,7 +611,7 @@ function deriveLocations(doc: CalendarDoc): DerivedLocation[] { routes.get("/api/locations", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const { granularity, parent, search, root } = c.req.query(); const doc = ensureDoc(dataSpace); @@ -638,7 +638,7 @@ routes.get("/api/locations", async (c) => { routes.get("/api/locations/tree", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); // Flat list with depth=0 since hierarchical parent_id data is not stored in Automerge @@ -690,7 +690,7 @@ routes.get("/api/lunar", async (c) => { routes.get("/api/stats", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); const events = Object.values(doc.events).length; @@ -704,7 +704,7 @@ routes.get("/api/stats", async (c) => { routes.get("/api/context/:tool", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tool = c.req.param("tool"); const entityId = c.req.query("entityId"); if (!entityId) return c.json({ error: "entityId required" }, 400); @@ -721,7 +721,7 @@ routes.get("/api/context/:tool", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — Calendar | rSpace`, moduleId: "rcal", diff --git a/modules/rdata/mod.ts b/modules/rdata/mod.ts index 61a9e4e..da6abcb 100644 --- a/modules/rdata/mod.ts +++ b/modules/rdata/mod.ts @@ -121,7 +121,7 @@ routes.post("/api/collect", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — Data | rSpace`, moduleId: "rdata", diff --git a/modules/rdesign/mod.ts b/modules/rdesign/mod.ts index f1c30fc..c671e78 100644 --- a/modules/rdesign/mod.ts +++ b/modules/rdesign/mod.ts @@ -19,7 +19,7 @@ routes.get("/api/health", (c) => { routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const view = c.req.query("view"); if (view === "demo") { diff --git a/modules/rdocs/mod.ts b/modules/rdocs/mod.ts index 2fc0575..aa17839 100644 --- a/modules/rdocs/mod.ts +++ b/modules/rdocs/mod.ts @@ -19,7 +19,7 @@ routes.get("/api/health", (c) => { routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const view = c.req.query("view"); if (view === "demo") { diff --git a/modules/rmaps/components/folk-map-viewer.ts b/modules/rmaps/components/folk-map-viewer.ts index c570f51..0d29e50 100644 --- a/modules/rmaps/components/folk-map-viewer.ts +++ b/modules/rmaps/components/folk-map-viewer.ts @@ -9,9 +9,11 @@ * and feature highlights matching standalone rMaps capabilities. */ -import { RoomSync, type RoomState, type ParticipantState, type LocationState } from "./map-sync"; +import { RoomSync, type RoomState, type ParticipantState, type LocationState, type ParticipantStatus, type PrecisionLevel, type PrivacySettings, type WaypointType } from "./map-sync"; import { loadRoomHistory, saveRoomVisit, updateRoomThumbnail } from "./map-room-history"; import { MapPushManager } from "./map-push"; +import { fuzzLocation, haversineDistance, formatDistance, formatTime } from "./map-privacy"; +import { parseGoogleMapsGeoJSON, type ParsedPlace } from "./map-import"; import { TourEngine } from "../../../shared/tour-engine"; import { ViewHistory } from "../../../shared/view-history.js"; import { requireAuth } from "../../../shared/auth-fetch"; @@ -22,30 +24,34 @@ import { getUsername } from "../../../shared/components/rstack-identity"; const MAPLIBRE_CSS = "https://unpkg.com/maplibre-gl@4.1.2/dist/maplibre-gl.css"; const MAPLIBRE_JS = "https://unpkg.com/maplibre-gl@4.1.2/dist/maplibre-gl.js"; +const OSM_ATTRIBUTION = '© OpenStreetMap contributors'; + const DARK_STYLE = { version: 8, sources: { - carto: { + osm: { type: "raster", - tiles: ["https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png"], + tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"], tileSize: 256, - attribution: '© CARTO © OSM', + attribution: OSM_ATTRIBUTION, + maxzoom: 19, }, }, - layers: [{ id: "carto", type: "raster", source: "carto" }], + layers: [{ id: "osm", type: "raster", source: "osm" }], }; const LIGHT_STYLE = { version: 8, sources: { - carto: { + osm: { type: "raster", - tiles: ["https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png"], + tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"], tileSize: 256, - attribution: '© CARTO © OSM', + attribution: OSM_ATTRIBUTION, + maxzoom: 19, }, }, - layers: [{ id: "carto", type: "raster", source: "carto" }], + layers: [{ id: "osm", type: "raster", source: "osm" }], }; const PARTICIPANT_COLORS = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#ec4899", "#14b8a6", "#f97316"]; @@ -90,6 +96,21 @@ class FolkMapViewer extends HTMLElement { private sharingLocation = false; private watchId: number | null = null; private pushManager: MapPushManager | null = null; + private privacySettings: PrivacySettings = { precision: "exact", ghostMode: false }; + private showPrivacyPanel = false; + private geoPermissionState: PermissionState | "" = ""; + private geoTimeoutCount = 0; + // Modals/panels state + private showShareModal = false; + private showMeetingModal = false; + private showImportModal = false; + private selectedParticipant: string | null = null; + private selectedWaypoint: string | null = null; + private activeRoute: { segments: any[]; totalDistance: number; estimatedTime: number; destination: string } | null = null; + private meetingSearchResults: { display_name: string; lat: string; lon: string }[] = []; + private meetingSearchQuery = ""; + private importParsedPlaces: { name: string; lat: number; lng: number; selected: boolean }[] = []; + private importStep: "upload" | "preview" | "done" = "upload"; private thumbnailTimer: ReturnType | null = null; private _themeObserver: MutationObserver | null = null; private _history = new ViewHistory<"lobby" | "map">("lobby"); @@ -1032,9 +1053,13 @@ class FolkMapViewer extends HTMLElement { trackUserLocation: false, }), "top-right"); + // Apply dark mode inversion filter to OSM tiles + this.applyDarkFilter(); + // Theme observer — swap map tiles on toggle this._themeObserver = new MutationObserver(() => { this.map?.setStyle(this.isDarkTheme() ? DARK_STYLE : LIGHT_STYLE); + this.applyDarkFilter(); this.updateMarkerTheme(); }); this._themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] }); @@ -1135,6 +1160,12 @@ class FolkMapViewer extends HTMLElement { label.textContent = p.name; el.appendChild(label); + el.addEventListener("click", () => { + this.selectedParticipant = id; + this.selectedWaypoint = null; + this.renderNavigationPanel(); + }); + const marker = new (window as any).maplibregl.Marker({ element: el }) .setLngLat(lngLat) .addTo(this.map); @@ -1164,6 +1195,11 @@ class FolkMapViewer extends HTMLElement { `; el.textContent = wp.emoji || "\u{1F4CD}"; el.title = wp.name; + el.addEventListener("click", () => { + this.selectedWaypoint = wp.id; + this.selectedParticipant = null; + this.renderNavigationPanel(); + }); const marker = new (window as any).maplibregl.Marker({ element: el }) .setLngLat([wp.longitude, wp.latitude]) .addTo(this.map); @@ -1184,19 +1220,54 @@ class FolkMapViewer extends HTMLElement { private updateParticipantList(state: RoomState) { const list = this.shadow.getElementById("participant-list"); if (!list) return; - const entries = Object.values(state.participants); - list.innerHTML = entries.map((p) => ` -
- ${this.esc(p.emoji)} + + // Dedup by name (keep most recent) + const byName = new Map(); + for (const p of Object.values(state.participants)) { + const existing = byName.get(p.name); + if (!existing || new Date(p.lastSeen) > new Date(existing.lastSeen)) { + byName.set(p.name, p); + } + } + const entries = Array.from(byName.values()); + + const myLoc = state.participants[this.participantId]?.location; + + const statusColors: Record = { online: "#22c55e", away: "#f59e0b", ghost: "#64748b", offline: "#ef4444" }; + + list.innerHTML = entries.map((p) => { + let distLabel = ""; + if (myLoc && p.location && p.id !== this.participantId) { + distLabel = formatDistance(haversineDistance(myLoc.latitude, myLoc.longitude, p.location.latitude, p.location.longitude)); + } + const statusColor = statusColors[p.status] || "#64748b"; + return ` +
+
+ ${this.esc(p.emoji)} + +
${this.esc(p.name)}
-
${p.location ? "sharing location" : "no location"}
+
+ ${p.status === "ghost" ? "ghost mode" : p.location ? "sharing" : "no location"} + ${distLabel ? ` \u2022 ${distLabel}` : ""} +
- ${p.id !== this.participantId ? `` : ""} -
- `).join(""); + ${p.id !== this.participantId && p.location ? `` : ""} + ${p.id !== this.participantId ? `` : ""} +
`; + }).join(""); - // Attach ping listeners + // Footer actions + list.insertAdjacentHTML("beforeend", ` +
+ + +
+ `); + + // Attach listeners list.querySelectorAll("[data-ping]").forEach((btn) => { btn.addEventListener("click", () => { const pid = (btn as HTMLElement).dataset.ping!; @@ -1205,6 +1276,22 @@ class FolkMapViewer extends HTMLElement { setTimeout(() => { (btn as HTMLElement).textContent = "\u{1F514}"; }, 2000); }); }); + list.querySelectorAll("[data-nav-participant]").forEach((btn) => { + btn.addEventListener("click", () => { + this.selectedParticipant = (btn as HTMLElement).dataset.navParticipant!; + this.selectedWaypoint = null; + this.renderNavigationPanel(); + }); + }); + list.querySelector("#sidebar-meeting-btn")?.addEventListener("click", () => { + this.showMeetingModal = true; + this.renderMeetingPointModal(); + }); + list.querySelector("#sidebar-import-btn")?.addEventListener("click", () => { + this.showImportModal = true; + this.importStep = "upload"; + this.renderImportModal(); + }); } private updateMarkerTheme() { @@ -1220,16 +1307,41 @@ class FolkMapViewer extends HTMLElement { } } + private applyDarkFilter() { + const container = this.shadow.getElementById("map-container"); + if (!container) return; + const canvas = container.querySelector("canvas"); + if (canvas) { + canvas.style.filter = this.isDarkTheme() ? "invert(1) hue-rotate(180deg)" : "none"; + } else { + // Canvas may not be ready yet — retry after tiles load + this.map?.once("load", () => { + const c = container.querySelector("canvas"); + if (c) c.style.filter = this.isDarkTheme() ? "invert(1) hue-rotate(180deg)" : "none"; + }); + } + } + // ─── Location sharing ──────────────────────────────────────── + private async checkGeoPermission() { + try { + const result = await navigator.permissions.query({ name: "geolocation" }); + this.geoPermissionState = result.state; + result.addEventListener("change", () => { this.geoPermissionState = result.state; }); + } catch { /* permissions API not available */ } + } + private toggleLocationSharing() { + if (this.privacySettings.ghostMode) return; // Ghost mode prevents sharing + if (this.sharingLocation) { - // Stop sharing if (this.watchId !== null) { navigator.geolocation.clearWatch(this.watchId); this.watchId = null; } this.sharingLocation = false; + this.geoTimeoutCount = 0; this.sync?.clearLocation(); this.updateShareButton(); return; @@ -1240,14 +1352,29 @@ class FolkMapViewer extends HTMLElement { return; } + this.checkGeoPermission(); let firstFix = true; + const useHighAccuracy = this.geoTimeoutCount < 2; + this.watchId = navigator.geolocation.watchPosition( (pos) => { this.sharingLocation = true; + this.geoTimeoutCount = 0; this.updateShareButton(); + + let lat = pos.coords.latitude; + let lng = pos.coords.longitude; + + // Apply privacy fuzzing + if (this.privacySettings.precision !== "exact") { + const fuzzed = fuzzLocation(lat, lng, this.privacySettings.precision); + lat = fuzzed.latitude; + lng = fuzzed.longitude; + } + const loc: LocationState = { - latitude: pos.coords.latitude, - longitude: pos.coords.longitude, + latitude: lat, + longitude: lng, accuracy: pos.coords.accuracy, altitude: pos.coords.altitude ?? undefined, heading: pos.coords.heading ?? undefined, @@ -1258,47 +1385,752 @@ class FolkMapViewer extends HTMLElement { this.sync?.updateLocation(loc); if (firstFix && this.map) { - this.map.flyTo({ center: [loc.longitude, loc.latitude], zoom: 14 }); + this.map.flyTo({ center: [pos.coords.longitude, pos.coords.latitude], zoom: 14 }); firstFix = false; } }, (err) => { - this.error = `Location error: ${err.message}`; - this.sharingLocation = false; - this.updateShareButton(); + if (err.code === err.TIMEOUT) { + this.geoTimeoutCount++; + if (this.geoTimeoutCount >= 2 && this.watchId !== null) { + // Restart with low accuracy + navigator.geolocation.clearWatch(this.watchId); + this.watchId = navigator.geolocation.watchPosition( + (pos) => { + this.sharingLocation = true; + this.updateShareButton(); + let lat = pos.coords.latitude; + let lng = pos.coords.longitude; + if (this.privacySettings.precision !== "exact") { + const fuzzed = fuzzLocation(lat, lng, this.privacySettings.precision); + lat = fuzzed.latitude; + lng = fuzzed.longitude; + } + this.sync?.updateLocation({ + latitude: lat, longitude: lng, + accuracy: pos.coords.accuracy, + altitude: pos.coords.altitude ?? undefined, + heading: pos.coords.heading ?? undefined, + speed: pos.coords.speed ?? undefined, + timestamp: new Date().toISOString(), + source: "network", + }); + }, + () => { this.sharingLocation = false; this.updateShareButton(); }, + { enableHighAccuracy: false, maximumAge: 10000, timeout: 30000 }, + ); + } + } else { + this.error = `Location error: ${err.message}`; + this.sharingLocation = false; + this.updateShareButton(); + } }, - { enableHighAccuracy: true, maximumAge: 5000, timeout: 15000 }, + { enableHighAccuracy: useHighAccuracy, maximumAge: 5000, timeout: 15000 }, ); } + private toggleGhostMode() { + this.privacySettings.ghostMode = !this.privacySettings.ghostMode; + if (this.privacySettings.ghostMode) { + if (this.watchId !== null) { + navigator.geolocation.clearWatch(this.watchId); + this.watchId = null; + } + this.sharingLocation = false; + this.sync?.updateStatus("ghost"); + this.sync?.clearLocation(); + } else { + this.sync?.updateStatus("online"); + } + this.renderPrivacyPanel(); + this.updateShareButton(); + } + private updateShareButton() { const btn = this.shadow.getElementById("share-location"); if (!btn) return; - if (this.sharingLocation) { + if (this.privacySettings.ghostMode) { + btn.textContent = "\u{1F47B} Ghost Mode"; + btn.classList.remove("sharing"); + btn.classList.add("ghost"); + } else if (this.sharingLocation) { btn.textContent = "\u{1F4CD} Stop Sharing"; btn.classList.add("sharing"); + btn.classList.remove("ghost"); } else { btn.textContent = "\u{1F4CD} Share Location"; btn.classList.remove("sharing"); + btn.classList.remove("ghost"); + } + // Update permission indicator + const permIndicator = this.shadow.getElementById("geo-perm-indicator"); + if (permIndicator) { + const colors: Record = { granted: "#22c55e", prompt: "#f59e0b", denied: "#ef4444" }; + permIndicator.style.background = colors[this.geoPermissionState] || "#64748b"; + permIndicator.title = `Geolocation: ${this.geoPermissionState || "unknown"}`; } } - // ─── Waypoint drop ─────────────────────────────────────────── + private renderPrivacyPanel() { + const panel = this.shadow.getElementById("privacy-panel"); + if (!panel) return; + const precisionLabels: Record = { + exact: "Exact", building: "~50m (Building)", area: "~500m (Area)", approximate: "~5km (Approximate)", + }; + panel.innerHTML = ` +
Privacy Settings
+ + + +
+ Ghost mode hides your location from all participants and stops GPS tracking. +
+ `; + panel.querySelector("#precision-select")?.addEventListener("change", (e) => { + this.privacySettings.precision = (e.target as HTMLSelectElement).value as PrecisionLevel; + }); + panel.querySelector("#ghost-toggle")?.addEventListener("change", () => { + this.toggleGhostMode(); + }); + } + + // ─── Waypoint drop / Meeting point modal ──────────────────── private dropWaypoint() { + this.showMeetingModal = true; + this.meetingSearchQuery = ""; + this.meetingSearchResults = []; + this.renderMeetingPointModal(); + } + + private renderMeetingPointModal() { + let modal = this.shadow.getElementById("meeting-modal"); + if (!this.showMeetingModal) { + modal?.remove(); + return; + } + if (!modal) { + modal = document.createElement("div"); + modal.id = "meeting-modal"; + modal.style.cssText = ` + position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center; + background:rgba(0,0,0,0.6);backdrop-filter:blur(4px); + `; + this.shadow.appendChild(modal); + } + + const center = this.map?.getCenter(); + const myLoc = this.sync?.getState().participants[this.participantId]?.location; + const meetingEmojis = ["\u{1F4CD}", "\u{2B50}", "\u{1F3E0}", "\u{1F37D}", "\u{26FA}", "\u{1F3AF}", "\u{1F680}", "\u{1F33F}", "\u{26A1}", "\u{1F48E}"]; + + modal.innerHTML = ` +
+
+
\u{1F4CD} Set Meeting Point
+ +
+ + + + + +
+ ${meetingEmojis.map((e, i) => ``).join("")} +
+ + +
+ + + +
+ +
+
+ ${center ? `Map center: ${center.lat.toFixed(5)}, ${center.lng.toFixed(5)}` : "Select a location mode"} +
+
+ + + + + + +
+ `; + + // Listeners + modal.querySelector("#meeting-close")?.addEventListener("click", () => { + this.showMeetingModal = false; + modal?.remove(); + }); + modal.addEventListener("click", (e) => { + if (e.target === modal) { this.showMeetingModal = false; modal?.remove(); } + }); + + // Emoji picker + let selectedEmoji = "\u{1F4CD}"; + modal.querySelectorAll(".emoji-opt").forEach(btn => { + btn.addEventListener("click", () => { + selectedEmoji = (btn as HTMLElement).dataset.emoji!; + (modal!.querySelector("#meeting-emoji") as HTMLInputElement).value = selectedEmoji; + modal!.querySelectorAll(".emoji-opt").forEach(b => (b as HTMLElement).style.borderColor = "var(--rs-border)"); + (btn as HTMLElement).style.borderColor = "#4f46e5"; + }); + }); + + // GPS mode + modal.querySelector("#loc-gps")?.addEventListener("click", () => { + if (myLoc) { + (modal!.querySelector("#meeting-lat") as HTMLInputElement).value = String(myLoc.latitude); + (modal!.querySelector("#meeting-lng") as HTMLInputElement).value = String(myLoc.longitude); + modal!.querySelector("#loc-mode-content")!.innerHTML = `
\u2713 Using your current GPS: ${myLoc.latitude.toFixed(5)}, ${myLoc.longitude.toFixed(5)}
`; + } else { + modal!.querySelector("#loc-mode-content")!.innerHTML = `
Share your location first
`; + } + }); + + // Search mode + modal.querySelector("#loc-search")?.addEventListener("click", () => { + modal!.querySelector("#loc-mode-content")!.innerHTML = ` +
+ + +
+
+ `; + modal!.querySelector("#address-search-btn")?.addEventListener("click", async () => { + const q = (modal!.querySelector("#address-search") as HTMLInputElement).value.trim(); + if (!q) return; + const resultsDiv = modal!.querySelector("#search-results")!; + resultsDiv.innerHTML = '
Searching...
'; + try { + const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q)}&limit=5`, { + headers: { "User-Agent": "rMaps/1.0" }, + signal: AbortSignal.timeout(5000), + }); + const data = await res.json(); + this.meetingSearchResults = data; + resultsDiv.innerHTML = data.length ? data.map((r: any, i: number) => ` +
+ ${this.esc(r.display_name?.substring(0, 80))} +
+ `).join("") : '
No results found
'; + resultsDiv.querySelectorAll("[data-sr]").forEach(el => { + el.addEventListener("click", () => { + const idx = parseInt((el as HTMLElement).dataset.sr!, 10); + const r = this.meetingSearchResults[idx]; + (modal!.querySelector("#meeting-lat") as HTMLInputElement).value = r.lat; + (modal!.querySelector("#meeting-lng") as HTMLInputElement).value = r.lon; + resultsDiv.querySelectorAll("[data-sr]").forEach(e => (e as HTMLElement).style.borderColor = "transparent"); + (el as HTMLElement).style.borderColor = "#4f46e5"; + }); + }); + } catch { + resultsDiv.innerHTML = '
Search failed
'; + } + }); + }); + + // Manual mode + modal.querySelector("#loc-manual")?.addEventListener("click", () => { + modal!.querySelector("#loc-mode-content")!.innerHTML = ` +
+
+ + +
+
+ + +
+
+ `; + modal!.querySelector("#manual-lat")?.addEventListener("input", (e) => { + (modal!.querySelector("#meeting-lat") as HTMLInputElement).value = (e.target as HTMLInputElement).value; + }); + modal!.querySelector("#manual-lng")?.addEventListener("input", (e) => { + (modal!.querySelector("#meeting-lng") as HTMLInputElement).value = (e.target as HTMLInputElement).value; + }); + }); + + // Create + modal.querySelector("#meeting-create")?.addEventListener("click", () => { + const name = (modal!.querySelector("#meeting-name") as HTMLInputElement).value.trim() || "Meeting point"; + const lat = parseFloat((modal!.querySelector("#meeting-lat") as HTMLInputElement).value); + const lng = parseFloat((modal!.querySelector("#meeting-lng") as HTMLInputElement).value); + const emoji = (modal!.querySelector("#meeting-emoji") as HTMLInputElement).value || "\u{1F4CD}"; + + if (isNaN(lat) || isNaN(lng)) return; + + this.sync?.addWaypoint({ + id: crypto.randomUUID(), + name, + emoji, + latitude: lat, + longitude: lng, + createdBy: this.participantId, + createdAt: new Date().toISOString(), + type: "meeting", + }); + this.showMeetingModal = false; + modal?.remove(); + }); + } + + // ─── Share modal with QR code ─────────────────────────────── + + private async renderShareModal() { + let modal = this.shadow.getElementById("share-modal"); + if (!this.showShareModal) { + modal?.remove(); + return; + } + if (!modal) { + modal = document.createElement("div"); + modal.id = "share-modal"; + modal.style.cssText = ` + position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center; + background:rgba(0,0,0,0.6);backdrop-filter:blur(4px); + `; + this.shadow.appendChild(modal); + } + + const shareUrl = `${window.location.origin}/${this.space}/rmaps/${this.room}`; + + modal.innerHTML = ` +
+
+
Share Room
+ +
+ +
+
Generating QR code...
+
+ +
+ ${this.esc(shareUrl)} +
+ +
+ + +
+
+ `; + + // Generate QR code + try { + const QRCode = await import("qrcode"); + const dataUrl = await QRCode.toDataURL(shareUrl, { width: 200, margin: 2, color: { dark: "#000000", light: "#ffffff" } }); + const qrContainer = modal.querySelector("#qr-container"); + if (qrContainer) { + qrContainer.innerHTML = `QR Code`; + } + } catch { + const qrContainer = modal.querySelector("#qr-container"); + if (qrContainer) qrContainer.innerHTML = `
QR code unavailable
`; + } + + // Listeners + modal.querySelector("#share-close")?.addEventListener("click", () => { + this.showShareModal = false; + modal?.remove(); + }); + modal.addEventListener("click", (e) => { + if (e.target === modal) { this.showShareModal = false; modal?.remove(); } + }); + modal.querySelector("#share-copy")?.addEventListener("click", () => { + navigator.clipboard.writeText(shareUrl).then(() => { + const btn = modal!.querySelector("#share-copy"); + if (btn) { btn.textContent = "\u2713 Copied!"; setTimeout(() => { btn.textContent = "\u{1F4CB} Copy Link"; }, 2000); } + }); + }); + modal.querySelector("#share-native")?.addEventListener("click", () => { + if (navigator.share) { + navigator.share({ title: `rMaps: ${this.room}`, url: shareUrl }).catch(() => {}); + } else { + navigator.clipboard.writeText(shareUrl).then(() => { + const btn = modal!.querySelector("#share-native"); + if (btn) { btn.textContent = "\u2713 Copied!"; setTimeout(() => { btn.textContent = "\u{1F4E4} Share"; }, 2000); } + }); + } + }); + } + + // ─── Import modal ─────────────────────────────────────────── + + private renderImportModal() { + let modal = this.shadow.getElementById("import-modal"); + if (!this.showImportModal) { + modal?.remove(); + return; + } + if (!modal) { + modal = document.createElement("div"); + modal.id = "import-modal"; + modal.style.cssText = ` + position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center; + background:rgba(0,0,0,0.6);backdrop-filter:blur(4px); + `; + this.shadow.appendChild(modal); + } + + if (this.importStep === "upload") { + modal.innerHTML = ` +
+
+
\u{1F4E5} Import Places
+ +
+ +
+
\u{1F4C2}
+
Drop a GeoJSON file here
+
or click to browse (.json, .geojson)
+ +
+ + +
+ `; + + const handleFile = (file: File) => { + if (file.size > 50 * 1024 * 1024) { + const errDiv = modal!.querySelector("#import-error") as HTMLElement; + errDiv.style.display = "block"; + errDiv.textContent = "File too large (max 50 MB)"; + return; + } + const reader = new FileReader(); + reader.onload = () => { + const result = parseGoogleMapsGeoJSON(reader.result as string); + if (!result.success) { + const errDiv = modal!.querySelector("#import-error") as HTMLElement; + errDiv.style.display = "block"; + errDiv.textContent = result.error || "No places found"; + return; + } + this.importParsedPlaces = result.places.map(p => ({ ...p, selected: true })); + this.importStep = "preview"; + this.renderImportModal(); + }; + reader.readAsText(file); + }; + + const dropZone = modal.querySelector("#drop-zone")!; + const fileInput = modal.querySelector("#file-input") as HTMLInputElement; + + dropZone.addEventListener("click", () => fileInput.click()); + dropZone.addEventListener("dragover", (e) => { e.preventDefault(); (dropZone as HTMLElement).style.borderColor = "#4f46e5"; }); + dropZone.addEventListener("dragleave", () => { (dropZone as HTMLElement).style.borderColor = "var(--rs-border)"; }); + dropZone.addEventListener("drop", (e) => { + e.preventDefault(); + (dropZone as HTMLElement).style.borderColor = "var(--rs-border)"; + const file = (e as DragEvent).dataTransfer?.files[0]; + if (file) handleFile(file); + }); + fileInput.addEventListener("change", () => { + if (fileInput.files?.[0]) handleFile(fileInput.files[0]); + }); + } else if (this.importStep === "preview") { + const selectedCount = this.importParsedPlaces.filter(p => p.selected).length; + modal.innerHTML = ` +
+
+
Preview (${this.importParsedPlaces.length} places)
+ +
+ +
+ ${this.importParsedPlaces.map((p, i) => ` + + `).join("")} +
+ + +
+ `; + + modal.querySelectorAll("[data-place-idx]").forEach(cb => { + cb.addEventListener("change", (e) => { + const idx = parseInt((cb as HTMLElement).dataset.placeIdx!, 10); + this.importParsedPlaces[idx].selected = (e.target as HTMLInputElement).checked; + const btn = modal!.querySelector("#import-confirm"); + const count = this.importParsedPlaces.filter(p => p.selected).length; + if (btn) btn.textContent = `Import ${count} Places as Waypoints`; + }); + }); + + modal.querySelector("#import-confirm")?.addEventListener("click", () => { + for (const p of this.importParsedPlaces) { + if (!p.selected) continue; + this.sync?.addWaypoint({ + id: crypto.randomUUID(), + name: p.name, + emoji: "\u{1F4CD}", + latitude: p.lat, + longitude: p.lng, + createdBy: this.participantId, + createdAt: new Date().toISOString(), + type: "poi", + }); + } + this.importStep = "done"; + this.renderImportModal(); + }); + } else if (this.importStep === "done") { + const count = this.importParsedPlaces.filter(p => p.selected).length; + modal.innerHTML = ` +
+
\u2705
+
Imported ${count} places!
+
They've been added as waypoints to this room.
+ +
+ `; + modal.querySelector("#import-done-btn")?.addEventListener("click", () => { + this.showImportModal = false; + modal?.remove(); + }); + } + + // Close handlers (shared) + modal.querySelector("#import-close")?.addEventListener("click", () => { + this.showImportModal = false; + modal?.remove(); + }); + modal.addEventListener("click", (e) => { + if (e.target === modal) { this.showImportModal = false; modal?.remove(); } + }); + } + + // ─── Route display ────────────────────────────────────────── + + private showRoute(route: { segments: any[]; totalDistance: number; estimatedTime: number }, destination: string) { if (!this.map) return; - const center = this.map.getCenter(); - const name = prompt("Waypoint name:", "Meeting point"); - if (!name?.trim()) return; - this.sync?.addWaypoint({ - id: crypto.randomUUID(), - name: name.trim(), - emoji: "\u{1F4CD}", - latitude: center.lat, - longitude: center.lng, - createdBy: this.participantId, - createdAt: new Date().toISOString(), - type: "meeting", + this.clearRoute(); + this.activeRoute = { ...route, destination }; + + const segmentColors: Record = { outdoor: "#3b82f6", indoor: "#8b5cf6", transition: "#f97316" }; + + route.segments.forEach((seg, i) => { + const sourceId = `route-seg-${i}`; + const layerId = `route-layer-${i}`; + this.map.addSource(sourceId, { + type: "geojson", + data: { + type: "Feature", + properties: {}, + geometry: { type: "LineString", coordinates: seg.coordinates }, + }, + }); + this.map.addLayer({ + id: layerId, + type: "line", + source: sourceId, + layout: { "line-join": "round", "line-cap": "round" }, + paint: { + "line-color": segmentColors[seg.type] || "#3b82f6", + "line-width": 5, + "line-opacity": 0.8, + ...(seg.type === "transition" ? { "line-dasharray": [2, 2] } : {}), + }, + }); + }); + + this.fitMapToRoute(route); + this.renderRoutePanel(); + } + + private clearRoute() { + if (!this.map) return; + // Remove all route layers/sources + for (let i = 0; i < 10; i++) { + try { this.map.removeLayer(`route-layer-${i}`); } catch {} + try { this.map.removeSource(`route-seg-${i}`); } catch {} + } + this.activeRoute = null; + const routePanel = this.shadow.getElementById("route-panel"); + if (routePanel) routePanel.remove(); + } + + private fitMapToRoute(route: { segments: any[] }) { + if (!this.map || !(window as any).maplibregl) return; + const bounds = new (window as any).maplibregl.LngLatBounds(); + for (const seg of route.segments) { + for (const coord of seg.coordinates) { + bounds.extend(coord); + } + } + if (!bounds.isEmpty()) { + this.map.fitBounds(bounds, { padding: 60, maxZoom: 16 }); + } + } + + private renderRoutePanel() { + if (!this.activeRoute) return; + let routePanel = this.shadow.getElementById("route-panel"); + if (!routePanel) { + routePanel = document.createElement("div"); + routePanel.id = "route-panel"; + routePanel.style.cssText = ` + position:absolute;bottom:12px;left:12px;right:12px;z-index:5; + background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong); + border-radius:10px;padding:14px;box-shadow:0 4px 16px rgba(0,0,0,0.3); + `; + this.shadow.getElementById("map-container")?.appendChild(routePanel); + } + + const segTypeLabels: Record = { outdoor: "Outdoor", indoor: "Indoor", transition: "Transition" }; + const segTypeColors: Record = { outdoor: "#3b82f6", indoor: "#8b5cf6", transition: "#f97316" }; + + routePanel.innerHTML = ` +
+
+ Route to ${this.esc(this.activeRoute.destination)} +
+ +
+
+ \u{1F4CF} ${formatDistance(this.activeRoute.totalDistance)} + \u{23F1} ${formatTime(this.activeRoute.estimatedTime)} +
+
+ ${this.activeRoute.segments.map(seg => ` + + ${segTypeLabels[seg.type] || seg.type}: ${formatDistance(seg.distance)} + + `).join("")} +
+ `; + routePanel.querySelector("#close-route")?.addEventListener("click", () => this.clearRoute()); + } + + // ─── Navigation panel (participant/waypoint selection) ─────── + + private async requestRoute(targetLat: number, targetLng: number, targetName: string) { + // Get user's current location + const myState = this.sync?.getState().participants[this.participantId]; + if (!myState?.location) { + this.error = "Share your location first to get directions"; + return; + } + + const base = this.getApiBase(); + try { + const res = await fetch(`${base}/api/routing`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + from: { lat: myState.location.latitude, lng: myState.location.longitude }, + to: { lat: targetLat, lng: targetLng }, + mode: "walking", + }), + signal: AbortSignal.timeout(12000), + }); + if (res.ok) { + const data = await res.json(); + if (data.success && data.route) { + this.showRoute(data.route, targetName); + } else { + this.error = "No route found"; + } + } + } catch { + this.error = "Routing request failed"; + } + } + + private renderNavigationPanel() { + let navPanel = this.shadow.getElementById("nav-panel"); + + // Get target details + const state = this.sync?.getState(); + let targetName = ""; + let targetLat = 0; + let targetLng = 0; + let targetEmoji = ""; + let targetDetail = ""; + + if (this.selectedParticipant && state) { + const p = state.participants[this.selectedParticipant]; + if (p?.location) { + targetName = p.name; + targetEmoji = p.emoji; + targetLat = p.location.latitude; + targetLng = p.location.longitude; + const myLoc = state.participants[this.participantId]?.location; + if (myLoc) { + targetDetail = formatDistance(haversineDistance(myLoc.latitude, myLoc.longitude, targetLat, targetLng)) + " away"; + } + } + } else if (this.selectedWaypoint && state) { + const wp = state.waypoints.find(w => w.id === this.selectedWaypoint); + if (wp) { + targetName = wp.name; + targetEmoji = wp.emoji || "\u{1F4CD}"; + targetLat = wp.latitude; + targetLng = wp.longitude; + const myLoc = state.participants[this.participantId]?.location; + if (myLoc) { + targetDetail = formatDistance(haversineDistance(myLoc.latitude, myLoc.longitude, targetLat, targetLng)) + " away"; + } + } + } + + if (!targetName) { + if (navPanel) navPanel.remove(); + return; + } + + if (!navPanel) { + navPanel = document.createElement("div"); + navPanel.id = "nav-panel"; + navPanel.style.cssText = ` + position:absolute;top:12px;left:12px;z-index:5; + background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong); + border-radius:10px;padding:12px;box-shadow:0 4px 16px rgba(0,0,0,0.3); + min-width:180px; + `; + this.shadow.getElementById("map-container")?.appendChild(navPanel); + } + + navPanel.innerHTML = ` +
+ ${targetEmoji} +
+
${this.esc(targetName)}
+ ${targetDetail ? `
${targetDetail}
` : ""} +
+ +
+ + `; + + navPanel.querySelector("#close-nav")?.addEventListener("click", () => { + this.selectedParticipant = null; + this.selectedWaypoint = null; + navPanel?.remove(); + }); + navPanel.querySelector("#navigate-btn")?.addEventListener("click", () => { + this.requestRoute(targetLat, targetLng, targetName); }); } @@ -1396,6 +2228,9 @@ class FolkMapViewer extends HTMLElement { .ctrl-btn.sharing { border-color: #22c55e; color: #22c55e; animation: pulse 2s infinite; } + .ctrl-btn.ghost { + border-color: #8b5cf6; color: #8b5cf6; + } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } @@ -1528,12 +2363,13 @@ class FolkMapViewer extends HTMLElement { } private renderMap(): string { - const shareUrl = `${window.location.origin}/${this.space}/maps/${this.room}`; return `
${this._history.canGoBack ? '' : ''} - 🗺 ${this.esc(this.room)} + \u{1F5FA} ${this.esc(this.room)} + +
@@ -1543,21 +2379,19 @@ class FolkMapViewer extends HTMLElement {
-
Connecting...
+
Connecting...
- - - + + + +
- +
`; } @@ -1588,12 +2422,24 @@ class FolkMapViewer extends HTMLElement { this.dropWaypoint(); }); - const copyUrl = this.shadow.getElementById("copy-url") || this.shadow.getElementById("copy-link"); - copyUrl?.addEventListener("click", () => { - const url = `${window.location.origin}/${this.space}/maps/${this.room}`; - navigator.clipboard.writeText(url).then(() => { - if (copyUrl) copyUrl.textContent = "Copied!"; - setTimeout(() => { if (copyUrl) copyUrl.textContent = "Copy"; }, 2000); + this.shadow.getElementById("share-room-btn")?.addEventListener("click", () => { + this.showShareModal = true; + this.renderShareModal(); + }); + + this.shadow.getElementById("privacy-toggle")?.addEventListener("click", () => { + this.showPrivacyPanel = !this.showPrivacyPanel; + const panel = this.shadow.getElementById("privacy-panel"); + if (panel) { + panel.style.display = this.showPrivacyPanel ? "block" : "none"; + if (this.showPrivacyPanel) this.renderPrivacyPanel(); + } + }); + + this.shadow.getElementById("bell-toggle")?.addEventListener("click", () => { + this.pushManager?.toggle(this.room, this.participantId).then(subscribed => { + const bell = this.shadow.getElementById("bell-toggle"); + if (bell) bell.textContent = subscribed ? "\u{1F514}" : "\u{1F515}"; }); }); diff --git a/modules/rmaps/components/map-sync.ts b/modules/rmaps/components/map-sync.ts index 7294bf9..d669231 100644 --- a/modules/rmaps/components/map-sync.ts +++ b/modules/rmaps/components/map-sync.ts @@ -3,6 +3,20 @@ * Ported from rmaps-online/src/lib/sync.ts (simplified — no @/types dependency). */ +// ── Typed unions ────────────────────────────────────────────── + +export type ParticipantStatus = "online" | "away" | "ghost" | "offline"; +export type WaypointType = "meeting" | "poi" | "parking" | "food" | "danger" | "custom"; +export type LocationSource = "gps" | "network" | "manual" | "ip" | "indoor"; +export type PrecisionLevel = "exact" | "building" | "area" | "approximate"; + +export interface PrivacySettings { + precision: PrecisionLevel; + ghostMode: boolean; +} + +// ── State interfaces ────────────────────────────────────────── + export interface RoomState { id: string; slug: string; @@ -19,7 +33,7 @@ export interface ParticipantState { color: string; joinedAt: string; lastSeen: string; - status: string; + status: ParticipantStatus; location?: LocationState; } @@ -31,7 +45,7 @@ export interface LocationState { heading?: number; speed?: number; timestamp: string; - source: string; + source: LocationSource; indoor?: { level: number; x: number; y: number; spaceName?: string }; } @@ -44,14 +58,14 @@ export interface WaypointState { indoor?: { level: number; x: number; y: number }; createdBy: string; createdAt: string; - type: string; + type: WaypointType; } export type SyncMessage = | { type: "join"; participant: ParticipantState } | { type: "leave"; participantId: string } | { type: "location"; participantId: string; location: LocationState } - | { type: "status"; participantId: string; status: string } + | { type: "status"; participantId: string; status: ParticipantStatus } | { type: "waypoint_add"; waypoint: WaypointState } | { type: "waypoint_remove"; waypointId: string } | { type: "full_state"; state: RoomState } @@ -263,7 +277,7 @@ export class RoomSync { } } - updateStatus(status: string): void { + updateStatus(status: ParticipantStatus): void { if (this.state.participants[this.participantId]) { this.state.participants[this.participantId].status = status; this.state.participants[this.participantId].lastSeen = new Date().toISOString(); diff --git a/modules/rmaps/mod.ts b/modules/rmaps/mod.ts index de0c5fe..f0b16f9 100644 --- a/modules/rmaps/mod.ts +++ b/modules/rmaps/mod.ts @@ -86,22 +86,111 @@ routes.post("/api/push/request-location", async (c) => { // ── Proxy: routing (OSRM + c3nav) ── routes.post("/api/routing", async (c) => { const body = await c.req.json(); - const { from, to, mode = "walking" } = body; + const { from, to, mode = "walking", accessibility, indoor } = body; if (!from?.lat || !from?.lng || !to?.lat || !to?.lng) { return c.json({ error: "from and to with lat/lng required" }, 400); } - // Use OSRM for outdoor routing + const segments: { type: string; coordinates: [number, number][]; distance: number; duration: number; steps?: any[] }[] = []; + let totalDistance = 0; + let estimatedTime = 0; + + // ── Indoor routing via c3nav ── + if (indoor?.event && (from.indoor || to.indoor)) { + try { + // If both are indoor, route fully indoors + if (from.indoor && to.indoor) { + const c3navRes = await fetch(`https://${indoor.event}.c3nav.de/api/v2/routing/route/`, { + method: "POST", + headers: { "Content-Type": "application/json", "X-API-Key": "anonymous", Accept: "application/json", "User-Agent": "rMaps/1.0" }, + body: JSON.stringify({ + origin: { level: from.indoor.level, x: from.indoor.x, y: from.indoor.y }, + destination: { level: to.indoor.level, x: to.indoor.x, y: to.indoor.y }, + ...(accessibility?.avoidStairs ? { options: { avoid_stairs: true } } : {}), + ...(accessibility?.wheelchair ? { options: { wheelchair: true } } : {}), + }), + signal: AbortSignal.timeout(8000), + }); + if (c3navRes.ok) { + const data = await c3navRes.json(); + const coords: [number, number][] = data.path?.map((p: any) => [p.lng || p.x, p.lat || p.y]) || []; + const dist = data.distance || 0; + const dur = data.duration || Math.round(dist / 1.2); + segments.push({ type: "indoor", coordinates: coords, distance: dist, duration: dur }); + totalDistance += dist; + estimatedTime += dur; + } + } else { + // Mixed indoor/outdoor: transition segment + outdoor OSRM + const outdoorPoint = from.indoor ? to : from; + const indoorPoint = from.indoor ? from : to; + const transitionCoords: [number, number][] = [ + [indoorPoint.lng, indoorPoint.lat], + [outdoorPoint.lng, outdoorPoint.lat], + ]; + segments.push({ type: "transition", coordinates: transitionCoords, distance: 50, duration: 30 }); + totalDistance += 50; + estimatedTime += 30; + + // OSRM for the outdoor portion + const profile = mode === "driving" ? "car" : "foot"; + const osrmRes = await fetch( + `https://router.project-osrm.org/route/v1/${profile}/${outdoorPoint.lng},${outdoorPoint.lat};${(from.indoor ? to : from).lng},${(from.indoor ? to : from).lat}?overview=full&geometries=geojson&steps=true`, + { signal: AbortSignal.timeout(10000) }, + ); + if (osrmRes.ok) { + const osrmData = await osrmRes.json(); + const route = osrmData.routes?.[0]; + if (route) { + segments.push({ + type: "outdoor", + coordinates: route.geometry?.coordinates || [], + distance: route.distance || 0, + duration: route.duration || 0, + steps: route.legs?.[0]?.steps, + }); + totalDistance += route.distance || 0; + estimatedTime += route.duration || 0; + } + } + } + + if (segments.length > 0) { + return c.json({ + success: true, + route: { segments, totalDistance, estimatedTime: Math.round(estimatedTime) }, + }); + } + } catch { /* fall through to outdoor-only routing */ } + } + + // ── Outdoor-only routing via OSRM ── const profile = mode === "driving" ? "car" : "foot"; try { const res = await fetch( `https://router.project-osrm.org/route/v1/${profile}/${from.lng},${from.lat};${to.lng},${to.lat}?overview=full&geometries=geojson&steps=true`, - { signal: AbortSignal.timeout(10000) } + { signal: AbortSignal.timeout(10000) }, ); if (res.ok) { const data = await res.json(); - return c.json(data); + const route = data.routes?.[0]; + if (route) { + return c.json({ + success: true, + route: { + segments: [{ + type: "outdoor", + coordinates: route.geometry?.coordinates || [], + distance: route.distance || 0, + duration: route.duration || 0, + steps: route.legs?.[0]?.steps, + }], + totalDistance: route.distance || 0, + estimatedTime: Math.round(route.duration || 0), + }, + }); + } } } catch {} return c.json({ error: "Routing failed" }, 502); @@ -134,7 +223,7 @@ routes.get("/api/c3nav/:event", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — Maps | rSpace`, moduleId: "rmaps", @@ -149,7 +238,7 @@ routes.get("/", (c) => { // Room-specific page routes.get("/:room", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const room = c.req.param("room"); return c.html(renderShell({ title: `${room} — Maps | rSpace`, diff --git a/modules/rnetwork/mod.ts b/modules/rnetwork/mod.ts index 5e6c7ed..8f4013a 100644 --- a/modules/rnetwork/mod.ts +++ b/modules/rnetwork/mod.ts @@ -67,7 +67,7 @@ const CACHE_TTL = 60_000; // ── API: Health ── routes.get("/api/health", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = getTokenForSpace(dataSpace); return c.json({ ok: true, module: "network", space, twentyConfigured: !!token }); }); @@ -75,7 +75,7 @@ routes.get("/api/health", (c) => { // ── API: Info ── routes.get("/api/info", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = getTokenForSpace(dataSpace); return c.json({ module: "network", @@ -90,7 +90,7 @@ routes.get("/api/info", (c) => { // ── API: People ── routes.get("/api/people", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = getTokenForSpace(dataSpace); const data = await twentyQuery(`{ people(first: 200) { @@ -116,7 +116,7 @@ routes.get("/api/people", async (c) => { // ── API: Companies ── routes.get("/api/companies", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = getTokenForSpace(dataSpace); const data = await twentyQuery(`{ companies(first: 200) { @@ -194,7 +194,7 @@ routes.get("/api/delegations", async (c) => { // ── API: Graph — transform entities to node/edge format ── routes.get("/api/graph", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = getTokenForSpace(dataSpace); // Check per-space cache (keyed by space + trust params) @@ -540,7 +540,7 @@ routes.get("/api/workspaces", (c) => { // ── API: Opportunities ── routes.get("/api/opportunities", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = getTokenForSpace(dataSpace); const data = await twentyQuery(`{ opportunities(first: 200) { diff --git a/modules/rnotes/mod.ts b/modules/rnotes/mod.ts index 003a319..0b9869d 100644 --- a/modules/rnotes/mod.ts +++ b/modules/rnotes/mod.ts @@ -250,7 +250,7 @@ function extractPlainText(content: string, format?: string): string { // GET /api/notebooks — list notebooks routes.get("/api/notebooks", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const notebooks = listNotebooks(dataSpace).map(({ doc }) => notebookToRest(doc)); notebooks.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()); @@ -260,7 +260,7 @@ routes.get("/api/notebooks", async (c) => { // POST /api/notebooks — create notebook routes.post("/api/notebooks", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); let claims; @@ -292,7 +292,7 @@ routes.post("/api/notebooks", async (c) => { // GET /api/notebooks/:id — notebook detail with notes routes.get("/api/notebooks/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = notebookDocId(dataSpace, id); @@ -314,7 +314,7 @@ routes.get("/api/notebooks/:id", async (c) => { // PUT /api/notebooks/:id — update notebook routes.put("/api/notebooks/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); let claims; @@ -349,7 +349,7 @@ routes.put("/api/notebooks/:id", async (c) => { // DELETE /api/notebooks/:id routes.delete("/api/notebooks/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = notebookDocId(dataSpace, id); @@ -376,7 +376,7 @@ routes.delete("/api/notebooks/:id", async (c) => { // GET /api/notes — list all notes routes.get("/api/notes", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const { notebook_id, type, q, limit = "50", offset = "0" } = c.req.query(); let allNotes: ReturnType[] = []; @@ -412,7 +412,7 @@ routes.get("/api/notes", async (c) => { // POST /api/notes — create note routes.post("/api/notes", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); let claims; @@ -464,7 +464,7 @@ routes.post("/api/notes", async (c) => { // GET /api/notes/:id — note detail routes.get("/api/notes/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const found = findNote(dataSpace, id); @@ -476,7 +476,7 @@ routes.get("/api/notes/:id", async (c) => { // PUT /api/notes/:id — update note routes.put("/api/notes/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const body = await c.req.json(); const { title, content, content_format, type, url, language, is_pinned, sort_order } = body; @@ -517,7 +517,7 @@ routes.put("/api/notes/:id", async (c) => { // DELETE /api/notes/:id routes.delete("/api/notes/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const found = findNote(dataSpace, id); @@ -601,7 +601,7 @@ function getConnectionDoc(space: string): ConnectionsDoc | null { // POST /api/import/upload — ZIP upload for Logseq/Obsidian routes.post("/api/import/upload", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } @@ -653,7 +653,7 @@ routes.post("/api/import/upload", async (c) => { // POST /api/import/notion — Import selected Notion pages routes.post("/api/import/notion", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } @@ -699,7 +699,7 @@ routes.post("/api/import/notion", async (c) => { // POST /api/import/google-docs — Import selected Google Docs routes.post("/api/import/google-docs", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } @@ -744,7 +744,7 @@ routes.post("/api/import/google-docs", async (c) => { // GET /api/import/notion/pages — Browse Notion pages for selection routes.get("/api/import/notion/pages", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const conn = getConnectionDoc(dataSpace); if (!conn?.notion?.accessToken) { return c.json({ error: "Notion not connected" }, 400); @@ -787,7 +787,7 @@ routes.get("/api/import/notion/pages", async (c) => { // GET /api/import/google-docs/list — Browse Google Docs for selection routes.get("/api/import/google-docs/list", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const conn = getConnectionDoc(dataSpace); if (!conn?.google?.accessToken) { return c.json({ error: "Google not connected" }, 400); @@ -818,7 +818,7 @@ routes.get("/api/import/google-docs/list", async (c) => { // GET /api/export/obsidian — Download Obsidian-format ZIP routes.get("/api/export/obsidian", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const notebookId = c.req.query("notebookId"); if (!notebookId) return c.json({ error: "notebookId is required" }, 400); @@ -841,7 +841,7 @@ routes.get("/api/export/obsidian", async (c) => { // GET /api/export/logseq — Download Logseq-format ZIP routes.get("/api/export/logseq", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const notebookId = c.req.query("notebookId"); if (!notebookId) return c.json({ error: "notebookId is required" }, 400); @@ -864,7 +864,7 @@ routes.get("/api/export/logseq", async (c) => { // GET /api/export/markdown — Download universal Markdown ZIP routes.get("/api/export/markdown", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const notebookId = c.req.query("notebookId"); const noteIds = c.req.query("noteIds"); @@ -904,7 +904,7 @@ routes.get("/api/export/markdown", async (c) => { // POST /api/export/notion — Push notes to Notion routes.post("/api/export/notion", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } @@ -944,7 +944,7 @@ routes.post("/api/export/notion", async (c) => { // POST /api/export/google-docs — Push notes to Google Docs routes.post("/api/export/google-docs", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } @@ -984,7 +984,7 @@ routes.post("/api/export/google-docs", async (c) => { // GET /api/connections — Status of all integrations routes.get("/api/connections", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const conn = getConnectionDoc(dataSpace); return c.json({ @@ -1315,7 +1315,7 @@ routes.get("/voice", (c) => { routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — Notes | rSpace`, moduleId: "rnotes", diff --git a/modules/rphotos/mod.ts b/modules/rphotos/mod.ts index f013d3f..e19a207 100644 --- a/modules/rphotos/mod.ts +++ b/modules/rphotos/mod.ts @@ -110,7 +110,7 @@ routes.get("/api/assets/:id/original", async (c) => { // ── Embedded Immich UI ── routes.get("/album", (c) => { const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; return c.html(renderExternalAppShell({ title: `${spaceSlug} — Immich | rSpace`, moduleId: "rphotos", @@ -125,7 +125,7 @@ routes.get("/album", (c) => { // ── Page route ── routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; return c.html(renderShell({ title: `${spaceSlug} — Photos | rSpace`, moduleId: "rphotos", diff --git a/modules/rpubs/mod.ts b/modules/rpubs/mod.ts index 297ff0b..b1907e7 100644 --- a/modules/rpubs/mod.ts +++ b/modules/rpubs/mod.ts @@ -328,7 +328,7 @@ routes.get("/zine", (c) => { // ── Page: Editor ── routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "personal"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; return c.html(renderShell({ title: `${spaceSlug} — rPubs Editor | rSpace`, moduleId: "rpubs", diff --git a/modules/rschedule/mod.ts b/modules/rschedule/mod.ts index 2e6c098..00ab567 100644 --- a/modules/rschedule/mod.ts +++ b/modules/rschedule/mod.ts @@ -799,7 +799,7 @@ function seedDefaultJobs(space: string) { // GET / — serve schedule UI routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html( renderShell({ title: `${space} — Schedule | rSpace`, @@ -817,7 +817,7 @@ routes.get("/", (c) => { // GET /api/jobs — list all jobs routes.get("/api/jobs", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); const jobs = Object.values(doc.jobs).map((j) => ({ ...j, @@ -830,7 +830,7 @@ routes.get("/api/jobs", (c) => { // POST /api/jobs — create a new job routes.post("/api/jobs", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const body = await c.req.json(); const { name, description, cronExpression, timezone, actionType, actionConfig, enabled } = body; @@ -878,7 +878,7 @@ routes.post("/api/jobs", async (c) => { // GET /api/jobs/:id routes.get("/api/jobs/:id", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -890,7 +890,7 @@ routes.get("/api/jobs/:id", (c) => { // PUT /api/jobs/:id — update a job routes.put("/api/jobs/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const body = await c.req.json(); @@ -933,7 +933,7 @@ routes.put("/api/jobs/:id", async (c) => { // DELETE /api/jobs/:id routes.delete("/api/jobs/:id", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = scheduleDocId(dataSpace); @@ -950,7 +950,7 @@ routes.delete("/api/jobs/:id", (c) => { // POST /api/jobs/:id/run — manually trigger a job routes.post("/api/jobs/:id/run", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = scheduleDocId(dataSpace); @@ -997,7 +997,7 @@ routes.post("/api/jobs/:id/run", async (c) => { // GET /api/log — execution log routes.get("/api/log", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); const log = [...doc.log].reverse(); // newest first return c.json({ count: log.length, results: log }); @@ -1006,7 +1006,7 @@ routes.get("/api/log", (c) => { // GET /api/log/:jobId — execution log filtered by job routes.get("/api/log/:jobId", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const jobId = c.req.param("jobId"); const doc = ensureDoc(dataSpace); const log = doc.log.filter((e) => e.jobId === jobId).reverse(); @@ -1156,7 +1156,7 @@ async function executeReminderEmail( // GET /api/reminders — list reminders routes.get("/api/reminders", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); let reminders = Object.values(doc.reminders); @@ -1185,7 +1185,7 @@ routes.get("/api/reminders", (c) => { // POST /api/reminders — create a reminder routes.post("/api/reminders", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const body = await c.req.json(); const { title, description, remindAt, allDay, timezone, notifyEmail, syncToCalendar, cronExpression } = body; @@ -1239,7 +1239,7 @@ routes.post("/api/reminders", async (c) => { // GET /api/reminders/:id — get single reminder routes.get("/api/reminders/:id", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -1251,7 +1251,7 @@ routes.get("/api/reminders/:id", (c) => { // PUT /api/reminders/:id — update a reminder routes.put("/api/reminders/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const body = await c.req.json(); @@ -1279,7 +1279,7 @@ routes.put("/api/reminders/:id", async (c) => { // DELETE /api/reminders/:id — delete (cascades to calendar) routes.delete("/api/reminders/:id", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = scheduleDocId(dataSpace); @@ -1302,7 +1302,7 @@ routes.delete("/api/reminders/:id", (c) => { // POST /api/reminders/:id/complete — mark completed routes.post("/api/reminders/:id/complete", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = scheduleDocId(dataSpace); @@ -1323,7 +1323,7 @@ routes.post("/api/reminders/:id/complete", (c) => { // POST /api/reminders/:id/snooze — reschedule to a new date routes.post("/api/reminders/:id/snooze", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const body = await c.req.json(); @@ -1384,7 +1384,7 @@ routes.get("/reminders", (c) => { routes.get("/api/workflows", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); // Ensure workflows field exists on older docs const workflows = Object.values(doc.workflows || {}); @@ -1394,7 +1394,7 @@ routes.get("/api/workflows", (c) => { routes.post("/api/workflows", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const body = await c.req.json(); const docId = scheduleDocId(dataSpace); @@ -1426,7 +1426,7 @@ routes.post("/api/workflows", async (c) => { routes.get("/api/workflows/:id", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -1437,7 +1437,7 @@ routes.get("/api/workflows/:id", (c) => { routes.put("/api/workflows/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const body = await c.req.json(); @@ -1468,7 +1468,7 @@ routes.put("/api/workflows/:id", async (c) => { routes.delete("/api/workflows/:id", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = scheduleDocId(dataSpace); @@ -1919,7 +1919,7 @@ function appendWorkflowLog( // POST /api/workflows/:id/run — manual execute routes.post("/api/workflows/:id/run", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = scheduleDocId(dataSpace); @@ -1947,7 +1947,7 @@ routes.post("/api/workflows/:id/run", async (c) => { // POST /api/workflows/webhook/:hookId — external webhook trigger routes.post("/api/workflows/webhook/:hookId", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const hookId = c.req.param("hookId"); const doc = ensureDoc(dataSpace); @@ -1981,7 +1981,7 @@ routes.post("/api/workflows/webhook/:hookId", async (c) => { // GET /api/workflows/log — workflow execution log routes.get("/api/workflows/log", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); const log = [...(doc.workflowLog || [])].reverse(); // newest first return c.json({ count: log.length, results: log }); diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts index f790da9..8581885 100644 --- a/modules/rsocials/mod.ts +++ b/modules/rsocials/mod.ts @@ -195,7 +195,7 @@ routes.get("/api/feed", (c) => routes.post("/api/threads/:id/image", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400); @@ -240,7 +240,7 @@ routes.post("/api/threads/:id/image", async (c) => { routes.post("/api/threads/:id/upload-image", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400); @@ -277,7 +277,7 @@ routes.post("/api/threads/:id/upload-image", async (c) => { routes.post("/api/threads/:id/tweet/:index/upload-image", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const index = c.req.param("index"); if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400); @@ -318,7 +318,7 @@ routes.post("/api/threads/:id/tweet/:index/upload-image", async (c) => { routes.post("/api/threads/:id/tweet/:index/image", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const index = c.req.param("index"); if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400); @@ -374,7 +374,7 @@ routes.post("/api/threads/:id/tweet/:index/image", async (c) => { routes.delete("/api/threads/:id/tweet/:index/image", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const index = c.req.param("index"); if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400); @@ -403,7 +403,7 @@ routes.delete("/api/threads/:id/tweet/:index/image", async (c) => { routes.delete("/api/threads/:id/images", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400); @@ -640,7 +640,7 @@ Rules: routes.get("/api/campaign-workflows", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); const workflows = Object.values(doc.campaignWorkflows || {}); workflows.sort((a, b) => a.name.localeCompare(b.name)); @@ -649,7 +649,7 @@ routes.get("/api/campaign-workflows", (c) => { routes.post("/api/campaign-workflows", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const body = await c.req.json(); const docId = socialsDocId(dataSpace); @@ -681,7 +681,7 @@ routes.post("/api/campaign-workflows", async (c) => { routes.get("/api/campaign-workflows/:id", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -692,7 +692,7 @@ routes.get("/api/campaign-workflows/:id", (c) => { routes.put("/api/campaign-workflows/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const body = await c.req.json(); @@ -722,7 +722,7 @@ routes.put("/api/campaign-workflows/:id", async (c) => { routes.delete("/api/campaign-workflows/:id", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const docId = socialsDocId(dataSpace); @@ -739,7 +739,7 @@ routes.delete("/api/campaign-workflows/:id", (c) => { // POST /api/campaign-workflows/:id/run — manual execute (stub) routes.post("/api/campaign-workflows/:id/run", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -777,7 +777,7 @@ routes.post("/api/campaign-workflows/:id/run", async (c) => { routes.post("/api/campaign-workflows/webhook/:hookId", async (c) => { const hookId = c.req.param("hookId"); const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const doc = ensureDoc(dataSpace); // Find workflow containing a webhook-trigger node with this hookId @@ -831,7 +831,7 @@ function topologicalSortCampaign(nodes: CampaignWorkflowNode[], edges: CampaignW routes.get("/campaign", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `Campaign — rSocials | rSpace`, moduleId: "rsocials", @@ -846,7 +846,7 @@ routes.get("/campaign", (c) => { routes.get("/thread/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); if (!id || id.includes("..") || id.includes("/")) return c.text("Not found", 404); @@ -889,7 +889,7 @@ routes.get("/thread/:id", async (c) => { routes.get("/thread-editor/:id/edit", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const id = c.req.param("id"); if (!id || id.includes("..") || id.includes("/")) return c.text("Not found", 404); @@ -912,7 +912,7 @@ routes.get("/thread-editor/:id/edit", async (c) => { routes.get("/thread-editor", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `Thread Editor — rSocials | rSpace`, moduleId: "rsocials", @@ -927,7 +927,7 @@ routes.get("/thread-editor", (c) => { routes.get("/threads", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `Threads — rSocials | rSpace`, moduleId: "rsocials", @@ -999,7 +999,7 @@ const POSTIZ_URL = process.env.POSTIZ_URL || "https://demo.rsocials.online"; routes.get("/scheduler", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderExternalAppShell({ title: `Post Scheduler — rSocials | rSpace`, moduleId: "rsocials", @@ -1039,7 +1039,7 @@ routes.get("/newsletter-list", async (c) => { routes.get("/feed", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const isDemo = space === "demo"; const body = isDemo ? renderDemoFeedHTML() : renderLanding(); const styles = isDemo @@ -1058,7 +1058,7 @@ routes.get("/feed", (c) => { routes.get("/landing", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — rSocials | rSpace`, moduleId: "rsocials", diff --git a/modules/rsplat/mod.ts b/modules/rsplat/mod.ts index 305e2ea..46eca3e 100644 --- a/modules/rsplat/mod.ts +++ b/modules/rsplat/mod.ts @@ -205,7 +205,7 @@ const routes = new Hono(); // ── API: List splats ── routes.get("/api/splats", async (c) => { const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const tag = c.req.query("tag"); const limit = Math.min(parseInt(c.req.query("limit") || "50"), 100); const offset = parseInt(c.req.query("offset") || "0"); @@ -231,7 +231,7 @@ routes.get("/api/splats", async (c) => { // ── API: Get splat details ── routes.get("/api/splats/:id", async (c) => { const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -256,7 +256,7 @@ routes.get("/api/splats/:id", async (c) => { // Matches both /api/splats/:id/file and /api/splats/:id/:filename (e.g. rainbow-sphere.splat) routes.get("/api/splats/:id/:filename", async (c) => { const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -310,7 +310,7 @@ routes.post("/api/splats", async (c) => { } const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const formData = await c.req.formData(); const file = formData.get("file") as File | null; const title = (formData.get("title") as string || "").trim(); @@ -422,7 +422,7 @@ routes.post("/api/splats/from-media", async (c) => { } const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const formData = await c.req.formData(); const title = (formData.get("title") as string || "").trim(); const description = (formData.get("description") as string || "").trim() || null; @@ -554,7 +554,7 @@ routes.delete("/api/splats/:id", async (c) => { } const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); @@ -580,7 +580,7 @@ routes.delete("/api/splats/:id", async (c) => { // ── Page: Gallery ── routes.get("/", async (c) => { const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const doc = ensureDoc(dataSpace); @@ -619,7 +619,7 @@ routes.get("/", async (c) => { // ── Page: Viewer ── routes.get("/view/:id", async (c) => { const spaceSlug = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || spaceSlug; + const dataSpace = c.get("effectiveSpace") || spaceSlug; const id = c.req.param("id"); const doc = ensureDoc(dataSpace); diff --git a/modules/rswag/mod.ts b/modules/rswag/mod.ts index aa9d6d5..ee3c87e 100644 --- a/modules/rswag/mod.ts +++ b/modules/rswag/mod.ts @@ -230,7 +230,7 @@ routes.get("/api/artifact/:id", async (c) => { // ── Page route: swag designer ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `Swag Designer | rSpace`, moduleId: "rswag", diff --git a/modules/rtasks/mod.ts b/modules/rtasks/mod.ts index 696de10..201f645 100644 --- a/modules/rtasks/mod.ts +++ b/modules/rtasks/mod.ts @@ -455,7 +455,7 @@ routes.get("/api/spaces/:slug/activity", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — Tasks | rSpace`, moduleId: "rtasks", diff --git a/modules/rtrips/mod.ts b/modules/rtrips/mod.ts index fd0a599..a14e207 100644 --- a/modules/rtrips/mod.ts +++ b/modules/rtrips/mod.ts @@ -68,7 +68,7 @@ const routes = new Hono(); // GET /api/trips — list trips routes.get("/api/trips", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const docIds = listTripDocIds(dataSpace); const rows = docIds.map((docId) => { @@ -103,7 +103,7 @@ routes.post("/api/trips", async (c) => { if (!title?.trim()) return c.json({ error: "Title required" }, 400); const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = newId(); const slug = title.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); const now = Date.now(); @@ -142,7 +142,7 @@ routes.post("/api/trips", async (c) => { // GET /api/trips/:id — trip detail with all sub-resources routes.get("/api/trips/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = c.req.param("id"); const docId = tripDocId(dataSpace, tripId); const doc = _syncServer!.getDoc(docId); @@ -166,7 +166,7 @@ routes.get("/api/trips/:id", async (c) => { // PUT /api/trips/:id — update trip routes.put("/api/trips/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = c.req.param("id"); const docId = tripDocId(dataSpace, tripId); const doc = _syncServer!.getDoc(docId); @@ -202,7 +202,7 @@ routes.post("/api/trips/:id/destinations", async (c) => { try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = c.req.param("id"); ensureDoc(dataSpace, tripId); const docId = tripDocId(dataSpace, tripId); @@ -239,7 +239,7 @@ routes.post("/api/trips/:id/itinerary", async (c) => { try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = c.req.param("id"); ensureDoc(dataSpace, tripId); const docId = tripDocId(dataSpace, tripId); @@ -276,7 +276,7 @@ routes.post("/api/trips/:id/bookings", async (c) => { try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = c.req.param("id"); ensureDoc(dataSpace, tripId); const docId = tripDocId(dataSpace, tripId); @@ -314,7 +314,7 @@ routes.post("/api/trips/:id/expenses", async (c) => { try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = c.req.param("id"); ensureDoc(dataSpace, tripId); const docId = tripDocId(dataSpace, tripId); @@ -346,7 +346,7 @@ routes.post("/api/trips/:id/expenses", async (c) => { routes.get("/api/trips/:id/packing", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = c.req.param("id"); const docId = tripDocId(dataSpace, tripId); const doc = _syncServer!.getDoc(docId); @@ -365,7 +365,7 @@ routes.post("/api/trips/:id/packing", async (c) => { try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const tripId = c.req.param("id"); ensureDoc(dataSpace, tripId); const docId = tripDocId(dataSpace, tripId); @@ -394,7 +394,7 @@ routes.post("/api/trips/:id/packing", async (c) => { routes.patch("/api/packing/:id", async (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; const packingId = c.req.param("id"); // Find the trip doc containing this packing item @@ -434,7 +434,7 @@ routes.post("/api/route", async (c) => { // ── Route planner page ── routes.get("/routes", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — Route Planner | rTrips`, moduleId: "rtrips", @@ -586,7 +586,7 @@ routes.get("/demo", (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — Trips | rSpace`, moduleId: "rtrips", diff --git a/modules/rtube/mod.ts b/modules/rtube/mod.ts index c700869..ad0d0bd 100644 --- a/modules/rtube/mod.ts +++ b/modules/rtube/mod.ts @@ -192,7 +192,7 @@ routes.get("/api/health", (c) => c.json({ ok: true })); // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - const dataSpace = (c.get("effectiveSpace" as any) as string) || space; + const dataSpace = c.get("effectiveSpace") || space; return c.html(renderShell({ title: `${space} — Tube | rSpace`, moduleId: "rtube", diff --git a/server/index.ts b/server/index.ts index 1b2ab17..311126b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1663,7 +1663,7 @@ for (const mod of getAllModules()) { // Resolve effective data space (global vs space-scoped) const overrides = doc?.meta?.moduleScopeOverrides ?? null; const effectiveSpace = resolveDataSpace(mod.id, space, overrides); - c.set("effectiveSpace" as any, effectiveSpace); + c.set("effectiveSpace", effectiveSpace); // Resolve caller's role for write-method blocking const method = c.req.method; @@ -1675,8 +1675,8 @@ for (const mod of getAllModules()) { } const resolved = await resolveCallerRole(space, claims); if (resolved) { - c.set("spaceRole" as any, resolved.role); - c.set("isOwner" as any, resolved.isOwner); + c.set("spaceRole", resolved.role); + c.set("isOwner", resolved.isOwner); if (resolved.role === "viewer") { return c.json({ error: "Write access required" }, 403); } diff --git a/types/hono.d.ts b/types/hono.d.ts new file mode 100644 index 0000000..42d68e2 --- /dev/null +++ b/types/hono.d.ts @@ -0,0 +1,9 @@ +import 'hono'; + +declare module 'hono' { + interface ContextVariableMap { + effectiveSpace: string; + spaceRole: string; + isOwner: boolean; + } +}