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 `
✓
@@ -509,6 +513,7 @@ class FolkPaymentPage extends HTMLElement {
${p.txHash ? `
` : ''}
${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 = `
+
+
+
+
+
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 = `
`;
+ }
+ } 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 {
-
-
-
+
+
+
+
-
- ${this.esc(shareUrl)}
-
-
+
`;
}
@@ -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