Files
cim_summary/.planning/milestones/v1.0-phases/04-frontend/04-RESEARCH.md
admin 38a0f0619d chore: complete v1.0 Analytics & Monitoring milestone
Archive milestone artifacts (roadmap, requirements, audit, phase directories)
to .planning/milestones/. Evolve PROJECT.md with validated requirements and
decision outcomes. Create MILESTONES.md and RETROSPECTIVE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 10:34:18 -05:00

23 KiB

Phase 4: Frontend - Research

Researched: 2026-02-24 Domain: React + TypeScript frontend integration with admin monitoring APIs Confidence: HIGH

Summary

Phase 4 wires the existing React/TypeScript/Tailwind frontend to the three admin API endpoints delivered in Phase 3: GET /admin/health, GET /admin/analytics, and GET /admin/alerts + POST /admin/alerts/:id/acknowledge. The frontend already has a complete admin-detection pattern, tab-based navigation, an axios-backed adminService.ts, and a ProtectedRoute component. The work is pure frontend integration — no new infrastructure, no new libraries, no new backend routes.

The stack is locked: React 18 + TypeScript + Tailwind CSS + lucide-react icons + react-router-dom v6 + axios (via adminService.ts). The project uses clsx and tailwind-merge (via cn()) for conditional class composition. No charting library is installed. The existing Analytics.tsx component shows the styling and layout patterns to follow. The existing UploadMonitoringDashboard.tsx shows the health indicator pattern with colored circles.

The primary implementation risk is the alert acknowledgement UX: after calling POST /admin/alerts/:id/acknowledge, the local state must update immediately (optimistic update or re-fetch) so the banner disappears without waiting for a full page refresh. The alert banner must render above the tab navigation because it is a global signal, not scoped to a specific tab.

Primary recommendation: Add new components to the existing monitoring tab in App.tsx, extend adminService.ts with the three monitoring API methods, add an AdminMonitoringDashboard component, add an AlertBanner component that renders above the nav inside Dashboard, and add an AdminRoute wrapper that shows access-denied for non-admins who somehow hit the monitoring tab directly.


<phase_requirements>

Phase Requirements

ID Description Research Support
ALRT-03 Admin sees in-app alert banner for active critical issues; banner disappears after acknowledgement alertBanner component using GET /admin/alerts + POST /admin/alerts/:id/acknowledge; optimistic state update on acknowledge
ANLY-02 (UI) Admin dashboard shows processing summary: upload counts, success/failure rates, avg processing time AdminMonitoringDashboard section consuming GET /admin/analytics response shape (AnalyticsSummary interface)
HLTH-01 (UI) Admin dashboard shows health status indicators (green/yellow/red) for all four services with last-checked timestamp ServiceHealthPanel component consuming GET /admin/health; status → color mapping green=healthy, yellow=degraded, red=down/unknown

Note: ANLY-02 and HLTH-01 were marked Complete in Phase 3 (backend side). Phase 4 completes their UI delivery. </phase_requirements>


Standard Stack

Core (already installed — no new packages needed)

Library Version Purpose Why Standard
react ^18.2.0 Component rendering Project standard
typescript ^5.2.2 Type safety Project standard
tailwindcss ^3.3.5 Utility CSS Project standard
lucide-react ^0.294.0 Icons (AlertTriangle, CheckCircle, Activity, Clock, etc.) Already used throughout
axios ^1.6.2 HTTP client (via adminService) Already used in adminService.ts
clsx + tailwind-merge ^2.0.0 / ^2.0.0 Conditional class composition via cn() Already used in Analytics.tsx
react-router-dom ^6.20.1 Routing, Navigate Already used

No New Libraries Required

All required UI capabilities exist in the current stack:

  • Status indicators: plain div with Tailwind bg-green-500 / bg-yellow-500 / bg-red-500
  • Alert banner: fixed/sticky div above nav, standard Tailwind layout
  • Timestamps: new Date(ts).toLocaleString() or toRelative() — no date library needed
  • Loading state: existing spinner pattern (animate-spin rounded-full border-b-2)

Installation: None required.


Architecture Patterns

frontend/src/
├── components/
│   ├── AlertBanner.tsx          # NEW — global alert banner above nav
│   ├── AdminMonitoringDashboard.tsx  # NEW — health + analytics panel
│   └── (existing files unchanged)
├── services/
│   └── adminService.ts          # EXTEND — add getHealth(), getAnalytics(), getAlerts(), acknowledgeAlert()
└── App.tsx                      # MODIFY — render AlertBanner, wire monitoring tab to new component

Pattern 1: Alert Banner — global, above nav, conditional render

What: A dismissible/acknowledgeable banner rendered inside Dashboard ABOVE the <nav> element, only when alerts.length > 0 and there is at least one status === 'active' alert with a critical alert_type (service_down or service_degraded).

When to use: This pattern matches the existing App.tsx structure — Dashboard is the single top-level component after login, so mounting AlertBanner inside it ensures it is always visible when the admin is on any tab.

Data flow:

  1. Dashboard fetches active alerts on mount via adminService.getAlerts()
  2. Stores in activeAlerts state
  3. Passes alerts and onAcknowledge callback to AlertBanner
  4. onAcknowledge(id) calls adminService.acknowledgeAlert(id) then updates local state by filtering out the acknowledged alert (optimistic update)

Example:

// In Dashboard component
const [activeAlerts, setActiveAlerts] = useState<AlertEvent[]>([]);

useEffect(() => {
  adminService.getAlerts().then(setActiveAlerts).catch(() => {});
}, []);

const handleAcknowledge = async (id: string) => {
  await adminService.acknowledgeAlert(id);
  setActiveAlerts(prev => prev.filter(a => a.id !== id));
};

// Rendered before <nav>:
{isAdmin && activeAlerts.length > 0 && (
  <AlertBanner alerts={activeAlerts} onAcknowledge={handleAcknowledge} />
)}

AlertBanner props:

interface AlertBannerProps {
  alerts: AlertEvent[];
  onAcknowledge: (id: string) => Promise<void>;
}

Pattern 2: Service Health Panel — status dot + service name + timestamp

What: A 2x2 or 1x4 grid of service health cards. Each card shows: colored dot (green=healthy, yellow=degraded, red=down, gray=unknown), service display name, last-checked timestamp, and latency_ms if available.

Status → color mapping (matches REQUIREMENTS.md "green/yellow/red"):

const statusColor = {
  healthy: 'bg-green-500',
  degraded: 'bg-yellow-500',
  down: 'bg-red-500',
  unknown: 'bg-gray-400',
} as const;

Service name display mapping (backend sends snake_case):

const serviceDisplayName: Record<string, string> = {
  document_ai: 'Document AI',
  llm_api: 'LLM API',
  supabase: 'Supabase',
  firebase_auth: 'Firebase Auth',
};

Example card:

// Source: admin.ts route response shape
interface ServiceHealthEntry {
  service: 'document_ai' | 'llm_api' | 'supabase' | 'firebase_auth';
  status: 'healthy' | 'degraded' | 'down' | 'unknown';
  checkedAt: string | null;
  latencyMs: number | null;
  errorMessage: string | null;
}

Pattern 3: Analytics Summary Panel — upload counts + rates + avg time

What: A stat card grid showing totalUploads, succeeded, failed, successRate (as %, formatted), and avgProcessingMs (formatted as seconds or minutes). Matches AnalyticsSummary response from analyticsService.ts.

AnalyticsSummary interface (from backend analyticsService.ts):

interface AnalyticsSummary {
  range: string;           // e.g. "24h"
  totalUploads: number;
  succeeded: number;
  failed: number;
  successRate: number;     // 0.0 to 1.0
  avgProcessingMs: number | null;
  generatedAt: string;
}

Range selector: Include a <select> for 24h, 7d, 30d as query params passed to getAnalytics(range). Matches the pattern in existing Analytics.tsx.

Pattern 4: Admin-only route protection in existing tab system

What: The app uses a tab system inside Dashboard, not separate React Router routes for admin tabs. Admin-only tabs (analytics, monitoring) are already conditionally rendered with isAdmin && (...). The "access-denied" state for non-admin users already exists as inline fallback JSX in App.tsx.

For Phase 4 success criterion 4 (non-admin sees access-denied): The monitoring tab already shows an inline "Access Denied" card for !isAdmin users. The new AdminMonitoringDashboard will render inside the monitoring tab, guarded the same way. No new route is needed.

Pattern 5: adminService.ts extensions

The existing adminService.ts already has an axios client with auto-attached Firebase token. Extend it with typed methods:

// Add to adminService.ts

// Types — import or co-locate
export interface AlertEvent {
  id: string;
  service_name: string;
  alert_type: 'service_down' | 'service_degraded' | 'recovery';
  status: 'active' | 'acknowledged' | 'resolved';
  message: string | null;
  details: Record<string, unknown> | null;
  created_at: string;
  acknowledged_at: string | null;
  resolved_at: string | null;
}

export interface ServiceHealthEntry {
  service: string;
  status: 'healthy' | 'degraded' | 'down' | 'unknown';
  checkedAt: string | null;
  latencyMs: number | null;
  errorMessage: string | null;
}

export interface AnalyticsSummary {
  range: string;
  totalUploads: number;
  succeeded: number;
  failed: number;
  successRate: number;
  avgProcessingMs: number | null;
  generatedAt: string;
}

// Methods to add to AdminService class:
async getHealth(): Promise<ServiceHealthEntry[]> {
  const response = await apiClient.get('/admin/health');
  return response.data.data;
}

async getAnalytics(range: string = '24h'): Promise<AnalyticsSummary> {
  const response = await apiClient.get(`/admin/analytics?range=${range}`);
  return response.data.data;
}

async getAlerts(): Promise<AlertEvent[]> {
  const response = await apiClient.get('/admin/alerts');
  return response.data.data;
}

async acknowledgeAlert(id: string): Promise<AlertEvent> {
  const response = await apiClient.post(`/admin/alerts/${id}/acknowledge`);
  return response.data.data;
}

Anti-Patterns to Avoid

  • Awaiting acknowledgeAlert before updating UI: The banner should disappear immediately on click, not wait for the API round-trip. Use optimistic state update: setActiveAlerts(prev => prev.filter(a => a.id !== id)) before the await.
  • Polling alerts on a short interval: Out of scope (WebSockets/SSE out of scope per REQUIREMENTS.md). Fetch alerts once on Dashboard mount. A "Refresh" button on the monitoring panel is acceptable.
  • Using console.log: The frontend already uses console.log extensively. New components should match the existing pattern (the backend Winston logger rule does not apply to frontend code — frontend has no logger.ts).
  • Building a new ProtectedRoute for admin tabs: The existing tab-visibility pattern (isAdmin &&) is sufficient. No new routes needed.
  • Using any type: Type the API responses explicitly with interfaces matching backend response shapes.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Colored status indicators Custom SVG status icon component Tailwind bg-green-500/bg-yellow-500/bg-red-500 on a w-3 h-3 rounded-full div Already established in Analytics.tsx and UploadMonitoringDashboard.tsx
Token-attached API calls Custom fetch with manual header attachment Extend existing apiClient axios instance in adminService.ts Interceptor already handles token attachment automatically
Date formatting Custom date utility new Date(ts).toLocaleString() inline Sufficient; no date library installed
Conditional class composition String concatenation cn() from ../utils/cn Already imported in every component

Key insight: Every UI pattern needed already exists in the codebase. The implementation is wiring existing patterns to new API endpoints, not building new UI infrastructure.


Common Pitfalls

Pitfall 1: Alert banner position — wrong mount point

What goes wrong: AlertBanner mounted inside a specific tab panel instead of at the top of Dashboard. The banner would only show on one tab.

Why it happens: Placing alert logic where other data-fetching lives (in the monitoring tab content).

How to avoid: Mount AlertBanner in Dashboard return JSX, before the <nav> element. Alert fetching state lives in Dashboard, not in a child component.

Warning signs: Alert banner only visible when "Monitoring" tab is active.

Pitfall 2: Banner not disappearing after acknowledge

What goes wrong: Admin clicks "Acknowledge" on the banner. API call succeeds but banner stays. Admin must refresh the page.

Why it happens: State update waits for API response, or state is not updated at all (only the API called).

How to avoid: Use optimistic state update. Remove the alert from activeAlerts immediately before or during the API call:

const handleAcknowledge = async (id: string) => {
  setActiveAlerts(prev => prev.filter(a => a.id !== id)); // optimistic
  try {
    await adminService.acknowledgeAlert(id);
  } catch {
    // on failure: re-fetch to restore correct state
    adminService.getAlerts().then(setActiveAlerts).catch(() => {});
  }
};

Pitfall 3: AdminService.isAdmin hardcoded email

What goes wrong: The existing adminService.ts has private readonly ADMIN_EMAIL = 'jpressnell@bluepointcapital.com' hardcoded. isAdmin(user?.email) works correctly for the current single admin. The backend also enforces admin-email check independently, so this is not a security issue — but it is a code smell.

How to avoid: Do not change this in Phase 4. It is the existing pattern. The admin check is backend-enforced; frontend admin detection is UI-only (for showing/hiding tabs and fetching admin data).

Pitfall 4: Type mismatch between backend AlertEvent and frontend usage

What goes wrong: Frontend defines AlertEvent with fields that differ from backend's AlertEvent interface — particularly status values or field names (checkedAt vs checked_at).

Why it happens: Backend uses snake_case internally; admin route returns camelCase (see admin.ts: checkedAt, latencyMs, errorMessage). Alert model uses snake_case throughout (returned directly from model without remapping).

How to avoid: Check admin.ts response shapes carefully:

  • GET /admin/health remaps to camelCase: { service, status, checkedAt, latencyMs, errorMessage }
  • GET /admin/alerts returns raw AlertEvent model data (snake_case): { id, service_name, alert_type, status, message, created_at, acknowledged_at }
  • GET /admin/analytics returns AnalyticsSummary (camelCase from analyticsService.ts)

Frontend types must match these shapes exactly.

Pitfall 5: Fetching alerts/health when user is not admin

What goes wrong: Dashboard calls adminService.getAlerts() on mount regardless of whether the user is admin. Non-admin users trigger a 404 response (backend returns 404 for non-admin, not 403, per STATE.md decision: "requireAdminEmail returns 404 not 403").

How to avoid: Gate all admin API calls behind isAdmin check:

useEffect(() => {
  if (isAdmin) {
    adminService.getAlerts().then(setActiveAlerts).catch(() => {});
  }
}, [isAdmin]);

Code Examples

AlertBanner Component Structure

// Source: admin.ts route — alerts returned as raw AlertEvent (snake_case)
import { AlertTriangle, X } from 'lucide-react';
import { cn } from '../utils/cn';

interface AlertEvent {
  id: string;
  service_name: string;
  alert_type: 'service_down' | 'service_degraded' | 'recovery';
  status: 'active' | 'acknowledged' | 'resolved';
  message: string | null;
  created_at: string;
  acknowledged_at: string | null;
  resolved_at: string | null;
  details: Record<string, unknown> | null;
}

interface AlertBannerProps {
  alerts: AlertEvent[];
  onAcknowledge: (id: string) => Promise<void>;
}

const AlertBanner: React.FC<AlertBannerProps> = ({ alerts, onAcknowledge }) => {
  const criticalAlerts = alerts.filter(a =>
    a.status === 'active' && (a.alert_type === 'service_down' || a.alert_type === 'service_degraded')
  );

  if (criticalAlerts.length === 0) return null;

  return (
    <div className="bg-red-600 px-4 py-3">
      {criticalAlerts.map(alert => (
        <div key={alert.id} className="flex items-center justify-between text-white">
          <div className="flex items-center space-x-2">
            <AlertTriangle className="h-5 w-5 flex-shrink-0" />
            <span className="text-sm font-medium">
              {alert.service_name}: {alert.message ?? alert.alert_type}
            </span>
          </div>
          <button
            onClick={() => onAcknowledge(alert.id)}
            className="flex items-center space-x-1 text-sm underline hover:no-underline ml-4"
          >
            <X className="h-4 w-4" />
            <span>Acknowledge</span>
          </button>
        </div>
      ))}
    </div>
  );
};

ServiceHealthPanel Card

// Source: admin.ts GET /admin/health response (camelCase remapped)
interface ServiceHealthEntry {
  service: string;
  status: 'healthy' | 'degraded' | 'down' | 'unknown';
  checkedAt: string | null;
  latencyMs: number | null;
  errorMessage: string | null;
}

const statusStyles = {
  healthy: { dot: 'bg-green-500', label: 'Healthy', text: 'text-green-700' },
  degraded: { dot: 'bg-yellow-500', label: 'Degraded', text: 'text-yellow-700' },
  down: { dot: 'bg-red-500', label: 'Down', text: 'text-red-700' },
  unknown: { dot: 'bg-gray-400', label: 'Unknown', text: 'text-gray-600' },
} as const;

const serviceDisplayName: Record<string, string> = {
  document_ai: 'Document AI',
  llm_api: 'LLM API',
  supabase: 'Supabase',
  firebase_auth: 'Firebase Auth',
};

AdminMonitoringDashboard fetch pattern

// Matches existing Analytics.tsx pattern — useEffect + setLoading + error state
const AdminMonitoringDashboard: React.FC = () => {
  const [health, setHealth] = useState<ServiceHealthEntry[]>([]);
  const [analytics, setAnalytics] = useState<AnalyticsSummary | null>(null);
  const [range, setRange] = useState('24h');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const loadData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const [healthData, analyticsData] = await Promise.all([
        adminService.getHealth(),
        adminService.getAnalytics(range),
      ]);
      setHealth(healthData);
      setAnalytics(analyticsData);
    } catch {
      setError('Failed to load monitoring data');
    } finally {
      setLoading(false);
    }
  }, [range]);

  useEffect(() => { loadData(); }, [loadData]);
  // ...
};

State of the Art

Old Approach Current Approach Impact
UploadMonitoringDashboard.tsx reads from in-memory upload tracking New AdminMonitoringDashboard reads from Supabase via admin API Data survives cold starts (HLTH-04 fulfilled)
Analytics.tsx reads from documentService (old agentic session tables) New analytics panel reads from document_processing_events via GET /admin/analytics Sourced from the persistent monitoring schema built in Phase 1-3
No alert visibility in UI AlertBanner above nav, auto-populated from GET /admin/alerts ALRT-03 fulfilled

Deprecated/outdated:

  • The existing Analytics.tsx component (uses documentService.getAnalytics() which hits old session/agent tables) — Phase 4 does NOT replace it; it adds a new monitoring section alongside it. The monitoring tab is separate from the analytics tab.
  • UploadMonitoringDashboard.tsx — may be kept or replaced; Phase 4 should use the new AdminMonitoringDashboard in the monitoring tab.

Open Questions

  1. Does UploadMonitoringDashboard get replaced or supplemented?

    • What we know: The monitoring tab currently renders UploadMonitoringDashboard
    • What's unclear: The Phase 4 requirement says "admin dashboard" — it's ambiguous whether to replace UploadMonitoringDashboard entirely or add AdminMonitoringDashboard below/beside it
    • Recommendation: Replace the monitoring tab content with AdminMonitoringDashboard. The old UploadMonitoringDashboard tracked in-memory state that is now superseded by Supabase-backed data.
  2. Alert polling: once on mount or refresh button?

    • What we know: WebSockets/SSE are explicitly out of scope (REQUIREMENTS.md). No polling interval is specified.
    • What's unclear: Whether a manual "Refresh" button on the banner is expected
    • Recommendation: Fetch alerts once on Dashboard mount (gated by isAdmin). Include a Refresh button on AdminMonitoringDashboard that also re-fetches alerts. This matches the existing Analytics.tsx refresh pattern.
  3. Which alert types trigger the banner?

    • What we know: ALRT-03 says "active critical issues". AlertEvent.alert_type is service_down | service_degraded | recovery.
    • What's unclear: Does service_degraded count as "critical"?
    • Recommendation: Show banner for both service_down and service_degraded (both are actionable alerts that indicate a real problem). Filter out recovery type alerts as they are informational.

Validation Architecture

workflow.nyquist_validation is not present in .planning/config.json — the config only has workflow.research, workflow.plan_check, and workflow.verifier. No nyquist_validation key. Skipping this section.


Sources

Primary (HIGH confidence)

  • Direct codebase inspection — frontend/src/App.tsx (tab structure, admin detection pattern)
  • Direct codebase inspection — frontend/src/services/adminService.ts (axios client, existing methods)
  • Direct codebase inspection — frontend/src/components/Analytics.tsx (data-fetch pattern, Tailwind layout)
  • Direct codebase inspection — frontend/src/contexts/AuthContext.tsx (useAuth hook, token/user state)
  • Direct codebase inspection — backend/src/routes/admin.ts (exact API response shapes)
  • Direct codebase inspection — backend/src/models/AlertEventModel.ts (AlertEvent type, field names and casing)
  • Direct codebase inspection — backend/src/services/analyticsService.ts (AnalyticsSummary interface)
  • Direct codebase inspection — frontend/package.json (installed libraries, confirmed no test framework)

Secondary (MEDIUM confidence)

  • Pattern inference from existing components (UploadMonitoringDashboard.tsx status indicator pattern)
  • React 18 state/effect patterns — verified against direct code inspection in existing components

Tertiary (LOW confidence)

  • None

Metadata

Confidence breakdown:

  • Standard stack: HIGH — verified directly from package.json and existing component files
  • Architecture: HIGH — patterns derived directly from existing App.tsx tab system and adminService.ts structure
  • API response shapes: HIGH — read directly from backend admin.ts route and model files
  • Pitfalls: HIGH — derived from direct code inspection and STATE.md decisions

Research date: 2026-02-24 Valid until: 2026-03-24 (stable — no moving dependencies; all external packages are pinned)