fix(admin): Support entry in sidebar + awaiting-admin badge
All checks were successful
Deploy to Production / deploy (push) Successful in 52s

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) <noreply@anthropic.com>
This commit is contained in:
Marco Sadjadi 2026-05-25 17:23:33 +02:00
parent ef30baf52a
commit 20910f5466
2 changed files with 42 additions and 1 deletions

View File

@ -3,6 +3,7 @@ import {
createDb,
desc,
eq,
sql,
supportMessages,
supportTickets,
users,
@ -182,6 +183,18 @@ export async function supportRoutes(app: FastifyInstance): Promise<void> {
});
// ─── Admin-side ────────────────────────────────────────────────────────
app.get(
'/v1/admin/support/counts',
{ preHandler: requireAdmin },
async (_req, reply) => {
const [row] = await db
.select({ count: sql<number>`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 },

View File

@ -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<MeUser | null>(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 })
)}
>
<Icon size={13} />
{item.label}
<span className="flex-1">{item.label}</span>
{item.href === '/admin/support' && supportPending > 0 && (
<span className="mono inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-amber-500/30 px-1 text-[10px] font-semibold text-amber-300">
{supportPending}
</span>
)}
</Link>
</li>
);