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/rcart/components/folk-payment-page.ts b/modules/rcart/components/folk-payment-page.ts index 109665b..504f820 100644 --- a/modules/rcart/components/folk-payment-page.ts +++ b/modules/rcart/components/folk-payment-page.ts @@ -500,6 +500,10 @@ class FolkPaymentPage extends HTMLElement { }; const explorer = explorerBase[p.chainId] || ''; + const cartLink = p.linkedCartId + ? `
Return to Cart
` + : ''; + return `
@@ -509,6 +513,7 @@ class FolkPaymentPage extends HTMLElement { ${p.txHash ? `
Transaction: ${p.txHash.slice(0, 10)}...${p.txHash.slice(-8)}
` : ''} ${p.paid_at ? `
Paid: ${new Date(p.paid_at).toLocaleString()}
` : ''}
+ ${cartLink} `; } diff --git a/modules/rcart/mod.ts b/modules/rcart/mod.ts index 65b897f..04bcb65 100644 --- a/modules/rcart/mod.ts +++ b/modules/rcart/mod.ts @@ -919,7 +919,7 @@ routes.get("/api/shopping-carts", async (c) => { // POST /api/shopping-carts — Create cart routes.post("/api/shopping-carts", async (c) => { const space = c.req.param("space") || "demo"; - const { name, description = "", targetAmount = 0, currency = "USD" } = await c.req.json(); + const { name, description = "", targetAmount = 0, currency = "USD", recipientAddress = null } = await c.req.json(); if (!name) return c.json({ error: "Required: name" }, 400); const cartId = crypto.randomUUID(); @@ -934,6 +934,7 @@ routes.post("/api/shopping-carts", async (c) => { d.cart.name = name; d.cart.description = description; d.cart.status = 'OPEN'; + d.cart.recipientAddress = recipientAddress || null; d.cart.targetAmount = targetAmount; d.cart.fundedAmount = 0; d.cart.currency = currency; @@ -969,6 +970,7 @@ routes.get("/api/shopping-carts/:cartId", async (c) => { name: doc.cart.name, description: doc.cart.description, status: doc.cart.status, + recipientAddress: doc.cart.recipientAddress || null, targetAmount: doc.cart.targetAmount, fundedAmount: doc.cart.fundedAmount, currency: doc.cart.currency, @@ -1191,6 +1193,58 @@ routes.post("/api/shopping-carts/:cartId/contribute", async (c) => { return c.json({ id: contribId, amount, fundedAmount: doc.cart.fundedAmount + amount }, 201); }); +// POST /api/shopping-carts/:cartId/contribute-pay — Create payment request for cart contribution +routes.post("/api/shopping-carts/:cartId/contribute-pay", async (c) => { + const space = c.req.param("space") || "demo"; + const cartId = c.req.param("cartId"); + const docId = shoppingCartDocId(space, cartId); + const doc = _syncServer!.getDoc(docId); + if (!doc) return c.json({ error: "Cart not found" }, 404); + + if (!doc.cart.recipientAddress) { + return c.json({ error: "Cart has no recipient wallet address" }, 400); + } + + const { amount, username = "Anonymous", chainId = 84532, token: payToken = "USDC" } = await c.req.json(); + if (typeof amount !== 'number' || amount <= 0) return c.json({ error: "amount must be > 0" }, 400); + + const paymentId = crypto.randomUUID(); + const now = Date.now(); + const payDocId = paymentRequestDocId(space, paymentId); + + const payDoc = Automerge.change(Automerge.init(), 'create cart contribution payment', (d) => { + const init = paymentRequestSchema.init(); + Object.assign(d, init); + d.meta.spaceSlug = space; + d.payment.id = paymentId; + d.payment.description = `Contribution to "${doc.cart.name}"`; + d.payment.amount = String(amount); + d.payment.amountEditable = false; + d.payment.token = payToken; + d.payment.chainId = chainId; + d.payment.recipientAddress = doc.cart.recipientAddress!; + d.payment.fiatAmount = String(amount); + d.payment.fiatCurrency = doc.cart.currency || 'USD'; + d.payment.creatorDid = ''; + d.payment.creatorUsername = username; + d.payment.status = 'pending'; + d.payment.paymentType = 'single'; + d.payment.maxPayments = 0; + d.payment.paymentCount = 0; + d.payment.enabledMethods = { card: true, wallet: true, encryptid: true }; + d.payment.linkedCartId = cartId; + d.payment.createdAt = now; + d.payment.updatedAt = now; + d.payment.expiresAt = 0; + }); + _syncServer!.setDoc(payDocId, payDoc); + + const host = c.req.header("host") || "rspace.online"; + const payUrl = `/${space}/rcart/pay/${paymentId}`; + + return c.json({ paymentId, payUrl, fullPayUrl: `https://${host}${payUrl}` }, 201); +}); + // ── Extension shortcut routes ── // POST /api/cart/quick-add — Simplified endpoint for extension @@ -1594,6 +1648,42 @@ routes.patch("/api/payments/:id/status", async (c) => { .catch((err) => console.error('[rcart] payment email failed:', err)); } + // Auto-record contribution on linked shopping cart + if (status === 'paid' && updated!.payment.linkedCartId) { + const linkedCartId = updated!.payment.linkedCartId; + const cartDocId = shoppingCartDocId(space, linkedCartId); + const cartDoc = _syncServer!.getDoc(cartDocId); + if (cartDoc) { + const contribAmount = parseFloat(updated!.payment.amount) || 0; + if (contribAmount > 0) { + const contribId = crypto.randomUUID(); + const contribNow = Date.now(); + _syncServer!.changeDoc(cartDocId, 'auto-record payment contribution', (d) => { + d.contributions[contribId] = { + userId: null, + username: updated!.payment.creatorUsername || 'Anonymous', + amount: contribAmount, + currency: d.cart.currency, + paymentMethod: updated!.payment.paymentMethod || 'wallet', + status: 'confirmed', + txHash: updated!.payment.txHash || null, + createdAt: contribNow, + updatedAt: contribNow, + }; + d.cart.fundedAmount = Math.round((d.cart.fundedAmount + contribAmount) * 100) / 100; + d.cart.updatedAt = contribNow; + d.events.push({ + type: 'contribution', + actor: updated!.payment.creatorUsername || 'Anonymous', + detail: `Paid $${contribAmount.toFixed(2)} via ${updated!.payment.paymentMethod || 'wallet'}`, + timestamp: contribNow, + }); + }); + reindexCart(space, linkedCartId); + } + } + } + return c.json(paymentToResponse(updated!.payment)); }); @@ -2022,6 +2112,7 @@ function paymentToResponse(p: PaymentRequestMeta) { paymentCount: p.paymentCount || 0, enabledMethods: p.enabledMethods || { card: true, wallet: true, encryptid: true }, creatorUsername: p.creatorUsername || '', + linkedCartId: p.linkedCartId || null, interval: p.interval || null, nextDueAt: p.nextDueAt ? new Date(p.nextDueAt).toISOString() : null, paymentHistory: (p.paymentHistory || []).map(h => ({ diff --git a/modules/rcart/schemas.ts b/modules/rcart/schemas.ts index 4f74484..bc2220e 100644 --- a/modules/rcart/schemas.ts +++ b/modules/rcart/schemas.ts @@ -193,6 +193,7 @@ export interface ShoppingCartDoc { description: string; status: CartStatus; createdBy: string | null; + recipientAddress: string | null; targetAmount: number; fundedAmount: number; currency: string; @@ -246,6 +247,7 @@ export const shoppingCartSchema: DocSchema = { description: '', status: 'OPEN', createdBy: null, + recipientAddress: null, targetAmount: 0, fundedAmount: 0, currency: 'USD', @@ -322,6 +324,8 @@ export interface PaymentRequestMeta { nextDueAt: number; // Subscriber email (for payment reminders) subscriberEmail: string | null; + // Linked shopping cart (for contribute-pay flow) + linkedCartId: string | null; // Payment history (all individual payments) paymentHistory: PaymentRecord[]; createdAt: number; @@ -377,6 +381,7 @@ export const paymentRequestSchema: DocSchema = { interval: null, nextDueAt: 0, subscriberEmail: null, + linkedCartId: null, paymentHistory: [], createdAt: Date.now(), updatedAt: Date.now(), 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/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts index b3753c3..02cd821 100644 --- a/modules/rwallet/components/folk-wallet-viewer.ts +++ b/modules/rwallet/components/folk-wallet-viewer.ts @@ -61,6 +61,11 @@ const CHAIN_COLORS: Record = { "324": "#8c8dfc", "11155111": "#f59e0b", "84532": "#f59e0b", + "421614": "#9ca3af", + "11155420": "#ff6680", + "80002": "#a855f7", + "43113": "#fb923c", + "97": "#fbbf24", }; const CHAIN_NAMES: Record = { diff --git a/modules/rwallet/mod.ts b/modules/rwallet/mod.ts index f9f5ee1..df2b1bd 100644 --- a/modules/rwallet/mod.ts +++ b/modules/rwallet/mod.ts @@ -121,9 +121,14 @@ const CHAIN_MAP: Record = { "324": { name: "zkSync", prefix: "zksync" }, "11155111": { name: "Sepolia", prefix: "sep" }, "84532": { name: "Base Sepolia", prefix: "basesep" }, + "421614": { name: "Arbitrum Sepolia", prefix: "arbsep" }, + "11155420": { name: "Optimism Sepolia", prefix: "optsep" }, + "80002": { name: "Polygon Amoy", prefix: "polyamoy" }, + "43113": { name: "Avalanche Fuji", prefix: "avaxfuji" }, + "97": { name: "BSC Testnet", prefix: "bsctest" }, }; -const TESTNET_CHAIN_IDS = new Set(["11155111", "84532"]); +const TESTNET_CHAIN_IDS = new Set(["11155111", "84532", "421614", "11155420", "80002", "43113", "97"]); function getChains(includeTestnets: boolean): [string, { name: string; prefix: string }][] { return Object.entries(CHAIN_MAP).filter(([id]) => includeTestnets || !TESTNET_CHAIN_IDS.has(id)); @@ -151,6 +156,11 @@ const DEFAULT_RPC_URLS: Record = { "324": "https://mainnet.era.zksync.io", "11155111": "https://rpc.sepolia.org", "84532": "https://sepolia.base.org", + "421614": "https://sepolia-rollup.arbitrum.io/rpc", + "11155420": "https://sepolia.optimism.io", + "80002": "https://rpc-amoy.polygon.technology", + "43113": "https://api.avax-test.network/ext/bc/C/rpc", + "97": "https://data-seed-prebsc-1-s1.binance.org:8545", }; // Chain ID → env var name fragment + Alchemy subdomain (for auto-construct) @@ -165,8 +175,13 @@ const CHAIN_ENV_NAMES: Record "43114": { envName: "AVALANCHE" }, "56": { envName: "BSC" }, "324": { envName: "ZKSYNC" }, - "11155111": { envName: "SEPOLIA" }, - "84532": { envName: "BASE_SEPOLIA" }, + "11155111": { envName: "SEPOLIA", alchemySlug: "eth-sepolia" }, + "84532": { envName: "BASE_SEPOLIA", alchemySlug: "base-sepolia" }, + "421614": { envName: "ARB_SEPOLIA", alchemySlug: "arb-sepolia" }, + "11155420": { envName: "OPT_SEPOLIA", alchemySlug: "opt-sepolia" }, + "80002": { envName: "POLYGON_AMOY", alchemySlug: "polygon-amoy" }, + "43113": { envName: "AVAX_FUJI" }, + "97": { envName: "BSC_TESTNET" }, }; /** @@ -204,6 +219,11 @@ const NATIVE_TOKENS: Record { @@ -440,6 +460,17 @@ const POPULAR_TOKENS: Record