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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-28 21:54:27 -08:00
parent 1db8341fb2
commit f8c51fad0b
1 changed files with 61 additions and 60 deletions

View File

@ -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<string, number> = {};
for (const shape of Object.values(shapes)) {
const t = (shape as Record<string, unknown>).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<string, number> = {};
for (const shape of Object.values(shapes)) {
const t = (shape as Record<string, unknown>).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
// ══════════════════════════════════════════════════════════════════════════════