docs(04-frontend): research phase frontend integration

This commit is contained in:
admin
2026-02-24 16:15:11 -05:00
parent 400342456f
commit 21eea7f828

View File

@@ -0,0 +1,511 @@
# 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
### Recommended File Changes
```
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:**
```typescript
// 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:**
```typescript
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"):
```typescript
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):
```typescript
const serviceDisplayName: Record<string, string> = {
document_ai: 'Document AI',
llm_api: 'LLM API',
supabase: 'Supabase',
firebase_auth: 'Firebase Auth',
};
```
**Example card:**
```typescript
// 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):
```typescript
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:
```typescript
// 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:
```typescript
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:
```typescript
useEffect(() => {
if (isAdmin) {
adminService.getAlerts().then(setActiveAlerts).catch(() => {});
}
}, [isAdmin]);
```
---
## Code Examples
### AlertBanner Component Structure
```typescript
// 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
```typescript
// 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
```typescript
// 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)