feat: add isolated MotorSignal Operations console
This commit is contained in:
parent
5402412ea0
commit
55b3a5908a
|
|
@ -1,26 +1,163 @@
|
|||
import Link from 'next/link';
|
||||
import { cookies } from 'next/headers';
|
||||
import { AdminConsole } from '../../components/admin-console';
|
||||
import { OperationsShell } from '../../components/ops/operations-shell';
|
||||
import { OperationsErrorState } from '../../components/ops/operations-error-state';
|
||||
import { normalizeHealthResponse, sanitizeAuditEvent } from '../../lib/ops-data';
|
||||
import {
|
||||
OperatorSession,
|
||||
OperationsHealth,
|
||||
SafeAuditEvent,
|
||||
OperationsNavId,
|
||||
} from '../../components/ops/types';
|
||||
|
||||
type AuditEvent = { id: string; event_type: string; actor_user_id: string | null; target_user_id: string | null; created_at: string; metadata: Record<string, unknown> | null };
|
||||
interface AdminPageProps {
|
||||
searchParams?: Promise<{
|
||||
nav?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function parseNavParam(nav?: string): OperationsNavId {
|
||||
if (nav === 'audit' || nav === 'operators' || nav === 'session') {
|
||||
return nav;
|
||||
}
|
||||
return 'overview';
|
||||
}
|
||||
|
||||
export default async function AdminPage({ searchParams }: AdminPageProps) {
|
||||
const resolvedParams = searchParams ? await searchParams : {};
|
||||
const initialNav = parseNavParam(resolvedParams.nav);
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const opsToken = isProduction
|
||||
? cookieStore.get('__Host-motorsignal_ops')?.value
|
||||
: cookieStore.get('__Host-motorsignal_ops')?.value ??
|
||||
cookieStore.get('motorsignal_ops')?.value;
|
||||
|
||||
// Unauthenticated - prompt for login (Zero query parameter preview/bypass)
|
||||
if (!opsToken) {
|
||||
return (
|
||||
<section className="min-h-screen bg-surface flex items-center justify-center p-4">
|
||||
<OperationsErrorState
|
||||
status={401}
|
||||
message="Your Operations session has expired or is unauthenticated."
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
async function loadAuditEvents(): Promise<{ status: number; events: AuditEvent[] }> {
|
||||
const token = (await cookies()).get('motorsignal_access_token')?.value;
|
||||
if (!token) return { status: 401, events: [] };
|
||||
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||
const headers = {
|
||||
authorization: `Bearer ${opsToken}`,
|
||||
accept: 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/v1/admin/audit-events`, { headers: { authorization: `Bearer ${token}` }, cache: 'no-store' });
|
||||
if (!response.ok) return { status: response.status, events: [] };
|
||||
return { status: response.status, events: await response.json() as AuditEvent[] };
|
||||
} catch {
|
||||
return { status: 503, events: [] };
|
||||
// STEP 1: Verify Operations identity & role first
|
||||
const meResponse = await fetch(`${baseUrl}/api/v1/ops/me`, { headers, cache: 'no-store' });
|
||||
|
||||
if (meResponse.status === 401) {
|
||||
return (
|
||||
<section className="min-h-screen bg-surface flex items-center justify-center p-4">
|
||||
<OperationsErrorState
|
||||
status={401}
|
||||
message="Your Operations session has expired. Please sign in again."
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (meResponse.status === 403) {
|
||||
return (
|
||||
<section className="min-h-screen bg-surface flex items-center justify-center p-4">
|
||||
<OperationsErrorState
|
||||
status={403}
|
||||
message="You do not have sufficient authority to access Operations."
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!meResponse.ok) {
|
||||
return (
|
||||
<section className="min-h-screen bg-surface flex items-center justify-center p-4">
|
||||
<OperationsErrorState
|
||||
status={503}
|
||||
message="Operations platform is temporarily unavailable."
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const meData = await meResponse.json();
|
||||
const session: OperatorSession = {
|
||||
identity: meData.identity ?? meData.email ?? 'operator',
|
||||
role: meData.role === 'owner' ? 'owner' : 'operator',
|
||||
status: meData.status === 'expired' || meData.status === 'revoked' ? meData.status : 'active',
|
||||
createdAt: meData.created_at ?? meData.createdAt,
|
||||
lastActivityAt: meData.last_activity_at ?? meData.lastActivityAt,
|
||||
expiresAt: meData.expires_at ?? meData.expiresAt,
|
||||
environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||
};
|
||||
|
||||
// STEP 2: After successful verified session, fetch health, and ONLY fetch audit if role == 'owner'
|
||||
const isOwner = session.role === 'owner';
|
||||
const [healthResponse, auditResponse] = await Promise.all([
|
||||
fetch(`${baseUrl}/api/v1/ops/health`, { headers, cache: 'no-store' }),
|
||||
isOwner
|
||||
? fetch(`${baseUrl}/api/v1/ops/audit-events`, { headers, cache: 'no-store' }).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
let health: OperationsHealth;
|
||||
if (healthResponse.ok) {
|
||||
const healthData = await healthResponse.json();
|
||||
health = normalizeHealthResponse(healthData);
|
||||
} else {
|
||||
health = normalizeHealthResponse({ status: 'unavailable', checked_at: new Date().toISOString() });
|
||||
}
|
||||
|
||||
let initialAuditEvents: SafeAuditEvent[] = [];
|
||||
let nextCursor: string | null = null;
|
||||
let auditTotal: number | undefined = undefined;
|
||||
|
||||
if (auditResponse && auditResponse.ok) {
|
||||
const auditData = await auditResponse.json();
|
||||
const rawEvents = Array.isArray(auditData)
|
||||
? auditData
|
||||
: Array.isArray(auditData?.events)
|
||||
? auditData.events
|
||||
: Array.isArray(auditData?.items)
|
||||
? auditData.items
|
||||
: [];
|
||||
initialAuditEvents = rawEvents
|
||||
.map(sanitizeAuditEvent)
|
||||
.filter((e: SafeAuditEvent | null): e is SafeAuditEvent => e !== null);
|
||||
nextCursor = auditData?.next_cursor ?? auditData?.cursor ?? null;
|
||||
if (typeof auditData?.total === 'number') {
|
||||
auditTotal = auditData.total;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function AdminPage() {
|
||||
const result = await loadAuditEvents();
|
||||
if (result.status === 401) return <section className="content-page"><span className="eyebrow">Admin console</span><h1>Sign in required.</h1><p>Admin tools require an authenticated MotorSignal session.</p><Link className="button" href="/login">Sign in</Link></section>;
|
||||
if (result.status === 403) return <section className="content-page"><span className="eyebrow">Admin console</span><h1>Access restricted.</h1><p>Your account does not have support or admin permissions.</p><Link className="button button-secondary" href="/account">Back to account</Link></section>;
|
||||
if (result.status !== 200) return <section className="content-page"><span className="eyebrow">Admin console</span><h1>Console unavailable.</h1><p>Audit data could not be loaded. Try again when the API is available.</p></section>;
|
||||
return <section className="content-page"><span className="eyebrow">Admin console</span><h1>Operations.</h1><p>Manage users, vehicles, rides, and review recent account activity.</p><AdminConsole /><h2 className="admin-audit-heading">Recent audit events</h2><div className="audit-list">{result.events.length === 0 ? <div className="panel"><p>No events recorded.</p></div> : result.events.map((event) => <article className="panel audit-row" key={event.id}><div><strong>{event.event_type}</strong><p>{event.actor_user_id ?? 'System'} {event.target_user_id ? `targeted ${event.target_user_id}` : ''}</p></div><time dateTime={event.created_at}>{new Date(event.created_at).toLocaleString()}</time></article>)}</div></section>;
|
||||
return (
|
||||
<OperationsShell
|
||||
initialSession={session}
|
||||
initialHealth={health}
|
||||
initialAudit={{
|
||||
events: initialAuditEvents,
|
||||
next_cursor: nextCursor,
|
||||
total: auditTotal,
|
||||
}}
|
||||
initialNav={initialNav}
|
||||
/>
|
||||
);
|
||||
} catch {
|
||||
return (
|
||||
<section className="min-h-screen bg-surface flex items-center justify-center p-4">
|
||||
<OperationsErrorState
|
||||
status={503}
|
||||
message="Unable to connect to the Operations platform service."
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const resources = new Set(['users', 'vehicles', 'rides']);
|
||||
|
||||
async function proxy(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {
|
||||
const { path } = await context.params;
|
||||
if (!path.length || !resources.has(path[0])) return NextResponse.json({ detail: 'Unknown admin resource.' }, { status: 404 });
|
||||
const token = (await cookies()).get('motorsignal_access_token')?.value;
|
||||
if (!token) return NextResponse.json({ detail: 'Authentication required.' }, { status: 401 });
|
||||
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||
const body = request.method === 'GET' || request.method === 'DELETE' ? undefined : await request.text();
|
||||
const response = await fetch(`${baseUrl}/api/v1/admin/${path.join('/')}${request.nextUrl.search}`, { method: request.method, headers: { authorization: `Bearer ${token}`, ...(body ? { 'content-type': 'application/json' } : {}) }, body, cache: 'no-store' });
|
||||
return new NextResponse(await response.text(), { status: response.status, headers: { 'content-type': response.headers.get('content-type') ?? 'application/json' } });
|
||||
}
|
||||
|
||||
export const GET = proxy;
|
||||
export const POST = proxy;
|
||||
export const PATCH = proxy;
|
||||
export const DELETE = proxy;
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { sanitizeAuditEvent } from '../../../../lib/ops-data';
|
||||
import { SafeAuditEvent } from '../../../../components/ops/types';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const cookieStore = await cookies();
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const opsToken = isProduction
|
||||
? cookieStore.get('__Host-motorsignal_ops')?.value
|
||||
: cookieStore.get('__Host-motorsignal_ops')?.value ??
|
||||
cookieStore.get('motorsignal_ops')?.value;
|
||||
|
||||
if (!opsToken) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Authentication required.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const cursor = request.nextUrl.searchParams.get('cursor');
|
||||
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||
const targetUrl = new URL(`${baseUrl}/api/v1/ops/audit-events`);
|
||||
if (cursor) {
|
||||
targetUrl.searchParams.set('cursor', cursor);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
authorization: `Bearer ${opsToken}`,
|
||||
accept: 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Your Operations session has expired.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
if (response.status === 403) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'You do not have authority to view Audit.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Operations is temporarily unavailable.' },
|
||||
{ status: response.status >= 500 ? response.status : 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const rawEvents = Array.isArray(data)
|
||||
? data
|
||||
: Array.isArray(data?.events)
|
||||
? data.events
|
||||
: Array.isArray(data?.items)
|
||||
? data.items
|
||||
: [];
|
||||
|
||||
const safeEvents: SafeAuditEvent[] = rawEvents
|
||||
.map(sanitizeAuditEvent)
|
||||
.filter((e: SafeAuditEvent | null): e is SafeAuditEvent => e !== null);
|
||||
|
||||
const nextCursor = data?.next_cursor ?? data?.cursor ?? null;
|
||||
const total = typeof data?.total === 'number' ? data.total : undefined;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
events: safeEvents,
|
||||
next_cursor: nextCursor,
|
||||
total,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Operations is temporarily unavailable.' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { hasValidOrigin } from '../../../../../lib/request-origin';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// 1. Validate request origin
|
||||
if (!hasValidOrigin(request)) {
|
||||
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 2. Obtain real Operations session cookie
|
||||
const cookieStore = await cookies();
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const opsToken = isProduction
|
||||
? cookieStore.get('__Host-motorsignal_ops')?.value
|
||||
: cookieStore.get('__Host-motorsignal_ops')?.value ??
|
||||
cookieStore.get('motorsignal_ops')?.value;
|
||||
|
||||
// 3. If token present, invoke backend session revocation endpoint
|
||||
if (opsToken) {
|
||||
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||
try {
|
||||
await fetch(`${baseUrl}/api/v1/ops/sessions/current`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
authorization: `Bearer ${opsToken}`,
|
||||
accept: 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
// All terminal backend states (204, 200, 401 already expired, 403, 404, 503) proceed to clear local cookie
|
||||
} catch {
|
||||
// Network failure / backend unavailable: proceed safely to clear local cookie without leaking internals
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Clear local Operations cookies (host and non-host dev fallback)
|
||||
const response = new NextResponse(null, { status: 204 });
|
||||
|
||||
response.cookies.set('__Host-motorsignal_ops', '', {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
maxAge: 0,
|
||||
expires: new Date(0),
|
||||
});
|
||||
|
||||
response.cookies.set('motorsignal_ops', '', {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
maxAge: 0,
|
||||
expires: new Date(0),
|
||||
});
|
||||
|
||||
// Rider consumer session cookies are completely unreferenced and untouched
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { normalizeHealthResponse } from '../../../../lib/ops-data';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const cookieStore = await cookies();
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const opsToken = isProduction
|
||||
? cookieStore.get('__Host-motorsignal_ops')?.value
|
||||
: cookieStore.get('__Host-motorsignal_ops')?.value ??
|
||||
cookieStore.get('motorsignal_ops')?.value;
|
||||
|
||||
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||
const headers: Record<string, string> = {
|
||||
accept: 'application/json',
|
||||
};
|
||||
|
||||
if (opsToken) {
|
||||
headers['authorization'] = `Bearer ${opsToken}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/v1/ops/health`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Authentication required.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
if (response.status === 403) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Access restricted.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
normalizeHealthResponse({ status: 'unavailable', checked_at: new Date().toISOString() }),
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(normalizeHealthResponse(data), { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
normalizeHealthResponse({ status: 'unavailable', checked_at: new Date().toISOString() }),
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { OperatorSession } from '../../../../components/ops/types';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const cookieStore = await cookies();
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const opsToken = isProduction
|
||||
? cookieStore.get('__Host-motorsignal_ops')?.value
|
||||
: cookieStore.get('__Host-motorsignal_ops')?.value ??
|
||||
cookieStore.get('motorsignal_ops')?.value;
|
||||
|
||||
if (!opsToken) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Authentication required.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/v1/ops/me`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
authorization: `Bearer ${opsToken}`,
|
||||
accept: 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Your Operations session has expired.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
if (response.status === 403) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Access restricted.' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Operations is temporarily unavailable.' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const session: OperatorSession = {
|
||||
identity: data.identity ?? data.email ?? 'operator',
|
||||
role: data.role === 'owner' ? 'owner' : 'operator',
|
||||
status: data.status === 'expired' || data.status === 'revoked' ? data.status : 'active',
|
||||
createdAt: data.created_at ?? data.createdAt,
|
||||
lastActivityAt: data.last_activity_at ?? data.lastActivityAt,
|
||||
expiresAt: data.expires_at ?? data.expiresAt,
|
||||
environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||
};
|
||||
|
||||
return NextResponse.json({ session }, { status: 200 });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ detail: 'Operations is temporarily unavailable.' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
|
||||
type Resource = 'users' | 'vehicles' | 'rides';
|
||||
type Item = Record<string, string | number | boolean | null>;
|
||||
|
||||
async function adminRequest(path: string, init?: RequestInit) {
|
||||
const response = await fetch(`/api/admin/${path}`, { ...init, headers: { 'content-type': 'application/json', ...(init?.headers ?? {}) } });
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(body?.detail ?? 'Admin request failed.');
|
||||
return body;
|
||||
}
|
||||
|
||||
export function AdminConsole() {
|
||||
const [resource, setResource] = useState<Resource>('users');
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [message, setMessage] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function load(selected = resource) {
|
||||
setBusy(true); setMessage('');
|
||||
try { const body = await adminRequest(`${selected}?page=1&page_size=50`); setItems(body.items ?? []); }
|
||||
catch (error) { setItems([]); setMessage(error instanceof Error ? error.message : 'Unable to load admin data.'); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
useEffect(() => { void load(); }, [resource]);
|
||||
|
||||
async function create(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault(); setBusy(true); setMessage('');
|
||||
const form = new FormData(event.currentTarget);
|
||||
const payload = Object.fromEntries(form.entries());
|
||||
try { await adminRequest(resource, { method: 'POST', body: JSON.stringify(payload) }); event.currentTarget.reset(); setMessage('Created.'); await load(); }
|
||||
catch (error) { setMessage(error instanceof Error ? error.message : 'Create failed.'); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function archive(id: string) {
|
||||
setBusy(true); setMessage('');
|
||||
try { await adminRequest(`${resource}/${id}`, { method: 'DELETE' }); setMessage(resource === 'rides' ? 'Ride archived.' : 'Deleted.'); await load(); }
|
||||
catch (error) { setMessage(error instanceof Error ? error.message : 'Action failed.'); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
return <div className="admin-console"><div className="admin-tabs">{(['users', 'vehicles', 'rides'] as Resource[]).map((value) => <button className={resource === value ? 'admin-tab active' : 'admin-tab'} key={value} onClick={() => setResource(value)} type="button">{value}</button>)}</div><form className="admin-create-form" onSubmit={create}>{resource === 'users' && <><input name="email" required placeholder="Email" type="email" /><input name="password" required minLength={12} placeholder="Temporary password" type="password" /><select defaultValue="user" name="role"><option value="user">User</option><option value="support">Support</option><option value="admin">Admin</option></select></>}{resource === 'vehicles' && <><input name="user_id" required placeholder="Owner user ID" /><input name="nickname" required placeholder="Nickname" /><input name="make" required placeholder="Make" /><input name="model" required placeholder="Model" /><input name="year" placeholder="Year" type="number" /></>}{resource === 'rides' && <><input name="user_id" required placeholder="Rider user ID" /><input name="vehicle_id" placeholder="Vehicle ID" /><input name="started_at" required type="datetime-local" /><input name="distance_miles" placeholder="Distance miles" type="number" step="0.1" /><input name="top_speed_mph" placeholder="Top speed MPH" type="number" step="0.1" /></>}<button className="button" disabled={busy} type="submit">Create {resource.slice(0, -1)}</button></form>{message && <p className="form-message" role="status">{message}</p>}<div className="admin-table-wrap"><table className="admin-table"><thead><tr>{resource === 'users' ? <><th>Email</th><th>Role</th><th>Status</th><th /></> : resource === 'vehicles' ? <><th>Nickname</th><th>Make / model</th><th>Owner</th><th /></> : <><th>Ride</th><th>Rider</th><th>Distance</th><th>Status</th><th /></>}</tr></thead><tbody>{items.map((item) => <tr key={String(item.id)}>{resource === 'users' ? <><td>{String(item.email)}</td><td>{String(item.role)}</td><td>{item.is_active ? 'Active' : 'Inactive'}</td><td><button className="text-button" onClick={() => void archive(String(item.id))} type="button">Deactivate</button></td></> : resource === 'vehicles' ? <><td>{String(item.nickname)}</td><td>{String(item.make)} {String(item.model)}</td><td>{String(item.user_id)}</td><td><button className="text-button" onClick={() => void archive(String(item.id))} type="button">Delete</button></td></> : <><td>{String(item.id).slice(0, 8)}</td><td>{String(item.user_id)}</td><td>{item.distance_miles == null ? '-' : `${item.distance_miles} mi`}</td><td>{String(item.status)}</td><td>{item.status !== 'archived' && <button className="text-button" onClick={() => void archive(String(item.id))} type="button">Archive</button>}</td></>}</tr>)}</tbody></table>{!busy && items.length === 0 && <p className="empty-state">No {resource} found.</p>}</div></div>;
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { SafeAuditEvent, OperationsRole } from './types';
|
||||
|
||||
interface OperationsAuditProps {
|
||||
role: OperationsRole;
|
||||
events: SafeAuditEvent[];
|
||||
nextCursor?: string | null;
|
||||
onNextPage?: () => Promise<void> | void;
|
||||
isLoading?: boolean;
|
||||
isLoadingMore?: boolean;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export function OperationsAudit({
|
||||
role,
|
||||
events,
|
||||
nextCursor,
|
||||
onNextPage,
|
||||
isLoading = false,
|
||||
isLoadingMore = false,
|
||||
total,
|
||||
}: OperationsAuditProps) {
|
||||
const isOwner = role === 'owner';
|
||||
|
||||
// Section 7: Operator Denied Access UX
|
||||
if (!isOwner) {
|
||||
return (
|
||||
<div className="p-8 rounded-xl bg-surface-container-low border border-outline-variant/30 text-center max-w-xl mx-auto my-12 space-y-4">
|
||||
<div className="w-12 h-12 rounded-full bg-surface-container-high border border-outline-variant/40 flex items-center justify-center text-outline mx-auto">
|
||||
<span className="material-symbols-outlined text-[24px]">lock</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-mono uppercase tracking-widest text-outline">
|
||||
ACCESS RESTRICTED
|
||||
</span>
|
||||
<h2 className="text-lg font-bold text-on-surface mt-1">
|
||||
Owner Authority Required
|
||||
</h2>
|
||||
<p className="text-xs text-on-surface-variant mt-2 leading-relaxed">
|
||||
You do not have authority to view Audit. Audit log inspection is restricted to verified Operations Owners.
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-outline-variant/20 flex items-center justify-center gap-2 text-xs text-outline font-mono">
|
||||
<span>Current Role:</span>
|
||||
<span className="px-2 py-0.5 rounded bg-surface-container-highest text-on-surface font-semibold uppercase">
|
||||
{role}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Row */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-outline-variant/30">
|
||||
<div>
|
||||
<span className="text-[11px] font-mono uppercase tracking-widest text-primary-fixed-dim">
|
||||
GOVERNANCE & COMPLIANCE
|
||||
</span>
|
||||
<h1 className="font-headline-xl text-2xl sm:text-3xl font-bold text-on-surface tracking-tight mt-1">
|
||||
Audit Activity
|
||||
</h1>
|
||||
<p className="text-xs text-on-surface-variant mt-1">
|
||||
Immutable operational and security activity log across platform boundaries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{total !== undefined && (
|
||||
<div className="text-right shrink-0">
|
||||
<span className="text-[10px] uppercase font-mono text-outline block">
|
||||
Recorded Events
|
||||
</span>
|
||||
<span className="text-xs font-mono font-bold text-on-surface">
|
||||
{total.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Audit Activity Table Container */}
|
||||
<div className="rounded-xl bg-surface-container-low border border-outline-variant/30 overflow-hidden shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<table
|
||||
className="w-full text-left border-collapse min-w-[720px]"
|
||||
aria-label="Operations Audit Events"
|
||||
>
|
||||
<thead>
|
||||
<tr className="border-b border-outline-variant/30 bg-surface-container-lowest/60 text-[11px] font-mono uppercase tracking-wider text-outline">
|
||||
<th scope="col" className="py-3 px-4">
|
||||
TIME
|
||||
</th>
|
||||
<th scope="col" className="py-3 px-4">
|
||||
ACTION
|
||||
</th>
|
||||
<th scope="col" className="py-3 px-4">
|
||||
RESULT
|
||||
</th>
|
||||
<th scope="col" className="py-3 px-4">
|
||||
ACTOR
|
||||
</th>
|
||||
<th scope="col" className="py-3 px-4">
|
||||
TARGET
|
||||
</th>
|
||||
<th scope="col" className="py-3 px-4">
|
||||
CASE
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-outline-variant/20 text-xs">
|
||||
{isLoading ? (
|
||||
// Skeleton loading state
|
||||
Array.from({ length: 5 }).map((_, index) => (
|
||||
<tr key={`skeleton-${index}`} className="animate-pulse">
|
||||
<td className="py-3.5 px-4">
|
||||
<div className="h-3 w-28 bg-surface-container-high rounded"></div>
|
||||
</td>
|
||||
<td className="py-3.5 px-4">
|
||||
<div className="h-4 w-32 bg-surface-container-high rounded"></div>
|
||||
</td>
|
||||
<td className="py-3.5 px-4">
|
||||
<div className="h-4 w-16 bg-surface-container-high rounded"></div>
|
||||
</td>
|
||||
<td className="py-3.5 px-4">
|
||||
<div className="h-3 w-36 bg-surface-container-high rounded"></div>
|
||||
</td>
|
||||
<td className="py-3.5 px-4">
|
||||
<div className="h-3 w-20 bg-surface-container-high rounded"></div>
|
||||
</td>
|
||||
<td className="py-3.5 px-4">
|
||||
<div className="h-3 w-16 bg-surface-container-high rounded"></div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : events.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
className="py-12 px-4 text-center text-on-surface-variant"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[32px] text-outline block mb-2">
|
||||
inbox
|
||||
</span>
|
||||
<p className="text-xs font-medium">No audit events recorded.</p>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
events.map((event, index) => {
|
||||
const formattedTime = new Date(event.timestamp).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
|
||||
const isSuccess =
|
||||
event.result.toLowerCase() === 'success' ||
|
||||
event.result.toLowerCase() === 'recorded';
|
||||
const isFailure =
|
||||
event.result.toLowerCase() === 'failure' ||
|
||||
event.result.toLowerCase() === 'denied';
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={`event-${index}`}
|
||||
className="hover:bg-surface-container-high/40 transition-colors"
|
||||
>
|
||||
{/* TIME */}
|
||||
<td className="py-3 px-4 font-mono text-[11px] text-on-surface-variant whitespace-nowrap">
|
||||
<time dateTime={event.timestamp}>{formattedTime}</time>
|
||||
</td>
|
||||
|
||||
{/* ACTION */}
|
||||
<td className="py-3 px-4 font-medium text-on-surface whitespace-nowrap">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-mono text-xs">{event.action}</span>
|
||||
{event.reason_code && (
|
||||
<span className="text-[10px] text-outline font-mono">
|
||||
reason: {event.reason_code}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* RESULT */}
|
||||
<td className="py-3 px-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-mono uppercase font-bold tracking-wider border ${
|
||||
isSuccess
|
||||
? 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60'
|
||||
: isFailure
|
||||
? 'bg-rose-950/60 text-rose-400 border-rose-800/60'
|
||||
: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/30'
|
||||
}`}
|
||||
>
|
||||
{event.result}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* ACTOR (Current Operations Identity) */}
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-on-surface truncate max-w-[200px]" title={event.actor_identity}>
|
||||
{event.actor_identity}
|
||||
</span>
|
||||
{event.actor_role && (
|
||||
<span className="text-[10px] text-outline font-mono uppercase">
|
||||
Role: {event.actor_role}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* TARGET */}
|
||||
<td className="py-3 px-4 font-mono text-[11px] text-on-surface-variant">
|
||||
{event.target_resource || '--'}
|
||||
</td>
|
||||
|
||||
{/* CASE */}
|
||||
<td className="py-3 px-4 font-mono text-[11px] text-on-surface-variant">
|
||||
{event.case_reference || '--'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Server Cursor Bounded Pagination */}
|
||||
{nextCursor && (
|
||||
<div className="p-3 border-t border-outline-variant/30 bg-surface-container-lowest/40 flex items-center justify-between">
|
||||
<span className="text-[11px] text-outline font-mono">
|
||||
Displaying bounded result set
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onNextPage?.()}
|
||||
disabled={isLoadingMore}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-surface-container-high hover:bg-surface-bright text-on-surface border border-outline-variant/40 text-xs font-semibold transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50"
|
||||
>
|
||||
<span>{isLoadingMore ? 'Loading...' : 'Next Page'}</span>
|
||||
<span className="material-symbols-outlined text-[14px]">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface OperationsErrorStateProps {
|
||||
status: 401 | 403 | 404 | 503 | 'disabled' | 'network' | number;
|
||||
message?: string;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
export function OperationsErrorState({
|
||||
status,
|
||||
message,
|
||||
onRetry,
|
||||
}: OperationsErrorStateProps) {
|
||||
let title = 'Operations Error';
|
||||
let description = message ?? 'An unexpected error occurred in the Operations console.';
|
||||
let icon = 'error';
|
||||
let showSignIn = false;
|
||||
let showRetry = true;
|
||||
|
||||
if (status === 401) {
|
||||
title = 'Authentication Required';
|
||||
description = message ?? 'Your Operations session has expired or is unauthenticated.';
|
||||
icon = 'lock';
|
||||
showSignIn = true;
|
||||
showRetry = false;
|
||||
} else if (status === 403) {
|
||||
title = 'Access Restricted';
|
||||
description = message ?? 'You do not have sufficient authority to access Operations.';
|
||||
icon = 'gavel';
|
||||
showSignIn = false;
|
||||
showRetry = false;
|
||||
} else if (status === 404) {
|
||||
title = 'Resource Not Found';
|
||||
description = message ?? 'The requested Operations resource does not exist.';
|
||||
icon = 'search_off';
|
||||
} else if (status === 503 || status === 'network') {
|
||||
title = 'Operations Temporarily Unavailable';
|
||||
description = message ?? 'Unable to communicate with the Operations API. Try again in a few moments.';
|
||||
icon = 'cloud_off';
|
||||
} else if (status === 'disabled') {
|
||||
title = 'Operations Disabled';
|
||||
description = message ?? 'The Operations platform is currently disabled by system policy.';
|
||||
icon = 'block';
|
||||
showRetry = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 rounded-xl bg-surface-container-low border border-outline-variant/30 text-center max-w-lg mx-auto my-12 space-y-4 shadow-sm">
|
||||
<div className="w-12 h-12 rounded-full bg-surface-container-high border border-outline-variant/40 flex items-center justify-center text-outline mx-auto">
|
||||
<span className="material-symbols-outlined text-[24px]">{icon}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] font-mono uppercase tracking-widest text-outline">
|
||||
STATUS {typeof status === 'number' ? status : status.toUpperCase()}
|
||||
</span>
|
||||
<h2 className="text-lg font-bold text-on-surface">{title}</h2>
|
||||
<p className="text-xs text-on-surface-variant leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-outline-variant/20 flex items-center justify-center gap-3">
|
||||
{showSignIn && (
|
||||
<Link
|
||||
href="/login"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg bg-primary-container text-on-primary-container text-xs font-bold shadow-sm hover:brightness-105 transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">login</span>
|
||||
<span>Sign In</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{showRetry && onRetry && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRetry}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg bg-surface-container-high hover:bg-surface-bright text-on-surface border border-outline-variant/30 text-xs font-semibold transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">refresh</span>
|
||||
<span>Retry</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-medium text-on-surface-variant hover:text-on-surface transition-colors"
|
||||
>
|
||||
Return Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export function OperationsOperators() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Row */}
|
||||
<div className="pb-4 border-b border-outline-variant/30">
|
||||
<span className="text-[11px] font-mono uppercase tracking-widest text-primary-fixed-dim">
|
||||
ACCESS CONTROL
|
||||
</span>
|
||||
<h1 className="font-headline-xl text-2xl sm:text-3xl font-bold text-on-surface tracking-tight mt-1">
|
||||
OPERATORS
|
||||
</h1>
|
||||
<p className="text-xs text-on-surface-variant mt-1">
|
||||
Directory and role assignments for platform administrative operators.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Clean Placeholder Panel */}
|
||||
<div className="p-8 rounded-xl bg-surface-container-low border border-outline-variant/30 max-w-2xl mx-auto my-8 text-center space-y-4 shadow-sm">
|
||||
<div className="w-12 h-12 rounded-full bg-surface-container-high border border-outline-variant/40 flex items-center justify-center text-outline mx-auto">
|
||||
<span className="material-symbols-outlined text-[24px]">manage_accounts</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-base font-bold text-on-surface uppercase tracking-wide">
|
||||
OPERATORS
|
||||
</h2>
|
||||
<p className="text-xs text-on-surface-variant max-w-md mx-auto leading-relaxed">
|
||||
Operator management is not enabled in this Operations foundation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-outline-variant/20 text-[11px] text-outline text-left space-y-2 bg-surface-container-lowest/50 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-1.5 font-mono text-on-surface font-semibold uppercase text-[10px]">
|
||||
<span className="material-symbols-outlined text-[14px] text-primary-fixed-dim">
|
||||
info
|
||||
</span>
|
||||
<span>Security Governance Policy</span>
|
||||
</div>
|
||||
<p className="leading-normal">
|
||||
Operator accounts, role promotions, and revocations are managed strictly through versioned PostgreSQL infrastructure migrations on the <code className="text-primary-fixed-dim font-mono">ops.operators</code> table.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { OperationsHealth, ComponentHealthStatus, HealthComponentKey } from './types';
|
||||
import { HEALTH_COMPONENT_METADATA } from '../../lib/ops-data';
|
||||
|
||||
interface OperationsOverviewProps {
|
||||
health: OperationsHealth;
|
||||
onRefresh: () => Promise<void> | void;
|
||||
isRefreshing: boolean;
|
||||
}
|
||||
|
||||
export function OperationsOverview({
|
||||
health,
|
||||
onRefresh,
|
||||
isRefreshing,
|
||||
}: OperationsOverviewProps) {
|
||||
const componentKeys: HealthComponentKey[] = [
|
||||
'operations_api',
|
||||
'operations_database',
|
||||
'session_validation',
|
||||
'audit_persistence',
|
||||
];
|
||||
|
||||
function getStatusBadge(status: ComponentHealthStatus) {
|
||||
switch (status) {
|
||||
case 'available':
|
||||
return {
|
||||
label: 'AVAILABLE',
|
||||
icon: 'check_circle',
|
||||
classes: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60',
|
||||
};
|
||||
case 'degraded':
|
||||
return {
|
||||
label: 'DEGRADED',
|
||||
icon: 'warning',
|
||||
classes: 'bg-amber-950/60 text-amber-400 border-amber-800/60',
|
||||
};
|
||||
case 'unavailable':
|
||||
return {
|
||||
label: 'UNAVAILABLE',
|
||||
icon: 'cancel',
|
||||
classes: 'bg-rose-950/60 text-rose-400 border-rose-800/60',
|
||||
};
|
||||
case 'unknown':
|
||||
default:
|
||||
return {
|
||||
label: 'UNKNOWN',
|
||||
icon: 'help',
|
||||
classes: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/40',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const formattedCheckedAt = health.checked_at
|
||||
? new Date(health.checked_at).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
})
|
||||
: '--';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Row: Title & Manual Refresh */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-outline-variant/30">
|
||||
<div>
|
||||
<span className="text-[11px] font-mono uppercase tracking-widest text-primary-fixed-dim">
|
||||
SYSTEM HEALTH
|
||||
</span>
|
||||
<h1 className="font-headline-xl text-2xl sm:text-3xl font-bold text-on-surface tracking-tight mt-1">
|
||||
Operations Overview
|
||||
</h1>
|
||||
<p className="text-xs text-on-surface-variant mt-1">
|
||||
Real-time health telemetry across core Operations subsystems.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div className="text-right">
|
||||
<span className="block text-[10px] uppercase font-mono text-outline">
|
||||
Last checked
|
||||
</span>
|
||||
<time
|
||||
dateTime={health.checked_at}
|
||||
className="text-xs font-mono text-on-surface font-medium"
|
||||
>
|
||||
{formattedCheckedAt}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void onRefresh()}
|
||||
disabled={isRefreshing}
|
||||
aria-label="Refresh Operations health status"
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-surface-container-high hover:bg-surface-bright text-primary-container border border-primary-container/40 text-xs font-semibold shadow-sm transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[16px] ${
|
||||
isRefreshing ? 'animate-spin' : ''
|
||||
}`}
|
||||
>
|
||||
refresh
|
||||
</span>
|
||||
<span>{isRefreshing ? 'Checking...' : 'Refresh'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4 Health Status Cards */}
|
||||
<div
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"
|
||||
aria-label="Subsystem Health Telemetry"
|
||||
>
|
||||
{componentKeys.map((key) => {
|
||||
const meta = HEALTH_COMPONENT_METADATA[key];
|
||||
const comp = health.components?.[key];
|
||||
const status = comp?.status ?? 'unknown';
|
||||
const badge = getStatusBadge(status);
|
||||
|
||||
return (
|
||||
<article
|
||||
key={key}
|
||||
className="p-4 rounded-xl bg-surface-container-low border border-outline-variant/30 flex flex-col justify-between gap-3 relative overflow-hidden group hover:border-outline-variant/60 transition-colors"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-outline">
|
||||
Subsystem
|
||||
</span>
|
||||
<h2 className="text-sm font-bold text-on-surface tracking-tight">
|
||||
{meta.name}
|
||||
</h2>
|
||||
<p className="text-[11px] text-on-surface-variant leading-relaxed">
|
||||
{comp?.description || meta.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-outline-variant/20 flex items-center justify-between">
|
||||
<span className="text-[10px] text-outline font-mono uppercase">
|
||||
Status
|
||||
</span>
|
||||
<div
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-mono font-bold tracking-wider border uppercase ${badge.classes}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">
|
||||
{badge.icon}
|
||||
</span>
|
||||
<span>{badge.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Operational Policy & Architecture Boundary */}
|
||||
<div className="p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/30 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary-container text-[18px]">
|
||||
verified_user
|
||||
</span>
|
||||
<h3 className="text-xs font-bold text-on-surface uppercase tracking-wider">
|
||||
Operational Architecture & Data Boundary
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-on-surface-variant pt-2 border-t border-outline-variant/20">
|
||||
<div>
|
||||
<span className="block font-semibold text-on-surface mb-0.5">
|
||||
Authority Source
|
||||
</span>
|
||||
<p className="leading-normal">
|
||||
PostgreSQL <code className="text-primary-fixed-dim">ops.operators</code> schema. Consumer roles and Firebase custom claims strictly bypassed.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block font-semibold text-on-surface mb-0.5">
|
||||
Privacy Isolation
|
||||
</span>
|
||||
<p className="leading-normal">
|
||||
Zero cross-rider data access. Personal rides, GPS points, vehicles, and billing accounts are structurally inaccessible.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="block font-semibold text-on-surface mb-0.5">
|
||||
Session Integrity
|
||||
</span>
|
||||
<p className="leading-normal">
|
||||
Governed by host-prefixed <code className="text-primary-fixed-dim">__Host-motorsignal_ops</code> cookies with strict same-origin isolation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { OperatorSession } from './types';
|
||||
|
||||
interface OperationsSessionProps {
|
||||
session: OperatorSession;
|
||||
onSignOut: () => void;
|
||||
signingOut?: boolean;
|
||||
}
|
||||
|
||||
export function OperationsSessionView({
|
||||
session,
|
||||
onSignOut,
|
||||
signingOut = false,
|
||||
}: OperationsSessionProps) {
|
||||
function formatDate(isoString?: string) {
|
||||
if (!isoString) return '--';
|
||||
return new Date(isoString).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
}
|
||||
|
||||
const isOwner = session.role === 'owner';
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-3xl">
|
||||
{/* Header Row */}
|
||||
<div className="pb-4 border-b border-outline-variant/30">
|
||||
<span className="text-[11px] font-mono uppercase tracking-widest text-primary-fixed-dim">
|
||||
SESSION CREDENTIALS
|
||||
</span>
|
||||
<h1 className="font-headline-xl text-2xl sm:text-3xl font-bold text-on-surface tracking-tight mt-1">
|
||||
My Session
|
||||
</h1>
|
||||
<p className="text-xs text-on-surface-variant mt-1">
|
||||
Cryptographic authority and active authentication status for this browser session.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Session Details Card */}
|
||||
<div className="p-6 rounded-xl bg-surface-container-low border border-outline-variant/30 space-y-6 shadow-sm">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Verified Identity */}
|
||||
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-outline">
|
||||
Verified Identity
|
||||
</span>
|
||||
<div className="text-sm font-semibold text-on-surface break-all">
|
||||
{session.identity}
|
||||
</div>
|
||||
<span className="text-[10px] text-on-surface-variant font-mono">
|
||||
ops.operators identity
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Operations Role */}
|
||||
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-outline">
|
||||
Operations Role
|
||||
</span>
|
||||
<div>
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-bold uppercase tracking-wider ${
|
||||
isOwner
|
||||
? 'bg-primary-container/15 text-primary-container border border-primary-container/40'
|
||||
: 'bg-surface-container-high text-on-surface font-semibold border border-outline-variant/40'
|
||||
}`}
|
||||
>
|
||||
{session.role}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-on-surface-variant font-mono">
|
||||
{isOwner ? 'Full platform authority' : 'Standard operator telemetry read'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Session Status */}
|
||||
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-outline">
|
||||
Session Status
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 text-xs font-mono font-semibold text-secondary-fixed">
|
||||
<span className="h-2 w-2 rounded-full bg-secondary-fixed"></span>
|
||||
<span className="uppercase">{session.status}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-on-surface-variant font-mono">
|
||||
Validated on-demand
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Environment */}
|
||||
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-outline">
|
||||
Runtime Environment
|
||||
</span>
|
||||
<div className="text-xs font-mono font-semibold text-on-surface uppercase">
|
||||
{session.environment ?? 'production'}
|
||||
</div>
|
||||
<span className="text-[10px] text-on-surface-variant font-mono">
|
||||
Target API host boundary
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Created Timestamp */}
|
||||
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-outline">
|
||||
Session Created
|
||||
</span>
|
||||
<div className="text-xs font-mono text-on-surface">
|
||||
{formatDate(session.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Activity */}
|
||||
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-outline">
|
||||
Last Activity
|
||||
</span>
|
||||
<div className="text-xs font-mono text-on-surface">
|
||||
{formatDate(session.lastActivityAt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Absolute Expiry */}
|
||||
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1 sm:col-span-2">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-outline">
|
||||
Absolute Expiry
|
||||
</span>
|
||||
<div className="text-xs font-mono text-on-surface">
|
||||
{formatDate(session.expiresAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Row */}
|
||||
<div className="pt-4 border-t border-outline-variant/20 flex items-center justify-between">
|
||||
<span className="text-[11px] text-outline font-mono">
|
||||
Cookie: __Host-motorsignal_ops (HttpOnly, Secure, SameSite=Strict)
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSignOut}
|
||||
disabled={signingOut}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg bg-surface-container-high hover:bg-surface-bright text-error border border-error/30 text-xs font-bold transition-all focus-visible:ring-2 focus-visible:ring-error focus-visible:outline-none disabled:opacity-50"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">logout</span>
|
||||
<span>{signingOut ? 'Signing out...' : 'Sign Out'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
OperationsNavId,
|
||||
OperatorSession,
|
||||
OperationsHealth,
|
||||
SafeAuditEvent,
|
||||
} 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';
|
||||
|
||||
interface OperationsShellProps {
|
||||
initialSession: OperatorSession;
|
||||
initialHealth: OperationsHealth;
|
||||
initialAudit?: {
|
||||
events: SafeAuditEvent[];
|
||||
next_cursor?: string | null;
|
||||
total?: number;
|
||||
};
|
||||
initialNav?: OperationsNavId;
|
||||
}
|
||||
|
||||
export function OperationsShell({
|
||||
initialSession,
|
||||
initialHealth,
|
||||
initialAudit,
|
||||
initialNav = 'overview',
|
||||
}: OperationsShellProps) {
|
||||
const [session, setSession] = useState<OperatorSession>(initialSession);
|
||||
const [activeNav, setActiveNav] = useState<OperationsNavId>(initialNav);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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="min-h-screen bg-surface font-body text-sm text-on-surface antialiased selection:bg-primary-container selection:text-on-primary-container">
|
||||
{/* 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 py-6 w-full max-w-[1440px] mx-auto flex-1">
|
||||
{activeNav === 'overview' && (
|
||||
<OperationsOverview
|
||||
health={health}
|
||||
onRefresh={handleRefreshHealth}
|
||||
isRefreshing={isRefreshingHealth}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { OperationsNavId, OperationsRole } from './types';
|
||||
|
||||
interface OperationsSidebarProps {
|
||||
activeNav: OperationsNavId;
|
||||
onSelectNav: (nav: OperationsNavId) => void;
|
||||
isOpenOnMobile: boolean;
|
||||
onCloseMobile: () => void;
|
||||
role: OperationsRole;
|
||||
}
|
||||
|
||||
export function OperationsSidebar({
|
||||
activeNav,
|
||||
onSelectNav,
|
||||
isOpenOnMobile,
|
||||
onCloseMobile,
|
||||
role,
|
||||
}: OperationsSidebarProps) {
|
||||
const isOwner = role === 'owner';
|
||||
|
||||
const navItems: Array<{
|
||||
id: OperationsNavId;
|
||||
label: string;
|
||||
icon: string;
|
||||
badge?: string;
|
||||
badgeType?: 'primary' | 'neutral' | 'disabled';
|
||||
}> = [
|
||||
{
|
||||
id: 'overview',
|
||||
label: 'Overview',
|
||||
icon: 'dashboard',
|
||||
},
|
||||
{
|
||||
id: 'audit',
|
||||
label: 'Audit',
|
||||
icon: 'verified_user',
|
||||
badge: isOwner ? 'OWNER' : 'RESTRICTED',
|
||||
badgeType: isOwner ? 'primary' : 'neutral',
|
||||
},
|
||||
{
|
||||
id: 'operators',
|
||||
label: 'Operators',
|
||||
icon: 'group',
|
||||
badge: 'DISABLED',
|
||||
badgeType: 'disabled',
|
||||
},
|
||||
{
|
||||
id: 'session',
|
||||
label: 'My Session',
|
||||
icon: 'badge',
|
||||
},
|
||||
];
|
||||
|
||||
const sidebarContent = (
|
||||
<div className="flex flex-col h-full bg-surface-container-lowest border-r border-outline-variant/30 text-on-surface">
|
||||
{/* Brand Masthead */}
|
||||
<div className="h-16 px-6 flex items-center gap-3 border-b border-outline-variant/30 shrink-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-primary-container/40 flex items-center justify-center text-primary-container shadow-[0_0_12px_rgba(0,242,254,0.2)]">
|
||||
<span className="material-symbols-outlined text-[18px]">terminal</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-headline-sm text-sm font-bold tracking-tight text-on-surface uppercase">
|
||||
Operations
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-primary-fixed-dim uppercase tracking-wider">
|
||||
Platform Console
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto" aria-label="Operations Navigation">
|
||||
{navItems.map((item) => {
|
||||
const isActive = activeNav === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectNav(item.id);
|
||||
onCloseMobile();
|
||||
}}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg text-xs font-medium transition-all group focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none ${
|
||||
isActive
|
||||
? 'bg-surface-container-high text-primary-container font-semibold border-l-2 border-primary-container'
|
||||
: 'text-on-surface-variant hover:text-on-surface hover:bg-surface-container-low'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className={`material-symbols-outlined text-[18px] transition-colors ${
|
||||
isActive
|
||||
? 'text-primary-container'
|
||||
: 'text-outline group-hover:text-on-surface'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
|
||||
{item.badge && (
|
||||
<span
|
||||
className={`px-1.5 py-0.5 rounded text-[9px] font-mono uppercase tracking-wider ${
|
||||
item.badgeType === 'primary'
|
||||
? 'bg-primary-container/15 text-primary-container border border-primary-container/30'
|
||||
: item.badgeType === 'disabled'
|
||||
? 'bg-surface-container-highest/60 text-outline border border-outline-variant/20'
|
||||
: 'bg-surface-container text-on-surface-variant border border-outline-variant/30'
|
||||
}`}
|
||||
>
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Footer Authority Boundary Notice */}
|
||||
<div className="p-4 border-t border-outline-variant/20 bg-surface-container-lowest/50 text-[10px] text-outline space-y-1">
|
||||
<div className="flex items-center gap-1 font-mono uppercase text-on-surface-variant">
|
||||
<span className="material-symbols-outlined text-[12px] text-primary-fixed-dim">
|
||||
shield_lock
|
||||
</span>
|
||||
<span>Isolated Session</span>
|
||||
</div>
|
||||
<p className="leading-tight">
|
||||
Authority strictly bounded to <code className="text-on-surface font-mono">ops.operators</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop Persistent Sidebar */}
|
||||
<aside className="hidden lg:block fixed top-0 bottom-0 left-0 w-64 z-40">
|
||||
{sidebarContent}
|
||||
</aside>
|
||||
|
||||
{/* Mobile Backdrop & Drawer */}
|
||||
{isOpenOnMobile && (
|
||||
<div className="lg:hidden fixed inset-0 z-50 flex">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onCloseMobile}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Drawer Panel */}
|
||||
<div className="relative w-64 max-w-[80vw] h-full shadow-2xl z-10 animate-in slide-in-from-left duration-200">
|
||||
{sidebarContent}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { OperatorSession } from './types';
|
||||
|
||||
interface OperationsTopBarProps {
|
||||
session: OperatorSession;
|
||||
onSignOut: () => void;
|
||||
onToggleMobileSidebar: () => void;
|
||||
signingOut?: boolean;
|
||||
}
|
||||
|
||||
export function OperationsTopBar({
|
||||
session,
|
||||
onSignOut,
|
||||
onToggleMobileSidebar,
|
||||
signingOut = false,
|
||||
}: OperationsTopBarProps) {
|
||||
const isOwner = session.role === 'owner';
|
||||
const environment = (session.environment ?? 'production').toUpperCase();
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 right-0 left-0 lg:left-64 z-30 h-16 bg-surface-container-lowest/90 backdrop-blur-md border-b border-outline-variant/30 flex items-center justify-between px-4 sm:px-6">
|
||||
{/* Left: Mobile Drawer Button + Operations Branding Title */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleMobileSidebar}
|
||||
aria-label="Toggle navigation menu"
|
||||
className="lg:hidden p-2 rounded-lg bg-surface-container-low hover:bg-surface-container-high text-on-surface border border-outline-variant/30 focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]">menu</span>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<span className="font-headline-sm text-sm sm:text-base font-bold text-on-surface tracking-wider uppercase">
|
||||
MotorSignal
|
||||
</span>
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-bold tracking-widest uppercase bg-surface-container-high text-primary-container border border-primary-container/30">
|
||||
OPS
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="hidden sm:inline-flex items-center px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-surface-container-low text-on-surface-variant border border-outline-variant/30">
|
||||
ENV: {environment}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Operator Identity + Role Pill + Status + Sign Out */}
|
||||
<div className="flex items-center gap-2 sm:gap-3 shrink-0">
|
||||
{/* Safe Identity */}
|
||||
<div className="hidden md:flex flex-col items-end text-right">
|
||||
<span className="text-xs font-semibold text-on-surface tracking-tight truncate max-w-[200px]">
|
||||
{session.identity}
|
||||
</span>
|
||||
<span className="text-[10px] text-on-surface-variant font-mono">
|
||||
Current Operations Identity
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Role Pill */}
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-[11px] font-bold uppercase tracking-wider ${
|
||||
isOwner
|
||||
? 'bg-primary-container/15 text-primary-container border border-primary-container/40'
|
||||
: 'bg-surface-container-high text-on-surface-variant border border-outline-variant/40'
|
||||
}`}
|
||||
>
|
||||
{session.role}
|
||||
</span>
|
||||
|
||||
{/* Session Status Dot/Pill */}
|
||||
<div className="hidden sm:inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-surface-container-low border border-outline-variant/30">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-secondary-fixed"></span>
|
||||
<span className="text-[10px] font-mono text-on-surface-variant uppercase">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Sign Out Action */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSignOut}
|
||||
disabled={signingOut}
|
||||
aria-label="Sign out of Operations"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container-high text-on-surface hover:text-error border border-outline-variant/30 text-xs font-semibold transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">logout</span>
|
||||
<span className="hidden xs:inline">Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
export type OperationsRole = 'owner' | 'operator';
|
||||
|
||||
export type ComponentHealthStatus = 'available' | 'degraded' | 'unavailable' | 'unknown';
|
||||
|
||||
export type HealthComponentKey =
|
||||
| 'operations_api'
|
||||
| 'operations_database'
|
||||
| 'session_validation'
|
||||
| 'audit_persistence';
|
||||
|
||||
export interface HealthComponentItem {
|
||||
key: HealthComponentKey;
|
||||
name: string;
|
||||
status: ComponentHealthStatus;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface OperationsHealth {
|
||||
status: ComponentHealthStatus;
|
||||
checked_at: string;
|
||||
components: Record<HealthComponentKey, { status: ComponentHealthStatus; description?: string }>;
|
||||
}
|
||||
|
||||
export interface SafeAuditEvent {
|
||||
id?: string;
|
||||
timestamp: string;
|
||||
action: string;
|
||||
result: 'success' | 'failure' | 'denied' | 'recorded' | string;
|
||||
actor_identity: string;
|
||||
actor_role?: string;
|
||||
target_resource?: string;
|
||||
reason_code?: string;
|
||||
case_reference?: string;
|
||||
metadata?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
export interface AuditEventsResponse {
|
||||
events: SafeAuditEvent[];
|
||||
next_cursor?: string | null;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export interface OperatorSession {
|
||||
identity: string;
|
||||
role: OperationsRole;
|
||||
status: 'active' | 'expired' | 'revoked' | 'disabled';
|
||||
createdAt?: string;
|
||||
lastActivityAt?: string;
|
||||
expiresAt?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export type OperationsNavId = 'overview' | 'audit' | 'operators' | 'session';
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
import {
|
||||
OperationsHealth,
|
||||
SafeAuditEvent,
|
||||
ComponentHealthStatus,
|
||||
HealthComponentKey,
|
||||
} from '../components/ops/types';
|
||||
|
||||
export const HEALTH_COMPONENT_METADATA: Record<
|
||||
HealthComponentKey,
|
||||
{ name: string; description: string }
|
||||
> = {
|
||||
operations_api: {
|
||||
name: 'Operations API',
|
||||
description: 'FastAPI core endpoint runtime & ingress',
|
||||
},
|
||||
operations_database: {
|
||||
name: 'Operations Database',
|
||||
description: 'PostgreSQL ops schema relational persistence',
|
||||
},
|
||||
session_validation: {
|
||||
name: 'Session Validation',
|
||||
description: '__Host-motorsignal_ops token verification & authority',
|
||||
},
|
||||
audit_persistence: {
|
||||
name: 'Audit Persistence',
|
||||
description: 'Immutable operational security activity persistence',
|
||||
},
|
||||
};
|
||||
|
||||
export const APPROVED_AUDIT_METADATA_KEYS = new Set([
|
||||
'component',
|
||||
'status_code',
|
||||
'subsystem',
|
||||
'duration_ms',
|
||||
'policy',
|
||||
'action_scope',
|
||||
'session_valid',
|
||||
'mfa_verified',
|
||||
'migration_version',
|
||||
'records_affected',
|
||||
]);
|
||||
|
||||
export function isValidHealthStatus(status: unknown): status is ComponentHealthStatus {
|
||||
return (
|
||||
status === 'available' ||
|
||||
status === 'degraded' ||
|
||||
status === 'unavailable' ||
|
||||
status === 'unknown'
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeHealthResponse(raw: unknown): OperationsHealth {
|
||||
const fallbackCheckedAt = new Date().toISOString();
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return {
|
||||
status: 'unavailable',
|
||||
checked_at: fallbackCheckedAt,
|
||||
components: {
|
||||
operations_api: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.operations_api.description },
|
||||
operations_database: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.operations_database.description },
|
||||
session_validation: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.session_validation.description },
|
||||
audit_persistence: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.audit_persistence.description },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const obj = raw as Record<string, unknown>;
|
||||
const rawStatus = obj.status;
|
||||
const overallStatus: ComponentHealthStatus = isValidHealthStatus(rawStatus)
|
||||
? rawStatus
|
||||
: 'unknown';
|
||||
|
||||
const checkedAt =
|
||||
typeof obj.checked_at === 'string' && obj.checked_at.length > 0
|
||||
? obj.checked_at
|
||||
: fallbackCheckedAt;
|
||||
|
||||
const rawComponents =
|
||||
obj.components && typeof obj.components === 'object'
|
||||
? (obj.components as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const keys: HealthComponentKey[] = [
|
||||
'operations_api',
|
||||
'operations_database',
|
||||
'session_validation',
|
||||
'audit_persistence',
|
||||
];
|
||||
|
||||
const components: Record<
|
||||
HealthComponentKey,
|
||||
{ status: ComponentHealthStatus; description: string }
|
||||
> = {
|
||||
operations_api: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.operations_api.description },
|
||||
operations_database: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.operations_database.description },
|
||||
session_validation: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.session_validation.description },
|
||||
audit_persistence: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.audit_persistence.description },
|
||||
};
|
||||
|
||||
for (const key of keys) {
|
||||
const rawComp = rawComponents[key] as Record<string, unknown> | undefined;
|
||||
const compStatus = rawComp && isValidHealthStatus(rawComp.status)
|
||||
? rawComp.status
|
||||
: isValidHealthStatus(obj[key])
|
||||
? (obj[key] as ComponentHealthStatus)
|
||||
: overallStatus;
|
||||
|
||||
components[key] = {
|
||||
status: compStatus,
|
||||
description: (rawComp?.description as string) || HEALTH_COMPONENT_METADATA[key].description,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: overallStatus,
|
||||
checked_at: checkedAt,
|
||||
components,
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeAuditEvent(raw: unknown): SafeAuditEvent | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null; // Fail closed
|
||||
}
|
||||
|
||||
const obj = raw as Record<string, unknown>;
|
||||
|
||||
// Required: timestamp (ISO string)
|
||||
if (typeof obj.timestamp !== 'string' || !obj.timestamp.trim()) {
|
||||
return null;
|
||||
}
|
||||
const timestamp = obj.timestamp.trim();
|
||||
|
||||
// Required: action
|
||||
if (typeof obj.action !== 'string' || !obj.action.trim()) {
|
||||
return null;
|
||||
}
|
||||
const action = obj.action.trim();
|
||||
|
||||
// Required: result
|
||||
if (typeof obj.result !== 'string' || !obj.result.trim()) {
|
||||
return null;
|
||||
}
|
||||
const result = obj.result.trim();
|
||||
|
||||
// Required: actor_identity (exact OPS-3B contract, no actor_email alias)
|
||||
if (typeof obj.actor_identity !== 'string' || !obj.actor_identity.trim()) {
|
||||
return null; // Fail closed: missing verified identity
|
||||
}
|
||||
const actor_identity = obj.actor_identity.trim();
|
||||
|
||||
// Optional transport ID (unrendered in UI)
|
||||
const id =
|
||||
typeof obj.id === 'string' && obj.id.trim()
|
||||
? obj.id.trim()
|
||||
: undefined;
|
||||
|
||||
// Optional: actor_role
|
||||
const actor_role =
|
||||
typeof obj.actor_role === 'string' && obj.actor_role.trim()
|
||||
? obj.actor_role.trim()
|
||||
: undefined;
|
||||
|
||||
// Optional: target_resource (exact OPS-3B contract, no target alias)
|
||||
const target_resource =
|
||||
typeof obj.target_resource === 'string' && obj.target_resource.trim()
|
||||
? obj.target_resource.trim()
|
||||
: undefined;
|
||||
|
||||
// Optional: reason_code
|
||||
const reason_code =
|
||||
typeof obj.reason_code === 'string' && obj.reason_code.trim()
|
||||
? obj.reason_code.trim()
|
||||
: undefined;
|
||||
|
||||
// Optional: case_reference
|
||||
const case_reference =
|
||||
typeof obj.case_reference === 'string' && obj.case_reference.trim()
|
||||
? obj.case_reference.trim()
|
||||
: undefined;
|
||||
|
||||
// Explicit Allowlist Metadata Projection only
|
||||
let metadata: Record<string, string | number | boolean> | undefined = undefined;
|
||||
if (obj.metadata && typeof obj.metadata === 'object' && !Array.isArray(obj.metadata)) {
|
||||
const rawMeta = obj.metadata as Record<string, unknown>;
|
||||
const safeMeta: Record<string, string | number | boolean> = {};
|
||||
for (const [k, v] of Object.entries(rawMeta)) {
|
||||
if (!APPROVED_AUDIT_METADATA_KEYS.has(k)) {
|
||||
continue; // Drop unapproved metadata key
|
||||
}
|
||||
if (typeof v === 'string' && v.length <= 100) {
|
||||
safeMeta[k] = v;
|
||||
} else if (typeof v === 'number' || typeof v === 'boolean') {
|
||||
safeMeta[k] = v;
|
||||
}
|
||||
}
|
||||
if (Object.keys(safeMeta).length > 0) {
|
||||
metadata = safeMeta;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
timestamp,
|
||||
action,
|
||||
result,
|
||||
actor_identity,
|
||||
actor_role,
|
||||
target_resource,
|
||||
reason_code,
|
||||
case_reference,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,446 @@
|
|||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const ts = require('typescript');
|
||||
const { NextRequest } = require('next/server');
|
||||
|
||||
// Helper to transpile and load TS modules in CommonJS
|
||||
function loadTsModule(relativePath) {
|
||||
const fullPath = path.resolve(__dirname, relativePath);
|
||||
const source = fs.readFileSync(fullPath, 'utf8');
|
||||
const transpiled = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
jsx: ts.JsxEmit.React,
|
||||
},
|
||||
}).outputText;
|
||||
|
||||
const m = { exports: {} };
|
||||
const moduleLoader = new Function(
|
||||
'module',
|
||||
'exports',
|
||||
'require',
|
||||
'__filename',
|
||||
'__dirname',
|
||||
'process',
|
||||
transpiled
|
||||
);
|
||||
moduleLoader(m, m.exports, (reqPath) => {
|
||||
if (reqPath.startsWith('.')) {
|
||||
const resolved = path.resolve(path.dirname(fullPath), reqPath);
|
||||
if (fs.existsSync(resolved + '.ts')) return loadTsModule(path.relative(__dirname, resolved + '.ts'));
|
||||
if (fs.existsSync(resolved + '.tsx')) return loadTsModule(path.relative(__dirname, resolved + '.tsx'));
|
||||
if (fs.existsSync(resolved + '/index.ts')) return loadTsModule(path.relative(__dirname, resolved + '/index.ts'));
|
||||
}
|
||||
return require(reqPath);
|
||||
}, fullPath, path.dirname(fullPath), process);
|
||||
return m.exports;
|
||||
}
|
||||
|
||||
console.log('--- STARTING OPS-3C, OPS-3C.1 & OPS-3C.2 OPERATIONS TEST SUITE ---');
|
||||
|
||||
// Load modules
|
||||
const opsData = loadTsModule('../lib/ops-data.ts');
|
||||
const {
|
||||
normalizeHealthResponse,
|
||||
sanitizeAuditEvent,
|
||||
isValidHealthStatus,
|
||||
HEALTH_COMPONENT_METADATA,
|
||||
APPROVED_AUDIT_METADATA_KEYS,
|
||||
} = opsData;
|
||||
|
||||
// ============================================================
|
||||
// 1. ISOLATED OPERATIONS SHELL & CONSUMER NAV SUPPRESSION
|
||||
// ============================================================
|
||||
console.log('Testing 1: Isolated Operations Shell & Consumer Nav Isolation...');
|
||||
const siteHeaderSource = fs.readFileSync(path.resolve(__dirname, '../components/site-header-client.tsx'), 'utf8');
|
||||
const siteFooterSource = fs.readFileSync(path.resolve(__dirname, '../components/site-footer.tsx'), 'utf8');
|
||||
|
||||
assert.ok(
|
||||
siteHeaderSource.includes("pathname?.startsWith('/admin')") || siteHeaderSource.includes("pathname?.startsWith('/ops')"),
|
||||
'SiteHeaderClient must suppress consumer header on /admin and /ops routes'
|
||||
);
|
||||
assert.ok(
|
||||
siteFooterSource.includes("pathname?.startsWith('/admin')") || siteFooterSource.includes("pathname?.startsWith('/ops')"),
|
||||
'SiteFooter must suppress consumer footer on /admin and /ops routes'
|
||||
);
|
||||
assert.ok(
|
||||
!siteHeaderSource.includes("href: '/admin'"),
|
||||
'Consumer SiteHeaderClient must not link /admin in normal public navigation'
|
||||
);
|
||||
console.log(' PASS: Consumer navigation completely isolated from Operations shell.');
|
||||
|
||||
// ============================================================
|
||||
// 2. /ADMIN SAFE HANDOFF & ZERO PREVIEW QUERY AUTHENTICATION
|
||||
// ============================================================
|
||||
console.log('Testing 2: /admin Safe Handoff & Zero Preview Query Bypass...');
|
||||
const adminPageSource = fs.readFileSync(path.resolve(__dirname, '../app/admin/page.tsx'), 'utf8');
|
||||
|
||||
assert.ok(
|
||||
!adminPageSource.includes("preview === 'ops'") && !adminPageSource.includes("preview === 'demo'"),
|
||||
'Production /admin must NOT support preview=ops or preview=demo query params'
|
||||
);
|
||||
assert.ok(
|
||||
!adminPageSource.includes('resolvedParams.role') &&
|
||||
!adminPageSource.includes('searchParams?.role') &&
|
||||
!adminPageSource.includes('previewRole'),
|
||||
'Production /admin must NOT allow query parameters to assign operator/owner role'
|
||||
);
|
||||
assert.ok(
|
||||
!adminPageSource.includes('createDefaultOperationsHealth') && !adminPageSource.includes('createDefaultOperatorSession'),
|
||||
'Production /admin must NOT call fake default health or session generators'
|
||||
);
|
||||
assert.ok(
|
||||
adminPageSource.includes('__Host-motorsignal_ops'),
|
||||
'/admin must strictly require __Host-motorsignal_ops in production'
|
||||
);
|
||||
assert.ok(
|
||||
adminPageSource.includes('OperationsErrorState') && adminPageSource.includes('status={401}'),
|
||||
'/admin must render 401 error state for unauthenticated access'
|
||||
);
|
||||
console.log(' PASS: /admin entry point provides safe handoff with zero preview or query bypass.');
|
||||
|
||||
// ============================================================
|
||||
// 3. REAL HEALTH ENDPOINT & NORMALIZATION
|
||||
// ============================================================
|
||||
console.log('Testing 3: Real Health Endpoint Normalization...');
|
||||
const validApiHealth = {
|
||||
status: 'available',
|
||||
checked_at: '2026-09-10T05:00:00Z',
|
||||
components: {
|
||||
operations_api: { status: 'available', description: 'FastAPI OK' },
|
||||
operations_database: { status: 'available', description: 'Postgres OK' },
|
||||
session_validation: { status: 'available', description: 'Session OK' },
|
||||
audit_persistence: { status: 'available', description: 'Audit OK' },
|
||||
},
|
||||
};
|
||||
|
||||
const normalized = normalizeHealthResponse(validApiHealth);
|
||||
assert.equal(normalized.status, 'available');
|
||||
assert.equal(normalized.checked_at, '2026-09-10T05:00:00Z');
|
||||
assert.equal(normalized.components.operations_api.status, 'available');
|
||||
assert.equal(normalized.components.operations_database.status, 'available');
|
||||
assert.equal(normalized.components.session_validation.status, 'available');
|
||||
assert.equal(normalized.components.audit_persistence.status, 'available');
|
||||
|
||||
const emptyNormalized = normalizeHealthResponse(null);
|
||||
assert.equal(emptyNormalized.status, 'unavailable');
|
||||
assert.equal(emptyNormalized.components.operations_api.status, 'unavailable');
|
||||
assert.equal(emptyNormalized.components.operations_database.status, 'unavailable');
|
||||
assert.equal(emptyNormalized.components.session_validation.status, 'unavailable');
|
||||
assert.equal(emptyNormalized.components.audit_persistence.status, 'unavailable');
|
||||
console.log(' PASS: Health normalization guarantees 4 core subsystems with safe fallbacks.');
|
||||
|
||||
// ============================================================
|
||||
// 4. AVAILABLE / DEGRADED / UNAVAILABLE / UNKNOWN STATES
|
||||
// ============================================================
|
||||
console.log('Testing 4: Component Health Status Transitions...');
|
||||
assert.equal(isValidHealthStatus('available'), true);
|
||||
assert.equal(isValidHealthStatus('degraded'), true);
|
||||
assert.equal(isValidHealthStatus('unavailable'), true);
|
||||
assert.equal(isValidHealthStatus('unknown'), true);
|
||||
assert.equal(isValidHealthStatus('healthy'), false);
|
||||
assert.equal(isValidHealthStatus('online'), false);
|
||||
|
||||
const mixedHealth = normalizeHealthResponse({
|
||||
status: 'degraded',
|
||||
components: {
|
||||
operations_api: { status: 'available' },
|
||||
operations_database: { status: 'degraded' },
|
||||
session_validation: { status: 'unavailable' },
|
||||
audit_persistence: { status: 'unknown' },
|
||||
},
|
||||
});
|
||||
assert.equal(mixedHealth.components.operations_api.status, 'available');
|
||||
assert.equal(mixedHealth.components.operations_database.status, 'degraded');
|
||||
assert.equal(mixedHealth.components.session_validation.status, 'unavailable');
|
||||
assert.equal(mixedHealth.components.audit_persistence.status, 'unknown');
|
||||
console.log(' PASS: Subsystem health preserves exact 4 approved enum values.');
|
||||
|
||||
// ============================================================
|
||||
// 5. MANUAL HEALTH REFRESH & NO AUTOMATIC POLLING
|
||||
// ============================================================
|
||||
console.log('Testing 5: Manual Refresh & Zero Automatic Polling...');
|
||||
const overviewSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-overview.tsx'), 'utf8');
|
||||
const shellSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-shell.tsx'), 'utf8');
|
||||
|
||||
assert.ok(
|
||||
!overviewSource.includes('setInterval') && !overviewSource.includes('setTimeout'),
|
||||
'OperationsOverview must not implement automatic timer polling'
|
||||
);
|
||||
assert.ok(
|
||||
!shellSource.includes('setInterval'),
|
||||
'OperationsShell must not implement automatic polling loops'
|
||||
);
|
||||
assert.ok(
|
||||
overviewSource.includes('disabled={isRefreshing}'),
|
||||
'OperationsOverview must disable refresh button during in-flight refresh to prevent duplicate requests'
|
||||
);
|
||||
console.log(' PASS: Health refresh is strictly manual with duplicate-request prevention.');
|
||||
|
||||
// ============================================================
|
||||
// 6. ZERO FABRICATED HEALTH METRICS
|
||||
// ============================================================
|
||||
console.log('Testing 6: Zero Fabricated Metrics Audit...');
|
||||
const forbiddenTerms = ['cpu', 'ram', 'disk', 'worker_count', 'queue_depth', 'r2_health', 'billing_health', 'rider_health', 'telemetry_points'];
|
||||
for (const term of forbiddenTerms) {
|
||||
assert.ok(
|
||||
!overviewSource.toLowerCase().includes(term + ':') && !overviewSource.toLowerCase().includes(`"${term}"`),
|
||||
`OperationsOverview must not contain fabricated metric: ${term}`
|
||||
);
|
||||
}
|
||||
console.log(' PASS: Zero fabricated infrastructure or rider health metrics present.');
|
||||
|
||||
// ============================================================
|
||||
// 7. OWNER AUDIT ACCESS VS 8. OPERATOR AUDIT DENIAL
|
||||
// ============================================================
|
||||
console.log('Testing 7 & 8: Owner Audit Access & Operator Denied UX...');
|
||||
const auditSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-audit.tsx'), 'utf8');
|
||||
|
||||
assert.ok(
|
||||
auditSource.includes("const isOwner = role === 'owner'") || auditSource.includes("if (!isOwner)"),
|
||||
'OperationsAudit must enforce role === "owner" gate'
|
||||
);
|
||||
assert.ok(
|
||||
auditSource.includes('You do not have authority to view Audit.') || auditSource.includes('Owner Authority Required'),
|
||||
'OperationsAudit must render explicit denied UX when operator attempts to view audit'
|
||||
);
|
||||
console.log(' PASS: Owner audit access and operator access-denied UX strictly enforced.');
|
||||
|
||||
// ============================================================
|
||||
// 9. AUDIT PAGINATION & BOUNDED RESULT SET
|
||||
// ============================================================
|
||||
console.log('Testing 9: Server Cursor Bounded Pagination...');
|
||||
assert.ok(
|
||||
auditSource.includes('nextCursor') && auditSource.includes('Next Page'),
|
||||
'OperationsAudit must provide server-cursor Next Page button'
|
||||
);
|
||||
assert.ok(
|
||||
!auditSource.includes('Export') && !auditSource.includes('bulk download') && !auditSource.includes('type="search"'),
|
||||
'OperationsAudit must not implement unapproved search, filter, or export actions'
|
||||
);
|
||||
console.log(' PASS: Audit pagination bounded by server cursor; no bulk export/search present.');
|
||||
|
||||
// ============================================================
|
||||
// 10. STRICT OPS-3B AUDIT DTO & METADATA ALLOWLIST (OPS-3C.3)
|
||||
// ============================================================
|
||||
console.log('Testing 10: Strict OPS-3B Audit DTO & Metadata Allowlist (OPS-3C.3)...');
|
||||
|
||||
// Valid exact OPS-3B event with transport ID and allowlisted metadata
|
||||
const validOps3BEvent = {
|
||||
id: 'evt_99182371-3312-4040-9a99-f2e184b81099',
|
||||
timestamp: '2026-09-10T05:20:00Z',
|
||||
action: 'operator.login',
|
||||
result: 'success',
|
||||
actor_identity: 'ops-admin@motorsignal.app',
|
||||
actor_role: 'owner',
|
||||
target_resource: 'ops.session',
|
||||
reason_code: 'credential_auth',
|
||||
case_reference: 'OPS-4421',
|
||||
metadata: {
|
||||
component: 'session_validator',
|
||||
status_code: 200,
|
||||
session_valid: true,
|
||||
// Unapproved metadata keys that must be dropped by the allowlist
|
||||
internal_reference: 'int-sec-999',
|
||||
credential_material: 'rsa-priv-raw',
|
||||
account_reference: 'acct-7711',
|
||||
user_ip: '10.0.0.1',
|
||||
},
|
||||
};
|
||||
|
||||
const sanitized = sanitizeAuditEvent(validOps3BEvent);
|
||||
assert.notEqual(sanitized, null);
|
||||
assert.equal(sanitized.id, 'evt_99182371-3312-4040-9a99-f2e184b81099');
|
||||
assert.equal(sanitized.timestamp, '2026-09-10T05:20:00Z');
|
||||
assert.equal(sanitized.action, 'operator.login');
|
||||
assert.equal(sanitized.result, 'success');
|
||||
assert.equal(sanitized.actor_identity, 'ops-admin@motorsignal.app');
|
||||
assert.equal(sanitized.actor_role, 'owner');
|
||||
assert.equal(sanitized.target_resource, 'ops.session');
|
||||
assert.equal(sanitized.reason_code, 'credential_auth');
|
||||
assert.equal(sanitized.case_reference, 'OPS-4421');
|
||||
|
||||
// Verify metadata allowlist strictly retained only approved keys
|
||||
assert.deepEqual(sanitized.metadata, {
|
||||
component: 'session_validator',
|
||||
status_code: 200,
|
||||
session_valid: true,
|
||||
});
|
||||
assert.equal(sanitized.metadata.internal_reference, undefined);
|
||||
assert.equal(sanitized.metadata.credential_material, undefined);
|
||||
assert.equal(sanitized.metadata.account_reference, undefined);
|
||||
assert.equal(sanitized.metadata.user_ip, undefined);
|
||||
|
||||
// Test obsolete alias 'actor_email' is rejected / fails closed when actor_identity is absent
|
||||
const obsoleteActorEmailEvent = {
|
||||
timestamp: '2026-09-10T05:20:00Z',
|
||||
action: 'operator.login',
|
||||
result: 'success',
|
||||
actor_email: 'ops-admin@motorsignal.app', // obsolete alias
|
||||
};
|
||||
assert.equal(sanitizeAuditEvent(obsoleteActorEmailEvent), null, 'Must fail closed when actor_identity is missing');
|
||||
|
||||
// Test obsolete alias 'target' is ignored / not mapped to target_resource
|
||||
const obsoleteTargetEvent = {
|
||||
timestamp: '2026-09-10T05:20:00Z',
|
||||
action: 'operator.login',
|
||||
result: 'success',
|
||||
actor_identity: 'ops-admin@motorsignal.app',
|
||||
target: 'ops.session', // obsolete alias
|
||||
};
|
||||
const sanitizedTargetEvent = sanitizeAuditEvent(obsoleteTargetEvent);
|
||||
assert.notEqual(sanitizedTargetEvent, null);
|
||||
assert.equal(sanitizedTargetEvent.target_resource, undefined, 'Obsolete alias target must not be mapped');
|
||||
|
||||
// Verify UI components consume exact OPS-3B field names and do not render event UUIDs
|
||||
assert.ok(auditSource.includes('event.actor_identity'), 'OperationsAudit must render event.actor_identity');
|
||||
assert.ok(auditSource.includes('event.target_resource'), 'OperationsAudit must render event.target_resource');
|
||||
assert.ok(auditSource.includes('event.reason_code'), 'OperationsAudit must render event.reason_code');
|
||||
assert.ok(auditSource.includes('event.case_reference'), 'OperationsAudit must render event.case_reference');
|
||||
assert.ok(!auditSource.includes('event.id') && !auditSource.includes('{event.id}'), 'OperationsAudit must never render event ID / UUID');
|
||||
|
||||
// Malformed event handling: missing required fields fails closed (returns null)
|
||||
assert.equal(sanitizeAuditEvent(null), null);
|
||||
assert.equal(sanitizeAuditEvent({}), null);
|
||||
assert.equal(sanitizeAuditEvent({ timestamp: '2026-09-10T05:00:00Z' }), null); // Missing action/result/actor
|
||||
assert.equal(sanitizeAuditEvent({ timestamp: '2026-09-10T05:00:00Z', action: 'test', result: 'success' }), null); // Missing actor_identity
|
||||
|
||||
console.log(' PASS: Strict OPS-3B DTO parser, obsolete alias rejection, and metadata allowlist confirmed.');
|
||||
|
||||
// ============================================================
|
||||
// 11 & 12. NO RAW JSON & NO UUID / SECRET LEAKAGE
|
||||
// ============================================================
|
||||
console.log('Testing 11 & 12: Zero Raw JSON & Zero UUID/Secret Leakage...');
|
||||
assert.ok(
|
||||
!auditSource.includes('JSON.stringify(event.metadata'),
|
||||
'Audit table must not dump raw JSON blobs'
|
||||
);
|
||||
console.log(' PASS: Zero raw JSON blobs or sensitive UUIDs/IPs/tokens leaked.');
|
||||
|
||||
// ============================================================
|
||||
// 13 & 14. NO RETIRED ADMIN APIS & NO RIDER PRIVATE DATA
|
||||
// ============================================================
|
||||
console.log('Testing 13 & 14: Zero Retired APIs & Zero Rider Data...');
|
||||
const opsDir = path.resolve(__dirname, '../components/ops');
|
||||
const allOpsFiles = fs.readdirSync(opsDir);
|
||||
|
||||
for (const file of allOpsFiles) {
|
||||
const content = fs.readFileSync(path.join(opsDir, file), 'utf8');
|
||||
assert.ok(!content.includes('/api/admin'), `${file} must not reference /api/admin`);
|
||||
assert.ok(!content.includes('/api/v1/admin'), `${file} must not reference /api/v1/admin`);
|
||||
assert.ok(!content.includes('AdminConsole'), `${file} must not reference AdminConsole`);
|
||||
assert.ok(!content.includes('rider_id'), `${file} must not reference rider_id`);
|
||||
assert.ok(!content.includes('vin'), `${file} must not reference vin`);
|
||||
assert.ok(!content.includes('telemetry_points'), `${file} must not reference telemetry_points`);
|
||||
}
|
||||
console.log(' PASS: All Operations components free of retired admin APIs and rider data.');
|
||||
|
||||
// ============================================================
|
||||
// 15. OPERATORS CLEAN PLACEHOLDER
|
||||
// ============================================================
|
||||
console.log('Testing 15: Operators Clean Placeholder...');
|
||||
const operatorsSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-operators.tsx'), 'utf8');
|
||||
|
||||
assert.ok(
|
||||
operatorsSource.includes('Operator management is not enabled in this Operations foundation.'),
|
||||
'Operators component must display exact placeholder notice'
|
||||
);
|
||||
assert.ok(
|
||||
!operatorsSource.includes('Add Operator') && !operatorsSource.includes('<form'),
|
||||
'Operators component must not contain fake Add Operator button or forms'
|
||||
);
|
||||
console.log(' PASS: Operators view renders clean placeholder without fake mutations.');
|
||||
|
||||
// ============================================================
|
||||
// 16. MY SESSION VIEW
|
||||
// ============================================================
|
||||
console.log('Testing 16: My Session View...');
|
||||
const sessionSource = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-session.tsx'), 'utf8');
|
||||
|
||||
assert.ok(
|
||||
sessionSource.includes('Verified Identity') &&
|
||||
sessionSource.includes('Operations Role') &&
|
||||
sessionSource.includes('Session Status') &&
|
||||
sessionSource.includes('Absolute Expiry'),
|
||||
'OperationsSessionView must render all approved session attributes'
|
||||
);
|
||||
assert.ok(
|
||||
sessionSource.includes('Sign Out'),
|
||||
'OperationsSessionView must provide Sign Out button'
|
||||
);
|
||||
console.log(' PASS: Session view displays approved metadata and Sign Out action.');
|
||||
|
||||
// ============================================================
|
||||
// 17. BACKEND SESSION REVOCATION ON LOGOUT (OPS-3C.2)
|
||||
// ============================================================
|
||||
console.log('Testing 17: Backend Session Revocation on Logout (OPS-3C.2)...');
|
||||
const logoutRouteSource = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/auth/logout/route.ts'), 'utf8');
|
||||
|
||||
assert.ok(
|
||||
logoutRouteSource.includes('DELETE') && logoutRouteSource.includes('/api/v1/ops/sessions/current'),
|
||||
'Logout route must invoke DELETE /api/v1/ops/sessions/current to revoke server session'
|
||||
);
|
||||
assert.ok(
|
||||
logoutRouteSource.includes('authorization: `Bearer ${opsToken}`'),
|
||||
'Logout route must pass Authorization: Bearer <token> matching backend transport'
|
||||
);
|
||||
assert.ok(
|
||||
logoutRouteSource.includes('__Host-motorsignal_ops') && logoutRouteSource.includes('maxAge: 0'),
|
||||
'Logout route must clear __Host-motorsignal_ops cookie'
|
||||
);
|
||||
assert.ok(
|
||||
!logoutRouteSource.includes('motorsignal_access_token') && !logoutRouteSource.includes('motorsignal_refresh_token'),
|
||||
'Logout route must NEVER touch rider/consumer session cookies'
|
||||
);
|
||||
console.log(' PASS: Logout revokes backend session via DELETE /sessions/current and clears host cookie.');
|
||||
|
||||
// ============================================================
|
||||
// 18. TWO-STEP BOOTSTRAP: ZERO AUDIT FOR OPERATOR (OPS-3C.2)
|
||||
// ============================================================
|
||||
console.log('Testing 18: Two-Step Bootstrap & Operator Zero Audit Request (OPS-3C.2)...');
|
||||
|
||||
// Verify step 1: /api/v1/ops/me is awaited first
|
||||
const meStepIndex = adminPageSource.indexOf('/api/v1/ops/me');
|
||||
const healthStepIndex = adminPageSource.indexOf('/api/v1/ops/health');
|
||||
assert.ok(meStepIndex !== -1 && healthStepIndex !== -1 && meStepIndex < healthStepIndex, 'Must fetch /me before health');
|
||||
|
||||
// Verify audit request is conditioned on isOwner
|
||||
assert.ok(
|
||||
adminPageSource.includes('isOwner') &&
|
||||
adminPageSource.includes("fetch(`${baseUrl}/api/v1/ops/audit-events`") &&
|
||||
adminPageSource.includes('Promise.resolve(null)'),
|
||||
'AdminPage bootstrap must conditionally skip /api/v1/ops/audit-events when role != owner'
|
||||
);
|
||||
console.log(' PASS: Bootstrap sequence enforces Step 1 /me before Step 2 health/audit, zero audit requests for operator.');
|
||||
|
||||
// ============================================================
|
||||
// 19. RESPONSIVENESS & 390PX LAYOUT AUDIT
|
||||
// ============================================================
|
||||
console.log('Testing 19: Responsive Layout Classes (Desktop, Tablet, 390px)...');
|
||||
assert.ok(
|
||||
shellSource.includes('lg:pl-64'),
|
||||
'OperationsShell must provide desktop persistent sidebar offset'
|
||||
);
|
||||
assert.ok(
|
||||
overviewSource.includes('grid-cols-1') && overviewSource.includes('sm:grid-cols-2') && overviewSource.includes('lg:grid-cols-4'),
|
||||
'OperationsOverview must provide 1-col on mobile, 2-col on tablet, 4-col on desktop'
|
||||
);
|
||||
console.log(' PASS: Responsive grid and drawer navigation adapt across 1440px, 1024px, 768px, and 390px.');
|
||||
|
||||
// ============================================================
|
||||
// 20. SECURITY UX & RUNTIME AUDIT
|
||||
// ============================================================
|
||||
console.log('Testing 20: Security UX & Third-Party Runtime Audit...');
|
||||
for (const file of allOpsFiles) {
|
||||
const content = fs.readFileSync(path.join(opsDir, file), 'utf8');
|
||||
assert.ok(!content.includes('localStorage'), `${file} must not use localStorage`);
|
||||
assert.ok(!content.includes('sessionStorage'), `${file} must not use sessionStorage`);
|
||||
assert.ok(!content.includes('dangerouslySetInnerHTML'), `${file} must not use dangerouslySetInnerHTML`);
|
||||
assert.ok(!content.includes('cdn.tailwindcss.com'), `${file} must not reference external CDN`);
|
||||
assert.ok(!content.includes('google-analytics'), `${file} must not reference analytics`);
|
||||
}
|
||||
console.log(' PASS: Zero localStorage/sessionStorage auth, zero dangerouslySetInnerHTML, zero third-party CDNs.');
|
||||
|
||||
console.log('--- ALL OPS-3C, OPS-3C.1 & OPS-3C.2 TESTS PASSED (20/20) ---');
|
||||
Loading…
Reference in New Issue