MotorSignal-App/website/components/ops/operations-shell.tsx

300 lines
9.4 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import {
OperationsNavId,
OperatorSession,
OperationsHealth,
SafeAuditEvent,
OperationsOverviewData,
} from './types';
import { OperationsTopBar } from './operations-top-bar';
import { OperationsSidebar } from './operations-sidebar';
import { OperationsOverview } from './operations-overview';
import { OperationsAudit } from './operations-audit';
import { OperationsOperators } from './operations-operators';
import { OperationsSessionView } from './operations-session';
import { OperationsIncidentsList } from './operations-incidents-list';
import { OperationsCreateIncident } from './operations-create-incident';
import { OperationsIncidentDetail } from './operations-incident-detail';
import { OperationsUsers } from './operations-users';
import { OperationsSupport } from './operations-support';
import { OperationsCms } from './operations-cms';
interface OperationsShellProps {
initialSession: OperatorSession;
initialHealth: OperationsHealth;
initialOverview?: OperationsOverviewData | null;
initialAudit?: {
events: SafeAuditEvent[];
next_cursor?: string | null;
total?: number;
};
initialNav?: OperationsNavId;
initialCaseRef?: string | null;
}
export function OperationsShell({
initialSession,
initialHealth,
initialOverview = null,
initialAudit,
initialNav = 'overview',
initialCaseRef = null,
}: OperationsShellProps) {
const [session, setSession] = useState<OperatorSession>(initialSession);
const [activeNav, setActiveNav] = useState<OperationsNavId>(initialNav);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
// Incidents navigation sub-state
const [incidentView, setIncidentView] = useState<'list' | 'create' | 'detail'>(
initialCaseRef ? 'detail' : 'list'
);
const [selectedCaseRef, setSelectedCaseRef] = useState<string | null>(initialCaseRef);
// Overview state
const [overviewData, setOverviewData] = useState<OperationsOverviewData | null>(initialOverview);
const [isRefreshingOverview, setIsRefreshingOverview] = useState(false);
// Health state
const [health, setHealth] = useState<OperationsHealth>(initialHealth);
const [isRefreshingHealth, setIsRefreshingHealth] = useState(false);
// Audit state
const [auditEvents, setAuditEvents] = useState<SafeAuditEvent[]>(
initialAudit?.events ?? []
);
const [nextCursor, setNextCursor] = useState<string | null>(
initialAudit?.next_cursor ?? null
);
const [auditTotal, setAuditTotal] = useState<number | undefined>(
initialAudit?.total
);
const [isLoadingAudit, setIsLoadingAudit] = useState(false);
const [isLoadingMoreAudit, setIsLoadingMoreAudit] = useState(false);
// Sign out state
const [signingOut, setSigningOut] = useState(false);
// Manual Health Refresh (Zero automatic polling)
async function handleRefreshHealth() {
if (isRefreshingHealth) return;
setIsRefreshingHealth(true);
try {
const response = await fetch('/api/ops/health', {
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
});
if (response.ok) {
const data = (await response.json()) as OperationsHealth;
setHealth(data);
} else {
setHealth((prev: OperationsHealth) => ({
...prev,
status: 'unavailable',
checked_at: new Date().toISOString(),
}));
}
} catch {
setHealth((prev: OperationsHealth) => ({
...prev,
status: 'unavailable',
checked_at: new Date().toISOString(),
}));
} finally {
setIsRefreshingHealth(false);
}
}
// Manual Overview Refresh (Zero automatic polling)
async function handleRefreshOverview() {
if (isRefreshingOverview) return;
setIsRefreshingOverview(true);
try {
const response = await fetch('/api/ops/overview', {
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
});
if (response.ok) {
const data = (await response.json()) as OperationsOverviewData;
setOverviewData(data);
if (data.health) {
setHealth(data.health);
}
}
} catch {
// Retain current state on failure
} finally {
setIsRefreshingOverview(false);
}
}
// Single initial fetch on mount (Zero interval polling)
React.useEffect(() => {
void handleRefreshOverview();
}, []);
// Next Page of Audit Events (Owner only, server cursor)
async function handleLoadNextAuditPage() {
if (!nextCursor || isLoadingMoreAudit) return;
setIsLoadingMoreAudit(true);
try {
const response = await fetch(
`/api/ops/audit-events?cursor=${encodeURIComponent(nextCursor)}`,
{
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
}
);
if (response.ok) {
const data = await response.json();
setAuditEvents((prev) => [...prev, ...(data.events ?? [])]);
setNextCursor(data.next_cursor ?? null);
if (typeof data.total === 'number') {
setAuditTotal(data.total);
}
}
} catch {
// Retain current events on failure
} finally {
setIsLoadingMoreAudit(false);
}
}
// Sign Out Handler
async function handleSignOut() {
if (signingOut) return;
setSigningOut(true);
try {
await fetch('/api/ops/auth/logout', {
method: 'POST',
headers: {
origin: window.location.origin,
},
});
} catch {
// Continue client cleanup regardless of network error
} finally {
window.location.href = '/login';
}
}
return (
<div className="ops-shell-root min-h-screen bg-surface font-body text-sm text-on-surface antialiased selection:bg-primary-container selection:text-on-primary-container w-full">
{/* Sidebar (Desktop persistent + Mobile off-canvas drawer) */}
<OperationsSidebar
activeNav={activeNav}
onSelectNav={(nav) => setActiveNav(nav)}
isOpenOnMobile={mobileSidebarOpen}
onCloseMobile={() => setMobileSidebarOpen(false)}
role={session.role}
/>
{/* Main Workspace Area */}
<div className="lg:pl-64 flex flex-col min-h-screen">
{/* Top App Bar */}
<OperationsTopBar
session={session}
onSignOut={handleSignOut}
onToggleMobileSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
signingOut={signingOut}
/>
{/* Content Body Container */}
<main className="relative pt-20 px-4 sm:px-6 lg:px-8 py-8 w-full flex-1">
{activeNav === 'overview' && (
<OperationsOverview
overview={overviewData}
health={overviewData?.health ?? health}
onRefresh={async () => {
await Promise.all([handleRefreshHealth(), handleRefreshOverview()]);
}}
isRefreshing={isRefreshingHealth || isRefreshingOverview}
onSelectIncident={(caseRef) => {
setSelectedCaseRef(caseRef);
setIncidentView('detail');
setActiveNav('incidents');
}}
onNavigateNav={(nav) => setActiveNav(nav)}
/>
)}
{activeNav === 'users' && (
<OperationsUsers role={session.role} />
)}
{activeNav === 'incidents' && (
<>
{incidentView === 'list' && (
<OperationsIncidentsList
role={session.role}
onSelectIncident={(caseRef) => {
setSelectedCaseRef(caseRef);
setIncidentView('detail');
}}
onCreateIncident={() => setIncidentView('create')}
/>
)}
{incidentView === 'create' && (
<OperationsCreateIncident
onIncidentCreated={(caseRef) => {
setSelectedCaseRef(caseRef);
setIncidentView('detail');
}}
onCancel={() => setIncidentView('list')}
/>
)}
{incidentView === 'detail' && selectedCaseRef && (
<OperationsIncidentDetail
caseReference={selectedCaseRef}
role={session.role}
onBack={() => {
setSelectedCaseRef(null);
setIncidentView('list');
}}
/>
)}
</>
)}
{activeNav === 'support' && (
<OperationsSupport role={session.role} />
)}
{activeNav === 'cms' && (
<OperationsCms role={session.role} />
)}
{activeNav === 'audit' && (
<OperationsAudit
role={session.role}
events={auditEvents}
nextCursor={nextCursor}
onNextPage={handleLoadNextAuditPage}
isLoading={isLoadingAudit}
isLoadingMore={isLoadingMoreAudit}
total={auditTotal}
/>
)}
{activeNav === 'operators' && <OperationsOperators />}
{activeNav === 'session' && (
<OperationsSessionView
session={session}
onSignOut={handleSignOut}
signingOut={signingOut}
/>
)}
</main>
</div>
</div>
);
}