/** * Data module — privacy-first analytics dashboard. * * Lightweight module that shows analytics stats from the * self-hosted Umami instance. No database — proxies to Umami API. */ import { Hono } from "hono"; import { renderShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; const routes = new Hono(); const UMAMI_URL = process.env.UMAMI_URL || "https://analytics.rspace.online"; const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID || "292f6ac6-79f8-497b-ba6a-7a51e3b87b9f"; const TRACKED_APPS = [ "rSpace", "rNotes", "rVote", "rFunds", "rCart", "rWallet", "rPubs", "rChoices", "rCal", "rForum", "rInbox", "rFiles", "rTrips", "rTube", "rWork", "rNetwork", "rData", ]; // ── API routes ── // GET /api/info — module info routes.get("/api/info", (c) => { return c.json({ module: "data", name: "rData", umamiUrl: UMAMI_URL, umamiConfigured: !!UMAMI_URL, features: ["privacy-first", "cookieless", "self-hosted"], trackedApps: TRACKED_APPS.length, }); }); // GET /api/health routes.get("/api/health", (c) => c.json({ ok: true })); // GET /api/stats — proxy to Umami stats API routes.get("/api/stats", async (c) => { const startAt = c.req.query("startAt") || String(Date.now() - 24 * 3600_000); const endAt = c.req.query("endAt") || String(Date.now()); try { const res = await fetch( `${UMAMI_URL}/api/websites/${UMAMI_WEBSITE_ID}/stats?startAt=${startAt}&endAt=${endAt}`, { signal: AbortSignal.timeout(5000) } ); if (res.ok) { const data = await res.json(); return c.json({ ...data as Record, trackedApps: TRACKED_APPS.length, apps: TRACKED_APPS, selfHosted: true, dashboardUrl: UMAMI_URL, }); } } catch {} // Fallback when Umami unreachable return c.json({ trackedApps: TRACKED_APPS.length, cookiesSet: 0, scriptSize: "~2KB", selfHosted: true, dashboardUrl: UMAMI_URL, apps: TRACKED_APPS, }); }); // GET /api/active — proxy to Umami active visitors routes.get("/api/active", async (c) => { try { const res = await fetch( `${UMAMI_URL}/api/websites/${UMAMI_WEBSITE_ID}/active`, { signal: AbortSignal.timeout(5000) } ); if (res.ok) return c.json(await res.json()); } catch {} return c.json({ x: 0 }); }); // GET /collect.js — proxy Umami tracker script routes.get("/collect.js", async (c) => { try { const res = await fetch(`${UMAMI_URL}/script.js`, { signal: AbortSignal.timeout(5000) }); if (res.ok) { const script = await res.text(); return new Response(script, { headers: { "Content-Type": "application/javascript", "Cache-Control": "public, max-age=3600", }, }); } } catch {} return new Response("/* umami unavailable */", { headers: { "Content-Type": "application/javascript" }, }); }); // POST /api/collect — proxy Umami event collection routes.post("/api/collect", async (c) => { try { const body = await c.req.text(); const res = await fetch(`${UMAMI_URL}/api/send`, { method: "POST", headers: { "Content-Type": "application/json" }, body, signal: AbortSignal.timeout(5000), }); if (res.ok) return c.json(await res.json()); } catch {} return c.json({ ok: true }); }); // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; return c.html(renderShell({ title: `${space} — Data | rSpace`, moduleId: "data", spaceSlug: space, modules: getModuleInfoList(), theme: "light", styles: ``, body: ``, scripts: ``, })); }); export const dataModule: RSpaceModule = { id: "data", name: "rData", icon: "\u{1F4CA}", description: "Privacy-first analytics for the r* ecosystem", routes, standaloneDomain: "rdata.online", };