Improve dashboard big-screen UX

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funnywolf
2026-06-27 23:47:39 +08:00
co-authored by Copilot
parent 8902cbeda2
commit aa63e0aa42
2 changed files with 527 additions and 53 deletions
+329 -13
View File
@@ -31,14 +31,14 @@
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px; gap: 16px;
min-height: 100%; min-height: 100%;
} }
.dashboard-hero { .dashboard-hero {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
padding: 20px; padding: 22px;
border: 1px solid rgba(22, 119, 255, 0.38); border: 1px solid rgba(22, 119, 255, 0.38);
border-radius: 18px; border-radius: 18px;
background: background:
@@ -69,6 +69,10 @@
gap: 16px; gap: 16px;
} }
.dashboard-hero-copy {
min-width: 0;
}
.dashboard-eyebrow { .dashboard-eyebrow {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -84,11 +88,48 @@
.dashboard-title { .dashboard-title {
margin: 0; margin: 0;
color: #f5faff; color: #f5faff;
font-size: 30px; font-size: 32px;
font-weight: 700; font-weight: 700;
line-height: 1.1; line-height: 1.1;
} }
.dashboard-command-summary {
display: inline-flex;
align-items: center;
max-width: 760px;
gap: 10px;
margin-top: 14px;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--posture-color, #69b1ff) 45%, transparent);
border-radius: 999px;
background:
linear-gradient(90deg, color-mix(in srgb, var(--posture-color, #69b1ff) 18%, transparent), rgba(6, 21, 40, 0.48)),
rgba(6, 21, 40, 0.46);
box-shadow: 0 0 22px color-mix(in srgb, var(--posture-color, #69b1ff) 16%, transparent);
}
.dashboard-command-state {
flex: none;
padding: 4px 9px;
border-radius: 999px;
color: #061528;
font-family: 'JetBrains Mono', Consolas, monospace;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.08em;
background: var(--posture-color, #69b1ff);
box-shadow: 0 0 16px color-mix(in srgb, var(--posture-color, #69b1ff) 42%, transparent);
}
.dashboard-command-copy {
overflow: hidden;
color: rgba(245, 250, 255, 0.86);
font-size: 13px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.dashboard-hero-actions { .dashboard-hero-actions {
position: relative; position: relative;
z-index: 2; z-index: 2;
@@ -155,6 +196,60 @@
background: rgba(250, 173, 20, 0.08); background: rgba(250, 173, 20, 0.08);
} }
.dashboard-command-strip {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1.4fr repeat(4, minmax(150px, 1fr));
gap: 10px;
margin-top: 18px;
}
.dashboard-command-pill {
position: relative;
overflow: hidden;
display: grid;
gap: 6px;
padding: 11px 12px;
border: 1px solid color-mix(in srgb, var(--signal-color, #69b1ff) 35%, rgba(255, 255, 255, 0.08));
border-radius: 14px;
background:
linear-gradient(135deg, color-mix(in srgb, var(--signal-color, #69b1ff) 14%, transparent), rgba(255, 255, 255, 0.035)),
rgba(6, 21, 40, 0.62);
}
.dashboard-command-pill::before {
position: absolute;
inset: 0 auto 0 0;
width: 3px;
content: '';
background: var(--signal-color, #69b1ff);
box-shadow: 0 0 18px var(--signal-color, #69b1ff);
}
.dashboard-command-pill span {
color: rgba(214, 234, 255, 0.54);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.dashboard-command-pill strong {
overflow: hidden;
color: #f5faff;
font-family: 'JetBrains Mono', Consolas, monospace;
font-size: 18px;
line-height: 1;
text-overflow: ellipsis;
text-shadow: 0 0 16px color-mix(in srgb, var(--signal-color, #69b1ff) 26%, transparent);
white-space: nowrap;
}
.dashboard-command-pill.emphasis strong {
font-size: 22px;
}
.dashboard-grid { .dashboard-grid {
display: grid; display: grid;
gap: 14px; gap: 14px;
@@ -208,6 +303,7 @@
.dashboard-card, .dashboard-card,
.dashboard-metric-card { .dashboard-metric-card {
position: relative;
border: 1px solid rgba(22, 119, 255, 0.22); border: 1px solid rgba(22, 119, 255, 0.22);
border-radius: 16px; border-radius: 16px;
background: background:
@@ -240,12 +336,25 @@
} }
.dashboard-risk-body { .dashboard-risk-body {
position: relative;
display: grid; display: grid;
grid-template-columns: minmax(170px, 0.85fr) minmax(180px, 1fr); grid-template-columns: minmax(170px, 0.85fr) minmax(180px, 1fr);
gap: 12px; gap: 12px;
align-items: center; align-items: center;
} }
.dashboard-risk-body::before {
position: absolute;
inset: 8px auto auto 8px;
width: 74px;
height: 74px;
pointer-events: none;
content: '';
border-radius: 50%;
background: radial-gradient(circle, color-mix(in srgb, var(--risk-color, #69b1ff) 22%, transparent), transparent 68%);
filter: blur(2px);
}
.dashboard-risk-gauge { .dashboard-risk-gauge {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -260,12 +369,31 @@
gap: 8px; gap: 8px;
} }
.dashboard-risk-value-row {
display: flex;
align-items: flex-end;
gap: 10px;
}
.dashboard-risk-value { .dashboard-risk-value {
color: #f5faff; color: var(--risk-color, #f5faff);
font-size: 54px; font-size: 54px;
font-weight: 800; font-weight: 800;
line-height: 1; line-height: 1;
text-shadow: 0 0 24px rgba(22, 119, 255, 0.42); text-shadow: 0 0 24px color-mix(in srgb, var(--risk-color, #69b1ff) 44%, transparent);
}
.dashboard-risk-level {
margin-bottom: 7px;
padding: 5px 9px;
border: 1px solid color-mix(in srgb, var(--risk-color, #69b1ff) 52%, transparent);
border-radius: 999px;
color: var(--risk-color, #69b1ff);
font-family: 'JetBrains Mono', Consolas, monospace;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.1em;
background: color-mix(in srgb, var(--risk-color, #69b1ff) 13%, transparent);
} }
.dashboard-risk-label { .dashboard-risk-label {
@@ -275,6 +403,42 @@
text-transform: uppercase; text-transform: uppercase;
} }
.dashboard-risk-summary {
color: rgba(245, 250, 255, 0.76);
font-size: 13px;
line-height: 1.35;
}
.dashboard-risk-band {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 4px;
margin: 2px 0 4px;
}
.dashboard-risk-band span {
overflow: hidden;
padding: 5px 4px;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 7px;
color: rgba(255, 255, 255, 0.35);
font-family: 'JetBrains Mono', Consolas, monospace;
font-size: 9px;
font-weight: 800;
letter-spacing: 0.05em;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
background: rgba(255, 255, 255, 0.03);
}
.dashboard-risk-band span.active {
border-color: color-mix(in srgb, var(--risk-color, #69b1ff) 54%, transparent);
color: #f5faff;
background: color-mix(in srgb, var(--risk-color, #69b1ff) 20%, transparent);
box-shadow: 0 0 14px color-mix(in srgb, var(--risk-color, #69b1ff) 20%, transparent);
}
.dashboard-risk-meta { .dashboard-risk-meta {
display: grid; display: grid;
gap: 8px; gap: 8px;
@@ -303,6 +467,17 @@
overflow: hidden; overflow: hidden;
} }
.dashboard-metric-card.priority {
border-color: color-mix(in srgb, var(--metric-color, #1677ff) 42%, rgba(255, 255, 255, 0.08));
background:
linear-gradient(135deg, color-mix(in srgb, var(--metric-color, #1677ff) 16%, transparent), rgba(13, 20, 33, 0.94) 48%),
rgba(20, 20, 20, 0.92);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.06),
0 18px 38px rgba(0, 0, 0, 0.22),
0 0 22px color-mix(in srgb, var(--metric-color, #1677ff) 14%, transparent);
}
.dashboard-metric-card::before { .dashboard-metric-card::before {
position: absolute; position: absolute;
inset: 0 auto 0 0; inset: 0 auto 0 0;
@@ -322,6 +497,36 @@
font-family: 'JetBrains Mono', Consolas, monospace; font-family: 'JetBrains Mono', Consolas, monospace;
} }
.dashboard-metric-card.priority .ant-statistic-content {
text-shadow: 0 0 18px color-mix(in srgb, var(--metric-color, #1677ff) 28%, transparent);
}
.dashboard-metric-category {
margin-bottom: 7px;
color: var(--metric-color, #69b1ff);
font-family: 'JetBrains Mono', Consolas, monospace;
font-size: 10px;
font-weight: 900;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.dashboard-metric-card.tone-critical {
border-color: rgba(255, 77, 79, 0.3);
}
.dashboard-metric-card.tone-warning {
border-color: rgba(250, 140, 22, 0.3);
}
.dashboard-metric-card.tone-success {
border-color: rgba(82, 196, 26, 0.26);
}
.dashboard-metric-card.tone-intel {
border-color: rgba(114, 46, 209, 0.26);
}
.dashboard-metric-sub { .dashboard-metric-sub {
margin-top: 8px; margin-top: 8px;
color: rgba(255, 255, 255, 0.48); color: rgba(255, 255, 255, 0.48);
@@ -336,6 +541,10 @@
min-height: 190px; min-height: 190px;
} }
.dashboard-trend-card .dashboard-panel-body {
min-height: 230px;
}
.dashboard-mini-list { .dashboard-mini-list {
display: grid; display: grid;
gap: 10px; gap: 10px;
@@ -545,6 +754,51 @@
white-space: nowrap; white-space: nowrap;
} }
.dashboard-radar-wrap {
display: grid;
gap: 10px;
}
.dashboard-radar-legend {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
gap: 6px;
}
.dashboard-radar-legend span {
display: grid;
grid-template-columns: 8px auto minmax(0, 1fr);
align-items: center;
gap: 6px;
min-width: 0;
padding: 5px 7px;
border: 1px solid rgba(255, 255, 255, 0.055);
border-radius: 8px;
background: rgba(255, 255, 255, 0.025);
}
.dashboard-radar-legend i {
width: 7px;
height: 7px;
border-radius: 50%;
box-shadow: 0 0 10px currentColor;
}
.dashboard-radar-legend strong {
color: rgba(245, 250, 255, 0.82);
font-family: 'JetBrains Mono', Consolas, monospace;
font-size: 11px;
}
.dashboard-radar-legend em {
overflow: hidden;
color: rgba(255, 255, 255, 0.38);
font-size: 11px;
font-style: normal;
text-overflow: ellipsis;
white-space: nowrap;
}
.dashboard-mitre-matrix { .dashboard-mitre-matrix {
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -655,26 +909,78 @@
gap: 10px; gap: 10px;
} }
.dashboard-event-stream::before {
position: absolute;
top: 14px;
bottom: 14px;
left: 72px;
width: 1px;
content: '';
background: linear-gradient(to bottom, transparent, rgba(105, 177, 255, 0.42), transparent);
}
.dashboard-event { .dashboard-event {
position: relative; position: relative;
display: grid; display: grid;
gap: 5px; grid-template-columns: 62px minmax(0, 1fr);
padding: 11px 12px 11px 16px; gap: 8px;
border: 1px solid rgba(255, 255, 255, 0.08); align-items: center;
padding: 12px;
border: 1px solid color-mix(in srgb, var(--event-color, #1677ff) 22%, rgba(255, 255, 255, 0.08));
border-radius: 12px; border-radius: 12px;
background: rgba(255, 255, 255, 0.035); background:
linear-gradient(90deg, color-mix(in srgb, var(--event-color, #1677ff) 10%, transparent), rgba(255, 255, 255, 0.035)),
rgba(255, 255, 255, 0.035);
} }
.dashboard-event::before { .dashboard-event::before {
position: absolute; position: absolute;
top: 14px; top: 50%;
bottom: 14px; left: 67px;
width: 10px;
height: 10px;
content: '';
border: 2px solid rgba(6, 21, 40, 0.94);
border-radius: 999px;
background: var(--event-color, #1677ff);
box-shadow: 0 0 16px var(--event-color, #1677ff);
transform: translateY(-50%);
}
.dashboard-event::after {
position: absolute;
top: 12px;
bottom: 12px;
left: 0; left: 0;
width: 3px; width: 3px;
content: ''; content: '';
border-radius: 999px; border-radius: 999px;
background: var(--event-color, #1677ff); background: var(--severity-color, #ff4d4f);
box-shadow: 0 0 14px var(--event-color, #1677ff); opacity: 0.72;
}
.dashboard-event-time {
display: grid;
gap: 4px;
justify-items: end;
padding-right: 14px;
font-family: 'JetBrains Mono', Consolas, monospace;
}
.dashboard-event-time span {
color: #f5faff;
font-size: 13px;
font-weight: 800;
}
.dashboard-event-time small {
color: rgba(255, 255, 255, 0.42);
font-size: 10px;
text-transform: uppercase;
}
.dashboard-event-body {
min-width: 0;
} }
.dashboard-event-title { .dashboard-event-title {
@@ -691,6 +997,10 @@
white-space: nowrap; white-space: nowrap;
} }
.dashboard-event-title .ant-badge-status-text {
color: rgba(245, 250, 255, 0.7);
}
.dashboard-event-meta { .dashboard-event-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -699,6 +1009,12 @@
font-size: 12px; font-size: 12px;
} }
.dashboard-event-meta span:first-child {
color: var(--event-color, #69b1ff);
font-family: 'JetBrains Mono', Consolas, monospace;
font-weight: 800;
}
@media (max-width: 1380px) { @media (max-width: 1380px) {
.dashboard-posture-grid { .dashboard-posture-grid {
grid-template-columns: minmax(300px, 0.9fr) minmax(520px, 1.8fr); grid-template-columns: minmax(300px, 0.9fr) minmax(520px, 1.8fr);
+198 -40
View File
@@ -1,4 +1,4 @@
import {useCallback, useEffect, useMemo, useState, type ReactNode} from 'react' import {useCallback, useEffect, useMemo, useState, type CSSProperties, type ReactNode} from 'react'
import {Area, Gauge, Radar as RadarChart} from '@ant-design/charts' import {Area, Gauge, Radar as RadarChart} from '@ant-design/charts'
import {Alert, Badge, Button, Card, Progress, Segmented, Skeleton, Space, Statistic, Tag, Tooltip, Typography} from 'antd' import {Alert, Badge, Button, Card, Progress, Segmented, Skeleton, Space, Statistic, Tag, Tooltip, Typography} from 'antd'
import {InfoCircleOutlined, ReloadOutlined} from '@ant-design/icons' import {InfoCircleOutlined, ReloadOutlined} from '@ant-design/icons'
@@ -16,6 +16,16 @@ const windowOptions: { label: string; value: DashboardWindow }[] = [
] ]
const chartTheme = 'classicDark' as const const chartTheme = 'classicDark' as const
const lucideIconProps = { size: 16, strokeWidth: 2 } const lucideIconProps = { size: 16, strokeWidth: 2 }
const riskBands = ['STABLE', 'GUARDED', 'ELEVATED', 'CRITICAL'] as const
type RiskBand = typeof riskBands[number]
type MetricTone = 'critical' | 'warning' | 'signal' | 'success' | 'intel' | 'neutral'
interface RiskPostureLevel {
band: RiskBand
color: string
summary: string
}
function hasValues(data: DashboardLabelValue[]) { function hasValues(data: DashboardLabelValue[]) {
return data.some((item) => item.value > 0) return data.some((item) => item.value > 0)
@@ -53,6 +63,83 @@ function severityColor(severity: string) {
return severityColors[severity] || '#1677ff' return severityColors[severity] || '#1677ff'
} }
function getRiskPostureLevel(value: number | null | undefined): RiskPostureLevel {
const score = value ?? 0
if (score >= 80) {
return { band: 'CRITICAL', color: '#ff4d4f', summary: 'Critical pressure requires command attention.' }
}
if (score >= 55) {
return { band: 'ELEVATED', color: '#fa8c16', summary: 'Elevated risk pressure across active signals.' }
}
if (score >= 35) {
return { band: 'GUARDED', color: '#fadb14', summary: 'Guarded posture with monitored signal pressure.' }
}
return { band: 'STABLE', color: '#52c41a', summary: 'Stable posture with no dominant pressure.' }
}
function commandNarrative(data: DashboardOverview | null) {
if (!data) return 'Waiting for telemetry handshake.'
const {summary} = data
const posture = getRiskPostureLevel(summary.active_risk_index)
const leadSignal = summary.open_critical_cases > 0
? `${formatNumber(summary.open_critical_cases)} critical case${summary.open_critical_cases === 1 ? '' : 's'} open`
: summary.critical_high_alerts > 0
? `${formatNumber(summary.critical_high_alerts)} critical/high alert${summary.critical_high_alerts === 1 ? '' : 's'} observed`
: 'critical signal quiet'
const automation = summary.failed_playbooks > 0
? `${formatNumber(summary.failed_playbooks)} failed playbook${summary.failed_playbooks === 1 ? '' : 's'}`
: `${formatPercent(summary.automation_success_rate)} automation success`
return `${posture.band} posture · ${leadSignal} · ${automation}`
}
function automationTone(summary: DashboardOverview['summary'] | undefined): MetricTone {
if (!summary) return 'neutral'
return summary.failed_playbooks > 0 ? 'critical' : 'success'
}
function eventTimeParts(timestamp: string) {
const value = new Date(timestamp)
return {
time: value.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
date: value.toLocaleDateString([], { month: 'short', day: '2-digit' }),
}
}
function compactMitreTacticLabel(label: string) {
const normalized = label.trim()
const labels: Record<string, string> = {
Reconnaissance: 'Recon',
'Resource Development': 'Resource',
'Initial Access': 'Initial',
Execution: 'Exec',
Persistence: 'Persist',
'Privilege Escalation': 'Priv Esc',
'Defense Evasion': 'Defense',
'Credential Access': 'Cred',
Discovery: 'Discover',
'Lateral Movement': 'Lateral',
Collection: 'Collect',
'Command and Control': 'C2',
'Command & Control': 'C2',
Exfiltration: 'Exfil',
Impact: 'Impact',
}
if (labels[normalized]) return labels[normalized]
if (normalized.length <= 10) return normalized
return normalized
.replace(/\band\b/gi, '&')
.split(/\s+/)
.map((part) => part.slice(0, 4))
.slice(0, 2)
.join(' ')
}
function eventAccentColor(item: DashboardHighlight) {
if (item.kind === 'case') return statusColor(item.status)
if (item.status === 'Resolved' || item.status === 'Closed') return '#52c41a'
return '#b37feb'
}
function generatedAtLabel(value: string | undefined) { function generatedAtLabel(value: string | undefined) {
if (!value) return 'Waiting for telemetry' if (!value) return 'Waiting for telemetry'
return `Last refresh ${new Date(value).toLocaleString()}` return `Last refresh ${new Date(value).toLocaleString()}`
@@ -88,6 +175,9 @@ function MetricCard({
tooltip, tooltip,
accent = '#1677ff', accent = '#1677ff',
sub, sub,
category,
tone = 'signal',
priority = false,
}: { }: {
title: string title: string
value: ReactNode value: ReactNode
@@ -95,6 +185,9 @@ function MetricCard({
tooltip?: string tooltip?: string
accent?: string accent?: string
sub?: ReactNode sub?: ReactNode
category?: string
tone?: MetricTone
priority?: boolean
}) { }) {
const statisticTitle = ( const statisticTitle = (
<Space size={6}> <Space size={6}>
@@ -108,7 +201,8 @@ function MetricCard({
) )
return ( return (
<Card className="dashboard-metric-card" style={{ '--metric-color': accent } as React.CSSProperties} styles={{ body: { padding: 16 } }}> <Card className={`dashboard-metric-card tone-${tone}${priority ? ' priority' : ''}`} style={{ '--metric-color': accent } as CSSProperties} styles={{ body: { padding: 16 } }}>
{category && <div className="dashboard-metric-category">{category}</div>}
<Statistic title={statisticTitle} value={value as string | number} suffix={suffix} styles={{ content: { color: '#f5faff' } }} /> <Statistic title={statisticTitle} value={value as string | number} suffix={suffix} styles={{ content: { color: '#f5faff' } }} />
{sub && <div className="dashboard-metric-sub">{sub}</div>} {sub && <div className="dashboard-metric-sub">{sub}</div>}
</Card> </Card>
@@ -122,6 +216,8 @@ function MeanTimeCard({ title, metric, tooltip, accent }: { title: string; metri
value={formatDuration(metric.seconds)} value={formatDuration(metric.seconds)}
tooltip={tooltip} tooltip={tooltip}
accent={accent} accent={accent}
category="Response Clock"
tone="signal"
sub={`${metric.sample_count} valid sample${metric.sample_count === 1 ? '' : 's'}`} sub={`${metric.sample_count} valid sample${metric.sample_count === 1 ? '' : 's'}`}
/> />
) )
@@ -130,29 +226,30 @@ function MeanTimeCard({ title, metric, tooltip, accent }: { title: string; metri
function RiskPostureCard({ data, loading }: { data: DashboardOverview | null; loading: boolean }) { function RiskPostureCard({ data, loading }: { data: DashboardOverview | null; loading: boolean }) {
const riskIndex = data?.summary.active_risk_index ?? 0 const riskIndex = data?.summary.active_risk_index ?? 0
const clampedRiskIndex = Math.min(100, Math.max(0, riskIndex)) const clampedRiskIndex = Math.min(100, Math.max(0, riskIndex))
const riskLevel = getRiskPostureLevel(riskIndex)
const gaugeConfig = useMemo(() => ({ const gaugeConfig = useMemo(() => ({
data: { data: {
percent: clampedRiskIndex / 100, percent: clampedRiskIndex / 100,
thresholds: [0.55, 0.8, 1], thresholds: [0.35, 0.55, 0.8, 1],
}, },
height: 190, height: 190,
theme: chartTheme, theme: chartTheme,
scale: { scale: {
color: { color: {
range: ['#52c41a', '#fadb14', '#fa8c16'], range: ['#52c41a', '#13c2c2', '#fa8c16', '#ff4d4f'],
}, },
}, },
style: { style: {
arcLineWidth: 9, arcLineWidth: 9,
pointerLineWidth: 3, pointerLineWidth: 3,
pointerLineCap: 'round', pointerLineCap: 'round',
pointerStroke: '#69b1ff', pointerStroke: riskLevel.color,
pinR: 7, pinR: 7,
pinFill: '#061528', pinFill: '#061528',
pinStroke: '#69b1ff', pinStroke: riskLevel.color,
textContent: () => '', textContent: () => '',
}, },
}), [clampedRiskIndex]) }), [clampedRiskIndex, riskLevel.color])
return ( return (
<CyberCard <CyberCard
@@ -167,13 +264,22 @@ function RiskPostureCard({ data, loading }: { data: DashboardOverview | null; lo
{loading && !data ? ( {loading && !data ? (
<Skeleton active paragraph={{ rows: 4 }} /> <Skeleton active paragraph={{ rows: 4 }} />
) : ( ) : (
<div className="dashboard-risk-body"> <div className="dashboard-risk-body" style={{ '--risk-color': riskLevel.color } as CSSProperties}>
<div className="dashboard-risk-gauge"> <div className="dashboard-risk-gauge">
<Gauge {...gaugeConfig} /> <Gauge {...gaugeConfig} />
</div> </div>
<div className="dashboard-risk-score"> <div className="dashboard-risk-score">
<div className="dashboard-risk-value">{riskIndex}</div> <div className="dashboard-risk-value-row">
<div className="dashboard-risk-value">{riskIndex}</div>
<span className="dashboard-risk-level">{riskLevel.band}</span>
</div>
<div className="dashboard-risk-label">weighted pressure / 100</div> <div className="dashboard-risk-label">weighted pressure / 100</div>
<div className="dashboard-risk-summary">{riskLevel.summary}</div>
<div className="dashboard-risk-band">
{riskBands.map((band) => (
<span className={band === riskLevel.band ? 'active' : ''} key={band}>{band}</span>
))}
</div>
<div className="dashboard-risk-meta"> <div className="dashboard-risk-meta">
<div className="dashboard-chip"><span>Open cases</span><span>{formatNumber(data?.summary.open_cases)}</span></div> <div className="dashboard-chip"><span>Open cases</span><span>{formatNumber(data?.summary.open_cases)}</span></div>
<div className="dashboard-chip"><span>Critical cases</span><span>{formatNumber(data?.summary.open_critical_cases)}</span></div> <div className="dashboard-chip"><span>Critical cases</span><span>{formatNumber(data?.summary.open_critical_cases)}</span></div>
@@ -198,7 +304,7 @@ function statusColor(status: string) {
} }
function signalColor(index: number) { function signalColor(index: number) {
return ['#69b1ff', '#13c2c2', '#b37feb', '#f759ab', '#faad14', '#52c41a'][index % 6] return ['#69b1ff', '#13c2c2', '#b37feb', '#597ef7', '#36cfc9', '#9254de'][index % 6]
} }
function ChartPanel({ title, children, compact = false, className = '' }: { title: ReactNode; children: ReactNode; compact?: boolean; className?: string }) { function ChartPanel({ title, children, compact = false, className = '' }: { title: ReactNode; children: ReactNode; compact?: boolean; className?: string }) {
@@ -214,7 +320,7 @@ function AlertTrend({ data }: { data: DashboardLabelValue[] }) {
data, data,
xField: 'label', xField: 'label',
yField: 'value', yField: 'value',
height: 150, height: 190,
autoFit: true, autoFit: true,
smooth: true, smooth: true,
colorField: 'value', colorField: 'value',
@@ -226,7 +332,7 @@ function AlertTrend({ data }: { data: DashboardLabelValue[] }) {
} }
function SeverityPressure({ data }: { data: DashboardLabelValue[] }) { function SeverityPressure({ data }: { data: DashboardLabelValue[] }) {
const visibleData = data.filter((item) => item.label !== 'Info') const visibleData = data.filter((item) => !['Info', 'Informational'].includes(item.label))
const maxValue = Math.max(...visibleData.map((item) => item.value), 0) const maxValue = Math.max(...visibleData.map((item) => item.value), 0)
const total = visibleData.reduce((sum, item) => sum + item.value, 0) const total = visibleData.reduce((sum, item) => sum + item.value, 0)
@@ -358,12 +464,17 @@ function ThreatKeywordCloud({ data }: { data: DashboardKeyword[] }) {
function SecurityDomainRadar({ data }: { data: DashboardLabelValue[] }) { function SecurityDomainRadar({ data }: { data: DashboardLabelValue[] }) {
const visibleData = data.filter((item) => item.value > 0) const visibleData = data.filter((item) => item.value > 0)
if (!hasValues(visibleData)) return <CyberEmpty title="No Domain Signals" /> if (!hasValues(visibleData)) return <CyberEmpty title="No Domain Signals" />
const maxValue = Math.max(...visibleData.map((item) => item.value), 1) const radarData = visibleData.map((item) => ({
...item,
fullLabel: item.label,
label: compactMitreTacticLabel(item.label),
}))
const maxValue = Math.max(...radarData.map((item) => item.value), 1)
const config = { const config = {
data: visibleData, data: radarData,
xField: 'label', xField: 'label',
yField: 'value', yField: 'value',
height: 260, height: 220,
autoFit: true, autoFit: true,
theme: chartTheme, theme: chartTheme,
coordinateType: 'polar' as const, coordinateType: 'polar' as const,
@@ -395,7 +506,20 @@ function SecurityDomainRadar({ data }: { data: DashboardLabelValue[] }) {
}, },
} }
return <RadarChart {...config} /> return (
<div className="dashboard-radar-wrap">
<RadarChart {...config} />
<div className="dashboard-radar-legend">
{radarData.map((item, index) => (
<span key={`${item.fullLabel}-${index}`} title={`${item.fullLabel}: ${item.value}`}>
<i style={{ background: signalColor(index), color: signalColor(index) }} />
<strong>{item.label}</strong>
<em>{item.fullLabel}</em>
</span>
))}
</div>
</div>
)
} }
function MitreSeverityMatrix({ data }: { data: DashboardMitreSeverityCell[] }) { function MitreSeverityMatrix({ data }: { data: DashboardMitreSeverityCell[] }) {
@@ -514,21 +638,43 @@ function HighlightStream({ highlights }: { highlights: DashboardHighlight[] }) {
return ( return (
<div className="dashboard-event-stream"> <div className="dashboard-event-stream">
{highlights.map((item) => ( {highlights.map((item) => {
<div className="dashboard-event" key={`${item.kind}-${item.id}`} style={{ '--event-color': severityColor(item.severity) } as React.CSSProperties}> const timestamp = eventTimeParts(item.timestamp)
<div className="dashboard-event-title"> const eventAccent = eventAccentColor(item)
<strong title={item.title}>{item.title || item.readable_id}</strong> return (
<Badge color={severityColor(item.severity)} text={item.severity || 'Unknown'} /> <div
className="dashboard-event"
key={`${item.kind}-${item.id}`}
style={{ '--event-color': eventAccent, '--severity-color': severityColor(item.severity) } as CSSProperties}
>
<div className="dashboard-event-time">
<span>{timestamp.time}</span>
<small>{timestamp.date}</small>
</div>
<div className="dashboard-event-body">
<div className="dashboard-event-title">
<strong title={item.title}>{item.title || item.readable_id}</strong>
<Badge color={severityColor(item.severity)} text={item.severity || 'Unknown'} />
</div>
<div className="dashboard-event-meta">
<span>{item.kind.toUpperCase()}</span>
<span>{item.readable_id}</span>
<span>{item.status || 'Unknown'}</span>
<span>{item.subtitle}</span>
</div>
</div>
</div> </div>
<div className="dashboard-event-meta"> )
<span>{item.kind.toUpperCase()}</span> })}
<span>{item.readable_id}</span> </div>
<span>{item.status || 'Unknown'}</span> )
<span>{item.subtitle}</span> }
<span>{new Date(item.timestamp).toLocaleString()}</span>
</div> function CommandSignalPill({ label, value, color, emphasis = false }: { label: string; value: ReactNode; color: string; emphasis?: boolean }) {
</div> return (
))} <div className={`dashboard-command-pill${emphasis ? ' emphasis' : ''}`} style={{ '--signal-color': color } as CSSProperties}>
<span>{label}</span>
<strong>{value}</strong>
</div> </div>
) )
} }
@@ -558,15 +704,20 @@ export default function Dashboard() {
}, [loadOverview, selectedWindow]) }, [loadOverview, selectedWindow])
const summary = data?.summary const summary = data?.summary
const riskLevel = getRiskPostureLevel(summary?.active_risk_index)
return ( return (
<div className="dashboard-page"> <div className="dashboard-page">
<div className="dashboard-shell"> <div className="dashboard-shell">
<section className="dashboard-hero"> <section className="dashboard-hero">
<div className="dashboard-hero-content"> <div className="dashboard-hero-content">
<div> <div className="dashboard-hero-copy">
<div className="dashboard-eyebrow"><RadioTower size={14} /> Cyber Command Center</div> <div className="dashboard-eyebrow"><RadioTower size={14} /> Cyber Command Center</div>
<h1 className="dashboard-title">Security Posture Dashboard</h1> <h1 className="dashboard-title">Security Posture Dashboard</h1>
<div className="dashboard-command-summary" style={{ '--posture-color': riskLevel.color } as CSSProperties}>
<span className="dashboard-command-state">{riskLevel.band}</span>
<span className="dashboard-command-copy">{commandNarrative(data)}</span>
</div>
</div> </div>
<div className="dashboard-hero-actions"> <div className="dashboard-hero-actions">
{error && <Alert className="dashboard-soft-alert" type="warning" title={error} showIcon />} {error && <Alert className="dashboard-soft-alert" type="warning" title={error} showIcon />}
@@ -586,18 +737,25 @@ export default function Dashboard() {
</div> </div>
</div> </div>
</div> </div>
<div className="dashboard-command-strip">
<CommandSignalPill label="Risk Index" value={`${formatNumber(summary?.active_risk_index)} / 100`} color={riskLevel.color} emphasis />
<CommandSignalPill label="Critical Cases" value={formatNumber(summary?.open_critical_cases)} color="#ff4d4f" />
<CommandSignalPill label="Critical/High Alerts" value={formatNumber(summary?.critical_high_alerts)} color="#fa8c16" />
<CommandSignalPill label="Automation" value={formatPercent(summary?.automation_success_rate)} color={summary?.failed_playbooks ? '#ff4d4f' : '#52c41a'} />
<CommandSignalPill label="Window" value={selectedWindow.toUpperCase()} color="#69b1ff" />
</div>
</section> </section>
<div className="dashboard-posture-grid"> <div className="dashboard-posture-grid">
<RiskPostureCard data={data} loading={loading} /> <RiskPostureCard data={data} loading={loading} />
<div className="dashboard-posture-stack"> <div className="dashboard-posture-stack">
<div className="dashboard-grid posture-kpis"> <div className="dashboard-grid posture-kpis">
<MetricCard title="Open Critical Cases" value={formatNumber(summary?.open_critical_cases)} accent="#ff4d4f" sub={`${formatNumber(summary?.open_cases)} open cases`} /> <MetricCard title="Open Critical Cases" value={formatNumber(summary?.open_critical_cases)} accent="#ff4d4f" category="Primary Risk" tone="critical" priority sub={`${formatNumber(summary?.open_cases)} open cases`} />
<MetricCard title="Cases In Window" value={formatNumber(summary?.total_cases)} accent="#1677ff" sub={`${selectedWindow} operating window`} /> <MetricCard title="Critical / High Alerts" value={formatNumber(summary?.critical_high_alerts)} accent="#fa8c16" category="Primary Risk" tone="warning" priority sub={`${formatNumber(summary?.total_alerts)} alerts observed`} />
<MetricCard title="Critical / High Alerts" value={formatNumber(summary?.critical_high_alerts)} accent="#fa8c16" sub={`${formatNumber(summary?.total_alerts)} alerts observed`} /> <MetricCard title="Automation Success" value={formatPercent(summary?.automation_success_rate)} accent={summary?.failed_playbooks ? '#ff4d4f' : '#52c41a'} category="Response Capacity" tone={automationTone(summary)} sub={`${formatNumber(summary?.running_playbooks)} running · ${formatNumber(summary?.failed_playbooks)} failed`} />
<MetricCard title="Artifacts In Scope" value={formatNumber(summary?.total_artifacts)} accent="#722ed1" sub="Distinct entities linked to alerts" /> <MetricCard title="Cases In Window" value={formatNumber(summary?.total_cases)} accent="#1677ff" category="Operating Volume" tone="signal" sub={`${selectedWindow} operating window`} />
<MetricCard title="Automation Success" value={formatPercent(summary?.automation_success_rate)} accent="#52c41a" sub={`${formatNumber(summary?.running_playbooks)} running · ${formatNumber(summary?.failed_playbooks)} failed`} /> <MetricCard title="Artifacts In Scope" value={formatNumber(summary?.total_artifacts)} accent="#722ed1" category="Entity Surface" tone="intel" sub="Distinct entities linked to alerts" />
<MetricCard title="Knowledge Signals" value={formatNumber(summary?.total_knowledge)} accent="#13c2c2" sub={`${formatNumber(summary?.total_enrichments)} enrichments`} /> <MetricCard title="Knowledge Signals" value={formatNumber(summary?.total_knowledge)} accent="#13c2c2" category="Intel Depth" tone="intel" sub={`${formatNumber(summary?.total_enrichments)} enrichments`} />
</div> </div>
<div className="dashboard-grid posture-mean-times"> <div className="dashboard-grid posture-mean-times">
<MeanTimeCard <MeanTimeCard
@@ -635,7 +793,7 @@ export default function Dashboard() {
</div> </div>
<div className="dashboard-grid operations"> <div className="dashboard-grid operations">
<ChartPanel title={<><ChartSpline {...lucideIconProps} /> Alert Telemetry Trend</>} compact> <ChartPanel title={<><ChartSpline {...lucideIconProps} /> Alert Telemetry Trend</>} className="dashboard-trend-card">
{loading && !data ? <Skeleton active paragraph={{ rows: 6 }} /> : <AlertTrend data={data?.alert_trend || []} />} {loading && !data ? <Skeleton active paragraph={{ rows: 6 }} /> : <AlertTrend data={data?.alert_trend || []} />}
</ChartPanel> </ChartPanel>
<ChartPanel title={<><Workflow {...lucideIconProps} /> Automation State</>}> <ChartPanel title={<><Workflow {...lucideIconProps} /> Automation State</>}>
@@ -650,8 +808,8 @@ export default function Dashboard() {
<ChartPanel title={<><TextSearch {...lucideIconProps} /> Threat Keyword Cloud</>} className="dashboard-keyword-card"> <ChartPanel title={<><TextSearch {...lucideIconProps} /> Threat Keyword Cloud</>} className="dashboard-keyword-card">
{loading && !data ? <Skeleton active paragraph={{ rows: 6 }} /> : <ThreatKeywordCloud data={data?.threat_keywords || []} />} {loading && !data ? <Skeleton active paragraph={{ rows: 6 }} /> : <ThreatKeywordCloud data={data?.threat_keywords || []} />}
</ChartPanel> </ChartPanel>
<ChartPanel title={<><RadarIcon {...lucideIconProps} /> Security Domain Radar</>}> <ChartPanel title={<><RadarIcon {...lucideIconProps} /> MITRE Tactic Radar</>}>
{loading && !data ? <Skeleton active paragraph={{ rows: 6 }} /> : <SecurityDomainRadar data={data?.product_category_distribution || []} />} {loading && !data ? <Skeleton active paragraph={{ rows: 6 }} /> : <SecurityDomainRadar data={data?.mitre_tactics || []} />}
</ChartPanel> </ChartPanel>
<ChartPanel title={<><Target {...lucideIconProps} /> MITRE Severity Matrix</>} className="dashboard-mitre-card"> <ChartPanel title={<><Target {...lucideIconProps} /> MITRE Severity Matrix</>} className="dashboard-mitre-card">
{loading && !data ? <Skeleton active paragraph={{ rows: 6 }} /> : <MitreSeverityMatrix data={data?.mitre_severity_heatmap || []} />} {loading && !data ? <Skeleton active paragraph={{ rows: 6 }} /> : <MitreSeverityMatrix data={data?.mitre_severity_heatmap || []} />}