fix(admin): Support entry in sidebar + awaiting-admin badge
All checks were successful
Deploy to Production / deploy (push) Successful in 52s
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:
parent
ef30baf52a
commit
20910f5466
@ -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 },
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user