feat(ops): add incident management, user administration, support tickets, and website CMS

This commit is contained in:
Developer 2026-09-18 18:35:12 -04:00
parent 0282a2972f
commit fa2fccd137
37 changed files with 8705 additions and 303 deletions

View File

@ -8,6 +8,7 @@ DATABASE_NAME=motorsignal_dev
DATABASE_USER=motorsignal DATABASE_USER=motorsignal
DATABASE_PASSWORD=replace-me DATABASE_PASSWORD=replace-me
WEB_BASE_URL=http://localhost:3000 WEB_BASE_URL=http://localhost:3000
FIREBASE_PROJECT_ID=motoapp-d22e0
# Optional email delivery. Without SMTP_HOST, token emails are not sent. # Optional email delivery. Without SMTP_HOST, token emails are not sent.
SMTP_HOST= SMTP_HOST=

File diff suppressed because it is too large Load Diff

View File

@ -119,8 +119,12 @@ def main() -> None:
assert status == 201 and telemetry["accepted"] == 1 assert status == 201 and telemetry["accepted"] == 1
status, telemetry = request("GET", f"/api/v1/rides/{created_ride['id']}/telemetry", token=admin_access) status, telemetry = request("GET", f"/api/v1/rides/{created_ride['id']}/telemetry", token=admin_access)
assert status == 200 and telemetry["count"] == 1 assert status == 200 and telemetry["count"] == 1
status, summary = request("GET", f"/api/v1/rides/{created_ride['id']}/telemetry/summary", token=admin_access)
assert status == 200 and summary["point_count"] == 1 and summary["max_speed_mph"] == 42.0
status, telemetry = request("GET", f"/api/v1/admin/rides/{created_ride['id']}/telemetry", token=admin_access) status, telemetry = request("GET", f"/api/v1/admin/rides/{created_ride['id']}/telemetry", token=admin_access)
assert status == 200 and telemetry["count"] == 1 assert status == 200 and telemetry["count"] == 1
status, summary = request("GET", f"/api/v1/admin/rides/{created_ride['id']}/telemetry/summary", token=admin_access)
assert status == 200 and summary["point_count"] == 1
status, subscription = request("GET", "/api/v1/me/subscription", token=admin_access) status, subscription = request("GET", "/api/v1/me/subscription", token=admin_access)
assert status == 200 and subscription["plan"] == "free" assert status == 200 and subscription["plan"] == "free"
status, entitlements = request("GET", "/api/v1/me/entitlements", token=admin_access) status, entitlements = request("GET", "/api/v1/me/entitlements", token=admin_access)

View File

@ -0,0 +1,276 @@
import hashlib
import os
import sys
import unittest
import uuid
from datetime import datetime, timedelta, timezone
ROOT = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, os.path.join(ROOT, ".python-packages"))
sys.path.insert(0, os.path.join(ROOT, "app"))
os.environ.setdefault("JWT_SECRET", "local-test-secret")
os.environ["FIREBASE_PROJECT_ID"] = "motoapp-d22e0"
os.environ["DATABASE_USER"] = "test"
os.environ["DATABASE_PASSWORD"] = "test"
os.environ["DATABASE_NAME"] = "test"
from sqlalchemy import create_engine, event, select
from sqlalchemy.orm import Session as DbSession
from starlette.testclient import TestClient
import main
from main import app, get_db, SUPPORT_TICKETS
from models import Base, Operator, OpsAuditEvent, OpsSession
def hash_token(raw_token: str) -> str:
return hashlib.sha256(raw_token.encode()).hexdigest()
class TestOpsSupport(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.db_path = os.path.join(ROOT, "tests", "test_ops_support.db")
cls.ops_db_path = os.path.join(ROOT, "tests", "test_ops_support_schema.db")
for p in (cls.db_path, cls.ops_db_path):
if os.path.exists(p):
try:
os.remove(p)
except OSError:
pass
cls.engine = create_engine(f"sqlite:///{cls.db_path}")
@event.listens_for(cls.engine, "connect")
def attach_ops(dbapi_con, con_record):
dbapi_con.execute(f"ATTACH DATABASE '{cls.ops_db_path}' AS ops")
Base.metadata.create_all(cls.engine)
def override_get_db():
db = DbSession(cls.engine)
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
cls.client = TestClient(app, raise_server_exceptions=False)
@classmethod
def tearDownClass(cls):
app.dependency_overrides.clear()
cls.engine.dispose()
for p in (cls.db_path, cls.ops_db_path):
if os.path.exists(p):
try:
os.remove(p)
except OSError:
pass
def setUp(self):
with DbSession(self.engine) as db:
db.query(OpsAuditEvent).delete()
db.query(OpsSession).delete()
db.query(Operator).delete()
db.commit()
self.op_id = uuid.uuid4()
self.op_identity = "operator@motorsignal.internal"
self.op_token = "valid-operator-token-123"
op = Operator(
id=self.op_id,
identity=self.op_identity,
role="operator",
status="active",
created_at=datetime.now(timezone.utc),
)
op_session = OpsSession(
operator_id=self.op_id,
session_token_hash=hash_token(self.op_token),
expires_at=datetime.now(timezone.utc) + timedelta(hours=8),
created_at=datetime.now(timezone.utc),
)
db.add(op)
db.add(op_session)
db.commit()
def test_list_tickets_unauthorized(self):
resp = self.client.get("/api/v1/ops/support/tickets")
self.assertEqual(resp.status_code, 401)
def test_list_tickets_authorized(self):
resp = self.client.get(
"/api/v1/ops/support/tickets",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertIn("items", data)
self.assertIn("total", data)
self.assertGreaterEqual(data["total"], 4)
def test_filter_tickets_by_type_and_status(self):
resp = self.client.get(
"/api/v1/ops/support/tickets?type_filter=bug_report",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 200)
items = resp.json()["items"]
for it in items:
self.assertEqual(it["ticket_type"], "bug_report")
resp = self.client.get(
"/api/v1/ops/support/tickets?status_filter=resolved",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 200)
items = resp.json()["items"]
for it in items:
self.assertEqual(it["status"], "resolved")
def test_search_tickets(self):
resp = self.client.get(
"/api/v1/ops/support/tickets?q=freeze",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 200)
items = resp.json()["items"]
self.assertGreaterEqual(len(items), 1)
self.assertTrue(any("freeze" in it["subject"].lower() for it in items))
def test_get_ticket_detail(self):
ticket_id = "e1a11111-2222-3333-4444-555555555551"
resp = self.client.get(
f"/api/v1/ops/support/tickets/{ticket_id}",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertEqual(data["case_reference"], "SUP-2026-000101")
self.assertEqual(data["has_diagnostics"], True)
self.assertIn("notes", data)
def test_create_ticket_and_audit(self):
payload = {
"ticket_type": "bug_report",
"reporter_email": "testrider@example.com",
"subject": "Bluetooth disconnects after 10 mins",
"description": "App drops connection to OBD reader after 10 minutes of riding.",
"severity": "medium",
}
resp = self.client.post(
"/api/v1/ops/support/tickets",
json=payload,
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 201)
data = resp.json()
self.assertTrue(data["case_reference"].startswith("SUP-2026-"))
self.assertEqual(data["status"], "open")
self.assertEqual(data["reporter_email"], "testrider@example.com")
# Verify audit event
with DbSession(self.engine) as db:
event = db.scalar(
select(OpsAuditEvent).where(
OpsAuditEvent.action == "support.ticket_created",
OpsAuditEvent.case_reference == data["case_reference"],
)
)
self.assertIsNotNone(event)
self.assertEqual(event.actor_identity, self.op_identity)
def test_add_ticket_note_and_audit(self):
ticket_id = "e1a11111-2222-3333-4444-555555555551"
resp = self.client.post(
f"/api/v1/ops/support/tickets/{ticket_id}/notes",
json={"body": "Investigated BLE stack disconnect log."},
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 201)
note = resp.json()
self.assertEqual(note["author_identity"], self.op_identity)
self.assertEqual(note["body"], "Investigated BLE stack disconnect log.")
with DbSession(self.engine) as db:
event = db.scalar(
select(OpsAuditEvent).where(
OpsAuditEvent.action == "support.note_added",
OpsAuditEvent.target_resource == f"ticket:{ticket_id}",
)
)
self.assertIsNotNone(event)
def test_patch_ticket_status_severity_and_audit(self):
ticket_id = "e1a11111-2222-3333-4444-555555555552"
resp = self.client.patch(
f"/api/v1/ops/support/tickets/{ticket_id}",
json={"status": "resolved", "severity": "low"},
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertEqual(data["status"], "resolved")
self.assertEqual(data["severity"], "low")
self.assertIsNotNone(data["resolved_at"])
with DbSession(self.engine) as db:
event = db.scalar(
select(OpsAuditEvent).where(
OpsAuditEvent.action == "support.ticket_updated",
OpsAuditEvent.target_resource == f"ticket:{ticket_id}",
)
)
self.assertIsNotNone(event)
def test_get_diagnostics_requires_reason(self):
ticket_id = "e1a11111-2222-3333-4444-555555555551"
# Reason missing
resp = self.client.get(
f"/api/v1/ops/support/tickets/{ticket_id}/diagnostics",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 422)
# Reason too short (< 3 chars)
resp = self.client.get(
f"/api/v1/ops/support/tickets/{ticket_id}/diagnostics?reason=ab",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 422)
# Valid reason
reason_text = "Debugging gyro freeze issue for user"
resp = self.client.get(
f"/api/v1/ops/support/tickets/{ticket_id}/diagnostics?reason={reason_text}",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 200)
bundle = resp.json()
self.assertEqual(bundle["device_model"], "Google Pixel 8 Pro")
self.assertIn("SensorFusionException", bundle["stack_trace"])
# Audit event recorded
with DbSession(self.engine) as db:
event = db.scalar(
select(OpsAuditEvent).where(
OpsAuditEvent.action == "support.diagnostics_viewed",
OpsAuditEvent.target_resource == f"ticket:{ticket_id}",
)
)
self.assertIsNotNone(event)
self.assertEqual(event.reason_code, reason_text)
def test_get_diagnostics_not_found(self):
ticket_id = "e1a11111-2222-3333-4444-555555555552"
resp = self.client.get(
f"/api/v1/ops/support/tickets/{ticket_id}/diagnostics?reason=investigation",
headers={"Authorization": f"Bearer {self.op_token}"},
)
self.assertEqual(resp.status_code, 404)
if __name__ == "__main__":
unittest.main()

View File

@ -1,4 +1,5 @@
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { OperationsShell } from '../../components/ops/operations-shell'; import { OperationsShell } from '../../components/ops/operations-shell';
import { OperationsErrorState } from '../../components/ops/operations-error-state'; import { OperationsErrorState } from '../../components/ops/operations-error-state';
import { normalizeHealthResponse, sanitizeAuditEvent } from '../../lib/ops-data'; import { normalizeHealthResponse, sanitizeAuditEvent } from '../../lib/ops-data';
@ -13,9 +14,11 @@ interface AdminPageProps {
searchParams?: Promise<{ searchParams?: Promise<{
nav?: string; nav?: string;
case?: string; case?: string;
error?: string;
}>; }>;
} }
function parseNavParam(nav?: string): OperationsNavId { function parseNavParam(nav?: string): OperationsNavId {
if (nav === 'incidents' || nav === 'audit' || nav === 'operators' || nav === 'session') { if (nav === 'incidents' || nav === 'audit' || nav === 'operators' || nav === 'session') {
return nav; return nav;
@ -35,10 +38,26 @@ export default async function AdminPage({ searchParams }: AdminPageProps) {
: cookieStore.get('__Host-motorsignal_ops')?.value ?? : cookieStore.get('__Host-motorsignal_ops')?.value ??
cookieStore.get('motorsignal_ops')?.value; cookieStore.get('motorsignal_ops')?.value;
const riderToken = cookieStore.get('motorsignal_access_token')?.value;
if (resolvedParams.error === 'forbidden') {
return (
<section className="ops-error-root 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>
);
}
// Unauthenticated - prompt for login (Zero query parameter preview/bypass) // Unauthenticated - prompt for login (Zero query parameter preview/bypass)
if (!opsToken) { if (!opsToken) {
if (riderToken) {
redirect('/api/ops/auth/session');
}
return ( return (
<section className="min-h-screen bg-surface flex items-center justify-center p-4"> <section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
<OperationsErrorState <OperationsErrorState
status={401} status={401}
message="Your Operations session has expired or is unauthenticated." message="Your Operations session has expired or is unauthenticated."
@ -58,8 +77,11 @@ export default async function AdminPage({ searchParams }: AdminPageProps) {
const meResponse = await fetch(`${baseUrl}/api/v1/ops/me`, { headers, cache: 'no-store' }); const meResponse = await fetch(`${baseUrl}/api/v1/ops/me`, { headers, cache: 'no-store' });
if (meResponse.status === 401) { if (meResponse.status === 401) {
if (riderToken) {
redirect('/api/ops/auth/session');
}
return ( return (
<section className="min-h-screen bg-surface flex items-center justify-center p-4"> <section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
<OperationsErrorState <OperationsErrorState
status={401} status={401}
message="Your Operations session has expired. Please sign in again." message="Your Operations session has expired. Please sign in again."
@ -68,9 +90,10 @@ export default async function AdminPage({ searchParams }: AdminPageProps) {
); );
} }
if (meResponse.status === 403) { if (meResponse.status === 403) {
return ( return (
<section className="min-h-screen bg-surface flex items-center justify-center p-4"> <section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
<OperationsErrorState <OperationsErrorState
status={403} status={403}
message="You do not have sufficient authority to access Operations." message="You do not have sufficient authority to access Operations."
@ -81,7 +104,7 @@ export default async function AdminPage({ searchParams }: AdminPageProps) {
if (!meResponse.ok) { if (!meResponse.ok) {
return ( return (
<section className="min-h-screen bg-surface flex items-center justify-center p-4"> <section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
<OperationsErrorState <OperationsErrorState
status={503} status={503}
message="Operations platform is temporarily unavailable." message="Operations platform is temporarily unavailable."
@ -155,7 +178,7 @@ export default async function AdminPage({ searchParams }: AdminPageProps) {
); );
} catch { } catch {
return ( return (
<section className="min-h-screen bg-surface flex items-center justify-center p-4"> <section className="ops-error-root min-h-screen bg-surface flex items-center justify-center p-4">
<OperationsErrorState <OperationsErrorState
status={503} status={503}
message="Unable to connect to the Operations platform service." message="Unable to connect to the Operations platform service."

View File

@ -0,0 +1,119 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
function getSafeRedirectUrl(targetPath: string, request: NextRequest): URL {
const forwardedHost = request.headers.get('x-forwarded-host')?.split(',')[0].trim()
?? request.headers.get('host')
?? 'motorsignal.app';
const forwardedProto = request.headers.get('x-forwarded-proto')?.split(',')[0].trim()
?? (process.env.NODE_ENV === 'production' ? 'https' : 'http');
return new URL(targetPath, `${forwardedProto}://${forwardedHost}`);
}
export async function GET(request: NextRequest) {
const cookieStore = await cookies();
const riderToken = cookieStore.get('motorsignal_access_token')?.value;
if (!riderToken) {
return NextResponse.redirect(getSafeRedirectUrl('/login?returnTo=/admin', request));
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const upstream = await fetch(`${baseUrl}/api/v1/ops/session/bootstrap`, {
method: 'POST',
headers: {
authorization: `Bearer ${riderToken}`,
accept: 'application/json',
},
cache: 'no-store',
});
if (upstream.ok) {
const data = (await upstream.json()) as { ops_token?: string };
const opsToken = data.ops_token;
if (opsToken) {
const redirectUrl = getSafeRedirectUrl('/admin', request);
const response = NextResponse.redirect(redirectUrl);
const opsCookieOptions = {
httpOnly: true,
secure: true,
sameSite: 'lax' as const,
path: '/',
maxAge: 60 * 60 * 8,
};
response.cookies.set('__Host-motorsignal_ops', opsToken, opsCookieOptions);
response.cookies.set('motorsignal_ops', opsToken, {
...opsCookieOptions,
secure: process.env.NODE_ENV === 'production',
});
return response;
}
}
if (upstream.status === 403) {
return NextResponse.redirect(getSafeRedirectUrl('/admin?error=forbidden', request));
}
return NextResponse.redirect(getSafeRedirectUrl('/login?returnTo=/admin', request));
} catch {
return NextResponse.redirect(getSafeRedirectUrl('/login?returnTo=/admin', request));
}
}
export async function POST(request: NextRequest) {
const cookieStore = await cookies();
const riderToken = cookieStore.get('motorsignal_access_token')?.value;
if (!riderToken) {
return NextResponse.json({ detail: 'Rider authentication required.' }, { status: 401 });
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const upstream = await fetch(`${baseUrl}/api/v1/ops/session/bootstrap`, {
method: 'POST',
headers: {
authorization: `Bearer ${riderToken}`,
accept: 'application/json',
},
cache: 'no-store',
});
if (!upstream.ok) {
const err = await upstream.json().catch(() => ({}));
return NextResponse.json(err, { status: upstream.status });
}
const data = (await upstream.json()) as { ops_token?: string };
const opsToken = data.ops_token;
if (!opsToken) {
return NextResponse.json({ detail: 'Failed to issue operations token.' }, { status: 500 });
}
const response = NextResponse.json({ success: true }, { status: 200 });
const opsCookieOptions = {
httpOnly: true,
secure: true,
sameSite: 'lax' as const,
path: '/',
maxAge: 60 * 60 * 8,
};
response.cookies.set('__Host-motorsignal_ops', opsToken, opsCookieOptions);
response.cookies.set('motorsignal_ops', opsToken, {
...opsCookieOptions,
secure: process.env.NODE_ENV === 'production',
});
return response;
} catch {
return NextResponse.json({ detail: 'Operations bootstrap unavailable.' }, { status: 503 });
}
}

View File

@ -0,0 +1,95 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { hasValidOrigin } from '../../../../../../../lib/request-origin';
interface RouteContext {
params: Promise<{
slug: string;
}>;
}
export async function POST(request: NextRequest, context: RouteContext) {
if (!hasValidOrigin(request)) {
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
}
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { slug } = await context.params;
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ detail: 'Invalid request body.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/cms/documents/${encodeURIComponent(slug)}/rollback`, {
method: 'POST',
headers: {
authorization: `Bearer ${opsToken}`,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(body),
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 rollback website content.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'CMS document or revision not found.' },
{ status: 404 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Invalid rollback parameters.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,158 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { hasValidOrigin } from '../../../../../../lib/request-origin';
interface RouteContext {
params: Promise<{
slug: string;
}>;
}
export async function GET(_request: NextRequest, context: RouteContext) {
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { slug } = await context.params;
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/cms/documents/${encodeURIComponent(slug)}`, {
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 this content document.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'CMS document not found.' },
{ status: 404 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}
export async function PUT(request: NextRequest, context: RouteContext) {
if (!hasValidOrigin(request)) {
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
}
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { slug } = await context.params;
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ detail: 'Invalid request body.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/cms/documents/${encodeURIComponent(slug)}`, {
method: 'PUT',
headers: {
authorization: `Bearer ${opsToken}`,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(body),
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 update website content.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'CMS document not found.' },
{ status: 404 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Invalid CMS update fields.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,57 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(_request: NextRequest) {
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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/cms/documents`, {
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 website content.' },
{ 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();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,60 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { normalizeOverviewResponse } from '../../../../lib/ops-data';
export async function GET(request: NextRequest) {
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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';
const headers: Record<string, string> = {
accept: 'application/json',
authorization: `Bearer ${opsToken}`,
};
try {
const response = await fetch(`${baseUrl}/api/v1/ops/overview`, {
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(
normalizeOverviewResponse(null),
{ status: 503 }
);
}
const data = await response.json();
return NextResponse.json(normalizeOverviewResponse(data), { status: 200 });
} catch {
return NextResponse.json(
normalizeOverviewResponse(null),
{ status: 503 }
);
}
}

View File

@ -0,0 +1,93 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
interface RouteContext {
params: Promise<{
ticketId: string;
}>;
}
export async function GET(request: NextRequest, context: RouteContext) {
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { ticketId } = await context.params;
const reasonParam = request.nextUrl.searchParams.get('reason');
const reasonHeader = request.headers.get('x-diagnostic-reason');
const reason = (reasonParam ?? reasonHeader ?? '').trim();
if (!reason || reason.length < 3) {
return NextResponse.json(
{ detail: 'A valid reason of at least 3 characters is required to inspect diagnostics.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
const targetUrl = new URL(
`${baseUrl}/api/v1/ops/support/tickets/${encodeURIComponent(ticketId)}/diagnostics`
);
targetUrl.searchParams.set('reason', reason);
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 inspect diagnostics.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'No diagnostic bundle found for this ticket.' },
{ status: 404 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Invalid reason parameter.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,95 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { hasValidOrigin } from '../../../../../../../lib/request-origin';
interface RouteContext {
params: Promise<{
ticketId: string;
}>;
}
export async function POST(request: NextRequest, context: RouteContext) {
if (!hasValidOrigin(request)) {
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
}
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { ticketId } = await context.params;
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ detail: 'Invalid request body.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/support/tickets/${encodeURIComponent(ticketId)}/notes`, {
method: 'POST',
headers: {
authorization: `Bearer ${opsToken}`,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(body),
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 add notes.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'Support ticket not found.' },
{ status: 404 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Note body cannot be empty.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data, { status: 201 });
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,158 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { hasValidOrigin } from '../../../../../../lib/request-origin';
interface RouteContext {
params: Promise<{
ticketId: string;
}>;
}
export async function GET(request: NextRequest, context: RouteContext) {
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { ticketId } = await context.params;
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/support/tickets/${encodeURIComponent(ticketId)}`, {
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 this support ticket.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'Support ticket not found.' },
{ status: 404 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}
export async function PATCH(request: NextRequest, context: RouteContext) {
if (!hasValidOrigin(request)) {
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
}
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { ticketId } = await context.params;
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ detail: 'Invalid request body.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/support/tickets/${encodeURIComponent(ticketId)}`, {
method: 'PATCH',
headers: {
authorization: `Bearer ${opsToken}`,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(body),
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 update support tickets.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'Support ticket not found.' },
{ status: 404 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Invalid update values.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,150 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { hasValidOrigin } from '../../../../../lib/request-origin';
export async function GET(request: NextRequest) {
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 searchParams = request.nextUrl.searchParams;
const typeFilter = searchParams.get('type_filter');
const statusFilter = searchParams.get('status_filter');
const q = searchParams.get('q');
const cursor = searchParams.get('cursor');
const limit = searchParams.get('limit');
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
const targetUrl = new URL(`${baseUrl}/api/v1/ops/support/tickets`);
if (typeFilter) targetUrl.searchParams.set('type_filter', typeFilter);
if (statusFilter) targetUrl.searchParams.set('status_filter', statusFilter);
if (q) targetUrl.searchParams.set('q', q);
if (cursor) targetUrl.searchParams.set('cursor', cursor);
if (limit) targetUrl.searchParams.set('limit', limit);
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 support tickets.' },
{ 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();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}
export async function POST(request: NextRequest) {
if (!hasValidOrigin(request)) {
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
}
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 }
);
}
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ detail: 'Invalid request body.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/support/tickets`, {
method: 'POST',
headers: {
authorization: `Bearer ${opsToken}`,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(body),
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 create support tickets.' },
{ status: 403 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Invalid support ticket fields.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data, { status: 201 });
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,96 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { hasValidOrigin } from '../../../../../../lib/request-origin';
interface RouteContext {
params: Promise<{
userId: string;
}>;
}
export async function POST(request: NextRequest, context: RouteContext) {
if (!hasValidOrigin(request)) {
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
}
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { userId } = await context.params;
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ detail: 'Invalid request body.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/users/${encodeURIComponent(userId)}/reset-password`, {
method: 'POST',
headers: {
authorization: `Bearer ${opsToken}`,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(body),
cache: 'no-store',
});
if (response.status === 401) {
return NextResponse.json(
{ detail: 'Your Operations session has expired.' },
{ status: 401 }
);
}
if (response.status === 403) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'You do not have authority to reset user passwords.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'User not found.' },
{ status: 404 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Invalid request fields.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,96 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { hasValidOrigin } from '../../../../../../lib/request-origin';
interface RouteContext {
params: Promise<{
userId: string;
}>;
}
export async function POST(request: NextRequest, context: RouteContext) {
if (!hasValidOrigin(request)) {
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
}
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { userId } = await context.params;
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ detail: 'Invalid request body.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/users/${encodeURIComponent(userId)}/revoke-sessions`, {
method: 'POST',
headers: {
authorization: `Bearer ${opsToken}`,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(body),
cache: 'no-store',
});
if (response.status === 401) {
return NextResponse.json(
{ detail: 'Your Operations session has expired.' },
{ status: 401 }
);
}
if (response.status === 403) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'You do not have authority to revoke sessions.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'User not found.' },
{ status: 404 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Invalid request fields.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,71 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
interface RouteContext {
params: Promise<{
userId: string;
}>;
}
export async function GET(request: NextRequest, context: RouteContext) {
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { userId } = await context.params;
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/users/${encodeURIComponent(userId)}`, {
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 this User.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'User not found.' },
{ status: 404 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,96 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { hasValidOrigin } from '../../../../../../lib/request-origin';
interface RouteContext {
params: Promise<{
userId: string;
}>;
}
export async function POST(request: NextRequest, context: RouteContext) {
if (!hasValidOrigin(request)) {
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
}
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 { userId } = await context.params;
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ detail: 'Invalid request body.' },
{ status: 422 }
);
}
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
try {
const response = await fetch(`${baseUrl}/api/v1/ops/users/${encodeURIComponent(userId)}/status`, {
method: 'POST',
headers: {
authorization: `Bearer ${opsToken}`,
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(body),
cache: 'no-store',
});
if (response.status === 401) {
return NextResponse.json(
{ detail: 'Your Operations session has expired.' },
{ status: 401 }
);
}
if (response.status === 403) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'You do not have authority to update user status.' },
{ status: 403 }
);
}
if (response.status === 404) {
return NextResponse.json(
{ detail: 'User not found.' },
{ status: 404 }
);
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
return NextResponse.json(
{ detail: data?.detail ?? 'Invalid user status fields.' },
{ status: 422 }
);
}
if (!response.ok) {
return NextResponse.json(
{ detail: 'Operations is temporarily unavailable.' },
{ status: response.status >= 500 ? response.status : 503 }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -0,0 +1,71 @@
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const cookieStore = await cookies();
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_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 searchParams = request.nextUrl.searchParams;
const q = searchParams.get('q');
const cursor = searchParams.get('cursor');
const limit = searchParams.get('limit');
const statusFilter = searchParams.get('status_filter');
const verifiedFilter = searchParams.get('verified_filter');
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
const targetUrl = new URL(`${baseUrl}/api/v1/ops/users`);
if (q) targetUrl.searchParams.set('q', q);
if (cursor) targetUrl.searchParams.set('cursor', cursor);
if (limit) targetUrl.searchParams.set('limit', limit);
if (statusFilter) targetUrl.searchParams.set('status_filter', statusFilter);
if (verifiedFilter) targetUrl.searchParams.set('verified_filter', verifiedFilter);
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 Users.' },
{ 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();
return NextResponse.json(data);
} catch {
return NextResponse.json(
{ detail: 'Unable to connect to Operations platform.' },
{ status: 503 }
);
}
}

View File

@ -32,8 +32,8 @@ export function OperationsAudit({
<span className="material-symbols-outlined text-[24px]">lock</span> <span className="material-symbols-outlined text-[24px]">lock</span>
</div> </div>
<div> <div>
<span className="text-[10px] font-mono uppercase tracking-widest text-outline"> <span className="text-[10px] font-mono text-outline font-semibold">
ACCESS RESTRICTED Access Restricted
</span> </span>
<h2 className="text-lg font-bold text-on-surface mt-1"> <h2 className="text-lg font-bold text-on-surface mt-1">
Owner Authority Required Owner Authority Required
@ -44,7 +44,7 @@ export function OperationsAudit({
</div> </div>
<div className="pt-3 border-t border-outline-variant/20 flex items-center justify-center gap-2 text-xs text-outline font-mono"> <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>Current Role:</span>
<span className="px-2 py-0.5 rounded bg-surface-container-highest text-on-surface font-semibold uppercase"> <span className="px-2 py-0.5 rounded bg-surface-container-highest text-on-surface font-semibold capitalize">
{role} {role}
</span> </span>
</div> </div>
@ -57,23 +57,23 @@ export function OperationsAudit({
{/* Header Row */} {/* 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 className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-outline-variant/30">
<div> <div>
<span className="text-[11px] font-mono uppercase tracking-widest text-primary-fixed-dim"> <span className="text-[11px] font-mono text-primary-fixed-dim block font-semibold">
GOVERNANCE &amp; COMPLIANCE Governance &amp; Compliance
</span> </span>
<h1 className="font-headline-xl text-2xl sm:text-3xl font-bold text-on-surface tracking-tight mt-1"> <h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight mt-1">
Audit Activity Audit Activity
</h1> </h1>
<p className="text-xs text-on-surface-variant mt-1"> <p className="text-xs sm:text-sm text-outline mt-1.5 max-w-2xl">
Immutable operational and security activity log across platform boundaries. Immutable operational and security activity log across platform boundaries.
</p> </p>
</div> </div>
{total !== undefined && ( {total !== undefined && (
<div className="text-right shrink-0"> <div className="text-right shrink-0">
<span className="text-[10px] uppercase font-mono text-outline block"> <span className="text-[11px] font-mono text-outline block">
Recorded Events Recorded Events
</span> </span>
<span className="text-xs font-mono font-bold text-on-surface"> <span className="text-sm sm:text-base font-mono font-extrabold text-on-surface">
{total.toLocaleString()} {total.toLocaleString()}
</span> </span>
</div> </div>
@ -81,31 +81,31 @@ export function OperationsAudit({
</div> </div>
{/* Audit Activity Table Container */} {/* Audit Activity Table Container */}
<div className="rounded-xl bg-surface-container-low border border-outline-variant/30 overflow-hidden shadow-sm"> <div className="rounded-2xl bg-surface-container-low border border-outline-variant/30 overflow-hidden shadow-sm">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table <table
className="w-full text-left border-collapse min-w-[720px]" className="w-full text-left border-collapse min-w-[720px]"
aria-label="Operations Audit Events" aria-label="Operations Audit Events"
> >
<thead> <thead>
<tr className="border-b border-outline-variant/30 bg-surface-container-lowest/60 text-[11px] font-mono uppercase tracking-wider text-outline"> <tr className="border-b border-outline-variant/30 bg-surface-container-lowest/60 text-[11px] font-mono text-outline">
<th scope="col" className="py-3 px-4"> <th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
TIME Time
</th> </th>
<th scope="col" className="py-3 px-4"> <th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
ACTION Action
</th> </th>
<th scope="col" className="py-3 px-4"> <th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
RESULT Result
</th> </th>
<th scope="col" className="py-3 px-4"> <th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
ACTOR Actor
</th> </th>
<th scope="col" className="py-3 px-4"> <th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
TARGET Target
</th> </th>
<th scope="col" className="py-3 px-4"> <th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
CASE Case
</th> </th>
</tr> </tr>
</thead> </thead>
@ -169,16 +169,16 @@ export function OperationsAudit({
className="hover:bg-surface-container-high/40 transition-colors" className="hover:bg-surface-container-high/40 transition-colors"
> >
{/* TIME */} {/* TIME */}
<td className="py-3 px-4 font-mono text-[11px] text-on-surface-variant whitespace-nowrap"> <td className="py-4 px-5 sm:px-6 font-mono text-[11px] sm:text-xs text-on-surface-variant whitespace-nowrap">
<time dateTime={event.timestamp}>{formattedTime}</time> <time dateTime={event.timestamp}>{formattedTime}</time>
</td> </td>
{/* ACTION */} {/* ACTION */}
<td className="py-3 px-4 font-medium text-on-surface whitespace-nowrap"> <td className="py-4 px-5 sm:px-6 font-medium text-on-surface whitespace-nowrap">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-mono text-xs">{event.action}</span> <span className="font-mono text-xs sm:text-sm font-semibold">{event.action}</span>
{event.reason_code && ( {event.reason_code && (
<span className="text-[10px] text-outline font-mono"> <span className="text-[10px] text-outline font-mono mt-0.5">
reason: {event.reason_code} reason: {event.reason_code}
</span> </span>
)} )}
@ -186,9 +186,9 @@ export function OperationsAudit({
</td> </td>
{/* RESULT */} {/* RESULT */}
<td className="py-3 px-4 whitespace-nowrap"> <td className="py-4 px-5 sm:px-6 whitespace-nowrap">
<span <span
className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-mono uppercase font-bold tracking-wider border ${ className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-mono font-semibold capitalize border ${
isSuccess isSuccess
? 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60' ? 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60'
: isFailure : isFailure
@ -201,13 +201,13 @@ export function OperationsAudit({
</td> </td>
{/* ACTOR (Current Operations Identity) */} {/* ACTOR (Current Operations Identity) */}
<td className="py-3 px-4"> <td className="py-4 px-5 sm:px-6">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-semibold text-on-surface truncate max-w-[200px]" title={event.actor_identity}> <span className="font-semibold text-on-surface truncate max-w-xs sm:max-w-sm text-xs sm:text-sm" title={event.actor_identity}>
{event.actor_identity} {event.actor_identity}
</span> </span>
{event.actor_role && ( {event.actor_role && (
<span className="text-[10px] text-outline font-mono uppercase"> <span className="text-[10px] text-outline font-mono mt-0.5 capitalize">
Role: {event.actor_role} Role: {event.actor_role}
</span> </span>
)} )}
@ -215,12 +215,12 @@ export function OperationsAudit({
</td> </td>
{/* TARGET */} {/* TARGET */}
<td className="py-3 px-4 font-mono text-[11px] text-on-surface-variant"> <td className="py-4 px-5 sm:px-6 font-mono text-xs text-on-surface-variant">
{event.target_resource || '--'} {event.target_resource || '--'}
</td> </td>
{/* CASE */} {/* CASE */}
<td className="py-3 px-4 font-mono text-[11px] text-on-surface-variant"> <td className="py-4 px-5 sm:px-6 font-mono text-xs text-on-surface-variant">
{event.case_reference || '--'} {event.case_reference || '--'}
</td> </td>
</tr> </tr>

File diff suppressed because it is too large Load Diff

View File

@ -63,7 +63,8 @@ export function OperationsCreateIncident({
return; return;
} }
if (!response.ok) { if (!response.ok) {
setErrorMessage('Operations platform is temporarily unavailable.'); const data = await response.json().catch(() => null);
setErrorMessage(data?.detail ?? 'Operations platform is temporarily unavailable.');
setIsSubmitting(false); setIsSubmitting(false);
return; return;
} }
@ -81,14 +82,17 @@ export function OperationsCreateIncident({
} }
return ( return (
<div className="max-w-2xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto space-y-8">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between border-b border-outline-variant/20 pb-4"> <div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-5 border-b border-outline-variant/30">
<div> <div>
<h1 className="text-xl font-headline-sm font-bold tracking-tight text-on-surface uppercase"> <span className="text-xs font-mono text-primary-fixed-dim font-semibold">
New Incident Record
</span>
<h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight mt-1.5">
Create Operational Incident Create Operational Incident
</h1> </h1>
<p className="text-xs text-on-surface-variant mt-0.5"> <p className="text-xs sm:text-sm text-on-surface-variant mt-1.5">
Case reference and open status will be generated automatically. Case reference and open status will be generated automatically.
</p> </p>
</div> </div>
@ -96,7 +100,7 @@ export function OperationsCreateIncident({
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
className="px-3 py-1.5 rounded-lg bg-surface-container border border-outline-variant/30 text-xs font-medium text-on-surface-variant hover:text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none" className="inline-flex items-center gap-1.5 px-4 py-2.5 rounded-xl bg-surface-container-high hover:bg-surface-bright border border-outline-variant/40 text-xs sm:text-sm font-semibold text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none cursor-pointer"
> >
Cancel Cancel
</button> </button>
@ -104,17 +108,17 @@ export function OperationsCreateIncident({
{/* Error Alert */} {/* Error Alert */}
{errorMessage && ( {errorMessage && (
<div className="p-4 rounded-xl bg-error/10 border border-error/30 text-error flex items-center gap-3 text-xs"> <div className="p-4 rounded-xl bg-error/10 border border-error/30 text-error flex items-center gap-3 text-xs sm:text-sm">
<span className="material-symbols-outlined text-[18px]">error</span> <span className="material-symbols-outlined text-[20px]">error</span>
<span>{errorMessage}</span> <span>{errorMessage}</span>
</div> </div>
)} )}
{/* Form Card */} {/* Form Card */}
<form onSubmit={handleSubmit} className="bg-surface-container-low p-6 rounded-xl border border-outline-variant/30 space-y-5"> <form onSubmit={handleSubmit} className="bg-surface-container-low p-6 sm:p-8 rounded-2xl border border-outline-variant/30 space-y-6 shadow-sm">
{/* Title */} {/* Title */}
<div className="space-y-1.5"> <div className="space-y-2">
<label htmlFor="incident-title" className="block text-xs font-semibold text-on-surface"> <label htmlFor="incident-title" className="block text-xs sm:text-sm font-semibold text-on-surface">
Incident Title <span className="text-error">*</span> Incident Title <span className="text-error">*</span>
</label> </label>
<input <input
@ -125,7 +129,7 @@ export function OperationsCreateIncident({
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="e.g. Ingress latency spike on edge cluster" placeholder="e.g. Ingress latency spike on edge cluster"
className="w-full bg-surface-container px-3.5 py-2 rounded-lg border border-outline-variant/40 text-xs text-on-surface placeholder:text-outline focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none" className="w-full bg-surface-container px-4 py-2.5 rounded-xl border border-outline-variant/40 text-xs sm:text-sm text-on-surface placeholder:text-outline focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none"
/> />
</div> </div>
@ -142,7 +146,7 @@ export function OperationsCreateIncident({
key={sev} key={sev}
type="button" type="button"
onClick={() => setSeverity(sev)} onClick={() => setSeverity(sev)}
className={`px-3 py-2.5 rounded-lg border text-xs font-mono font-bold uppercase transition-all flex flex-col items-center gap-1 focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none ${ className={`px-3 py-2.5 rounded-lg border text-xs font-mono font-semibold capitalize transition-all flex flex-col items-center gap-1 focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none ${
isSelected isSelected
? sev === 'critical' ? sev === 'critical'
? 'bg-error/20 border-error text-error shadow-[0_0_12px_rgba(255,100,100,0.2)]' ? 'bg-error/20 border-error text-error shadow-[0_0_12px_rgba(255,100,100,0.2)]'
@ -184,22 +188,22 @@ export function OperationsCreateIncident({
</div> </div>
{/* Actions Bar */} {/* Actions Bar */}
<div className="flex items-center justify-end gap-3 pt-3 border-t border-outline-variant/20"> <div className="flex items-center justify-end gap-3 pt-4 border-t border-outline-variant/20">
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
disabled={isSubmitting} disabled={isSubmitting}
className="px-4 py-2 rounded-lg bg-surface-container border border-outline-variant/30 text-xs font-semibold text-on-surface-variant hover:text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none" className="px-5 py-2.5 rounded-xl bg-surface-container border border-outline-variant/30 text-xs sm:text-sm font-semibold text-on-surface-variant hover:text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none cursor-pointer"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="flex items-center gap-2 px-5 py-2 rounded-lg bg-primary-container text-on-primary-container text-xs font-bold shadow-md hover:bg-primary-container/90 transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50" className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-primary-container text-on-primary-container text-xs sm:text-sm font-bold shadow-md hover:bg-primary-container/90 transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50 cursor-pointer"
> >
{isSubmitting && ( {isSubmitting && (
<span className="material-symbols-outlined text-[16px] animate-spin"> <span className="material-symbols-outlined text-[18px] animate-spin">
progress_activity progress_activity
</span> </span>
)} )}

View File

@ -1,7 +1,9 @@
'use client'; 'use client';
import React from 'react'; import React, { useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { onAuthStateChanged } from 'firebase/auth';
import { firebaseAuth } from '../../lib/firebase-client';
interface OperationsErrorStateProps { interface OperationsErrorStateProps {
status: 401 | 403 | 404 | 503 | 'disabled' | 'network' | number; status: 401 | 403 | 404 | 503 | 'disabled' | 'network' | number;
@ -14,17 +16,49 @@ export function OperationsErrorState({
message, message,
onRetry, onRetry,
}: OperationsErrorStateProps) { }: OperationsErrorStateProps) {
const [authenticating, setAuthenticating] = useState(false);
useEffect(() => {
if (status !== 401) return;
const unsubscribe = onAuthStateChanged(firebaseAuth, async (user) => {
if (user) {
setAuthenticating(true);
try {
const idToken = await user.getIdToken();
const res = await fetch('/api/auth/firebase-login', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id_token: idToken }),
});
if (res.ok) {
window.location.reload();
return;
}
} catch {
// Ignore and remain on error state
} finally {
setAuthenticating(false);
}
}
});
return () => unsubscribe();
}, [status]);
let title = 'Operations Error'; let title = 'Operations Error';
let description = message ?? 'An unexpected error occurred in the Operations console.'; let description = authenticating
let icon = 'error'; ? 'Verifying operator credentials and activating session...'
: (message ?? 'An unexpected error occurred in the Operations console.');
let icon = authenticating ? 'sync' : 'error';
let showSignIn = false; let showSignIn = false;
let showRetry = true; let showRetry = true;
if (status === 401) { if (status === 401) {
title = 'Authentication Required'; title = authenticating ? 'Authenticating Operator...' : 'Authentication Required';
description = message ?? 'Your Operations session has expired or is unauthenticated.'; description = authenticating
icon = 'lock'; ? 'Verifying operator credentials and establishing secure session...'
showSignIn = true; : (message ?? 'Your Operations session has expired or is unauthenticated.');
icon = authenticating ? 'sync' : 'lock';
showSignIn = !authenticating;
showRetry = false; showRetry = false;
} else if (status === 403) { } else if (status === 403) {
title = 'Access Restricted'; title = 'Access Restricted';
@ -54,8 +88,8 @@ export function OperationsErrorState({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<span className="text-[10px] font-mono uppercase tracking-widest text-outline"> <span className="text-[10px] font-mono text-outline font-semibold">
STATUS {typeof status === 'number' ? status : status.toUpperCase()} Status {status}
</span> </span>
<h2 className="text-lg font-bold text-on-surface">{title}</h2> <h2 className="text-lg font-bold text-on-surface">{title}</h2>
<p className="text-xs text-on-surface-variant leading-relaxed"> <p className="text-xs text-on-surface-variant leading-relaxed">
@ -66,7 +100,7 @@ export function OperationsErrorState({
<div className="pt-4 border-t border-outline-variant/20 flex items-center justify-center gap-3"> <div className="pt-4 border-t border-outline-variant/20 flex items-center justify-center gap-3">
{showSignIn && ( {showSignIn && (
<Link <Link
href="/login" href="/login?returnTo=/admin"
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" 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 className="material-symbols-outlined text-[16px]">login</span>

View File

@ -317,27 +317,27 @@ export function OperationsIncidentDetail({
switch (severity) { switch (severity) {
case 'critical': case 'critical':
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-bold tracking-wider bg-error/15 text-error border border-error/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-semibold bg-error/15 text-error border border-error/30">
CRITICAL Critical
</span> </span>
); );
case 'high': case 'high':
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-bold tracking-wider bg-amber-500/15 text-amber-400 border border-amber-500/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-semibold bg-amber-500/15 text-amber-400 border border-amber-500/30">
HIGH High
</span> </span>
); );
case 'medium': case 'medium':
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-medium tracking-wider bg-primary-container/15 text-primary-container border border-primary-container/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-semibold bg-primary-container/15 text-primary-container border border-primary-container/30">
MEDIUM Medium
</span> </span>
); );
case 'low': case 'low':
default: default:
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-medium tracking-wider bg-surface-container text-on-surface-variant border border-outline-variant/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-medium bg-surface-container text-on-surface-variant border border-outline-variant/30">
LOW Low
</span> </span>
); );
} }
@ -347,39 +347,39 @@ export function OperationsIncidentDetail({
switch (status) { switch (status) {
case 'open': case 'open':
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-bold tracking-wider bg-blue-500/15 text-blue-400 border border-blue-500/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-semibold bg-blue-500/15 text-blue-400 border border-blue-500/30">
OPEN Open
</span> </span>
); );
case 'investigating': case 'investigating':
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-bold tracking-wider bg-amber-500/15 text-amber-300 border border-amber-500/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-semibold bg-amber-500/15 text-amber-300 border border-amber-500/30">
INVESTIGATING Investigating
</span> </span>
); );
case 'monitoring': case 'monitoring':
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-bold tracking-wider bg-purple-500/15 text-purple-300 border border-purple-500/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-semibold bg-purple-500/15 text-purple-300 border border-purple-500/30">
MONITORING Monitoring
</span> </span>
); );
case 'resolved': case 'resolved':
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-bold tracking-wider bg-emerald-500/15 text-emerald-400 border border-emerald-500/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-semibold bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
RESOLVED Resolved
</span> </span>
); );
case 'closed': case 'closed':
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-medium tracking-wider bg-surface-container-high text-outline border border-outline-variant/30"> <span className="px-2.5 py-1 rounded text-xs font-mono font-medium bg-surface-container-high text-outline border border-outline-variant/30">
CLOSED Closed
</span> </span>
); );
case 'archived': case 'archived':
default: default:
return ( return (
<span className="px-2.5 py-1 rounded text-xs font-mono uppercase font-medium tracking-wider bg-surface-container-highest/60 text-outline border border-outline-variant/20"> <span className="px-2.5 py-1 rounded text-xs font-mono font-medium bg-surface-container-highest/60 text-outline border border-outline-variant/20">
ARCHIVED Archived
</span> </span>
); );
} }
@ -419,27 +419,27 @@ export function OperationsIncidentDetail({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Navigation & Actions Topbar */} {/* Navigation & Actions Topbar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-outline-variant/20 pb-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-5 border-b border-outline-variant/30">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
type="button" type="button"
onClick={onBack} onClick={onBack}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-surface-container border border-outline-variant/30 text-xs font-semibold text-on-surface hover:bg-surface-container-high transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none" className="inline-flex items-center gap-1.5 px-4 py-2.5 rounded-xl bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 text-xs sm:text-sm font-semibold text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none cursor-pointer"
> >
← Back ← Back
</button> </button>
<span className="font-mono font-bold text-sm text-primary-container"> <span className="font-mono font-bold text-sm sm:text-base text-primary-container">
{incident.case_reference} {incident.case_reference}
</span> </span>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-3">
<button <button
type="button" type="button"
onClick={() => fetchIncidentData()} onClick={() => fetchIncidentData()}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-surface-container border border-outline-variant/30 text-xs font-medium text-on-surface-variant hover:text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none" className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-surface-container border border-outline-variant/30 text-xs sm:text-sm font-medium text-on-surface-variant hover:text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none cursor-pointer"
> >
<span className="material-symbols-outlined text-[16px]">refresh</span> <span className="material-symbols-outlined text-[18px]">refresh</span>
<span>Refresh</span> <span>Refresh</span>
</button> </button>
@ -451,7 +451,7 @@ export function OperationsIncidentDetail({
setModalAction('close'); setModalAction('close');
setModalOpen(true); setModalOpen(true);
}} }}
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg bg-surface-container-high border border-outline-variant/40 text-xs font-semibold text-on-surface hover:bg-surface-container-highest transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none" className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-surface-container-high border border-outline-variant/40 text-xs sm:text-sm font-semibold text-on-surface hover:bg-surface-container-highest transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none cursor-pointer"
> >
<span className="material-symbols-outlined text-[16px] text-emerald-400"> <span className="material-symbols-outlined text-[16px] text-emerald-400">
task_alt task_alt
@ -506,11 +506,11 @@ export function OperationsIncidentDetail({
)} )}
{/* Incident Header Card */} {/* Incident Header Card */}
<div className="bg-surface-container-low p-6 rounded-2xl border border-outline-variant/30 shadow-sm space-y-4"> <div className="bg-surface-container-low p-6 sm:p-8 rounded-2xl border border-outline-variant/30 shadow-sm space-y-6">
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
<div className="space-y-1.5"> <div className="space-y-2">
<div className="flex flex-wrap items-center gap-2.5"> <div className="flex flex-wrap items-center gap-2.5">
<span className="font-mono text-xs font-bold text-primary-fixed-dim uppercase tracking-wider"> <span className="font-mono text-xs sm:text-sm font-bold text-primary-fixed-dim">
{incident.case_reference} {incident.case_reference}
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -518,7 +518,7 @@ export function OperationsIncidentDetail({
{getStatusBadge(incident.status)} {getStatusBadge(incident.status)}
</div> </div>
</div> </div>
<h1 className="text-lg font-headline-sm font-bold text-on-surface"> <h1 className="font-headline-xl text-2xl sm:text-3xl font-extrabold text-on-surface tracking-tight mt-1">
{incident.title} {incident.title}
</h1> </h1>
</div> </div>
@ -528,41 +528,41 @@ export function OperationsIncidentDetail({
<div className="flex flex-wrap items-center gap-3 bg-surface-container p-3 rounded-xl border border-outline-variant/30 shrink-0"> <div className="flex flex-wrap items-center gap-3 bg-surface-container p-3 rounded-xl border border-outline-variant/30 shrink-0">
{/* Severity Selector */} {/* Severity Selector */}
<div className="space-y-1"> <div className="space-y-1">
<span className="block text-[10px] font-mono uppercase text-outline"> <span className="block text-[10px] font-mono text-outline">
Severity Severity
</span> </span>
<select <select
value={incident.severity} value={incident.severity}
disabled={isMutating} disabled={isMutating}
onChange={(e) => handleSeverityChange(e.target.value as IncidentSeverity)} onChange={(e) => handleSeverityChange(e.target.value as IncidentSeverity)}
className="bg-surface-container-high px-2.5 py-1 rounded-lg border border-outline-variant/40 text-xs font-mono font-bold text-on-surface focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50" className="bg-surface-container-high px-2.5 py-1 rounded-lg border border-outline-variant/40 text-xs font-mono font-semibold capitalize text-on-surface focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50"
> >
<option value="critical">CRITICAL</option> <option value="critical">Critical</option>
<option value="high">HIGH</option> <option value="high">High</option>
<option value="medium">MEDIUM</option> <option value="medium">Medium</option>
<option value="low">LOW</option> <option value="low">Low</option>
</select> </select>
</div> </div>
{/* Status Transition Selector */} {/* Status Transition Selector */}
<div className="space-y-1"> <div className="space-y-1">
<span className="block text-[10px] font-mono uppercase text-outline"> <span className="block text-[10px] font-mono text-outline">
Transition Status Transition Status
</span> </span>
<select <select
value={incident.status} value={incident.status}
disabled={isMutating} disabled={isMutating}
onChange={(e) => handleStatusChange(e.target.value as IncidentStatus)} onChange={(e) => handleStatusChange(e.target.value as IncidentStatus)}
className="bg-surface-container-high px-2.5 py-1 rounded-lg border border-outline-variant/40 text-xs font-mono font-bold text-on-surface focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50" className="bg-surface-container-high px-2.5 py-1 rounded-lg border border-outline-variant/40 text-xs font-mono font-semibold text-on-surface focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50"
> >
<option value={incident.status} disabled> <option value={incident.status} disabled>
Current ({incident.status.toUpperCase()}) Current ({incident.status.charAt(0).toUpperCase() + incident.status.slice(1)})
</option> </option>
{(['open', 'investigating', 'monitoring', 'resolved', 'closed', 'archived'] as IncidentStatus[]) {(['open', 'investigating', 'monitoring', 'resolved', 'closed', 'archived'] as IncidentStatus[])
.filter((s) => isValidStatusTransition(incident.status, s, role)) .filter((s) => isValidStatusTransition(incident.status, s, role))
.map((s) => ( .map((s) => (
<option key={s} value={s}> <option key={s} value={s}>
→ {s.toUpperCase()} → {s.charAt(0).toUpperCase() + s.slice(1)}
</option> </option>
))} ))}
</select> </select>
@ -574,25 +574,25 @@ export function OperationsIncidentDetail({
{/* Metadata Grid */} {/* Metadata Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 pt-4 border-t border-outline-variant/20 text-xs"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 pt-4 border-t border-outline-variant/20 text-xs">
<div> <div>
<span className="block text-[10px] font-mono uppercase text-outline">Created By</span> <span className="block text-[10px] font-mono text-outline">Created By</span>
<span className="font-mono text-on-surface-variant break-all"> <span className="font-mono text-on-surface-variant break-all">
{incident.created_by_identity} {incident.created_by_identity}
</span> </span>
</div> </div>
<div> <div>
<span className="block text-[10px] font-mono uppercase text-outline">Created At</span> <span className="block text-[10px] font-mono text-outline">Created At</span>
<span className="font-mono text-on-surface-variant"> <span className="font-mono text-on-surface-variant">
{formatDate(incident.created_at)} {formatDate(incident.created_at)}
</span> </span>
</div> </div>
<div> <div>
<span className="block text-[10px] font-mono uppercase text-outline">Last Updated</span> <span className="block text-[10px] font-mono text-outline">Last Updated</span>
<span className="font-mono text-on-surface-variant"> <span className="font-mono text-on-surface-variant">
{formatDate(incident.updated_at)} {formatDate(incident.updated_at)}
</span> </span>
</div> </div>
<div> <div>
<span className="block text-[10px] font-mono uppercase text-outline">Closed</span> <span className="block text-[10px] font-mono text-outline">Closed</span>
<span className="font-mono text-on-surface-variant"> <span className="font-mono text-on-surface-variant">
{incident.closed_at ? formatDate(incident.closed_at) : '—'} {incident.closed_at ? formatDate(incident.closed_at) : '—'}
</span> </span>
@ -602,7 +602,7 @@ export function OperationsIncidentDetail({
{/* Summary Card */} {/* Summary Card */}
<div className="bg-surface-container-low p-6 rounded-2xl border border-outline-variant/30 space-y-2"> <div className="bg-surface-container-low p-6 rounded-2xl border border-outline-variant/30 space-y-2">
<h2 className="text-xs font-headline-sm font-bold uppercase tracking-wider text-outline"> <h2 className="text-xs font-headline-sm font-semibold text-outline">
Incident Summary Incident Summary
</h2> </h2>
<div className="text-xs text-on-surface whitespace-pre-wrap leading-relaxed"> <div className="text-xs text-on-surface whitespace-pre-wrap leading-relaxed">
@ -617,7 +617,7 @@ export function OperationsIncidentDetail({
<span className="material-symbols-outlined text-emerald-400 text-[18px]"> <span className="material-symbols-outlined text-emerald-400 text-[18px]">
task_alt task_alt
</span> </span>
<h2 className="text-xs font-headline-sm font-bold uppercase tracking-wider text-emerald-400"> <h2 className="text-xs font-headline-sm font-semibold text-emerald-400">
Resolution Record Resolution Record
</h2> </h2>
</div> </div>
@ -636,7 +636,7 @@ export function OperationsIncidentDetail({
<span className="material-symbols-outlined text-[18px] text-primary-container"> <span className="material-symbols-outlined text-[18px] text-primary-container">
notes notes
</span> </span>
<h2 className="text-xs font-headline-sm font-bold uppercase tracking-wider text-on-surface"> <h2 className="text-xs font-headline-sm font-semibold text-on-surface">
Operator Notes Operator Notes
</h2> </h2>
</div> </div>
@ -719,7 +719,7 @@ export function OperationsIncidentDetail({
<span className="material-symbols-outlined text-[18px] text-primary-container"> <span className="material-symbols-outlined text-[18px] text-primary-container">
history history
</span> </span>
<h2 className="text-xs font-headline-sm font-bold uppercase tracking-wider text-on-surface"> <h2 className="text-xs font-headline-sm font-semibold text-on-surface">
Operational Timeline Operational Timeline
</h2> </h2>
</div> </div>
@ -729,11 +729,11 @@ export function OperationsIncidentDetail({
<div className="relative space-y-1"> <div className="relative space-y-1">
<span className="absolute -left-6 top-0.5 w-3 h-3 rounded-full bg-blue-500 border-2 border-surface-container-low" /> <span className="absolute -left-6 top-0.5 w-3 h-3 rounded-full bg-blue-500 border-2 border-surface-container-low" />
<div className="flex items-center justify-between text-[10px] font-mono text-outline"> <div className="flex items-center justify-between text-[10px] font-mono text-outline">
<span className="font-bold text-on-surface uppercase">Incident Opened</span> <span className="font-semibold text-on-surface">Incident Opened</span>
<span>{formatDate(incident.created_at)}</span> <span>{formatDate(incident.created_at)}</span>
</div> </div>
<p className="text-xs text-on-surface-variant"> <p className="text-xs text-on-surface-variant">
Created by <span className="font-mono text-primary-container">{incident.created_by_identity}</span> with initial severity <span className="font-mono uppercase font-semibold">{incident.severity}</span>. Created by <span className="font-mono text-primary-container">{incident.created_by_identity}</span> with initial severity <span className="font-mono capitalize font-medium">{incident.severity}</span>.
</p> </p>
</div> </div>
@ -742,7 +742,7 @@ export function OperationsIncidentDetail({
<div className="relative space-y-1"> <div className="relative space-y-1">
<span className="absolute -left-6 top-0.5 w-3 h-3 rounded-full bg-purple-400 border-2 border-surface-container-low" /> <span className="absolute -left-6 top-0.5 w-3 h-3 rounded-full bg-purple-400 border-2 border-surface-container-low" />
<div className="flex items-center justify-between text-[10px] font-mono text-outline"> <div className="flex items-center justify-between text-[10px] font-mono text-outline">
<span className="font-bold text-on-surface uppercase">Operator Notes ({notes.length})</span> <span className="font-semibold text-on-surface">Operator Notes ({notes.length})</span>
<span>{formatDate(notes[0]?.created_at)}</span> <span>{formatDate(notes[0]?.created_at)}</span>
</div> </div>
<p className="text-xs text-on-surface-variant"> <p className="text-xs text-on-surface-variant">
@ -756,7 +756,7 @@ export function OperationsIncidentDetail({
<div className="relative space-y-1"> <div className="relative space-y-1">
<span className="absolute -left-6 top-0.5 w-3 h-3 rounded-full bg-emerald-400 border-2 border-surface-container-low" /> <span className="absolute -left-6 top-0.5 w-3 h-3 rounded-full bg-emerald-400 border-2 border-surface-container-low" />
<div className="flex items-center justify-between text-[10px] font-mono text-outline"> <div className="flex items-center justify-between text-[10px] font-mono text-outline">
<span className="font-bold text-emerald-400 uppercase">Resolution Documented</span> <span className="font-semibold text-emerald-400">Resolution Documented</span>
<span>{formatDate(incident.updated_at)}</span> <span>{formatDate(incident.updated_at)}</span>
</div> </div>
<p className="text-xs text-on-surface-variant"> <p className="text-xs text-on-surface-variant">
@ -770,7 +770,7 @@ export function OperationsIncidentDetail({
<div className="relative space-y-1"> <div className="relative space-y-1">
<span className="absolute -left-6 top-0.5 w-3 h-3 rounded-full bg-outline border-2 border-surface-container-low" /> <span className="absolute -left-6 top-0.5 w-3 h-3 rounded-full bg-outline border-2 border-surface-container-low" />
<div className="flex items-center justify-between text-[10px] font-mono text-outline"> <div className="flex items-center justify-between text-[10px] font-mono text-outline">
<span className="font-bold text-on-surface uppercase">Incident Closed</span> <span className="font-semibold text-on-surface">Incident Closed</span>
<span>{formatDate(incident.closed_at)}</span> <span>{formatDate(incident.closed_at)}</span>
</div> </div>
<p className="text-xs text-on-surface-variant"> <p className="text-xs text-on-surface-variant">

View File

@ -97,7 +97,7 @@ export function OperationsIncidentModal({
<span className="material-symbols-outlined text-primary-container text-[20px]"> <span className="material-symbols-outlined text-primary-container text-[20px]">
{action === 'close' ? 'task_alt' : action === 'reopen' ? 'replay' : 'archive'} {action === 'close' ? 'task_alt' : action === 'reopen' ? 'replay' : 'archive'}
</span> </span>
<h2 id="modal-title" className="text-base font-headline-sm font-bold text-on-surface uppercase"> <h2 id="modal-title" className="text-base font-headline-sm font-bold text-on-surface">
{titles[action]} {titles[action]}
</h2> </h2>
</div> </div>

View File

@ -101,27 +101,27 @@ export function OperationsIncidentsList({
switch (severity) { switch (severity) {
case 'critical': case 'critical':
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-bold tracking-wider bg-error/15 text-error border border-error/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-error/15 text-error border border-error/30">
CRITICAL Critical
</span> </span>
); );
case 'high': case 'high':
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-bold tracking-wider bg-amber-500/15 text-amber-400 border border-amber-500/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-amber-500/15 text-amber-400 border border-amber-500/30">
HIGH High
</span> </span>
); );
case 'medium': case 'medium':
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-medium tracking-wider bg-primary-container/15 text-primary-container border border-primary-container/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-primary-container/15 text-primary-container border border-primary-container/30">
MEDIUM Medium
</span> </span>
); );
case 'low': case 'low':
default: default:
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-medium tracking-wider bg-surface-container text-on-surface-variant border border-outline-variant/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-medium bg-surface-container text-on-surface-variant border border-outline-variant/30">
LOW Low
</span> </span>
); );
} }
@ -131,39 +131,39 @@ export function OperationsIncidentsList({
switch (status) { switch (status) {
case 'open': case 'open':
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-semibold tracking-wider bg-blue-500/15 text-blue-400 border border-blue-500/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-blue-500/15 text-blue-400 border border-blue-500/30">
OPEN Open
</span> </span>
); );
case 'investigating': case 'investigating':
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-semibold tracking-wider bg-amber-500/15 text-amber-300 border border-amber-500/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-amber-500/15 text-amber-300 border border-amber-500/30">
INVESTIGATING Investigating
</span> </span>
); );
case 'monitoring': case 'monitoring':
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-semibold tracking-wider bg-purple-500/15 text-purple-300 border border-purple-500/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-purple-500/15 text-purple-300 border border-purple-500/30">
MONITORING Monitoring
</span> </span>
); );
case 'resolved': case 'resolved':
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-semibold tracking-wider bg-emerald-500/15 text-emerald-400 border border-emerald-500/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
RESOLVED Resolved
</span> </span>
); );
case 'closed': case 'closed':
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-medium tracking-wider bg-surface-container-high text-outline border border-outline-variant/30"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-medium bg-surface-container-high text-outline border border-outline-variant/30">
CLOSED Closed
</span> </span>
); );
case 'archived': case 'archived':
default: default:
return ( return (
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase font-medium tracking-wider bg-surface-container-highest/60 text-outline border border-outline-variant/20"> <span className="px-2 py-0.5 rounded text-[10px] font-mono font-medium bg-surface-container-highest/60 text-outline border border-outline-variant/20">
ARCHIVED Archived
</span> </span>
); );
} }
@ -188,48 +188,51 @@ export function OperationsIncidentsList({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Page Header */} {/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-outline-variant/20 pb-4"> <div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-5 border-b border-outline-variant/30">
<div> <div>
<div className="flex items-center gap-2"> <span className="text-xs font-mono text-primary-fixed-dim font-semibold">
<h1 className="text-xl font-headline-sm font-bold tracking-tight text-on-surface uppercase"> Reliability &amp; Incident Management
</span>
<div className="flex items-center gap-3 mt-1.5">
<h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight">
Operational Incidents Operational Incidents
</h1> </h1>
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase tracking-wider bg-primary-container/10 text-primary-container border border-primary-container/20"> <span className="px-2.5 py-1 rounded-full text-xs font-mono font-semibold bg-primary-container/10 text-primary-container border border-primary-container/30">
OPS-4 OPS-4
</span> </span>
</div> </div>
<p className="text-xs text-on-surface-variant mt-0.5"> <p className="text-xs sm:text-sm text-on-surface-variant mt-1.5">
Isolated reliability & incident tracking with immutable case references. Isolated reliability &amp; incident tracking with immutable case references.
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-3 shrink-0">
<button <button
type="button" type="button"
onClick={() => fetchIncidents()} onClick={() => fetchIncidents()}
disabled={isLoading} disabled={isLoading}
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-surface-container border border-outline-variant/30 text-xs font-medium text-on-surface-variant hover:text-on-surface hover:bg-surface-container-high transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none" className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-surface-container-high hover:bg-surface-bright text-primary-container border border-primary-container/40 text-xs sm:text-sm 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 cursor-pointer"
title="Refresh incidents list" title="Refresh incidents list"
> >
<span className={`material-symbols-outlined text-[16px] ${isLoading ? 'animate-spin' : ''}`}> <span className={`material-symbols-outlined text-[18px] ${isLoading ? 'animate-spin' : ''}`}>
refresh refresh
</span> </span>
<span>Refresh</span> <span>{isLoading ? 'Checking...' : 'Refresh'}</span>
</button> </button>
<button <button
type="button" type="button"
onClick={onCreateIncident} onClick={onCreateIncident}
className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-primary-container text-on-primary-container text-xs font-bold shadow-md hover:bg-primary-container/90 transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none" className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-primary-container text-on-primary-container text-xs sm:text-sm font-bold shadow-md hover:bg-primary-container/90 transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none cursor-pointer"
> >
<span className="material-symbols-outlined text-[16px]">add_circle</span> <span className="material-symbols-outlined text-[18px]">add_circle</span>
<span>New Incident</span> <span>New Incident</span>
</button> </button>
</div> </div>
</div> </div>
{/* Filter Bar */} {/* Filter Bar */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 bg-surface-container-low p-3.5 rounded-xl border border-outline-variant/20"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-surface-container-low p-4 sm:p-5 rounded-2xl border border-outline-variant/30 shadow-sm">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{/* Status Filter */} {/* Status Filter */}
<div className="flex items-center gap-1.5 text-xs"> <div className="flex items-center gap-1.5 text-xs">
@ -339,16 +342,16 @@ export function OperationsIncidentsList({
) : ( ) : (
<> <>
{/* Desktop Table View (hidden on small screens) */} {/* Desktop Table View (hidden on small screens) */}
<div className="hidden md:block overflow-hidden rounded-xl border border-outline-variant/30 bg-surface-container-lowest shadow-sm"> <div className="hidden md:block overflow-hidden rounded-2xl border border-outline-variant/30 bg-surface-container-lowest shadow-sm">
<table className="w-full text-left text-xs border-collapse"> <table className="w-full text-left text-xs sm:text-sm border-collapse">
<thead> <thead>
<tr className="border-b border-outline-variant/20 bg-surface-container-low text-[11px] font-mono text-outline uppercase tracking-wider"> <tr className="border-b border-outline-variant/20 bg-surface-container-low text-xs font-mono text-outline">
<th className="py-3 px-4 font-semibold">CASE</th> <th data-column="CASE" className="py-4 px-5 sm:px-6 font-semibold">Case</th>
<th className="py-3 px-4 font-semibold">TITLE</th> <th data-column="TITLE" className="py-4 px-5 sm:px-6 font-semibold">Title</th>
<th className="py-3 px-4 font-semibold">SEVERITY</th> <th data-column="SEVERITY" className="py-4 px-5 sm:px-6 font-semibold">Severity</th>
<th className="py-3 px-4 font-semibold">STATUS</th> <th data-column="STATUS" className="py-4 px-5 sm:px-6 font-semibold">Status</th>
<th className="py-3 px-4 font-semibold">UPDATED</th> <th data-column="UPDATED" className="py-4 px-5 sm:px-6 font-semibold">Updated</th>
<th className="py-3 px-4 font-semibold">CREATED</th> <th data-column="CREATED" className="py-4 px-5 sm:px-6 font-semibold">Created</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-outline-variant/10 text-on-surface"> <tbody className="divide-y divide-outline-variant/10 text-on-surface">
@ -365,18 +368,18 @@ export function OperationsIncidentsList({
} }
}} }}
> >
<td className="py-3 px-4 font-mono font-bold text-primary-container group-hover:underline"> <td className="py-4 px-5 sm:px-6 font-mono text-xs sm:text-sm font-bold text-primary-container group-hover:underline">
{incident.case_reference} {incident.case_reference}
</td> </td>
<td className="py-3 px-4 font-medium text-on-surface max-w-xs truncate"> <td className="py-4 px-5 sm:px-6 font-medium text-sm text-on-surface max-w-xl truncate">
{incident.title} {incident.title}
</td> </td>
<td className="py-3 px-4">{getSeverityBadge(incident.severity)}</td> <td className="py-4 px-5 sm:px-6">{getSeverityBadge(incident.severity)}</td>
<td className="py-3 px-4">{getStatusBadge(incident.status)}</td> <td className="py-4 px-5 sm:px-6">{getStatusBadge(incident.status)}</td>
<td className="py-3 px-4 text-[11px] font-mono text-on-surface-variant"> <td className="py-4 px-5 sm:px-6 text-xs sm:text-sm font-mono text-on-surface-variant">
{formatDate(incident.updated_at)} {formatDate(incident.updated_at)}
</td> </td>
<td className="py-3 px-4 text-[11px] font-mono text-outline"> <td className="py-4 px-5 sm:px-6 text-xs sm:text-sm font-mono text-outline">
{formatDate(incident.created_at)} {formatDate(incident.created_at)}
</td> </td>
</tr> </tr>

View File

@ -1,4 +1,4 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
@ -7,40 +7,40 @@ export function OperationsOperators() {
<div className="space-y-6"> <div className="space-y-6">
{/* Header Row */} {/* Header Row */}
<div className="pb-4 border-b border-outline-variant/30"> <div className="pb-4 border-b border-outline-variant/30">
<span className="text-[11px] font-mono uppercase tracking-widest text-primary-fixed-dim"> <span className="text-[11px] font-mono text-primary-fixed-dim block font-semibold">
ACCESS CONTROL Access Control
</span> </span>
<h1 className="font-headline-xl text-2xl sm:text-3xl font-bold text-on-surface tracking-tight mt-1"> <h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight mt-1">
OPERATORS Operators
</h1> </h1>
<p className="text-xs text-on-surface-variant mt-1"> <p className="text-xs sm:text-sm text-outline mt-1.5 max-w-2xl">
Directory and role assignments for platform administrative operators. Directory and role assignments for platform administrative operators.
</p> </p>
</div> </div>
{/* Clean Placeholder Panel */} {/* 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="p-8 sm:p-12 rounded-2xl bg-surface-container-low border border-outline-variant/30 max-w-4xl mx-auto my-10 text-center space-y-6 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"> <div className="w-16 h-16 rounded-2xl bg-surface-container-high border border-outline-variant/40 flex items-center justify-center text-outline mx-auto shadow-inner">
<span className="material-symbols-outlined text-[24px]">manage_accounts</span> <span className="material-symbols-outlined text-[32px]">manage_accounts</span>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h2 className="text-base font-bold text-on-surface uppercase tracking-wide"> <h2 className="text-lg sm:text-xl font-bold text-on-surface tracking-tight">
OPERATORS Operators
</h2> </h2>
<p className="text-xs text-on-surface-variant max-w-md mx-auto leading-relaxed"> <p className="text-xs sm:text-sm text-on-surface-variant max-w-lg mx-auto leading-relaxed">
Operator management is not enabled in this Operations foundation. Operator management is not enabled in this Operations foundation.
</p> </p>
</div> </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="pt-4 border-t border-outline-variant/20 text-xs text-outline text-left space-y-2.5 bg-surface-container-lowest/50 p-5 sm:p-6 rounded-xl">
<div className="flex items-center gap-1.5 font-mono text-on-surface font-semibold uppercase text-[10px]"> <div className="flex items-center gap-2 font-mono text-on-surface font-semibold text-xs">
<span className="material-symbols-outlined text-[14px] text-primary-fixed-dim"> <span className="material-symbols-outlined text-[16px] text-primary-fixed-dim">
info info
</span> </span>
<span>Security Governance Policy</span> <span>Security Governance Policy</span>
</div> </div>
<p className="leading-normal"> <p className="leading-relaxed">
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. 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> </p>
</div> </div>

View File

@ -1,19 +1,31 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { OperationsHealth, ComponentHealthStatus, HealthComponentKey } from './types'; import {
OperationsHealth,
ComponentHealthStatus,
HealthComponentKey,
OperationsOverviewData,
OperationsNavId,
} from './types';
import { HEALTH_COMPONENT_METADATA } from '../../lib/ops-data'; import { HEALTH_COMPONENT_METADATA } from '../../lib/ops-data';
interface OperationsOverviewProps { interface OperationsOverviewProps {
overview?: OperationsOverviewData | null;
health: OperationsHealth; health: OperationsHealth;
onRefresh: () => Promise<void> | void; onRefresh: () => Promise<void> | void;
isRefreshing: boolean; isRefreshing: boolean;
onSelectIncident?: (caseRef: string) => void;
onNavigateNav?: (nav: OperationsNavId) => void;
} }
export function OperationsOverview({ export function OperationsOverview({
overview,
health, health,
onRefresh, onRefresh,
isRefreshing, isRefreshing,
onSelectIncident,
onNavigateNav,
}: OperationsOverviewProps) { }: OperationsOverviewProps) {
const componentKeys: HealthComponentKey[] = [ const componentKeys: HealthComponentKey[] = [
'operations_api', 'operations_api',
@ -22,38 +34,70 @@ export function OperationsOverview({
'audit_persistence', 'audit_persistence',
]; ];
const effectiveHealth = overview?.health ?? health;
function getStatusBadge(status: ComponentHealthStatus) { function getStatusBadge(status: ComponentHealthStatus) {
switch (status) { switch (status) {
case 'available': case 'available':
return { return {
label: 'AVAILABLE', label: 'Available',
icon: 'check_circle', icon: 'check_circle',
classes: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60', classes: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60',
}; };
case 'degraded': case 'degraded':
return { return {
label: 'DEGRADED', label: 'Degraded',
icon: 'warning', icon: 'warning',
classes: 'bg-amber-950/60 text-amber-400 border-amber-800/60', classes: 'bg-amber-950/60 text-amber-400 border-amber-800/60',
}; };
case 'unavailable': case 'unavailable':
return { return {
label: 'UNAVAILABLE', label: 'Unavailable',
icon: 'cancel', icon: 'cancel',
classes: 'bg-rose-950/60 text-rose-400 border-rose-800/60', classes: 'bg-rose-950/60 text-rose-400 border-rose-800/60',
}; };
case 'unknown': case 'unknown':
default: default:
return { return {
label: 'UNKNOWN', label: 'Unknown',
icon: 'help', icon: 'help',
classes: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/40', classes: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/40',
}; };
} }
} }
const formattedCheckedAt = health.checked_at function getSeverityBadge(severity: string) {
? new Date(health.checked_at).toLocaleString('en-US', { switch (severity) {
case 'critical':
return 'bg-rose-950/60 text-rose-400 border border-rose-800/60';
case 'high':
return 'bg-amber-950/60 text-amber-400 border border-amber-800/60';
case 'medium':
return 'bg-yellow-950/60 text-yellow-400 border border-yellow-800/60';
case 'low':
default:
return 'bg-blue-950/60 text-blue-400 border border-blue-800/60';
}
}
function getStatusBadgeClass(status: string) {
switch (status) {
case 'open':
return 'bg-rose-950/60 text-rose-300 border border-rose-800/60';
case 'investigating':
return 'bg-amber-950/60 text-amber-300 border border-amber-800/60';
case 'monitoring':
return 'bg-cyan-950/60 text-cyan-300 border border-cyan-800/60';
case 'resolved':
case 'closed':
default:
return 'bg-emerald-950/60 text-emerald-300 border border-emerald-800/60';
}
}
const effectiveCheckedAt = effectiveHealth.checked_at;
const formattedCheckedAt = effectiveCheckedAt
? new Date(effectiveCheckedAt).toLocaleString('en-US', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
@ -65,29 +109,29 @@ export function OperationsOverview({
: '--'; : '--';
return ( return (
<div className="space-y-6"> <div className="space-y-8 w-full max-w-none">
{/* Header Row: Title & Manual Refresh */} {/* 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 className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-5 border-b border-outline-variant/30">
<div> <div>
<span className="text-[11px] font-mono uppercase tracking-widest text-primary-fixed-dim"> <span className="text-xs font-mono text-primary-fixed-dim font-semibold">
SYSTEM HEALTH System Health &amp; Operations
</span> </span>
<h1 className="font-headline-xl text-2xl sm:text-3xl font-bold text-on-surface tracking-tight mt-1"> <h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight mt-1.5">
Operations Overview System Health &amp; Operations Overview
</h1> </h1>
<p className="text-xs text-on-surface-variant mt-1"> <p className="text-xs sm:text-sm text-on-surface-variant mt-1.5">
Real-time health telemetry across core Operations subsystems. Unified telemetry, platform activity, and operational security triage.
</p> </p>
</div> </div>
<div className="flex items-center gap-3 shrink-0"> <div className="flex items-center gap-4 shrink-0">
<div className="text-right"> <div className="text-right">
<span className="block text-[10px] uppercase font-mono text-outline"> <span className="block text-[11px] font-mono text-outline font-semibold">
Last checked Last checked
</span> </span>
<time <time
dateTime={health.checked_at} dateTime={effectiveCheckedAt}
className="text-xs font-mono text-on-surface font-medium" className="text-xs sm:text-sm font-mono text-on-surface font-semibold"
> >
{formattedCheckedAt} {formattedCheckedAt}
</time> </time>
@ -98,10 +142,10 @@ export function OperationsOverview({
onClick={() => void onRefresh()} onClick={() => void onRefresh()}
disabled={isRefreshing} disabled={isRefreshing}
aria-label="Refresh Operations health status" 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" className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-surface-container-high hover:bg-surface-bright text-primary-container border border-primary-container/40 text-xs sm:text-sm 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 cursor-pointer"
> >
<span <span
className={`material-symbols-outlined text-[16px] ${ className={`material-symbols-outlined text-[18px] ${
isRefreshing ? 'animate-spin' : '' isRefreshing ? 'animate-spin' : ''
}`} }`}
> >
@ -112,44 +156,283 @@ export function OperationsOverview({
</div> </div>
</div> </div>
{/* 4 Health Status Cards */} {/* 1. KPI Row (4 Cards) */}
<div <div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5"
aria-label="Operations Key Metrics"
>
{/* Card 1: Registered Users */}
<article className="p-5 sm:p-6 rounded-2xl bg-surface-container-low border border-outline-variant/30 flex flex-col justify-between min-h-[140px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm">
<div className="flex items-start justify-between">
<span className="text-xs font-mono text-outline font-semibold">
Registered Users
</span>
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center text-outline">
<span className="material-symbols-outlined text-[20px]">
group
</span>
</div>
</div>
<div>
<div className="text-3xl sm:text-4xl lg:text-5xl font-extrabold font-mono text-on-surface tracking-tight my-1.5">
{overview ? overview.users.total_registered.toLocaleString() : '--'}
</div>
<div className="flex items-center gap-1.5 text-xs text-emerald-400 font-mono font-medium">
<span className="material-symbols-outlined text-[16px]">trending_up</span>
<span>+{overview ? overview.users.new_this_week.toLocaleString() : 0} this week</span>
</div>
</div>
</article>
{/* Card 2: Active Users 24h / 7d */}
<article className="p-5 sm:p-6 rounded-2xl bg-surface-container-low border border-outline-variant/30 flex flex-col justify-between min-h-[140px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm">
<div className="flex items-start justify-between">
<span className="text-xs font-mono text-outline font-semibold">
Active Users (24h)
</span>
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center text-outline">
<span className="material-symbols-outlined text-[20px]">
bolt
</span>
</div>
</div>
<div>
<div className="text-3xl sm:text-4xl lg:text-5xl font-extrabold font-mono text-on-surface tracking-tight my-1.5">
{overview ? overview.users.active_24h.toLocaleString() : '--'}
</div>
<div className="text-xs text-on-surface-variant font-mono">
{overview ? overview.users.active_7d.toLocaleString() : '--'} active in 7 days
</div>
</div>
</article>
{/* Card 3: Premium Members / Conversion Rate */}
<article className="p-5 sm:p-6 rounded-2xl bg-surface-container-low border border-outline-variant/30 flex flex-col justify-between min-h-[140px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm">
<div className="flex items-start justify-between">
<span className="text-xs font-mono text-outline font-semibold">
Premium Members
</span>
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center text-amber-400">
<span className="material-symbols-outlined text-[20px]">
workspace_premium
</span>
</div>
</div>
<div>
<div className="text-3xl sm:text-4xl lg:text-5xl font-extrabold font-mono text-on-surface tracking-tight my-1.5">
{overview ? overview.membership.premium_members.toLocaleString() : '--'}
</div>
<div className="text-xs text-on-surface-variant font-mono">
{overview ? overview.membership.conversion_rate_pct : 0}% conv. ({overview ? overview.membership.free_members.toLocaleString() : 0} free)
</div>
</div>
</article>
{/* Card 4: Open Incidents */}
<article className="p-5 sm:p-6 rounded-2xl bg-surface-container-low border border-outline-variant/30 flex flex-col justify-between min-h-[140px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm">
<div className="flex items-start justify-between">
<span className="text-xs font-mono text-outline font-semibold">
Open Incidents
</span>
<div className={`w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center ${
(overview?.incidents.critical_count ?? 0) > 0
? 'text-rose-400'
: (overview?.incidents.total_open ?? 0) > 0
? 'text-amber-400'
: 'text-emerald-400'
}`}>
<span className="material-symbols-outlined text-[20px]">
{(overview?.incidents.total_open ?? 0) > 0 ? 'warning' : 'check_circle'}
</span>
</div>
</div>
<div>
<div className={`text-3xl sm:text-4xl lg:text-5xl font-extrabold font-mono tracking-tight my-1.5 ${
(overview?.incidents.critical_count ?? 0) > 0
? 'text-rose-400'
: (overview?.incidents.total_open ?? 0) > 0
? 'text-amber-400'
: 'text-on-surface'
}`}>
{overview ? overview.incidents.total_open : 0}
</div>
<div className="text-xs text-on-surface-variant font-mono">
{overview ? overview.incidents.critical_count : 0} critical, {overview ? overview.incidents.high_count : 0} high
</div>
</div>
</article>
</div>
{/* 2. Platform Activity Split */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5" aria-label="Platform Activity">
{/* Website Activity */}
<section className="p-6 sm:p-7 rounded-2xl bg-surface-container-low border border-outline-variant/30 space-y-5 shadow-sm">
<div className="flex items-center justify-between pb-4 border-b border-outline-variant/20">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center text-primary-fixed-dim">
<span className="material-symbols-outlined text-[20px]">
language
</span>
</div>
<div>
<h2 className="text-base font-bold text-on-surface tracking-tight">
Website Platform Activity
</h2>
<p className="text-xs sm:text-sm text-on-surface-variant">
Portal logins, browser sessions, and web registrations
</p>
</div>
</div>
<span className="text-xs font-mono px-2.5 py-1 rounded-md bg-surface-container-highest text-on-surface-variant font-semibold">
Web Ingress
</span>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/20 flex flex-col justify-between">
<span className="block text-xs font-mono text-outline mb-1 font-semibold">
24h Sessions
</span>
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
{overview ? overview.activity.web_active_sessions_24h.toLocaleString() : '--'}
</span>
</div>
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/20 flex flex-col justify-between">
<span className="block text-xs font-mono text-outline mb-1 font-semibold">
24h Logins
</span>
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
{overview ? overview.activity.web_logins_24h.toLocaleString() : '--'}
</span>
</div>
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/20 flex flex-col justify-between">
<span className="block text-xs font-mono text-outline mb-1 font-semibold">
7d Signups
</span>
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
{overview ? overview.activity.web_signups_7d.toLocaleString() : '--'}
</span>
</div>
</div>
</section>
{/* Mobile Telemetry Activity */}
<section className="p-6 sm:p-7 rounded-2xl bg-surface-container-low border border-outline-variant/30 space-y-5 shadow-sm">
<div className="flex items-center justify-between pb-4 border-b border-outline-variant/20">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center text-primary-fixed-dim">
<span className="material-symbols-outlined text-[20px]">
two_wheeler
</span>
</div>
<div>
<h2 className="text-base font-bold text-on-surface tracking-tight">
Mobile Telemetry Activity
</h2>
<p className="text-xs sm:text-sm text-on-surface-variant">
Mobile ride recording, active sessions, and sensor data ingestion
</p>
</div>
</div>
<span className="text-xs font-mono px-2.5 py-1 rounded-md bg-surface-container-highest text-on-surface-variant font-semibold">
Mobile Ingress
</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/20 flex flex-col justify-between">
<span className="block text-xs font-mono text-outline mb-1 font-semibold whitespace-nowrap">
24h Sessions
</span>
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
{overview ? overview.activity.mobile_active_sessions_24h.toLocaleString() : '--'}
</span>
</div>
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/20 flex flex-col justify-between">
<span className="block text-xs font-mono text-outline mb-1 font-semibold whitespace-nowrap">
Rides Today
</span>
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
{overview ? overview.activity.mobile_rides_today.toLocaleString() : '--'}
</span>
</div>
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/20 flex flex-col justify-between">
<span className="block text-xs font-mono text-outline mb-1 font-semibold whitespace-nowrap">
Active Rides
</span>
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-emerald-400">
{overview ? overview.activity.mobile_rides_active.toLocaleString() : '--'}
</span>
</div>
<div className="p-4 sm:p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/20 flex flex-col justify-between">
<span className="block text-xs font-mono text-outline mb-1 font-semibold whitespace-nowrap">
Data Points
</span>
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
{overview ? overview.activity.mobile_datapoints_24h.toLocaleString() : '--'}
</span>
</div>
</div>
</section>
</div>
{/* 3. Subsystem Health Telemetry (4 Cards) */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xs font-semibold text-outline font-mono">
Core Subsystem Health
</h2>
<div
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-mono font-semibold border ${
getStatusBadge(effectiveHealth.status).classes
}`}
role="status"
aria-live="polite"
>
<span className="material-symbols-outlined text-[14px]">
{getStatusBadge(effectiveHealth.status).icon}
</span>
<span>System {getStatusBadge(effectiveHealth.status).label}</span>
</div>
</div>
<div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5"
aria-label="Subsystem Health Telemetry" aria-label="Subsystem Health Telemetry"
> >
{componentKeys.map((key) => { {componentKeys.map((key) => {
const meta = HEALTH_COMPONENT_METADATA[key]; const meta = HEALTH_COMPONENT_METADATA[key];
const comp = health.components?.[key]; const comp = effectiveHealth.components?.[key];
const status = comp?.status ?? 'unknown'; const status = comp?.status ?? 'unknown';
const badge = getStatusBadge(status); const badge = getStatusBadge(status);
return ( return (
<article <article
key={key} 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" className="p-5 sm:p-6 rounded-2xl bg-surface-container-low border border-outline-variant/30 flex flex-col justify-between min-h-[160px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm"
> >
<div className="space-y-1"> <div className="space-y-1.5">
<span className="text-[10px] font-mono uppercase tracking-wider text-outline"> <span className="text-xs font-mono text-outline font-semibold">
Subsystem Subsystem
</span> </span>
<h2 className="text-sm font-bold text-on-surface tracking-tight"> <h3 className="text-base font-bold text-on-surface tracking-tight">
{meta.name} {meta.name}
</h2> </h3>
<p className="text-[11px] text-on-surface-variant leading-relaxed"> <p className="text-xs sm:text-sm text-on-surface-variant leading-relaxed">
{comp?.description || meta.description} {comp?.description || meta.description}
</p> </p>
</div> </div>
<div className="pt-2 border-t border-outline-variant/20 flex items-center justify-between"> <div className="pt-3 border-t border-outline-variant/20 flex items-center justify-between mt-3">
<span className="text-[10px] text-outline font-mono uppercase"> <span className="text-xs text-outline font-mono font-medium">
Status Status
</span> </span>
<div <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}`} className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-mono font-semibold border ${badge.classes}`}
role="status" role="status"
aria-live="polite" aria-live="polite"
> >
<span className="material-symbols-outlined text-[12px]"> <span className="material-symbols-outlined text-[14px]">
{badge.icon} {badge.icon}
</span> </span>
<span>{badge.label}</span> <span>{badge.label}</span>
@ -159,40 +442,253 @@ export function OperationsOverview({
); );
})} })}
</div> </div>
</div>
{/* Operational Policy & Architecture Boundary */} {/* 4. Recent Security Errors / Denials Stream */}
<div className="p-5 rounded-xl bg-surface-container-lowest border border-outline-variant/30 space-y-3"> <section className="p-6 sm:p-7 rounded-2xl bg-surface-container-low border border-outline-variant/30 space-y-5 shadow-sm">
<div className="flex items-center gap-2"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-4 border-b border-outline-variant/20">
<span className="material-symbols-outlined text-primary-container text-[18px]"> <div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center text-rose-400">
<span className="material-symbols-outlined text-[20px]">
security
</span>
</div>
<div>
<h2 className="text-base sm:text-lg font-bold text-on-surface tracking-tight">
Security Error &amp; Denial Stream
</h2>
<p className="text-xs sm:text-sm text-on-surface-variant">
Live monitoring of unauthorized attempts and subsystem operation failures
</p>
</div>
</div>
<div className="flex items-center gap-2.5">
<span className="text-xs font-mono px-3 py-1 rounded-md bg-rose-950/60 text-rose-400 border border-rose-800/60 font-bold">
{overview?.errors.denials_24h ?? 0} Denials (24h)
</span>
<span className="text-xs font-mono px-3 py-1 rounded-md bg-amber-950/60 text-amber-400 border border-amber-800/60 font-bold">
{overview?.errors.errors_24h ?? 0} Errors (24h)
</span>
</div>
</div>
{overview?.errors.recent && overview.errors.recent.length > 0 ? (
<div className="space-y-2.5">
{overview.errors.recent.map((err, idx) => (
<div
key={err.id ?? idx}
className="p-4 rounded-xl bg-surface-container-lowest border border-outline-variant/20 flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs sm:text-sm"
>
<div className="flex items-center gap-3">
<span
className={`px-2.5 py-1 rounded-md text-xs font-mono font-semibold capitalize ${
err.result === 'denied'
? 'bg-rose-950/60 text-rose-400 border border-rose-800/60'
: 'bg-amber-950/60 text-amber-400 border border-amber-800/60'
}`}
>
{err.result}
</span>
<span className="font-mono font-bold text-on-surface">
{err.action}
</span>
<span className="text-on-surface-variant font-mono text-xs">
by {err.actor_identity}
</span>
</div>
<div className="flex items-center gap-3 text-xs font-mono text-outline shrink-0">
{err.target_resource && (
<span className="truncate max-w-[200px]">
res: {err.target_resource}
</span>
)}
{err.reason_code && (
<span className="truncate max-w-[160px] text-on-surface-variant">
({err.reason_code})
</span>
)}
<time dateTime={err.timestamp} className="font-semibold text-on-surface-variant">
{new Date(err.timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
</time>
</div>
</div>
))}
</div>
) : (
<div className="p-6 rounded-xl bg-surface-container-lowest border border-outline-variant/20 text-center text-xs sm:text-sm text-on-surface-variant">
Zero security errors or access denials recorded in recent event window.
</div>
)}
</section>
{/* 5. Open Incidents Triage */}
<section className="p-6 sm:p-7 rounded-2xl bg-surface-container-low border border-outline-variant/30 space-y-5 shadow-sm">
<div className="flex items-center justify-between pb-4 border-b border-outline-variant/20">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center text-amber-400">
<span className="material-symbols-outlined text-[20px]">
crisis_alert
</span>
</div>
<div>
<h2 className="text-base sm:text-lg font-bold text-on-surface tracking-tight">
Open Incidents Triage
</h2>
<p className="text-xs sm:text-sm text-on-surface-variant">
Active incidents requiring operator assessment or active investigation
</p>
</div>
</div>
{onNavigateNav && (
<button
type="button"
onClick={() => onNavigateNav('incidents')}
className="text-xs sm:text-sm font-mono text-primary-fixed-dim hover:text-primary-container flex items-center gap-1.5 transition-colors font-semibold cursor-pointer"
>
<span>View All</span>
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
</button>
)}
</div>
{overview?.incidents.recent_open && overview.incidents.recent_open.length > 0 ? (
<div className="overflow-hidden rounded-xl border border-outline-variant/20">
{/* Desktop Table */}
<div className="hidden md:block overflow-x-auto">
<table className="w-full text-left text-xs sm:text-sm">
<thead className="bg-surface-container text-xs font-mono text-outline border-b border-outline-variant/20">
<tr>
<th scope="col" className="px-5 py-3.5 font-semibold">Case</th>
<th scope="col" className="px-5 py-3.5 font-semibold">Title</th>
<th scope="col" className="px-5 py-3.5 font-semibold">Severity</th>
<th scope="col" className="px-5 py-3.5 font-semibold">Status</th>
<th scope="col" className="px-5 py-3.5 font-semibold text-right">Created</th>
</tr>
</thead>
<tbody className="divide-y divide-outline-variant/10 bg-surface-container-lowest font-mono">
{overview.incidents.recent_open.map((inc) => (
<tr
key={inc.case_reference}
onClick={() => onSelectIncident?.(inc.case_reference)}
className={`hover:bg-surface-bright/40 transition-colors ${
onSelectIncident ? 'cursor-pointer' : ''
}`}
>
<td className="px-5 py-4 font-bold text-primary-fixed-dim whitespace-nowrap">
{inc.case_reference}
</td>
<td className="px-5 py-4 font-sans text-on-surface font-semibold max-w-sm truncate">
{inc.title}
</td>
<td className="px-5 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-semibold capitalize ${getSeverityBadge(inc.severity)}`}>
{inc.severity}
</span>
</td>
<td className="px-5 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-semibold capitalize ${getStatusBadgeClass(inc.status)}`}>
{inc.status}
</span>
</td>
<td className="px-5 py-4 text-right text-outline whitespace-nowrap text-xs">
{new Date(inc.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Stacked Cards */}
<div className="md:hidden divide-y divide-outline-variant/15 bg-surface-container-lowest">
{overview.incidents.recent_open.map((inc) => (
<div
key={inc.case_reference}
onClick={() => onSelectIncident?.(inc.case_reference)}
className={`p-4 space-y-2.5 ${onSelectIncident ? 'cursor-pointer active:bg-surface-bright/40' : ''}`}
>
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-xs font-bold text-primary-fixed-dim">
{inc.case_reference}
</span>
<div className="flex items-center gap-1.5">
<span className={`px-2 py-0.5 rounded text-[10px] font-mono font-semibold capitalize ${getSeverityBadge(inc.severity)}`}>
{inc.severity}
</span>
<span className={`px-2 py-0.5 rounded text-[10px] font-mono font-semibold capitalize ${getStatusBadgeClass(inc.status)}`}>
{inc.status}
</span>
</div>
</div>
<div className="text-sm text-on-surface font-semibold line-clamp-1">
{inc.title}
</div>
<div className="text-xs font-mono text-outline">
Created {new Date(inc.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</div>
</div>
))}
</div>
</div>
) : (
<div className="p-6 rounded-xl bg-surface-container-lowest border border-outline-variant/20 text-center text-xs sm:text-sm text-on-surface-variant">
All operational systems normal. Zero open or active triage incidents.
</div>
)}
</section>
{/* 6. Operational Policy & Architecture Boundary */}
<div className="p-6 sm:p-7 rounded-2xl bg-surface-container-lowest border border-outline-variant/30 space-y-4 shadow-sm">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-surface-container-high border border-outline-variant/30 flex items-center justify-center text-primary-container">
<span className="material-symbols-outlined text-[20px]">
verified_user verified_user
</span> </span>
<h3 className="text-xs font-bold text-on-surface uppercase tracking-wider"> </div>
<h3 className="text-sm sm:text-base font-bold text-on-surface tracking-tight">
Operational Architecture &amp; Data Boundary Operational Architecture &amp; Data Boundary
</h3> </h3>
</div> </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 className="grid grid-cols-1 md:grid-cols-3 gap-6 text-xs sm:text-sm text-on-surface-variant pt-4 border-t border-outline-variant/20">
<div> <div>
<span className="block font-semibold text-on-surface mb-0.5"> <span className="block font-semibold text-on-surface mb-1">
Authority Source Authority Source
</span> </span>
<p className="leading-normal"> <p className="leading-relaxed">
PostgreSQL <code className="text-primary-fixed-dim">ops.operators</code> schema. Consumer roles and Firebase custom claims strictly bypassed. PostgreSQL <code className="text-primary-fixed-dim">ops.operators</code> schema. Consumer roles and Firebase custom claims strictly bypassed.
</p> </p>
</div> </div>
<div> <div>
<span className="block font-semibold text-on-surface mb-0.5"> <span className="block font-semibold text-on-surface mb-1">
Privacy Isolation Privacy Isolation
</span> </span>
<p className="leading-normal"> <p className="leading-relaxed">
Zero cross-rider data access. Personal rides, GPS points, vehicles, and billing accounts are structurally inaccessible. Zero cross-rider data access. Personal rides, GPS points, vehicles, and billing accounts are structurally inaccessible.
</p> </p>
</div> </div>
<div> <div>
<span className="block font-semibold text-on-surface mb-0.5"> <span className="block font-semibold text-on-surface mb-1">
Session Integrity Session Integrity
</span> </span>
<p className="leading-normal"> <p className="leading-relaxed">
Governed by host-prefixed <code className="text-primary-fixed-dim">__Host-motorsignal_ops</code> cookies with strict same-origin isolation. Governed by host-prefixed <code className="text-primary-fixed-dim">__Host-motorsignal_ops</code> cookies with strict same-origin isolation.
</p> </p>
</div> </div>

View File

@ -1,4 +1,4 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { OperatorSession } from './types'; import { OperatorSession } from './types';
@ -30,26 +30,26 @@ export function OperationsSessionView({
const isOwner = session.role === 'owner'; const isOwner = session.role === 'owner';
return ( return (
<div className="space-y-6 max-w-3xl"> <div className="space-y-6 w-full max-w-5xl">
{/* Header Row */} {/* Header Row */}
<div className="pb-4 border-b border-outline-variant/30"> <div className="pb-4 border-b border-outline-variant/30">
<span className="text-[11px] font-mono uppercase tracking-widest text-primary-fixed-dim"> <span className="text-[11px] font-mono text-primary-fixed-dim block font-semibold">
SESSION CREDENTIALS Session Credentials
</span> </span>
<h1 className="font-headline-xl text-2xl sm:text-3xl font-bold text-on-surface tracking-tight mt-1"> <h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight mt-1">
My Session My Session
</h1> </h1>
<p className="text-xs text-on-surface-variant mt-1"> <p className="text-xs sm:text-sm text-outline mt-1.5 max-w-2xl">
Cryptographic authority and active authentication status for this browser session. Cryptographic authority and active authentication status for this browser session.
</p> </p>
</div> </div>
{/* Session Details Card */} {/* 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="p-6 sm:p-8 rounded-2xl 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"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Verified Identity */} {/* Verified Identity */}
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1"> <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"> <span className="text-[10px] font-mono text-outline">
Verified Identity Verified Identity
</span> </span>
<div className="text-sm font-semibold text-on-surface break-all"> <div className="text-sm font-semibold text-on-surface break-all">
@ -62,12 +62,12 @@ export function OperationsSessionView({
{/* Operations Role */} {/* Operations Role */}
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1"> <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"> <span className="text-[10px] font-mono text-outline">
Operations Role Operations Role
</span> </span>
<div> <div>
<span <span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-bold uppercase tracking-wider ${ className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-semibold capitalize ${
isOwner isOwner
? 'bg-primary-container/15 text-primary-container border border-primary-container/40' ? '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' : 'bg-surface-container-high text-on-surface font-semibold border border-outline-variant/40'
@ -83,12 +83,12 @@ export function OperationsSessionView({
{/* Session Status */} {/* Session Status */}
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1"> <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"> <span className="text-[10px] font-mono text-outline">
Session Status Session Status
</span> </span>
<div className="flex items-center gap-1.5 text-xs font-mono font-semibold text-secondary-fixed"> <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="h-2 w-2 rounded-full bg-secondary-fixed"></span>
<span className="uppercase">{session.status}</span> <span className="capitalize">{session.status}</span>
</div> </div>
<span className="text-[10px] text-on-surface-variant font-mono"> <span className="text-[10px] text-on-surface-variant font-mono">
Validated on-demand Validated on-demand
@ -97,10 +97,10 @@ export function OperationsSessionView({
{/* Environment */} {/* Environment */}
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1"> <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"> <span className="text-[10px] font-mono text-outline">
Runtime Environment Runtime Environment
</span> </span>
<div className="text-xs font-mono font-semibold text-on-surface uppercase"> <div className="text-xs font-mono font-semibold text-on-surface capitalize">
{session.environment ?? 'production'} {session.environment ?? 'production'}
</div> </div>
<span className="text-[10px] text-on-surface-variant font-mono"> <span className="text-[10px] text-on-surface-variant font-mono">
@ -110,7 +110,7 @@ export function OperationsSessionView({
{/* Created Timestamp */} {/* Created Timestamp */}
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1"> <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"> <span className="text-[10px] font-mono text-outline">
Session Created Session Created
</span> </span>
<div className="text-xs font-mono text-on-surface"> <div className="text-xs font-mono text-on-surface">
@ -120,7 +120,7 @@ export function OperationsSessionView({
{/* Last Activity */} {/* Last Activity */}
<div className="p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/20 space-y-1"> <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"> <span className="text-[10px] font-mono text-outline">
Last Activity Last Activity
</span> </span>
<div className="text-xs font-mono text-on-surface"> <div className="text-xs font-mono text-on-surface">
@ -130,7 +130,7 @@ export function OperationsSessionView({
{/* Absolute Expiry */} {/* 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"> <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"> <span className="text-[10px] font-mono text-outline">
Absolute Expiry Absolute Expiry
</span> </span>
<div className="text-xs font-mono text-on-surface"> <div className="text-xs font-mono text-on-surface">

View File

@ -6,6 +6,7 @@ import {
OperatorSession, OperatorSession,
OperationsHealth, OperationsHealth,
SafeAuditEvent, SafeAuditEvent,
OperationsOverviewData,
} from './types'; } from './types';
import { OperationsTopBar } from './operations-top-bar'; import { OperationsTopBar } from './operations-top-bar';
import { OperationsSidebar } from './operations-sidebar'; import { OperationsSidebar } from './operations-sidebar';
@ -16,10 +17,14 @@ import { OperationsSessionView } from './operations-session';
import { OperationsIncidentsList } from './operations-incidents-list'; import { OperationsIncidentsList } from './operations-incidents-list';
import { OperationsCreateIncident } from './operations-create-incident'; import { OperationsCreateIncident } from './operations-create-incident';
import { OperationsIncidentDetail } from './operations-incident-detail'; import { OperationsIncidentDetail } from './operations-incident-detail';
import { OperationsUsers } from './operations-users';
import { OperationsSupport } from './operations-support';
import { OperationsCms } from './operations-cms';
interface OperationsShellProps { interface OperationsShellProps {
initialSession: OperatorSession; initialSession: OperatorSession;
initialHealth: OperationsHealth; initialHealth: OperationsHealth;
initialOverview?: OperationsOverviewData | null;
initialAudit?: { initialAudit?: {
events: SafeAuditEvent[]; events: SafeAuditEvent[];
next_cursor?: string | null; next_cursor?: string | null;
@ -32,6 +37,7 @@ interface OperationsShellProps {
export function OperationsShell({ export function OperationsShell({
initialSession, initialSession,
initialHealth, initialHealth,
initialOverview = null,
initialAudit, initialAudit,
initialNav = 'overview', initialNav = 'overview',
initialCaseRef = null, initialCaseRef = null,
@ -46,6 +52,10 @@ export function OperationsShell({
); );
const [selectedCaseRef, setSelectedCaseRef] = useState<string | null>(initialCaseRef); const [selectedCaseRef, setSelectedCaseRef] = useState<string | null>(initialCaseRef);
// Overview state
const [overviewData, setOverviewData] = useState<OperationsOverviewData | null>(initialOverview);
const [isRefreshingOverview, setIsRefreshingOverview] = useState(false);
// Health state // Health state
const [health, setHealth] = useState<OperationsHealth>(initialHealth); const [health, setHealth] = useState<OperationsHealth>(initialHealth);
const [isRefreshingHealth, setIsRefreshingHealth] = useState(false); const [isRefreshingHealth, setIsRefreshingHealth] = useState(false);
@ -97,6 +107,36 @@ export function OperationsShell({
} }
} }
// Manual Overview Refresh (Zero automatic polling)
async function handleRefreshOverview() {
if (isRefreshingOverview) return;
setIsRefreshingOverview(true);
try {
const response = await fetch('/api/ops/overview', {
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
});
if (response.ok) {
const data = (await response.json()) as OperationsOverviewData;
setOverviewData(data);
if (data.health) {
setHealth(data.health);
}
}
} catch {
// Retain current state on failure
} finally {
setIsRefreshingOverview(false);
}
}
// Single initial fetch on mount (Zero interval polling)
React.useEffect(() => {
void handleRefreshOverview();
}, []);
// Next Page of Audit Events (Owner only, server cursor) // Next Page of Audit Events (Owner only, server cursor)
async function handleLoadNextAuditPage() { async function handleLoadNextAuditPage() {
if (!nextCursor || isLoadingMoreAudit) return; if (!nextCursor || isLoadingMoreAudit) return;
@ -144,7 +184,7 @@ export function OperationsShell({
} }
return ( 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"> <div className="ops-shell-root min-h-screen bg-surface font-body text-sm text-on-surface antialiased selection:bg-primary-container selection:text-on-primary-container w-full">
{/* Sidebar (Desktop persistent + Mobile off-canvas drawer) */} {/* Sidebar (Desktop persistent + Mobile off-canvas drawer) */}
<OperationsSidebar <OperationsSidebar
activeNav={activeNav} activeNav={activeNav}
@ -165,15 +205,28 @@ export function OperationsShell({
/> />
{/* Content Body Container */} {/* Content Body Container */}
<main className="relative pt-20 px-4 sm:px-6 py-6 w-full max-w-[1440px] mx-auto flex-1"> <main className="relative pt-20 px-4 sm:px-6 lg:px-8 py-8 w-full flex-1">
{activeNav === 'overview' && ( {activeNav === 'overview' && (
<OperationsOverview <OperationsOverview
health={health} overview={overviewData}
onRefresh={handleRefreshHealth} health={overviewData?.health ?? health}
isRefreshing={isRefreshingHealth} onRefresh={async () => {
await Promise.all([handleRefreshHealth(), handleRefreshOverview()]);
}}
isRefreshing={isRefreshingHealth || isRefreshingOverview}
onSelectIncident={(caseRef) => {
setSelectedCaseRef(caseRef);
setIncidentView('detail');
setActiveNav('incidents');
}}
onNavigateNav={(nav) => setActiveNav(nav)}
/> />
)} )}
{activeNav === 'users' && (
<OperationsUsers role={session.role} />
)}
{activeNav === 'incidents' && ( {activeNav === 'incidents' && (
<> <>
{incidentView === 'list' && ( {incidentView === 'list' && (
@ -210,6 +263,14 @@ export function OperationsShell({
</> </>
)} )}
{activeNav === 'support' && (
<OperationsSupport role={session.role} />
)}
{activeNav === 'cms' && (
<OperationsCms role={session.role} />
)}
{activeNav === 'audit' && ( {activeNav === 'audit' && (
<OperationsAudit <OperationsAudit
role={session.role} role={session.role}

View File

@ -32,23 +32,38 @@ export function OperationsSidebar({
label: 'Overview', label: 'Overview',
icon: 'dashboard', icon: 'dashboard',
}, },
{
id: 'users',
label: 'Users',
icon: 'person_search',
},
{ {
id: 'incidents', id: 'incidents',
label: 'Incidents', label: 'Incidents',
icon: 'assignment', icon: 'assignment',
}, },
{
id: 'support',
label: 'Support Center',
icon: 'contact_support',
},
{
id: 'cms',
label: 'Website Content',
icon: 'edit_note',
},
{ {
id: 'audit', id: 'audit',
label: 'Audit', label: 'Audit',
icon: 'verified_user', icon: 'verified_user',
badge: isOwner ? 'OWNER' : 'RESTRICTED', badge: isOwner ? 'Owner' : 'Restricted',
badgeType: isOwner ? 'primary' : 'neutral', badgeType: isOwner ? 'primary' : 'neutral',
}, },
{ {
id: 'operators', id: 'operators',
label: 'Operators', label: 'Operators',
icon: 'group', icon: 'group',
badge: 'DISABLED', badge: 'Disabled',
badgeType: 'disabled', badgeType: 'disabled',
}, },
{ {
@ -66,10 +81,10 @@ export function OperationsSidebar({
<span className="material-symbols-outlined text-[18px]">terminal</span> <span className="material-symbols-outlined text-[18px]">terminal</span>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-headline-sm text-sm font-bold tracking-tight text-on-surface uppercase"> <span className="font-headline-sm text-sm font-bold tracking-tight text-on-surface">
Operations Operations
</span> </span>
<span className="text-[10px] font-mono text-primary-fixed-dim uppercase tracking-wider"> <span className="text-[10px] font-mono text-primary-fixed-dim">
Platform Console Platform Console
</span> </span>
</div> </div>
@ -109,7 +124,7 @@ export function OperationsSidebar({
{item.badge && ( {item.badge && (
<span <span
className={`px-1.5 py-0.5 rounded text-[9px] font-mono uppercase tracking-wider ${ className={`px-1.5 py-0.5 rounded text-[10px] font-mono ${
item.badgeType === 'primary' item.badgeType === 'primary'
? 'bg-primary-container/15 text-primary-container border border-primary-container/30' ? 'bg-primary-container/15 text-primary-container border border-primary-container/30'
: item.badgeType === 'disabled' : item.badgeType === 'disabled'
@ -127,7 +142,7 @@ export function OperationsSidebar({
{/* Footer Authority Boundary Notice */} {/* 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="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"> <div className="flex items-center gap-1 font-mono text-on-surface-variant">
<span className="material-symbols-outlined text-[12px] text-primary-fixed-dim"> <span className="material-symbols-outlined text-[12px] text-primary-fixed-dim">
shield_lock shield_lock
</span> </span>

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { OperatorSession } from './types'; import { OperatorSession } from './types';
@ -17,10 +17,12 @@ export function OperationsTopBar({
signingOut = false, signingOut = false,
}: OperationsTopBarProps) { }: OperationsTopBarProps) {
const isOwner = session.role === 'owner'; const isOwner = session.role === 'owner';
const environment = (session.environment ?? 'production').toUpperCase(); const environment = session.environment
? session.environment.charAt(0).toUpperCase() + session.environment.slice(1).toLowerCase()
: 'Production';
return ( 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"> <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 lg:px-8">
{/* Left: Mobile Drawer Button + Operations Branding Title */} {/* Left: Mobile Drawer Button + Operations Branding Title */}
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<button <button
@ -34,16 +36,16 @@ export function OperationsTopBar({
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<div className="flex items-center gap-1.5 shrink-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"> <span className="font-headline-sm text-sm sm:text-base font-bold text-on-surface tracking-tight">
MotorSignal MotorSignal
</span> </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"> <span className="px-1.5 py-0.5 rounded text-[10px] font-semibold bg-surface-container-high text-primary-container border border-primary-container/30">
OPS Ops
</span> </span>
</div> </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"> <span className="hidden sm:inline-flex items-center px-2 py-0.5 rounded text-[10px] font-mono bg-surface-container-low text-on-surface-variant border border-outline-variant/30">
ENV: {environment} Env: {environment}
</span> </span>
</div> </div>
</div> </div>
@ -62,7 +64,7 @@ export function OperationsTopBar({
{/* Role Pill */} {/* Role Pill */}
<span <span
className={`px-2 py-0.5 rounded text-[11px] font-bold uppercase tracking-wider ${ className={`px-2 py-0.5 rounded text-[11px] font-semibold capitalize ${
isOwner isOwner
? 'bg-primary-container/15 text-primary-container border border-primary-container/40' ? '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' : 'bg-surface-container-high text-on-surface-variant border border-outline-variant/40'
@ -74,7 +76,7 @@ export function OperationsTopBar({
{/* Session Status Dot/Pill */} {/* 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"> <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="h-1.5 w-1.5 rounded-full bg-secondary-fixed"></span>
<span className="text-[10px] font-mono text-on-surface-variant uppercase"> <span className="text-[10px] font-mono text-on-surface-variant">
Active Active
</span> </span>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -52,7 +52,10 @@ export interface OperatorSession {
export type OperationsNavId = export type OperationsNavId =
| 'overview' | 'overview'
| 'users'
| 'incidents' | 'incidents'
| 'support'
| 'cms'
| 'audit' | 'audit'
| 'operators' | 'operators'
| 'session'; | 'session';
@ -116,3 +119,205 @@ export interface UpdateIncidentInput {
export interface AddIncidentNoteInput { export interface AddIncidentNoteInput {
body: string; body: string;
} }
export interface OverviewUsersMetrics {
total_registered: number;
active_24h: number;
active_7d: number;
new_this_week: number;
}
export interface OverviewMembershipMetrics {
total_subscribers: number;
premium_members: number;
free_members: number;
conversion_rate_pct: number;
}
export interface OverviewPlatformActivity {
web_active_sessions_24h: number;
web_logins_24h: number;
web_signups_7d: number;
mobile_active_sessions_24h: number;
mobile_rides_today: number;
mobile_rides_active: number;
mobile_datapoints_24h: number;
}
export interface OverviewIncidentMetrics {
total_open: number;
critical_count: number;
high_count: number;
medium_count: number;
low_count: number;
recent_open: SafeIncidentSummary[];
}
export interface OverviewSecurityErrorItem {
id?: string;
timestamp: string;
action: string;
actor_identity: string;
reason_code?: string;
result: string;
target_resource?: string;
}
export interface OverviewSecurityErrors {
errors_24h: number;
denials_24h: number;
recent: OverviewSecurityErrorItem[];
}
export interface OperationsOverviewData {
generated_at: string;
users: OverviewUsersMetrics;
membership: OverviewMembershipMetrics;
activity: OverviewPlatformActivity;
incidents: OverviewIncidentMetrics;
errors: OverviewSecurityErrors;
health: OperationsHealth;
}
export interface SafeUserSummary {
id: string;
email: string;
email_verified: boolean;
is_active: boolean;
role: string;
created_at: string;
latest_app_version?: string | null;
last_sync_at?: string | null;
active_sessions_count: number;
total_rides: number;
}
export interface SafeUserSession {
id: string;
device_name?: string | null;
created_at: string;
last_used_at?: string | null;
is_revoked: boolean;
}
export interface SafeUserSubscription {
plan: string;
status: string;
expires_at?: string | null;
}
export interface SafeUserRideSummary {
total_rides: number;
last_ride_at?: string | null;
}
export interface SafeUserRecentAudit {
action: string;
timestamp: string;
ip?: string | null;
}
export interface SafeUserDetail {
id: string;
email: string;
email_verified: boolean;
is_active: boolean;
role: string;
created_at: string;
updated_at: string;
sessions: SafeUserSession[];
subscription?: SafeUserSubscription | null;
ride_summary: SafeUserRideSummary;
recent_audit: SafeUserRecentAudit[];
}
export type SupportTicketType =
| 'bug_report'
| 'contact_request'
| 'telemetry_issue'
| 'general';
export type SupportTicketSeverity = 'low' | 'medium' | 'high' | 'critical';
export type SupportTicketStatus = 'open' | 'in_progress' | 'resolved' | 'closed';
export interface SupportTicketNote {
id: string;
author_identity: string;
body: string;
created_at: string;
}
export interface SupportDiagnosticBundle {
app_version: string;
os_version: string;
device_model: string;
stack_trace?: string | null;
sensor_status: string;
logs_snippet?: string | null;
submitted_at: string;
}
export interface SupportTicketSummary {
id: string;
case_reference: string;
ticket_type: SupportTicketType;
reporter_email: string;
subject: string;
severity: SupportTicketSeverity;
status: SupportTicketStatus;
created_at: string;
updated_at: string;
resolved_at?: string | null;
has_diagnostics: boolean;
notes_count?: number;
}
export interface SupportTicketDetail {
id: string;
case_reference: string;
ticket_type: SupportTicketType;
reporter_email: string;
subject: string;
description: string;
severity: SupportTicketSeverity;
status: SupportTicketStatus;
created_at: string;
updated_at: string;
resolved_at?: string | null;
has_diagnostics: boolean;
notes: SupportTicketNote[];
diagnostic_bundle?: SupportDiagnosticBundle | null;
}
export interface CmsRevisionItem {
revision_id: string;
created_at: string;
created_by: string;
status: 'published' | 'draft';
change_summary: string;
content: Record<string, any>;
}
export interface CmsDocumentSummary {
slug: string;
title: string;
description: string;
has_draft: boolean;
updated_at: string;
updated_by: string;
}
export interface CmsDocumentDetail {
slug: string;
title: string;
description: string;
published: Record<string, any>;
draft: Record<string, any> | null;
has_draft?: boolean;
updated_at: string;
updated_by: string;
revisions: CmsRevisionItem[];
}

View File

@ -3,6 +3,9 @@ import {
SafeAuditEvent, SafeAuditEvent,
ComponentHealthStatus, ComponentHealthStatus,
HealthComponentKey, HealthComponentKey,
OperationsOverviewData,
OverviewSecurityErrorItem,
SafeIncidentSummary,
} from '../components/ops/types'; } from '../components/ops/types';
export const HEALTH_COMPONENT_METADATA: Record< export const HEALTH_COMPONENT_METADATA: Record<
@ -346,3 +349,125 @@ export function sanitizeIncidentNote(raw: unknown): import('../components/ops/ty
body, body,
}; };
} }
export function normalizeOverviewResponse(raw: unknown): OperationsOverviewData {
const fallbackGeneratedAt = new Date().toISOString();
const defaultHealth = normalizeHealthResponse(null);
if (!raw || typeof raw !== 'object') {
return {
generated_at: fallbackGeneratedAt,
users: {
total_registered: 0,
active_24h: 0,
active_7d: 0,
new_this_week: 0,
},
membership: {
total_subscribers: 0,
premium_members: 0,
free_members: 0,
conversion_rate_pct: 0,
},
activity: {
web_active_sessions_24h: 0,
web_logins_24h: 0,
web_signups_7d: 0,
mobile_active_sessions_24h: 0,
mobile_rides_today: 0,
mobile_rides_active: 0,
mobile_datapoints_24h: 0,
},
incidents: {
total_open: 0,
critical_count: 0,
high_count: 0,
medium_count: 0,
low_count: 0,
recent_open: [],
},
errors: {
errors_24h: 0,
denials_24h: 0,
recent: [],
},
health: defaultHealth,
};
}
const obj = raw as Record<string, unknown>;
const rawUsers = obj.users && typeof obj.users === 'object' ? (obj.users as Record<string, unknown>) : {};
const rawMembership = obj.membership && typeof obj.membership === 'object' ? (obj.membership as Record<string, unknown>) : {};
const rawActivity = obj.activity && typeof obj.activity === 'object' ? (obj.activity as Record<string, unknown>) : {};
const rawIncidents = obj.incidents && typeof obj.incidents === 'object' ? (obj.incidents as Record<string, unknown>) : {};
const rawErrors = obj.errors && typeof obj.errors === 'object' ? (obj.errors as Record<string, unknown>) : {};
const rawRecentOpen = Array.isArray(rawIncidents.recent_open) ? rawIncidents.recent_open : [];
const safeRecentOpen: SafeIncidentSummary[] = rawRecentOpen
.map(sanitizeIncidentSummary)
.filter((i): i is SafeIncidentSummary => i !== null);
const rawRecentErrors = Array.isArray(rawErrors.recent) ? rawErrors.recent : [];
const safeRecentErrors: OverviewSecurityErrorItem[] = rawRecentErrors
.map((e): OverviewSecurityErrorItem | null => {
if (!e || typeof e !== 'object') return null;
const errObj = e as Record<string, unknown>;
if (typeof errObj.timestamp !== 'string' || !errObj.timestamp.trim()) return null;
if (typeof errObj.action !== 'string' || !errObj.action.trim()) return null;
if (typeof errObj.actor_identity !== 'string' || !errObj.actor_identity.trim()) return null;
return {
id: typeof errObj.id === 'string' && errObj.id.trim() ? errObj.id.trim() : undefined,
timestamp: errObj.timestamp.trim(),
action: errObj.action.trim(),
actor_identity: errObj.actor_identity.trim(),
reason_code: typeof errObj.reason_code === 'string' && errObj.reason_code.trim() ? errObj.reason_code.trim() : undefined,
result: typeof errObj.result === 'string' && errObj.result.trim() ? errObj.result.trim() : 'failure',
target_resource: typeof errObj.target_resource === 'string' && errObj.target_resource.trim() ? errObj.target_resource.trim() : undefined,
};
})
.filter((e): e is OverviewSecurityErrorItem => e !== null);
return {
generated_at: typeof obj.generated_at === 'string' && obj.generated_at.trim() ? obj.generated_at.trim() : fallbackGeneratedAt,
users: {
total_registered: typeof rawUsers.total_registered === 'number' ? rawUsers.total_registered : 0,
active_24h: typeof rawUsers.active_24h === 'number' ? rawUsers.active_24h : 0,
active_7d: typeof rawUsers.active_7d === 'number' ? rawUsers.active_7d : 0,
new_this_week: typeof rawUsers.new_this_week === 'number' ? rawUsers.new_this_week : 0,
},
membership: {
total_subscribers: typeof rawMembership.total_subscribers === 'number' ? rawMembership.total_subscribers : 0,
premium_members: typeof rawMembership.premium_members === 'number'
? rawMembership.premium_members
: typeof rawMembership.pro_members === 'number'
? rawMembership.pro_members
: 0,
free_members: typeof rawMembership.free_members === 'number' ? rawMembership.free_members : 0,
conversion_rate_pct: typeof rawMembership.conversion_rate_pct === 'number' ? rawMembership.conversion_rate_pct : 0,
},
activity: {
web_active_sessions_24h: typeof rawActivity.web_active_sessions_24h === 'number' ? rawActivity.web_active_sessions_24h : 0,
web_logins_24h: typeof rawActivity.web_logins_24h === 'number' ? rawActivity.web_logins_24h : 0,
web_signups_7d: typeof rawActivity.web_signups_7d === 'number' ? rawActivity.web_signups_7d : 0,
mobile_active_sessions_24h: typeof rawActivity.mobile_active_sessions_24h === 'number' ? rawActivity.mobile_active_sessions_24h : 0,
mobile_rides_today: typeof rawActivity.mobile_rides_today === 'number' ? rawActivity.mobile_rides_today : 0,
mobile_rides_active: typeof rawActivity.mobile_rides_active === 'number' ? rawActivity.mobile_rides_active : 0,
mobile_datapoints_24h: typeof rawActivity.mobile_datapoints_24h === 'number' ? rawActivity.mobile_datapoints_24h : 0,
},
incidents: {
total_open: typeof rawIncidents.total_open === 'number' ? rawIncidents.total_open : safeRecentOpen.length,
critical_count: typeof rawIncidents.critical_count === 'number' ? rawIncidents.critical_count : 0,
high_count: typeof rawIncidents.high_count === 'number' ? rawIncidents.high_count : 0,
medium_count: typeof rawIncidents.medium_count === 'number' ? rawIncidents.medium_count : 0,
low_count: typeof rawIncidents.low_count === 'number' ? rawIncidents.low_count : 0,
recent_open: safeRecentOpen,
},
errors: {
errors_24h: typeof rawErrors.errors_24h === 'number' ? rawErrors.errors_24h : 0,
denials_24h: typeof rawErrors.denials_24h === 'number' ? rawErrors.denials_24h : 0,
recent: safeRecentErrors,
},
health: normalizeHealthResponse(obj.health),
};
}

View File

@ -44,6 +44,7 @@ console.log('--- STARTING OPS-3C, OPS-3C.1, OPS-3C.2, OPS-3C.3 & OPS-4 OPERATION
const opsData = loadTsModule('../lib/ops-data.ts'); const opsData = loadTsModule('../lib/ops-data.ts');
const { const {
normalizeHealthResponse, normalizeHealthResponse,
normalizeOverviewResponse,
sanitizeAuditEvent, sanitizeAuditEvent,
isValidHealthStatus, isValidHealthStatus,
HEALTH_COMPONENT_METADATA, HEALTH_COMPONENT_METADATA,
@ -837,4 +838,311 @@ assert.ok(getNotesMatch && !getNotesMatch[0].includes('hasValidOrigin'), 'GET no
console.log(' PASS: Read-only GET operations are unaffected while mutations require valid origin.'); console.log(' PASS: Read-only GET operations are unaffected while mutations require valid origin.');
console.log('--- ALL OPS-3C, OPS-3C.1, OPS-3C.2, OPS-3C.3, OPS-4 & OPS-4.2 TESTS PASSED (38/38) ---'); // ============================================================
// 39. OPERATIONS OVERVIEW DATA NORMALIZATION & PROXY ROUTE (SECTION 1)
// ============================================================
console.log('Testing 39: Operations Overview Normalization & Command Dashboard Contract...');
const overviewRoutePath = path.resolve(__dirname, '../app/api/ops/overview/route.ts');
assert.ok(fs.existsSync(overviewRoutePath), 'GET /api/ops/overview proxy route must exist');
const overviewRouteSrc = fs.readFileSync(overviewRoutePath, 'utf8');
assert.ok(overviewRouteSrc.includes('__Host-motorsignal_ops'), 'Overview route must check __Host-motorsignal_ops');
assert.ok(overviewRouteSrc.includes('/api/v1/ops/overview'), 'Overview route must query /api/v1/ops/overview');
assert.ok(overviewRouteSrc.includes('normalizeOverviewResponse'), 'Overview route must normalize response');
// Test normalizeOverviewResponse with null
const emptyOverview = normalizeOverviewResponse(null);
assert.equal(typeof emptyOverview.generated_at, 'string');
assert.equal(emptyOverview.users.total_registered, 0);
assert.equal(emptyOverview.membership.conversion_rate_pct, 0);
assert.equal(emptyOverview.incidents.total_open, 0);
assert.equal(emptyOverview.health.status, 'unavailable');
// Test normalizeOverviewResponse with sample backend payload
const sampleBackendOverview = {
generated_at: '2026-09-15T12:00:00Z',
users: {
total_registered: 1250,
active_24h: 320,
active_7d: 840,
new_this_week: 45,
},
membership: {
total_subscribers: 210,
premium_members: 180,
pro_members: 180,
free_members: 1070,
conversion_rate_pct: 14.4,
},
activity: {
web_active_sessions_24h: 150,
web_logins_24h: 95,
web_signups_7d: 45,
mobile_active_sessions_24h: 210,
mobile_rides_today: 88,
mobile_rides_active: 7,
mobile_datapoints_24h: 1420500,
},
incidents: {
total_open: 1,
critical_count: 0,
high_count: 1,
medium_count: 0,
low_count: 0,
recent_open: [
{
id: 'db-uuid-incident-should-be-stripped',
case_reference: 'OPS-2026-000088',
title: 'API rate limit threshold alert',
severity: 'high',
status: 'investigating',
created_at: '2026-09-15T10:00:00Z',
updated_at: '2026-09-15T10:15:00Z',
},
],
},
errors: {
errors_24h: 3,
denials_24h: 2,
recent: [
{
id: 'sec-evt-1',
timestamp: '2026-09-15T11:45:00Z',
action: 'operator.login',
actor_identity: 'attacker@unknown.io',
reason_code: 'invalid_credential',
result: 'denied',
target_resource: 'ops.session',
},
],
},
health: {
status: 'available',
checked_at: '2026-09-15T12:00:00Z',
components: {
operations_api: { status: 'available' },
operations_database: { status: 'available' },
session_validation: { status: 'available' },
audit_persistence: { status: 'available' },
},
},
};
const normalizedOverview = normalizeOverviewResponse(sampleBackendOverview);
assert.equal(normalizedOverview.users.total_registered, 1250);
assert.equal(normalizedOverview.users.active_24h, 320);
assert.equal(normalizedOverview.membership.premium_members, 180);
assert.equal(normalizedOverview.activity.mobile_rides_today, 88);
assert.equal(normalizedOverview.activity.mobile_datapoints_24h, 1420500);
assert.equal(normalizedOverview.incidents.total_open, 1);
assert.equal(normalizedOverview.incidents.recent_open.length, 1);
assert.equal(normalizedOverview.incidents.recent_open[0].case_reference, 'OPS-2026-000088');
assert.equal(normalizedOverview.incidents.recent_open[0].id, undefined, 'Database UUID must be stripped from incidents');
assert.equal(normalizedOverview.errors.denials_24h, 2);
assert.equal(normalizedOverview.errors.recent.length, 1);
assert.equal(normalizedOverview.health.status, 'available');
// Verify dashboard component structure
assert.ok(overviewSource.includes('Registered Users'), 'Overview must render Registered Users KPI');
assert.ok(overviewSource.includes('Active Users'), 'Overview must render Active Users KPI');
assert.ok(overviewSource.includes('Premium Members'), 'Overview must render Premium Members KPI');
assert.ok(overviewSource.includes('Open Incidents'), 'Overview must render Open Incidents KPI');
assert.ok(overviewSource.includes('Website Platform Activity'), 'Overview must render Website Platform Activity');
assert.ok(overviewSource.includes('Mobile Telemetry Activity'), 'Overview must render Mobile Telemetry Activity');
assert.ok(overviewSource.includes('Security Error & Denial Stream') || overviewSource.includes('Security Error'), 'Overview must render Security Error Stream');
assert.ok(overviewSource.includes('Open Incidents Triage'), 'Overview must render Open Incidents Triage');
assert.ok(overviewSource.includes('Operational Architecture & Data Boundary') || overviewSource.includes('Operational Architecture'), 'Overview must render Architecture Boundary');
// Shell integration check
assert.ok(shellSource.includes('/api/ops/overview'), 'OperationsShell must fetch /api/ops/overview');
assert.ok(shellSource.includes('overviewData') || shellSource.includes('overview='), 'OperationsShell must pass overview data to OperationsOverview');
console.log(' PASS: Operations overview DTO normalization, proxy route, and dashboard elements verified.');
// ============================================================
// 40. SECTION 2: USER MANAGEMENT ARCHITECTURE & SECURITY CONTRACT
// ============================================================
console.log('Testing 40: Section 2 User Management (Backend, Proxy Routes & UI)...');
// 40.1 Proxy routes verification
const usersRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/users/route.ts'), 'utf8');
const userDetailRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/users/[userId]/route.ts'), 'utf8');
const userStatusRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/users/[userId]/status/route.ts'), 'utf8');
const userRevokeRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/users/[userId]/revoke-sessions/route.ts'), 'utf8');
const userResetRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/users/[userId]/reset-password/route.ts'), 'utf8');
assert.ok(usersRouteSrc.includes('__Host-motorsignal_ops'), 'users/route.ts must check __Host-motorsignal_ops');
assert.ok(userDetailRouteSrc.includes('__Host-motorsignal_ops'), 'users/[userId]/route.ts must check __Host-motorsignal_ops');
assert.ok(userStatusRouteSrc.includes('__Host-motorsignal_ops'), 'users/[userId]/status/route.ts must check __Host-motorsignal_ops');
assert.ok(userRevokeRouteSrc.includes('__Host-motorsignal_ops'), 'users/[userId]/revoke-sessions/route.ts must check __Host-motorsignal_ops');
assert.ok(userResetRouteSrc.includes('__Host-motorsignal_ops'), 'users/[userId]/reset-password/route.ts must check __Host-motorsignal_ops');
// CSRF Origin enforcement on mutations
assert.ok(userStatusRouteSrc.includes('hasValidOrigin(request)'), 'User status route must enforce CSRF origin check');
assert.ok(userRevokeRouteSrc.includes('hasValidOrigin(request)'), 'User revoke sessions route must enforce CSRF origin check');
assert.ok(userResetRouteSrc.includes('hasValidOrigin(request)'), 'User reset password route must enforce CSRF origin check');
// 40.2 Navigation order & shell wiring
assert.ok(sidebarSource.includes("id: 'users'") && sidebarSource.includes("label: 'Users'") && sidebarSource.includes("icon: 'person_search'"), 'Sidebar must contain users nav item');
const usersNavIdx = sidebarSource.indexOf("id: 'users'");
assert.ok(overviewIdx < usersNavIdx && usersNavIdx < incidentsIdx, 'Users nav item must be placed right after overview and before incidents');
assert.ok(shellSource.includes("activeNav === 'users'") && shellSource.includes('<OperationsUsers'), 'OperationsShell must wire activeNav === users to OperationsUsers');
// 40.3 OperationsUsers component UI contract
const usersComponentSrc = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-users.tsx'), 'utf8');
assert.ok(usersComponentSrc.includes('hidden md:block'), 'Users list must render desktop table view');
assert.ok(usersComponentSrc.includes('md:hidden'), 'Users list must render mobile stacked cards');
assert.ok(usersComponentSrc.includes('Search user by email or account ID'), 'Users list must provide search bar');
assert.ok(usersComponentSrc.includes('statusFilter') && usersComponentSrc.includes('verifiedFilter'), 'Users list must provide status and verification filters');
assert.ok(usersComponentSrc.includes('suspend') && usersComponentSrc.includes('reinstate'), 'Users detail must support suspend and reinstate actions');
assert.ok(usersComponentSrc.includes('revoke-sessions'), 'Users detail must support session revocation');
assert.ok(usersComponentSrc.includes('reset-password'), 'Users detail must support password reset');
assert.ok(usersComponentSrc.includes('length < 3') || usersComponentSrc.includes('min 3 characters'), 'Action confirmation must require reason with minimum 3 chars');
assert.ok(!usersComponentSrc.includes('setInterval'), 'Users component must have zero setInterval');
assert.ok(!usersComponentSrc.includes('setTimeout'), 'Users component must have zero setTimeout');
// 40.4 Backend main.py contract check
const backendMainSrc = fs.readFileSync(path.resolve(__dirname, '../../backend/motorsignal_api/app/main.py'), 'utf8');
assert.ok(backendMainSrc.includes('class OpsUserStatusUpdate'), 'Backend must define OpsUserStatusUpdate');
assert.ok(backendMainSrc.includes('class OpsUserActionRequest'), 'Backend must define OpsUserActionRequest');
assert.ok(backendMainSrc.includes('@app.get("/api/v1/ops/users")'), 'Backend must implement GET /api/v1/ops/users');
assert.ok(backendMainSrc.includes('@app.get("/api/v1/ops/users/{user_id}")'), 'Backend must implement GET /api/v1/ops/users/{user_id}');
assert.ok(backendMainSrc.includes('@app.post("/api/v1/ops/users/{user_id}/status")'), 'Backend must implement POST /api/v1/ops/users/{user_id}/status');
assert.ok(backendMainSrc.includes('@app.post("/api/v1/ops/users/{user_id}/revoke-sessions")'), 'Backend must implement POST /api/v1/ops/users/{user_id}/revoke-sessions');
assert.ok(backendMainSrc.includes('@app.post("/api/v1/ops/users/{user_id}/reset-password")'), 'Backend must implement POST /api/v1/ops/users/{user_id}/reset-password');
assert.ok(backendMainSrc.includes('user.suspend') && backendMainSrc.includes('user.reinstate'), 'Backend must audit user.suspend and user.reinstate');
assert.ok(backendMainSrc.includes('user.sessions_revoked'), 'Backend must audit user.sessions_revoked');
assert.ok(backendMainSrc.includes('user.password_reset_initiated'), 'Backend must audit user.password_reset_initiated');
console.log(' PASS: Section 2 User Management backend, proxy routes, navigation, and UI contracts fully verified.');
// ============================================================
// 41. SECTION 3: SUPPORT CENTER & DIAGNOSTICS CONTRACT
// ============================================================
console.log('Testing 41: Section 3 Support Center & Diagnostics (Backend, Proxy Routes & UI)...');
// 41.1 Proxy routes verification
const supportTicketsRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/support/tickets/route.ts'), 'utf8');
const supportDetailRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/support/tickets/[ticketId]/route.ts'), 'utf8');
const supportNotesRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/support/tickets/[ticketId]/notes/route.ts'), 'utf8');
const supportDiagnosticsRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/support/tickets/[ticketId]/diagnostics/route.ts'), 'utf8');
assert.ok(supportTicketsRouteSrc.includes('__Host-motorsignal_ops'), 'support/tickets/route.ts must check __Host-motorsignal_ops');
assert.ok(supportDetailRouteSrc.includes('__Host-motorsignal_ops'), 'support/tickets/[ticketId]/route.ts must check __Host-motorsignal_ops');
assert.ok(supportNotesRouteSrc.includes('__Host-motorsignal_ops'), 'support/tickets/[ticketId]/notes/route.ts must check __Host-motorsignal_ops');
assert.ok(supportDiagnosticsRouteSrc.includes('__Host-motorsignal_ops'), 'support/tickets/[ticketId]/diagnostics/route.ts must check __Host-motorsignal_ops');
// CSRF Origin enforcement on mutations
assert.ok(supportTicketsRouteSrc.includes('hasValidOrigin(request)'), 'Support tickets create route must enforce CSRF origin check');
assert.ok(supportDetailRouteSrc.includes('hasValidOrigin(request)'), 'Support ticket patch route must enforce CSRF origin check');
assert.ok(supportNotesRouteSrc.includes('hasValidOrigin(request)'), 'Support notes route must enforce CSRF origin check');
// Diagnostics reason requirement
assert.ok(supportDiagnosticsRouteSrc.includes('reason.length < 3'), 'Support diagnostics route must require reason with min 3 chars');
// 41.2 Navigation order & shell wiring
assert.ok(sidebarSource.includes("id: 'support'") && sidebarSource.includes("label: 'Support Center'") && sidebarSource.includes("icon: 'contact_support'"), 'Sidebar must contain support nav item');
const supportNavIdx = sidebarSource.indexOf("id: 'support'");
assert.ok(incidentsIdx < supportNavIdx && supportNavIdx < auditIdx, 'Support nav item must be placed right after incidents and before audit');
assert.ok(shellSource.includes("activeNav === 'support'") && shellSource.includes('<OperationsSupport'), 'OperationsShell must wire activeNav === support to OperationsSupport');
// 41.3 OperationsSupport component UI contract
const supportComponentSrc = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-support.tsx'), 'utf8');
assert.ok(supportComponentSrc.includes('hidden md:block'), 'Support list must render desktop table view');
assert.ok(supportComponentSrc.includes('md:hidden'), 'Support list must render mobile stacked cards');
assert.ok(supportComponentSrc.includes('Search tickets by case ref'), 'Support list must provide search bar');
assert.ok(supportComponentSrc.includes('bug_reports') && supportComponentSrc.includes('contact_requests'), 'Support list must provide type filter pills');
assert.ok(supportComponentSrc.includes('in_progress') && supportComponentSrc.includes('resolved'), 'Support list must provide status filter pills');
assert.ok(supportComponentSrc.includes('Mark In Progress'), 'Support detail must support Mark In Progress transition action');
assert.ok(supportComponentSrc.includes('Resolve'), 'Support detail must support Resolve transition action');
assert.ok(supportComponentSrc.includes('Close'), 'Support detail must support Close transition action');
assert.ok(supportComponentSrc.includes('Add Note') || supportComponentSrc.includes('Add Internal Note'), 'Support detail must support adding internal notes');
assert.ok(supportComponentSrc.includes('Inspect Submitted Diagnostics') || supportComponentSrc.includes('Inspect Diagnostics'), 'Support detail must provide Inspect Submitted Diagnostics button');
assert.ok(supportComponentSrc.includes('length < 3') || supportComponentSrc.includes('min 3 characters'), 'Diagnostics confirmation must require reason with minimum 3 chars');
assert.ok(!supportComponentSrc.includes('setInterval'), 'Support component must have zero setInterval');
assert.ok(!supportComponentSrc.includes('setTimeout'), 'Support component must have zero setTimeout');
// 41.4 Backend main.py contract check
assert.ok(backendMainSrc.includes('class OpsSupportTicketCreate'), 'Backend must define OpsSupportTicketCreate');
assert.ok(backendMainSrc.includes('class OpsSupportTicketPatch'), 'Backend must define OpsSupportTicketPatch');
assert.ok(backendMainSrc.includes('class OpsSupportTicketNoteCreate'), 'Backend must define OpsSupportTicketNoteCreate');
assert.ok(backendMainSrc.includes('class OpsSupportDiagnosticBundle'), 'Backend must define OpsSupportDiagnosticBundle');
assert.ok(backendMainSrc.includes('@app.get("/api/v1/ops/support/tickets")'), 'Backend must implement GET /api/v1/ops/support/tickets');
assert.ok(backendMainSrc.includes('@app.get("/api/v1/ops/support/tickets/{ticket_id}")'), 'Backend must implement GET /api/v1/ops/support/tickets/{ticket_id}');
assert.ok(backendMainSrc.includes('@app.post("/api/v1/ops/support/tickets'), 'Backend must implement POST /api/v1/ops/support/tickets');
assert.ok(backendMainSrc.includes('@app.post("/api/v1/ops/support/tickets/{ticket_id}/notes'), 'Backend must implement POST /api/v1/ops/support/tickets/{ticket_id}/notes');
assert.ok(backendMainSrc.includes('@app.patch("/api/v1/ops/support/tickets/{ticket_id}")'), 'Backend must implement PATCH /api/v1/ops/support/tickets/{ticket_id}');
assert.ok(backendMainSrc.includes('@app.get("/api/v1/ops/support/tickets/{ticket_id}/diagnostics")'), 'Backend must implement GET /api/v1/ops/support/tickets/{ticket_id}/diagnostics');
assert.ok(backendMainSrc.includes('support.note_added'), 'Backend must audit support.note_added');
assert.ok(backendMainSrc.includes('support.ticket_updated'), 'Backend must audit support.ticket_updated');
assert.ok(backendMainSrc.includes('support.diagnostics_viewed'), 'Backend must audit support.diagnostics_viewed');
assert.ok(backendMainSrc.includes('SUP-'), 'Backend must generate SUP-YYYY-NNNNNN case references');
console.log(' PASS: Section 3 Support Center backend, proxy routes, navigation, and UI contracts fully verified.');
// ============================================================
// 42. SECTION 4: WEBSITE CONTENT CMS CONTRACT (Backend, Proxy Routes & UI)
// ============================================================
console.log('Testing 42: Section 4 Website Content CMS (Backend, Proxy Routes & UI)...');
// 42.1 Proxy routes verification
const cmsDocumentsRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/cms/documents/route.ts'), 'utf8');
const cmsDetailRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/cms/documents/[slug]/route.ts'), 'utf8');
const cmsRollbackRouteSrc = fs.readFileSync(path.resolve(__dirname, '../app/api/ops/cms/documents/[slug]/rollback/route.ts'), 'utf8');
assert.ok(cmsDocumentsRouteSrc.includes('__Host-motorsignal_ops'), 'cms/documents/route.ts must check __Host-motorsignal_ops');
assert.ok(cmsDetailRouteSrc.includes('__Host-motorsignal_ops'), 'cms/documents/[slug]/route.ts must check __Host-motorsignal_ops');
assert.ok(cmsRollbackRouteSrc.includes('__Host-motorsignal_ops'), 'cms/documents/[slug]/rollback/route.ts must check __Host-motorsignal_ops');
// CSRF Origin enforcement on mutations
assert.ok(cmsDetailRouteSrc.includes('hasValidOrigin(request)'), 'CMS detail PUT route must enforce CSRF origin check');
assert.ok(cmsRollbackRouteSrc.includes('hasValidOrigin(request)'), 'CMS rollback route must enforce CSRF origin check');
// 42.2 Types & Navigation order & shell wiring
const typesSrc = fs.readFileSync(path.resolve(__dirname, '../components/ops/types.ts'), 'utf8');
assert.ok(typesSrc.includes("'cms'"), "types.ts must include 'cms' in OperationsNavId");
assert.ok(typesSrc.includes('CmsDocumentSummary'), 'types.ts must define CmsDocumentSummary');
assert.ok(typesSrc.includes('CmsDocumentDetail'), 'types.ts must define CmsDocumentDetail');
assert.ok(typesSrc.includes('CmsRevisionItem'), 'types.ts must define CmsRevisionItem');
assert.ok(sidebarSource.includes("id: 'cms'") && sidebarSource.includes("label: 'Website Content'") && sidebarSource.includes("icon: 'edit_note'"), 'Sidebar must contain cms nav item');
const cmsNavIdx = sidebarSource.indexOf("id: 'cms'");
assert.ok(supportNavIdx < cmsNavIdx && cmsNavIdx < auditIdx, 'CMS nav item must be placed right after support and before audit');
assert.ok(shellSource.includes("activeNav === 'cms'") && shellSource.includes('<OperationsCms'), 'OperationsShell must wire activeNav === cms to OperationsCms');
// 42.3 OperationsCms component UI contract
const cmsComponentSrc = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-cms.tsx'), 'utf8');
assert.ok(cmsComponentSrc.includes('homepage_hero'), 'CMS component must support homepage_hero section');
assert.ok(cmsComponentSrc.includes('features'), 'CMS component must support features section');
assert.ok(cmsComponentSrc.includes('screenshots'), 'CMS component must support screenshots section');
assert.ok(cmsComponentSrc.includes('faqs'), 'CMS component must support faqs section');
assert.ok(cmsComponentSrc.includes('download_links'), 'CMS component must support download_links section');
assert.ok(cmsComponentSrc.includes('changelog'), 'CMS component must support changelog section');
assert.ok(cmsComponentSrc.includes('policy_pages'), 'CMS component must support policy_pages section');
assert.ok(cmsComponentSrc.includes('Published') && cmsComponentSrc.includes('Draft Pending'), 'CMS component must display Published vs Draft Pending status');
assert.ok(cmsComponentSrc.includes('Preview Changes'), 'CMS component must provide Preview Changes action');
assert.ok(cmsComponentSrc.includes('Save Draft'), 'CMS component must provide Save Draft action');
assert.ok(cmsComponentSrc.includes('Publish Live'), 'CMS component must provide Publish Live action');
assert.ok(cmsComponentSrc.includes('Revision History'), 'CMS component must provide Revision History action');
assert.ok(cmsComponentSrc.includes('Restore Revision'), 'CMS component must provide Restore Revision action');
assert.ok(cmsComponentSrc.includes('min 3 characters') || cmsComponentSrc.includes('length < 3'), 'Rollback confirmation must require reason with minimum 3 chars');
assert.ok(!cmsComponentSrc.includes('setInterval'), 'CMS component must have zero setInterval');
assert.ok(!cmsComponentSrc.includes('setTimeout'), 'CMS component must have zero setTimeout');
// 42.4 Backend main.py contract check
assert.ok(backendMainSrc.includes('class OpsCmsUpdateRequest'), 'Backend must define OpsCmsUpdateRequest');
assert.ok(backendMainSrc.includes('class OpsCmsRollbackRequest'), 'Backend must define OpsCmsRollbackRequest');
assert.ok(backendMainSrc.includes('@app.get("/api/v1/ops/cms/documents")'), 'Backend must implement GET /api/v1/ops/cms/documents');
assert.ok(backendMainSrc.includes('@app.get("/api/v1/ops/cms/documents/{slug}")'), 'Backend must implement GET /api/v1/ops/cms/documents/{slug}');
assert.ok(backendMainSrc.includes('@app.put("/api/v1/ops/cms/documents/{slug}")'), 'Backend must implement PUT /api/v1/ops/cms/documents/{slug}');
assert.ok(backendMainSrc.includes('@app.post("/api/v1/ops/cms/documents/{slug}/rollback")'), 'Backend must implement POST /api/v1/ops/cms/documents/{slug}/rollback');
assert.ok(backendMainSrc.includes('cms.draft_saved'), 'Backend must audit cms.draft_saved');
assert.ok(backendMainSrc.includes('cms.published'), 'Backend must audit cms.published');
assert.ok(backendMainSrc.includes('cms.revision_restored'), 'Backend must audit cms.revision_restored');
assert.ok(backendMainSrc.includes('homepage_hero') && backendMainSrc.includes('features') && backendMainSrc.includes('screenshots') && backendMainSrc.includes('faqs') && backendMainSrc.includes('download_links') && backendMainSrc.includes('changelog') && backendMainSrc.includes('policy_pages'), 'Backend must include all 7 CMS default documents');
console.log(' PASS: Section 4 Website Content CMS backend, proxy routes, navigation, and UI contracts fully verified.');
console.log('--- ALL OPS-3C, OPS-3C.1, OPS-3C.2, OPS-3C.3, OPS-4, OPS-4.2, OVERVIEW, SECTION 2 USERS, SECTION 3 SUPPORT CENTER & SECTION 4 CMS TESTS PASSED (42/42) ---');