From f8c51fad0b2e5d0089afe55c9f53b4b49420bdd5 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 28 Feb 2026 21:54:27 -0800 Subject: [PATCH] fix: move /notifications and /admin routes before /:slug wildcard Hono matches routes in definition order, so /:slug was catching "notifications" and "admin" as slug params and returning 404. Static routes must be defined before parameterized routes. Co-Authored-By: Claude Opus 4.6 --- server/spaces.ts | 121 ++++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/server/spaces.ts b/server/spaces.ts index aa67f94..7fc1318 100644 --- a/server/spaces.ts +++ b/server/spaces.ts @@ -201,8 +201,68 @@ spaces.post("/", async (c) => { }, 201); }); +// ── Static routes must be defined BEFORE /:slug to avoid matching as a slug ── + +// ── 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 }); +}); + // ── Get pending access requests for spaces the user owns ── -// NOTE: Must be defined BEFORE /:slug to avoid "notifications" matching as a slug spaces.get("/notifications", async (c) => { const token = extractToken(c.req.raw.headers); @@ -456,65 +516,6 @@ spaces.get("/:slug/access-requests", async (c) => { return c.json({ requests }); }); -// ── 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 }); -}); - // ══════════════════════════════════════════════════════════════════════════════ // NESTING API // ══════════════════════════════════════════════════════════════════════════════