From 20910f5466f18b7c51b35d4cf44b8568d21e79c7 Mon Sep 17 00:00:00 2001 From: Marco Sadjadi Date: Mon, 25 May 2026 17:23:33 +0200 Subject: [PATCH] fix(admin): Support entry in sidebar + awaiting-admin badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /admin/support page existed but was invisible from the panel — sidebar NAV array didn't list it. Adds Support as the 2nd nav item (right after Overview, since unanswered tickets are the most-time-sensitive thing an admin checks). Sidebar polls /v1/admin/support/counts every 30s and renders an amber count badge next to the entry when tickets are awaiting_admin. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/src/routes/support.ts | 13 +++++++++++++ apps/web/app/admin/layout.tsx | 30 +++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/support.ts b/apps/api/src/routes/support.ts index d7cb224..45b883e 100644 --- a/apps/api/src/routes/support.ts +++ b/apps/api/src/routes/support.ts @@ -3,6 +3,7 @@ import { createDb, desc, eq, + sql, supportMessages, supportTickets, users, @@ -182,6 +183,18 @@ export async function supportRoutes(app: FastifyInstance): Promise { }); // ─── Admin-side ──────────────────────────────────────────────────────── + app.get( + '/v1/admin/support/counts', + { preHandler: requireAdmin }, + async (_req, reply) => { + const [row] = await db + .select({ count: sql`count(*)::int` }) + .from(supportTickets) + .where(eq(supportTickets.status, 'awaiting_admin')); + return reply.send({ awaitingAdmin: row?.count ?? 0 }); + }, + ); + app.get( '/v1/admin/support/tickets', { preHandler: requireAdmin }, diff --git a/apps/web/app/admin/layout.tsx b/apps/web/app/admin/layout.tsx index 78161d2..daaa477 100644 --- a/apps/web/app/admin/layout.tsx +++ b/apps/web/app/admin/layout.tsx @@ -5,6 +5,7 @@ import { usePathname, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { LayoutGrid, + LifeBuoy, Users, Building2, Server, @@ -29,6 +30,7 @@ interface MeUser { const NAV: { href: string; label: string; icon: React.ComponentType<{ size?: number }> }[] = [ { href: '/admin', label: 'Overview', icon: LayoutGrid }, + { href: '/admin/support', label: 'Support', icon: LifeBuoy }, { href: '/admin/users', label: 'Users', icon: Users }, { href: '/admin/orgs', label: 'Organizations', icon: Building2 }, { href: '/admin/servers', label: 'MCP servers', icon: Server }, @@ -45,6 +47,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) const router = useRouter(); const [user, setUser] = useState(null); const [authState, setAuthState] = useState<'checking' | 'ok' | 'forbidden'>('checking'); + const [supportPending, setSupportPending] = useState(0); useEffect(() => { if (pathname === '/admin/login') { @@ -63,6 +66,26 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) .catch(() => setAuthState('forbidden')); }, [pathname]); + // Poll support count every 30s so the sidebar badge stays fresh while admin + // is doing other work in the panel. + useEffect(() => { + if (authState !== 'ok' || pathname === '/admin/login') return; + let cancelled = false; + const load = () => { + apiFetch<{ awaitingAdmin: number }>('/v1/admin/support/counts') + .then((r) => { + if (!cancelled) setSupportPending(r.awaitingAdmin); + }) + .catch(() => undefined); + }; + load(); + const t = setInterval(load, 30_000); + return () => { + cancelled = true; + clearInterval(t); + }; + }, [authState, pathname]); + useEffect(() => { if (authState === 'forbidden' && pathname !== '/admin/login') { router.replace('/admin/login'); @@ -126,7 +149,12 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) )} > - {item.label} + {item.label} + {item.href === '/admin/support' && supportPending > 0 && ( + + {supportPending} + + )} );