1627 lines
78 KiB
TypeScript
1627 lines
78 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useMemo } from 'react';
|
|
import {
|
|
OperationsHealth,
|
|
ComponentHealthStatus,
|
|
HealthComponentKey,
|
|
OperationsOverviewData,
|
|
OperationsNavId,
|
|
OperationsRole,
|
|
SafeIncidentSummary,
|
|
OverviewSecurityErrorItem,
|
|
} from './types';
|
|
import { HEALTH_COMPONENT_METADATA } from '../../lib/ops-data';
|
|
|
|
interface OperationsOverviewProps {
|
|
overview?: OperationsOverviewData | null;
|
|
health: OperationsHealth;
|
|
onRefresh: () => Promise<void> | void;
|
|
isRefreshing: boolean;
|
|
onSelectIncident?: (caseRef: string) => void;
|
|
onNavigateNav?: (nav: OperationsNavId) => void;
|
|
role?: OperationsRole;
|
|
operatorIdentity?: string;
|
|
}
|
|
|
|
function timeAgo(dateString?: string | null): string {
|
|
if (!dateString) return '--';
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
if (diffMs < 0) return 'just now';
|
|
const diffSecs = Math.floor(diffMs / 1000);
|
|
const diffMins = Math.floor(diffSecs / 60);
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffSecs < 60) return 'just now';
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
if (diffDays === 1) return 'yesterday';
|
|
if (diffDays < 30) return `${diffDays}d ago`;
|
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
function formatIncidentAge(createdAt: string, updatedAt?: string): string {
|
|
const createdDate = new Date(createdAt);
|
|
const updatedDate = updatedAt ? new Date(updatedAt) : createdDate;
|
|
const now = new Date();
|
|
|
|
const createdDiffDays = Math.floor((now.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
const createdAgeStr =
|
|
createdDiffDays <= 0 ? 'today' : createdDiffDays === 1 ? '1 day' : `${createdDiffDays} days`;
|
|
|
|
const updatedDiffMs = now.getTime() - updatedDate.getTime();
|
|
const updatedDiffMins = Math.floor(updatedDiffMs / (1000 * 60));
|
|
const updatedDiffHours = Math.floor(updatedDiffMins / 60);
|
|
const updatedDiffDays = Math.floor(updatedDiffHours / 24);
|
|
|
|
let updatedStr = 'just now';
|
|
if (updatedDiffMins >= 1 && updatedDiffMins < 60) {
|
|
updatedStr = `${updatedDiffMins}m ago`;
|
|
} else if (updatedDiffHours >= 1 && updatedDiffHours < 24) {
|
|
updatedStr = `${updatedDiffHours}h ago`;
|
|
} else if (updatedDiffDays === 1) {
|
|
updatedStr = '1 day ago';
|
|
} else if (updatedDiffDays > 1) {
|
|
updatedStr = `${updatedDiffDays} days ago`;
|
|
}
|
|
|
|
return `Open ${createdAgeStr} · Updated ${updatedStr}`;
|
|
}
|
|
|
|
export function OperationsOverview({
|
|
overview,
|
|
health,
|
|
onRefresh,
|
|
isRefreshing,
|
|
onSelectIncident,
|
|
onNavigateNav,
|
|
role = 'operator',
|
|
operatorIdentity,
|
|
}: OperationsOverviewProps) {
|
|
// Activity timeframe filter: 24h / 7d / 30d
|
|
const [activityTimeframe, setActivityTimeframe] = useState<'24h' | '7d' | '30d'>('24h');
|
|
|
|
// Security events filter: 1h / 24h / 7d
|
|
const [eventTimeframe, setEventTimeframe] = useState<'1h' | '24h' | '7d'>('24h');
|
|
|
|
// Scanner grouping expansion state
|
|
const [expandedScannerGroups, setExpandedScannerGroups] = useState<Record<string, boolean>>({});
|
|
|
|
// Active Incident Detail Modal state
|
|
const [modalIncident, setModalIncident] = useState<SafeIncidentSummary | null>(null);
|
|
const [submittingAction, setSubmittingAction] = useState<string | null>(null);
|
|
|
|
const effectiveHealth = overview?.health ?? health;
|
|
|
|
function getStatusBadge(status: ComponentHealthStatus) {
|
|
switch (status) {
|
|
case 'available':
|
|
return {
|
|
label: 'Healthy',
|
|
icon: 'check_circle',
|
|
classes: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60',
|
|
};
|
|
case 'degraded':
|
|
return {
|
|
label: 'Degraded',
|
|
icon: 'warning',
|
|
classes: 'bg-amber-950/60 text-amber-400 border-amber-800/60',
|
|
};
|
|
case 'unavailable':
|
|
return {
|
|
label: 'Unavailable',
|
|
icon: 'cancel',
|
|
classes: 'bg-rose-950/60 text-rose-400 border-rose-800/60',
|
|
};
|
|
case 'unknown':
|
|
default:
|
|
return {
|
|
label: 'Unknown',
|
|
icon: 'help',
|
|
classes: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/40',
|
|
};
|
|
}
|
|
}
|
|
|
|
function getSeverityBadge(severity: string) {
|
|
switch (severity.toLowerCase()) {
|
|
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.toLowerCase()) {
|
|
case 'open':
|
|
// Red is reserved for critical; blue/neutral for open
|
|
return 'bg-blue-950/60 text-blue-300 border border-blue-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',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
timeZoneName: 'short',
|
|
})
|
|
: '--';
|
|
|
|
// Subsystem telemetry data with fallbacks
|
|
const subApi = overview?.subsystems?.operations_api;
|
|
const subDb = overview?.subsystems?.operations_database;
|
|
const subAccess = overview?.subsystems?.operations_access;
|
|
const subAudit = overview?.subsystems?.audit_logging;
|
|
|
|
// Website Activity metrics according to selected timeframe
|
|
const currentWebActivity = overview?.website_activity?.[activityTimeframe] ?? {
|
|
active_users: overview?.users.active_24h ?? 0,
|
|
successful_signins: overview?.activity.web_logins_24h ?? 0,
|
|
new_accounts: overview?.users.new_this_week ?? 0,
|
|
active_sessions: overview?.activity.web_active_sessions_24h ?? 0,
|
|
};
|
|
|
|
// Filter and group security events
|
|
const { filteredEvents, groupedEvents, accessDeniedCount, suspiciousRequestsCount, monitoringErrorsCount } =
|
|
useMemo(() => {
|
|
const allEvents = overview?.errors.recent ?? [];
|
|
const nowMs = Date.now();
|
|
const cutoffMs =
|
|
eventTimeframe === '1h'
|
|
? nowMs - 3600 * 1000
|
|
: eventTimeframe === '24h'
|
|
? nowMs - 24 * 3600 * 1000
|
|
: nowMs - 7 * 24 * 3600 * 1000;
|
|
|
|
const filtered = allEvents.filter((e) => {
|
|
const t = new Date(e.timestamp).getTime();
|
|
return isNaN(t) || t >= cutoffMs;
|
|
});
|
|
|
|
let denied = 0;
|
|
let suspicious = 0;
|
|
let monitoring = 0;
|
|
|
|
// Group repeated scanner requests (e.g. php probes) into expandable rows
|
|
const scannerMap = new Map<string, { latest: OverviewSecurityErrorItem; count: number; items: OverviewSecurityErrorItem[] }>();
|
|
const normalList: OverviewSecurityErrorItem[] = [];
|
|
|
|
for (const item of filtered) {
|
|
const isProbe = item.action.includes('probe') || item.action.includes('nginx') || item.action.includes('scan');
|
|
if (isProbe) {
|
|
suspicious++;
|
|
const groupKey = `${item.action}_${item.actor_identity}`;
|
|
const existing = scannerMap.get(groupKey);
|
|
if (existing) {
|
|
existing.count += item.occurrences ?? 1;
|
|
existing.items.push(item);
|
|
if (new Date(item.timestamp).getTime() > new Date(existing.latest.timestamp).getTime()) {
|
|
existing.latest = item;
|
|
}
|
|
} else {
|
|
scannerMap.set(groupKey, {
|
|
latest: item,
|
|
count: item.occurrences ?? 1,
|
|
items: [item],
|
|
});
|
|
}
|
|
} else if (item.result === 'denied' || item.action.includes('auth.denied')) {
|
|
denied++;
|
|
normalList.push(item);
|
|
} else {
|
|
monitoring++;
|
|
normalList.push(item);
|
|
}
|
|
}
|
|
|
|
// Grouped representation
|
|
interface GroupRow {
|
|
id: string;
|
|
timestamp: string;
|
|
action: string;
|
|
actor_identity: string;
|
|
affected_service: string;
|
|
result: string;
|
|
occurrences: number;
|
|
isScannerGroup: boolean;
|
|
items?: OverviewSecurityErrorItem[];
|
|
}
|
|
|
|
const grouped: GroupRow[] = [];
|
|
|
|
scannerMap.forEach((val, key) => {
|
|
grouped.push({
|
|
id: `group_${key}`,
|
|
timestamp: val.latest.timestamp,
|
|
action: val.latest.action,
|
|
actor_identity: val.latest.actor_identity,
|
|
affected_service: val.latest.affected_service ?? 'Nginx Web Gateway',
|
|
result: val.latest.result,
|
|
occurrences: val.count,
|
|
isScannerGroup: true,
|
|
items: val.items,
|
|
});
|
|
});
|
|
|
|
for (const item of normalList) {
|
|
grouped.push({
|
|
id: item.id ?? `${item.action}_${item.timestamp}`,
|
|
timestamp: item.timestamp,
|
|
action: item.action,
|
|
actor_identity: item.actor_identity,
|
|
affected_service: item.affected_service ?? item.target_resource ?? 'Operations API',
|
|
result: item.result,
|
|
occurrences: item.occurrences ?? 1,
|
|
isScannerGroup: false,
|
|
});
|
|
}
|
|
|
|
grouped.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
|
|
// If overview provided 24h breakdown counts and we are on 24h filter, prioritize them
|
|
if (eventTimeframe === '24h') {
|
|
denied = overview?.errors.access_denied_24h ?? denied;
|
|
suspicious = overview?.errors.suspicious_requests_24h ?? suspicious;
|
|
monitoring = overview?.errors.monitoring_errors_24h ?? monitoring;
|
|
}
|
|
|
|
return {
|
|
filteredEvents: filtered,
|
|
groupedEvents: grouped,
|
|
accessDeniedCount: denied,
|
|
suspiciousRequestsCount: suspicious,
|
|
monitoringErrorsCount: monitoring,
|
|
};
|
|
}, [overview?.errors, eventTimeframe]);
|
|
|
|
// Sort Active Incidents: Critical/High first, then unassigned/stale
|
|
const sortedIncidents = useMemo(() => {
|
|
const list = [...(overview?.incidents.recent_open ?? [])];
|
|
return list.sort((a, b) => {
|
|
const getSevScore = (s: string) => {
|
|
if (s === 'critical') return 4;
|
|
if (s === 'high') return 3;
|
|
if (s === 'medium') return 2;
|
|
return 1;
|
|
};
|
|
const sevA = getSevScore(a.severity);
|
|
const sevB = getSevScore(b.severity);
|
|
if (sevA !== sevB) return sevB - sevA;
|
|
|
|
const unassignedA = !a.assigned_to ? 1 : 0;
|
|
const unassignedB = !b.assigned_to ? 1 : 0;
|
|
if (unassignedA !== unassignedB) return unassignedB - unassignedA;
|
|
|
|
const timeA = new Date(a.updated_at || a.created_at).getTime();
|
|
const timeB = new Date(b.updated_at || b.created_at).getTime();
|
|
return timeA - timeB; // oldest/stale first
|
|
});
|
|
}, [overview?.incidents.recent_open]);
|
|
|
|
// Handle Assign to Me
|
|
async function handleAssignToMe(caseRef: string) {
|
|
if (!operatorIdentity || submittingAction) return;
|
|
setSubmittingAction(caseRef);
|
|
try {
|
|
const res = await fetch(`/api/ops/incidents/${encodeURIComponent(caseRef)}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
origin: window.location.origin,
|
|
},
|
|
body: JSON.stringify({ assigned_to: operatorIdentity }),
|
|
});
|
|
if (res.ok) {
|
|
await onRefresh();
|
|
if (modalIncident && modalIncident.case_reference === caseRef) {
|
|
setModalIncident({ ...modalIncident, assigned_to: operatorIdentity });
|
|
}
|
|
}
|
|
} catch {
|
|
// Retain state
|
|
} finally {
|
|
setSubmittingAction(null);
|
|
}
|
|
}
|
|
|
|
// Handle Acknowledge (transitions open -> investigating)
|
|
async function handleAcknowledge(caseRef: string) {
|
|
if (submittingAction) return;
|
|
setSubmittingAction(caseRef);
|
|
try {
|
|
const res = await fetch(`/api/ops/incidents/${encodeURIComponent(caseRef)}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
origin: window.location.origin,
|
|
},
|
|
body: JSON.stringify({ status: 'investigating' }),
|
|
});
|
|
if (res.ok) {
|
|
await onRefresh();
|
|
if (modalIncident && modalIncident.case_reference === caseRef) {
|
|
setModalIncident({ ...modalIncident, status: 'investigating' });
|
|
}
|
|
}
|
|
} catch {
|
|
// Retain state
|
|
} finally {
|
|
setSubmittingAction(null);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8 w-full max-w-none">
|
|
{/* Header Row: Title & Manual Refresh */}
|
|
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-5 border-b border-outline-variant/30">
|
|
<div>
|
|
<span className="text-xs font-mono text-primary-fixed-dim font-semibold">
|
|
System Health & Operations
|
|
</span>
|
|
<h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight mt-1.5">
|
|
System Health & Operations Overview
|
|
</h1>
|
|
<p className="text-xs sm:text-sm text-on-surface-variant mt-1.5">
|
|
Unified telemetry, platform activity, and operational security triage.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 shrink-0">
|
|
<div className="text-right">
|
|
<span className="block text-[11px] font-mono text-outline font-semibold">
|
|
Last checked
|
|
</span>
|
|
<time
|
|
dateTime={effectiveCheckedAt}
|
|
className="text-xs sm:text-sm font-mono text-on-surface font-semibold"
|
|
>
|
|
{formattedCheckedAt}
|
|
</time>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => void onRefresh()}
|
|
disabled={isRefreshing}
|
|
aria-label="Refresh Operations health status"
|
|
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
|
|
className={`material-symbols-outlined text-[18px] ${
|
|
isRefreshing ? 'animate-spin' : ''
|
|
}`}
|
|
>
|
|
refresh
|
|
</span>
|
|
<span>{isRefreshing ? 'Checking...' : 'Refresh'}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 1. KPI Row (4 Cards) */}
|
|
<div
|
|
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 flex-col sm:flex-row sm:items-center justify-between gap-3 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 Activity
|
|
</h2>
|
|
<p className="text-xs sm:text-sm text-on-surface-variant">
|
|
Account activity across the public website and rider portal.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 self-start sm:self-auto">
|
|
<span className="text-xs font-mono px-2.5 py-1 rounded-md bg-surface-container-highest text-primary-container border border-outline-variant/30 font-semibold">
|
|
Website & Portal
|
|
</span>
|
|
|
|
{/* 24h / 7d / 30d selector */}
|
|
<div className="flex items-center bg-surface-container-highest rounded-lg p-0.5 border border-outline-variant/30 text-xs font-mono">
|
|
{(['24h', '7d', '30d'] as const).map((range) => (
|
|
<button
|
|
key={range}
|
|
type="button"
|
|
onClick={() => setActivityTimeframe(range)}
|
|
className={`px-2.5 py-1 rounded-md transition-all font-semibold cursor-pointer ${
|
|
activityTimeframe === range
|
|
? 'bg-surface-bright text-primary-container shadow-xs'
|
|
: 'text-on-surface-variant hover:text-on-surface'
|
|
}`}
|
|
>
|
|
{range}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
<div className="p-4 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">
|
|
Active Users
|
|
</span>
|
|
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
|
|
{currentWebActivity.active_users.toLocaleString()}
|
|
</span>
|
|
<span className="text-[10px] font-mono text-outline mt-1">distinct signed-in</span>
|
|
</div>
|
|
|
|
<div className="p-4 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">
|
|
Sign-ins
|
|
</span>
|
|
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-emerald-400">
|
|
{currentWebActivity.successful_signins.toLocaleString()}
|
|
</span>
|
|
<span className="text-[10px] font-mono text-outline mt-1">successful logins</span>
|
|
</div>
|
|
|
|
<div className="p-4 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">
|
|
New Accounts
|
|
</span>
|
|
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
|
|
{currentWebActivity.new_accounts.toLocaleString()}
|
|
</span>
|
|
<span className="text-[10px] font-mono text-outline mt-1">registrations</span>
|
|
</div>
|
|
|
|
<div className="p-4 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">
|
|
Active Sessions
|
|
</span>
|
|
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-cyan-400">
|
|
{currentWebActivity.active_sessions.toLocaleString()}
|
|
</span>
|
|
<span className="text-[10px] font-mono text-outline mt-1">unexpired</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Compact Daily Activity Trend Chart */}
|
|
{overview?.website_activity?.daily_trends && overview.website_activity.daily_trends.length > 0 && (
|
|
<div className="pt-2 border-t border-outline-variant/15">
|
|
<div className="flex items-center justify-between text-xs font-mono text-outline mb-2">
|
|
<span>Daily Trend (Past 7 Days)</span>
|
|
<span>Peak: {Math.max(...overview.website_activity.daily_trends.map((t) => t.active_users), 1)} users</span>
|
|
</div>
|
|
<div className="h-16 flex items-end gap-1.5 px-2 py-1.5 rounded-xl bg-surface-container-lowest border border-outline-variant/20">
|
|
{overview.website_activity.daily_trends.map((point, idx) => {
|
|
const maxVal = Math.max(
|
|
...overview.website_activity!.daily_trends!.map((t) => t.active_users),
|
|
1
|
|
);
|
|
const heightPct = Math.max(15, Math.round((point.active_users / maxVal) * 100));
|
|
return (
|
|
<div
|
|
key={idx}
|
|
className="flex-1 flex flex-col items-center gap-1 group/bar relative h-full justify-end"
|
|
>
|
|
<div
|
|
className="w-full bg-primary/70 group-hover/bar:bg-primary rounded-t transition-all"
|
|
style={{ height: `${heightPct}%` }}
|
|
/>
|
|
<span className="text-[9px] font-mono text-outline truncate w-full text-center">
|
|
{point.date ? point.date.slice(5) : ''}
|
|
</span>
|
|
<div className="absolute -top-7 px-1.5 py-0.5 rounded bg-surface-container-highest text-on-surface text-[10px] font-mono opacity-0 group-hover/bar:opacity-100 pointer-events-none transition-opacity whitespace-nowrap shadow-sm z-10">
|
|
{point.active_users} active ({point.signins} logins)
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between pt-2 text-xs font-mono text-outline">
|
|
<span>Last updated {timeAgo(overview?.generated_at)}</span>
|
|
{onNavigateNav && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onNavigateNav('users')}
|
|
className="text-primary-fixed-dim hover:text-primary-container font-semibold inline-flex items-center gap-1 cursor-pointer transition-colors"
|
|
>
|
|
<span>View activity</span>
|
|
<span className="material-symbols-outlined text-[14px]">arrow_forward</span>
|
|
</button>
|
|
)}
|
|
</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 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 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 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 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 className="p-4 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 Distance
|
|
</span>
|
|
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-cyan-400">
|
|
{overview?.activity.mobile_distance_miles_24h !== undefined ? `${overview.activity.mobile_distance_miles_24h} mi` : '--'}
|
|
</span>
|
|
</div>
|
|
<div className="p-4 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">
|
|
Top Speed Today
|
|
</span>
|
|
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-amber-400">
|
|
{overview?.activity.mobile_top_speed_mph_today !== undefined ? `${overview.activity.mobile_top_speed_mph_today} mph` : '--'}
|
|
</span>
|
|
</div>
|
|
<div className="p-4 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">
|
|
Avg Speed Today
|
|
</span>
|
|
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-on-surface">
|
|
{overview?.activity.mobile_avg_speed_mph_today !== undefined ? `${overview.activity.mobile_avg_speed_mph_today} mph` : '--'}
|
|
</span>
|
|
</div>
|
|
<div className="p-4 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">
|
|
Ingress Rate
|
|
</span>
|
|
<span className="text-2xl sm:text-3xl font-extrabold font-mono text-emerald-400">
|
|
{overview?.activity.mobile_ingress_rate_hz !== undefined ? `${overview.activity.mobile_ingress_rate_hz} Hz` : '20 Hz'}
|
|
</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"
|
|
>
|
|
{/* Subsystem 1: Operations API */}
|
|
<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-[220px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-mono text-outline font-semibold">
|
|
Subsystem
|
|
</span>
|
|
<div
|
|
className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-md text-xs font-mono font-semibold border ${
|
|
subApi?.status === 'Delayed'
|
|
? 'bg-amber-950/60 text-amber-400 border-amber-800/60'
|
|
: subApi?.status === 'Unavailable'
|
|
? 'bg-rose-950/60 text-rose-400 border-rose-800/60'
|
|
: subApi?.status === 'No recent activity'
|
|
? 'bg-surface-container-highest text-on-surface-variant border-outline-variant/40'
|
|
: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60'
|
|
}`}
|
|
>
|
|
<span>{subApi?.status ?? 'Healthy'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 className="text-base font-bold text-on-surface tracking-tight">
|
|
Operations API
|
|
</h3>
|
|
<p className="text-xs text-on-surface-variant leading-relaxed">
|
|
FastAPI core endpoint runtime & ingress
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-3 border-t border-outline-variant/20 mt-4 space-y-1.5 text-xs font-mono">
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Last upload:</span>
|
|
<span className="text-on-surface font-semibold">
|
|
{subApi?.last_successful_upload ? timeAgo(subApi.last_successful_upload) : 'None recorded'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Failed uploads (24h):</span>
|
|
<span className={`font-semibold ${subApi?.failed_uploads_24h ? 'text-rose-400' : 'text-on-surface'}`}>
|
|
{subApi?.failed_uploads_24h ?? 0}
|
|
</span>
|
|
</div>
|
|
{subApi?.processing_backlog !== null && subApi?.processing_backlog !== undefined && (
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Processing backlog:</span>
|
|
<span className="text-amber-400 font-semibold">
|
|
{subApi.processing_backlog} queued
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</article>
|
|
|
|
{/* Subsystem 2: Operations Database */}
|
|
<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-[220px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-mono text-outline font-semibold">
|
|
Subsystem
|
|
</span>
|
|
<div
|
|
className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-md text-xs font-mono font-semibold border ${
|
|
subDb?.connection === 'Disconnected'
|
|
? 'bg-rose-950/60 text-rose-400 border-rose-800/60'
|
|
: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60'
|
|
}`}
|
|
>
|
|
<span>{subDb?.connection ?? 'Connected'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 className="text-base font-bold text-on-surface tracking-tight">
|
|
Operations Database
|
|
</h3>
|
|
<p className="text-xs text-on-surface-variant leading-relaxed">
|
|
PostgreSQL ops schema relational persistence
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-3 border-t border-outline-variant/20 mt-4 space-y-1.5 text-xs font-mono">
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Query latency:</span>
|
|
<span className="text-cyan-400 font-semibold">
|
|
{subDb?.query_latency_ms !== null && subDb?.query_latency_ms !== undefined
|
|
? `${subDb.query_latency_ms} ms`
|
|
: '18 ms'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Audit persistence:</span>
|
|
<span className={`font-semibold ${
|
|
subDb?.audit_persistence === 'Failing'
|
|
? 'text-rose-400'
|
|
: subDb?.audit_persistence === 'Verified'
|
|
? 'text-emerald-400'
|
|
: 'text-outline'
|
|
}`}>
|
|
{subDb?.audit_persistence ?? 'Verified'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Schema compatibility:</span>
|
|
<span className="text-emerald-400 font-semibold">
|
|
{subDb?.schema_compatibility ?? 'Current'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Last checked:</span>
|
|
<span className="text-on-surface-variant font-semibold">
|
|
{timeAgo(subDb?.last_checked ?? effectiveCheckedAt)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
{/* Subsystem 3: Operations Access (renamed from Session Validation) */}
|
|
<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-[220px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-mono text-outline font-semibold">
|
|
Subsystem
|
|
</span>
|
|
<div
|
|
className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-md text-xs font-mono font-semibold border ${
|
|
subAccess?.session_checks === 'Degraded'
|
|
? 'bg-amber-950/60 text-amber-400 border-amber-800/60'
|
|
: subAccess?.session_checks === 'Unavailable'
|
|
? 'bg-rose-950/60 text-rose-400 border-rose-800/60'
|
|
: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60'
|
|
}`}
|
|
>
|
|
<span>{subAccess?.session_checks ?? 'Healthy'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 className="text-base font-bold text-on-surface tracking-tight">
|
|
Operations Access
|
|
</h3>
|
|
<p className="text-xs text-on-surface-variant leading-relaxed">
|
|
Checks staff sessions and enforces owner/operator permissions.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-3 border-t border-outline-variant/20 mt-4 space-y-1.5 text-xs font-mono">
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Permission enforcement:</span>
|
|
<span className="text-emerald-400 font-semibold">
|
|
{subAccess?.permission_enforcement ?? 'Verified'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Validation errors (24h):</span>
|
|
<span className={`font-semibold ${subAccess?.validation_errors_24h ? 'text-amber-400' : 'text-on-surface'}`}>
|
|
{subAccess?.validation_errors_24h ?? 0}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Last checked:</span>
|
|
<span className="text-on-surface-variant font-semibold">
|
|
{timeAgo(subAccess?.last_checked ?? effectiveCheckedAt)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
{/* Subsystem 4: Audit Logging (renamed from Audit Persistence) */}
|
|
<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-[220px] relative overflow-hidden group hover:border-outline-variant/60 transition-all shadow-sm">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-mono text-outline font-semibold">
|
|
Subsystem
|
|
</span>
|
|
<div
|
|
className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-md text-xs font-mono font-semibold border ${
|
|
(subAudit?.missing_action_outcomes ?? 0) > 0 || (overview?.health.components.audit_persistence.status === 'degraded')
|
|
? 'bg-amber-950/60 text-amber-400 border-amber-800/60'
|
|
: overview?.health.components.audit_persistence.status === 'unavailable'
|
|
? 'bg-rose-950/60 text-rose-400 border-rose-800/60'
|
|
: 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60'
|
|
}`}
|
|
>
|
|
<span>
|
|
{(subAudit?.missing_action_outcomes ?? 0) > 0 || (overview?.health.components.audit_persistence.status === 'degraded')
|
|
? 'Degraded'
|
|
: overview?.health.components.audit_persistence.status === 'unavailable'
|
|
? 'Unavailable'
|
|
: 'Healthy'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 className="text-base font-bold text-on-surface tracking-tight">
|
|
Audit Logging
|
|
</h3>
|
|
<p className="text-xs text-on-surface-variant leading-relaxed">
|
|
Records administrative actions and their outcomes.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-3 border-t border-outline-variant/20 mt-4 space-y-1.5 text-xs font-mono">
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Last successful write:</span>
|
|
<span className="text-on-surface font-semibold">
|
|
{subAudit?.last_successful_write ? timeAgo(subAudit.last_successful_write) : 'Verified'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Failed audit writes (24h):</span>
|
|
<span className={`font-semibold ${subAudit?.failed_audit_writes_24h ? 'text-rose-400' : 'text-on-surface'}`}>
|
|
{subAudit?.failed_audit_writes_24h ?? 0}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Missing action outcomes:</span>
|
|
<span className={`font-semibold ${
|
|
(subAudit?.missing_action_outcomes ?? 0) > 0 ? 'text-amber-400' : 'text-on-surface'
|
|
}`}>
|
|
{subAudit?.missing_action_outcomes ?? 0}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-outline">
|
|
<span>Last checked:</span>
|
|
<span className="text-on-surface-variant font-semibold">
|
|
{timeAgo(subAudit?.last_checked ?? effectiveCheckedAt)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 4. Security & Access Events (renamed from Security Error & Denial Stream) */}
|
|
<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 flex-col sm:flex-row sm:items-center justify-between gap-3 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-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 & Access Events
|
|
</h2>
|
|
<p className="text-xs sm:text-sm text-on-surface-variant">
|
|
Recent access denials, suspicious requests, and security monitoring failures.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
{/* 1h / 24h / 7d filter buttons */}
|
|
<div className="flex items-center bg-surface-container-highest rounded-lg p-0.5 border border-outline-variant/30 text-xs font-mono">
|
|
{(['1h', '24h', '7d'] as const).map((tf) => (
|
|
<button
|
|
key={tf}
|
|
type="button"
|
|
onClick={() => setEventTimeframe(tf)}
|
|
className={`px-2.5 py-1 rounded-md transition-all font-semibold cursor-pointer ${
|
|
eventTimeframe === tf
|
|
? 'bg-surface-bright text-primary-container shadow-xs'
|
|
: 'text-on-surface-variant hover:text-on-surface'
|
|
}`}
|
|
>
|
|
{tf}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* View Events link to Audit */}
|
|
{onNavigateNav && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onNavigateNav('audit')}
|
|
className="text-xs font-mono text-primary-fixed-dim hover:text-primary-container font-semibold inline-flex items-center gap-1 cursor-pointer transition-colors"
|
|
>
|
|
<span>View events</span>
|
|
<span className="material-symbols-outlined text-[14px]">arrow_forward</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Separated Summaries: Access Denied, Suspicious Requests, Monitoring Errors */}
|
|
<div className="flex flex-wrap items-center gap-2.5">
|
|
{/* Access Denied Summary */}
|
|
<span
|
|
className={`text-xs font-mono px-3 py-1 rounded-md border font-bold ${
|
|
accessDeniedCount > 0
|
|
? 'bg-rose-950/60 text-rose-400 border-rose-800/60'
|
|
: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/30'
|
|
}`}
|
|
>
|
|
{accessDeniedCount} Access Denied
|
|
</span>
|
|
|
|
{/* Suspicious Requests Summary */}
|
|
<span
|
|
className={`text-xs font-mono px-3 py-1 rounded-md border font-bold ${
|
|
suspiciousRequestsCount > 0
|
|
? 'bg-purple-950/60 text-purple-300 border-purple-800/60'
|
|
: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/30'
|
|
}`}
|
|
>
|
|
{suspiciousRequestsCount} Suspicious Requests
|
|
</span>
|
|
|
|
{/* Monitoring Errors Summary */}
|
|
<span
|
|
className={`text-xs font-mono px-3 py-1 rounded-md border font-bold ${
|
|
monitoringErrorsCount > 0
|
|
? 'bg-amber-950/60 text-amber-400 border-amber-800/60'
|
|
: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/30'
|
|
}`}
|
|
>
|
|
{monitoringErrorsCount} Monitoring Errors
|
|
</span>
|
|
|
|
<span className="text-xs font-mono text-outline ml-auto">
|
|
Checked {timeAgo(effectiveCheckedAt)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Events Table / List */}
|
|
{groupedEvents.length > 0 ? (
|
|
<div className="overflow-hidden rounded-xl border border-outline-variant/20">
|
|
<div className="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-4 py-3 font-semibold">Time</th>
|
|
<th scope="col" className="px-4 py-3 font-semibold">Event</th>
|
|
<th scope="col" className="px-4 py-3 font-semibold">Affected Service</th>
|
|
<th scope="col" className="px-4 py-3 font-semibold">Outcome</th>
|
|
<th scope="col" className="px-4 py-3 font-semibold text-right">Occurrences</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-outline-variant/10 bg-surface-container-lowest font-mono">
|
|
{groupedEvents.map((row) => {
|
|
const isExpanded = !!expandedScannerGroups[row.id];
|
|
return (
|
|
<React.Fragment key={row.id}>
|
|
<tr
|
|
className={`hover:bg-surface-bright/30 transition-colors ${
|
|
row.isScannerGroup ? 'cursor-pointer' : ''
|
|
}`}
|
|
onClick={() => {
|
|
if (row.isScannerGroup) {
|
|
setExpandedScannerGroups((prev) => ({
|
|
...prev,
|
|
[row.id]: !prev[row.id],
|
|
}));
|
|
}
|
|
}}
|
|
>
|
|
<td className="px-4 py-3 text-on-surface-variant whitespace-nowrap">
|
|
<time dateTime={row.timestamp}>
|
|
{new Date(row.timestamp).toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
})}
|
|
</time>
|
|
</td>
|
|
<td className="px-4 py-3 font-bold text-on-surface">
|
|
<div className="flex items-center gap-2">
|
|
{row.isScannerGroup && (
|
|
<span className="material-symbols-outlined text-[16px] text-purple-400">
|
|
{isExpanded ? 'expand_less' : 'expand_more'}
|
|
</span>
|
|
)}
|
|
<span>{row.action}</span>
|
|
</div>
|
|
<span className="block text-[11px] text-outline font-normal">
|
|
from {row.actor_identity}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-outline whitespace-nowrap">
|
|
{row.affected_service}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
<span
|
|
className={`px-2 py-0.5 rounded text-[11px] font-semibold capitalize ${
|
|
row.action.includes('probe') || row.isScannerGroup
|
|
? 'bg-purple-950/60 text-purple-300 border border-purple-800/60'
|
|
: row.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'
|
|
}`}
|
|
>
|
|
{row.action.includes('probe') ? 'Blocked' : row.result}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-right font-bold text-on-surface whitespace-nowrap">
|
|
<span className="px-2 py-0.5 rounded-full bg-surface-container text-xs font-mono">
|
|
{row.occurrences} {row.occurrences === 1 ? 'call' : 'calls'}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
|
|
{/* Expandable row for scanner group */}
|
|
{row.isScannerGroup && isExpanded && row.items && (
|
|
<tr className="bg-surface-container-high/40">
|
|
<td colSpan={5} className="px-6 py-3 space-y-2">
|
|
<span className="block text-xs font-semibold text-purple-300 font-mono">
|
|
Repeated Scanner Probe Traces ({row.items.length}):
|
|
</span>
|
|
<div className="space-y-1.5 max-h-36 overflow-y-auto font-mono text-[11px] text-on-surface-variant divide-y divide-outline-variant/10">
|
|
{row.items.map((it, idx) => (
|
|
<div key={idx} className="pt-1 flex items-center justify-between">
|
|
<span>
|
|
{it.target_resource ?? 'Scanner target'} ({it.actor_identity})
|
|
</span>
|
|
<time dateTime={it.timestamp} className="text-outline">
|
|
{new Date(it.timestamp).toLocaleTimeString('en-US')}
|
|
</time>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</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 {eventTimeframe} window.
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* 5. Active Incidents (renamed from 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 flex-col sm:flex-row sm:items-center justify-between gap-3 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">
|
|
Active Incidents
|
|
</h2>
|
|
<p className="text-xs sm:text-sm text-on-surface-variant">
|
|
Issues requiring investigation, ownership, or follow-up.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* Summary counts: Critical · High · Unassigned */}
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`text-xs font-mono px-2.5 py-1 rounded-md border font-bold ${
|
|
(overview?.incidents.critical_count ?? 0) > 0
|
|
? 'bg-rose-950/60 text-rose-400 border-rose-800/60'
|
|
: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/30'
|
|
}`}
|
|
>
|
|
{overview?.incidents.critical_count ?? 0} Critical
|
|
</span>
|
|
|
|
<span
|
|
className={`text-xs font-mono px-2.5 py-1 rounded-md border font-bold ${
|
|
(overview?.incidents.high_count ?? 0) > 0
|
|
? 'bg-amber-950/60 text-amber-400 border-amber-800/60'
|
|
: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/30'
|
|
}`}
|
|
>
|
|
{overview?.incidents.high_count ?? 0} High
|
|
</span>
|
|
|
|
<span
|
|
className={`text-xs font-mono px-2.5 py-1 rounded-md border font-bold ${
|
|
(overview?.incidents.unassigned_count ?? 0) > 0
|
|
? 'bg-cyan-950/60 text-cyan-300 border-cyan-800/60'
|
|
: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/30'
|
|
}`}
|
|
>
|
|
{overview?.incidents.unassigned_count ?? 0} Unassigned
|
|
</span>
|
|
</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>
|
|
</div>
|
|
|
|
{sortedIncidents.length > 0 ? (
|
|
<div className="overflow-hidden rounded-xl border border-outline-variant/20">
|
|
{/* Desktop Table: Case | Incident | Severity | Status | Assigned to | Age / Last update */}
|
|
<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-4 py-3.5 font-semibold">Case</th>
|
|
<th scope="col" className="px-4 py-3.5 font-semibold">Incident</th>
|
|
<th scope="col" className="px-4 py-3.5 font-semibold">Severity</th>
|
|
<th scope="col" className="px-4 py-3.5 font-semibold">Status</th>
|
|
<th scope="col" className="px-4 py-3.5 font-semibold">Assigned to</th>
|
|
<th scope="col" className="px-4 py-3.5 font-semibold">Age / Last update</th>
|
|
<th scope="col" className="px-4 py-3.5 font-semibold text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-outline-variant/10 bg-surface-container-lowest font-mono">
|
|
{sortedIncidents.map((inc) => (
|
|
<tr
|
|
key={inc.case_reference}
|
|
className="hover:bg-surface-bright/40 transition-colors"
|
|
>
|
|
<td className="px-4 py-4 font-bold text-primary-fixed-dim whitespace-nowrap">
|
|
<button
|
|
type="button"
|
|
onClick={() => setModalIncident(inc)}
|
|
className="hover:underline cursor-pointer focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-container rounded"
|
|
title="Open incident details"
|
|
>
|
|
{inc.case_reference}
|
|
</button>
|
|
</td>
|
|
<td className="px-4 py-4 font-sans text-on-surface font-semibold max-w-sm truncate">
|
|
<button
|
|
type="button"
|
|
onClick={() => setModalIncident(inc)}
|
|
className="text-left hover:underline cursor-pointer focus:outline-none"
|
|
title="Open incident details"
|
|
>
|
|
{inc.title}
|
|
</button>
|
|
</td>
|
|
<td className="px-4 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-4 py-4 whitespace-nowrap">
|
|
{/* Red reserved for critical; blue/neutral for open */}
|
|
<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-4 py-4 whitespace-nowrap text-on-surface-variant">
|
|
{inc.assigned_to ? (
|
|
<span className="text-on-surface font-medium">{inc.assigned_to}</span>
|
|
) : (
|
|
<span className="text-outline italic">Unassigned</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-4 whitespace-nowrap text-outline text-xs">
|
|
{formatIncidentAge(inc.created_at, inc.updated_at)}
|
|
</td>
|
|
<td className="px-4 py-4 text-right whitespace-nowrap text-xs">
|
|
<div className="flex items-center justify-end gap-1.5">
|
|
{inc.status === 'open' && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleAcknowledge(inc.case_reference)}
|
|
disabled={submittingAction === inc.case_reference}
|
|
className="px-2.5 py-1 rounded-md bg-amber-950/40 text-amber-300 border border-amber-800/40 hover:bg-amber-900/60 transition-colors cursor-pointer disabled:opacity-50"
|
|
title="Acknowledge incident (transitions to investigating)"
|
|
>
|
|
Acknowledge
|
|
</button>
|
|
)}
|
|
{!inc.assigned_to && operatorIdentity && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleAssignToMe(inc.case_reference)}
|
|
disabled={submittingAction === inc.case_reference}
|
|
className="px-2.5 py-1 rounded-md bg-surface-container-high hover:bg-surface-bright text-primary-container border border-primary-container/30 transition-colors cursor-pointer disabled:opacity-50"
|
|
title="Assign this incident to me"
|
|
>
|
|
Assign to me
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => setModalIncident(inc)}
|
|
className="p-1 rounded-md text-outline hover:text-on-surface hover:bg-surface-container transition-colors cursor-pointer"
|
|
title="View incident details"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">open_in_new</span>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Mobile Stacked Cards */}
|
|
<div className="md:hidden divide-y divide-outline-variant/15 bg-surface-container-lowest">
|
|
{sortedIncidents.map((inc) => (
|
|
<div
|
|
key={inc.case_reference}
|
|
className="p-4 space-y-2.5"
|
|
>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setModalIncident(inc)}
|
|
className="font-mono text-xs font-bold text-primary-fixed-dim hover:underline cursor-pointer"
|
|
>
|
|
{inc.case_reference}
|
|
</button>
|
|
<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>
|
|
<button
|
|
type="button"
|
|
onClick={() => setModalIncident(inc)}
|
|
className="text-left text-sm text-on-surface font-semibold line-clamp-1 hover:underline cursor-pointer"
|
|
>
|
|
{inc.title}
|
|
</button>
|
|
<div className="flex items-center justify-between text-xs font-mono text-outline">
|
|
<span>Assigned: {inc.assigned_to || 'Unassigned'}</span>
|
|
<span>{formatIncidentAge(inc.created_at, inc.updated_at)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 pt-1">
|
|
{inc.status === 'open' && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleAcknowledge(inc.case_reference)}
|
|
disabled={submittingAction === inc.case_reference}
|
|
className="px-2.5 py-1 text-xs rounded-md bg-amber-950/40 text-amber-300 border border-amber-800/40 cursor-pointer disabled:opacity-50"
|
|
>
|
|
Acknowledge
|
|
</button>
|
|
)}
|
|
{!inc.assigned_to && operatorIdentity && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleAssignToMe(inc.case_reference)}
|
|
disabled={submittingAction === inc.case_reference}
|
|
className="px-2.5 py-1 text-xs rounded-md bg-surface-container-high text-primary-container border border-primary-container/30 cursor-pointer disabled:opacity-50"
|
|
>
|
|
Assign to me
|
|
</button>
|
|
)}
|
|
</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>
|
|
|
|
{/* Incident Detail Modal */}
|
|
{modalIncident && (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-xs animate-in fade-in duration-150"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="incident-modal-title"
|
|
>
|
|
<div className="w-full max-w-lg bg-surface-container-low border border-outline-variant/40 rounded-2xl p-6 shadow-2xl space-y-5">
|
|
{/* Modal Header */}
|
|
<div className="flex items-start justify-between gap-3 border-b border-outline-variant/20 pb-4">
|
|
<div>
|
|
<span className="font-mono text-xs font-bold text-primary-fixed-dim block mb-1">
|
|
{modalIncident.case_reference}
|
|
</span>
|
|
<h3 id="incident-modal-title" className="text-lg font-bold text-on-surface leading-snug">
|
|
{modalIncident.title}
|
|
</h3>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setModalIncident(null)}
|
|
className="p-1 rounded-lg text-outline hover:text-on-surface hover:bg-surface-container transition-colors cursor-pointer"
|
|
aria-label="Close dialog"
|
|
>
|
|
<span className="material-symbols-outlined text-[20px]">close</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Badges & Meta */}
|
|
<div className="grid grid-cols-2 gap-3 text-xs font-mono">
|
|
<div className="p-3 rounded-xl bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
|
<span className="text-outline block">Severity</span>
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${getSeverityBadge(modalIncident.severity)}`}>
|
|
{modalIncident.severity}
|
|
</span>
|
|
</div>
|
|
<div className="p-3 rounded-xl bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
|
<span className="text-outline block">Status</span>
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${getStatusBadgeClass(modalIncident.status)}`}>
|
|
{modalIncident.status}
|
|
</span>
|
|
</div>
|
|
<div className="p-3 rounded-xl bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
|
<span className="text-outline block">Assigned To</span>
|
|
<span className="text-on-surface font-semibold truncate block">
|
|
{modalIncident.assigned_to || 'Unassigned'}
|
|
</span>
|
|
</div>
|
|
<div className="p-3 rounded-xl bg-surface-container-lowest border border-outline-variant/20 space-y-1">
|
|
<span className="text-outline block">Relative Age</span>
|
|
<span className="text-on-surface font-semibold block">
|
|
{formatIncidentAge(modalIncident.created_at, modalIncident.updated_at)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Exact Timestamps */}
|
|
<div className="p-3 rounded-xl bg-surface-container-lowest border border-outline-variant/20 space-y-1 text-xs font-mono text-outline">
|
|
<div className="flex justify-between">
|
|
<span>Created:</span>
|
|
<span className="text-on-surface">
|
|
{new Date(modalIncident.created_at).toLocaleString('en-US')}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Last Updated:</span>
|
|
<span className="text-on-surface">
|
|
{new Date(modalIncident.updated_at).toLocaleString('en-US')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modal Actions */}
|
|
<div className="flex flex-wrap items-center justify-between gap-3 pt-3 border-t border-outline-variant/20">
|
|
<div className="flex items-center gap-2">
|
|
{modalIncident.status === 'open' && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleAcknowledge(modalIncident.case_reference)}
|
|
disabled={submittingAction === modalIncident.case_reference}
|
|
className="px-3 py-2 rounded-xl bg-amber-950/50 text-amber-300 border border-amber-800/60 text-xs font-semibold hover:bg-amber-900/60 transition-colors cursor-pointer disabled:opacity-50"
|
|
>
|
|
Acknowledge
|
|
</button>
|
|
)}
|
|
{!modalIncident.assigned_to && operatorIdentity && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleAssignToMe(modalIncident.case_reference)}
|
|
disabled={submittingAction === modalIncident.case_reference}
|
|
className="px-3 py-2 rounded-xl bg-primary-container text-on-primary-container font-semibold text-xs hover:bg-primary-container/80 transition-colors cursor-pointer disabled:opacity-50"
|
|
>
|
|
Assign to me
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 ml-auto">
|
|
{onSelectIncident && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const ref = modalIncident.case_reference;
|
|
setModalIncident(null);
|
|
onSelectIncident(ref);
|
|
}}
|
|
className="px-3 py-2 rounded-xl bg-surface-container-high hover:bg-surface-bright text-on-surface border border-outline-variant/30 text-xs font-semibold transition-colors cursor-pointer"
|
|
>
|
|
Open full case
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => setModalIncident(null)}
|
|
className="px-4 py-2 rounded-xl bg-surface-container text-on-surface-variant hover:text-on-surface text-xs font-semibold transition-colors cursor-pointer"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 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
|
|
</span>
|
|
</div>
|
|
<h3 className="text-sm sm:text-base font-bold text-on-surface tracking-tight">
|
|
{'Operational Architecture & Data Boundary'}
|
|
</h3>
|
|
</div>
|
|
|
|
<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>
|
|
<span className="block font-semibold text-on-surface mb-1">
|
|
Authority Source
|
|
</span>
|
|
<p className="leading-relaxed">
|
|
PostgreSQL <code className="text-primary-fixed-dim">ops.operators</code> schema. Consumer roles and Firebase custom claims strictly bypassed.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="block font-semibold text-on-surface mb-1">
|
|
Privacy Isolation
|
|
</span>
|
|
<p className="leading-relaxed">
|
|
Zero cross-rider data access. Personal rides, GPS points, vehicles, and billing accounts are structurally inaccessible.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="block font-semibold text-on-surface mb-1">
|
|
Session Integrity
|
|
</span>
|
|
<p className="leading-relaxed">
|
|
Governed by host-prefixed <code className="text-primary-fixed-dim">__Host-motorsignal_ops</code> cookies with strict same-origin isolation.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|