'use client'; /** * AnalyticsPage — Tenant analytics dashboard for the SentryAgent portal. * * Displays: * - Token issuance trend (last 30 days) via TokenTrendChart (lazy-loaded) * - Agent activity by day-of-week via AgentHeatmap (lazy-loaded) * - Per-agent usage table for the current billing period * * All chart components are code-split via `next/dynamic` so that recharts * is excluded from the main bundle. * * Protected route: redirects to `/login` when no JWT is present (via useAuth). * * @module app/analytics/page */ import React, { useEffect, useState } from 'react'; import dynamic from 'next/dynamic'; import { useAuth } from '@/hooks/useAuth'; import type { TokenTrendDataPoint } from '@/components/charts/TokenTrendChart'; import type { AgentActivityBucket } from '@/components/charts/AgentHeatmap'; /* ------------------------------------------------------------------------- * Lazy-loaded chart components (recharts stays out of the main bundle) * ---------------------------------------------------------------------- */ const TokenTrendChart = dynamic( () => import('@/components/charts/TokenTrendChart'), { ssr: false, loading: () => ( ), }, ); const AgentHeatmap = dynamic( () => import('@/components/charts/AgentHeatmap'), { ssr: false, loading: () => ( ), }, ); /* ------------------------------------------------------------------------- * API response types * ---------------------------------------------------------------------- */ /** A single entry from `GET /api/analytics/agents`. */ interface AgentUsageSummary { agent_id: string; name: string; token_count: number; } /** Root shape of `GET /api/analytics/agents/activity`. */ type AgentActivityResponse = AgentActivityBucket[]; /** Root shape of `GET /api/analytics/tokens`. */ type TokenTrendResponse = TokenTrendDataPoint[]; /** Root shape of `GET /api/analytics/agents`. */ type AgentUsageResponse = AgentUsageSummary[]; /* ------------------------------------------------------------------------- * Page state * ---------------------------------------------------------------------- */ /** Loading / error / data state for a single async fetch. */ interface FetchState { data: T | null; loading: boolean; error: string | null; } function initFetchState(): FetchState { return { data: null, loading: true, error: null }; } /* ------------------------------------------------------------------------- * Helpers * ---------------------------------------------------------------------- */ /** * Fetches a JSON endpoint with an Authorization bearer token. * * @param url - Absolute URL to fetch * @param token - JWT bearer token * @returns Parsed JSON of type T * @throws Error with a descriptive message on non-2xx or network failure */ async function fetchWithAuth(url: string, token: string): Promise { const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) { const body = (await res.json().catch(() => ({}))) as { message?: string }; throw new Error(body.message ?? `Request failed: ${res.status}`); } return res.json() as Promise; } /* ------------------------------------------------------------------------- * Sub-components * ---------------------------------------------------------------------- */ /** Props for ChartSkeleton. */ interface ChartSkeletonProps { label: string; height: number; } /** * Placeholder shown while a dynamic chart chunk is loading. * * @param props - ChartSkeletonProps * @returns JSX element */ function ChartSkeleton({ label, height }: ChartSkeletonProps): React.ReactElement { return (
{label}
); } /** Props for SectionCard. */ interface SectionCardProps { title: string; children: React.ReactNode; } /** * Simple card wrapper for dashboard sections. * * @param props - SectionCardProps * @returns JSX element */ function SectionCard({ title, children }: SectionCardProps): React.ReactElement { return (

{title}

{children}
); } /** Props for ErrorBanner. */ interface ErrorBannerProps { message: string; } /** * Inline error banner for failed data fetches. * * @param props - ErrorBannerProps * @returns JSX element */ function ErrorBanner({ message }: ErrorBannerProps): React.ReactElement { return (

{message}

); } /* ------------------------------------------------------------------------- * Page component * ---------------------------------------------------------------------- */ /** * Renders the analytics dashboard page. * * Checks authentication via `useAuth` (redirects to /login if no token). * Fetches analytics data from the AgentIdP API using the stored JWT. * * @returns JSX element */ export default function AnalyticsPage(): React.ReactElement { const { token, loading: authLoading } = useAuth(true); const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000'; const [tokenTrend, setTokenTrend] = useState>(initFetchState); const [agentActivity, setAgentActivity] = useState>(initFetchState); const [agentUsage, setAgentUsage] = useState>(initFetchState); useEffect(() => { // Wait until auth state is resolved and token is available if (authLoading || token === null) return; void fetchWithAuth( `${apiUrl}/api/analytics/tokens?days=30`, token, ) .then((data) => setTokenTrend({ data, loading: false, error: null })) .catch((err: unknown) => setTokenTrend({ data: null, loading: false, error: err instanceof Error ? err.message : 'Failed to load token trend', }), ); void fetchWithAuth( `${apiUrl}/api/analytics/agents/activity`, token, ) .then((data) => setAgentActivity({ data, loading: false, error: null })) .catch((err: unknown) => setAgentActivity({ data: null, loading: false, error: err instanceof Error ? err.message : 'Failed to load agent activity', }), ); void fetchWithAuth( `${apiUrl}/api/analytics/agents`, token, ) .then((data) => setAgentUsage({ data, loading: false, error: null })) .catch((err: unknown) => setAgentUsage({ data: null, loading: false, error: err instanceof Error ? err.message : 'Failed to load agent usage', }), ); }, [authLoading, token, apiUrl]); // While auth state is being resolved, show a full-page loading state if (authLoading) { return (

Loading…

); } // token === null means useAuth is redirecting to /login; render nothing if (token === null) { return <>; } return (
{/* Page header */}

Analytics

Token issuance and agent activity for the last 30 days

{/* Token Trend */} {tokenTrend.error !== null && ( )} {tokenTrend.loading && tokenTrend.error === null && ( )} {tokenTrend.data !== null && ( )} {/* Agent Activity Heatmap */} {agentActivity.error !== null && ( )} {agentActivity.loading && agentActivity.error === null && ( )} {agentActivity.data !== null && ( )} {/* Per-Agent Usage Table */} {agentUsage.error !== null && ( )} {agentUsage.loading && agentUsage.error === null && (
{[1, 2, 3].map((n) => (
))}
)} {agentUsage.data !== null && agentUsage.data.length === 0 && (

No agents have issued tokens this month.

)} {agentUsage.data !== null && agentUsage.data.length > 0 && (
{agentUsage.data.map(({ agent_id, name, token_count }, i) => ( ))}
Agent Tokens Issued
{name} {agent_id} {token_count.toLocaleString()}
)}
); }