From 033f4a3d9286a1557c52dca9e3fc04a2eae8dbd0 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 24 Feb 2026 15:29:15 -0800 Subject: [PATCH] feat: add admin dashboard at /admin with space overview Adds a new /admin page showing all spaces with stats (shape count, member count, file size, visibility), search/filter/sort controls, and links to open or export each space. Co-Authored-By: Claude Opus 4.6 --- server/index.ts | 9 + server/spaces.ts | 60 +++++ vite.config.ts | 1 + website/admin.html | 617 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 687 insertions(+) create mode 100644 website/admin.html diff --git a/server/index.ts b/server/index.ts index 593089a..6f473c6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -328,6 +328,15 @@ app.get("/create-space", async (c) => { // Legacy redirect app.get("/new", (c) => c.redirect("/create-space", 301)); +// Admin dashboard +app.get("/admin", async (c) => { + const file = Bun.file(resolve(DIST_DIR, "admin.html")); + if (await file.exists()) { + return new Response(file, { headers: { "Content-Type": "text/html" } }); + } + return c.text("Admin", 200); +}); + // Space root: /:space → redirect to /:space/canvas app.get("/:space", (c) => { const space = c.req.param("space"); diff --git a/server/spaces.ts b/server/spaces.ts index e23fbee..e709bc9 100644 --- a/server/spaces.ts +++ b/server/spaces.ts @@ -7,6 +7,7 @@ */ import { Hono } from "hono"; +import { stat } from "node:fs/promises"; import { communityExists, createCommunity, @@ -149,4 +150,63 @@ spaces.get("/:slug", async (c) => { }); }); +// ── Admin: list ALL spaces with detailed stats ── + +spaces.get("/admin", async (c) => { + const STORAGE_DIR = process.env.STORAGE_DIR || "./data/communities"; + const slugs = await listCommunities(); + + const spacesList = []; + for (const slug of slugs) { + await loadCommunity(slug); + const data = getDocumentData(slug); + if (!data?.meta) continue; + + const shapes = data.shapes || {}; + const members = data.members || {}; + const shapeCount = Object.keys(shapes).length; + const memberCount = Object.keys(members).length; + + // Get file size on disk + let fileSizeBytes = 0; + try { + const s = await stat(`${STORAGE_DIR}/${slug}.automerge`); + fileSizeBytes = s.size; + } catch { + try { + const s = await stat(`${STORAGE_DIR}/${slug}.json`); + fileSizeBytes = s.size; + } catch { /* not on disk yet */ } + } + + // Count shapes by type + const shapeTypes: Record = {}; + for (const shape of Object.values(shapes)) { + const t = (shape as Record).type as string || "unknown"; + shapeTypes[t] = (shapeTypes[t] || 0) + 1; + } + + spacesList.push({ + slug: data.meta.slug, + name: data.meta.name, + visibility: data.meta.visibility || "public_read", + createdAt: data.meta.createdAt, + ownerDID: data.meta.ownerDID, + shapeCount, + memberCount, + fileSizeBytes, + shapeTypes, + }); + } + + // Sort by creation date descending (newest first) + spacesList.sort((a, b) => { + const da = a.createdAt ? new Date(a.createdAt).getTime() : 0; + const db = b.createdAt ? new Date(b.createdAt).getTime() : 0; + return db - da; + }); + + return c.json({ spaces: spacesList, total: spacesList.length }); +}); + export { spaces }; diff --git a/vite.config.ts b/vite.config.ts index ccfb5de..8904afb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -717,6 +717,7 @@ export default defineConfig({ index: resolve(__dirname, "./website/index.html"), canvas: resolve(__dirname, "./website/canvas.html"), "create-space": resolve(__dirname, "./website/create-space.html"), + admin: resolve(__dirname, "./website/admin.html"), }, }, modulePreload: { diff --git a/website/admin.html b/website/admin.html new file mode 100644 index 0000000..13a1de2 --- /dev/null +++ b/website/admin.html @@ -0,0 +1,617 @@ + + + + + + + Admin Dashboard — rSpace + + + + +
+
+ + +
+
+ +
+
+ +
+
+

Admin Dashboard

+ ← Back to rSpace +
+ +
+
+
--
+
Total Spaces
+
+
+
--
+
Total Shapes
+
+
+
--
+
Total Members
+
+
+
--
+
Storage Used
+
+
+ +
+ + + + + + + +
+ +
+
+
+

Loading spaces...

+
+
+
+ + + +