WIP: Dashboard redesign — design system overhaul and component updates

Frontend redesign in progress: updated styles, layout, and components
across all pages to align with new design system. Includes Jira API
compliance specs, property tests, and load test script.
This commit is contained in:
root
2026-04-29 14:20:23 +00:00
parent 37119b9c8a
commit 27192dd69f
78 changed files with 9902 additions and 1368 deletions

View File

@@ -27,7 +27,7 @@
<title>CVE Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {

View File

@@ -1,8 +1,10 @@
/* Tactical Intelligence Dashboard Styles */
/* IMPORTANT: This file MUST be imported in App.js */
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
* {
font-family: 'Outfit', system-ui, sans-serif;
font-family: var(--font-ui);
}
/* Pulse animation for glowing dots - used by inline styles */
@@ -18,21 +20,179 @@
}
:root {
/* Base Colors - Modern Slate Foundation */
--intel-darkest: #0F172A;
--intel-dark: #1E293B;
--intel-medium: #334155;
--intel-accent: #0EA5E9; /* Sky Blue - professional cyan */
--intel-warning: #F59E0B; /* Amber - sophisticated warning */
--intel-danger: #EF4444; /* Modern Red - urgent but refined */
--intel-success: #10B981; /* Emerald - professional green */
--intel-grid: rgba(14, 165, 233, 0.08);
/* ── Color · Surfaces (modern slate foundation) ─────────────── */
--intel-darkest: #0F172A; /* page background */
--intel-dark: #1E293B; /* card / panel surface */
--intel-medium: #334155; /* elevated surface, hover row */
--intel-light: #475569; /* muted border, disabled chip */
--intel-accent: #0EA5E9; /* Sky Blue - professional cyan */
--intel-warning: #F59E0B; /* Amber - sophisticated warning */
--intel-danger: #EF4444; /* Modern Red - urgent but refined */
--intel-success: #10B981; /* Emerald - professional green */
--intel-grid: rgba(14, 165, 233, 0.08); /* grid backdrop */
/* Text Colors with proper contrast */
--text-primary: #F8FAFC;
--text-secondary: #E2E8F0;
--text-tertiary: #CBD5E1;
--text-muted: #94A3B8;
/* Surface aliases — friendlier names */
--bg-page: var(--intel-darkest);
--bg-surface: var(--intel-dark);
--bg-elevated: var(--intel-medium);
--bg-hover: var(--intel-light);
--bg-input: rgba(30, 41, 59, 0.6);
--bg-overlay: rgba(10, 14, 39, 0.97);
/* ── Color · Foreground ─────────────────────────────────────── */
--text-primary: #F8FAFC;
--text-secondary: #E2E8F0;
--text-tertiary: #CBD5E1;
--text-muted: #94A3B8;
--text-disabled: #64748B;
--text-faint: #475569;
--text-on-accent: #0F172A;
/* Foreground aliases */
--fg-1: var(--text-primary);
--fg-2: var(--text-secondary);
--fg-3: var(--text-tertiary);
--fg-muted: var(--text-muted);
--fg-disabled: var(--text-disabled);
--fg-on-accent: var(--text-on-accent);
/* ── Color · Borders ────────────────────────────────────────── */
--border-subtle: rgba(14, 165, 233, 0.15);
--border-default: rgba(14, 165, 233, 0.25);
--border-strong: rgba(14, 165, 233, 0.40);
--border-focus: #0EA5E9;
--border-1: var(--border-subtle);
--border-2: var(--border-default);
--border-3: var(--border-strong);
/* ── Color · Brand accent (sky blue — primary signal) ───────── */
--intel-accent-bright: #38BDF8; /* sky-400 — text on dark */
--intel-accent-soft: #7DD3FC; /* sky-300 */
--intel-accent-15: rgba(14, 165, 233, 0.15);
--intel-accent-08: rgba(14, 165, 233, 0.08);
--accent: var(--intel-accent);
--accent-bright: var(--intel-accent-bright);
--accent-soft: var(--intel-accent-soft);
--accent-wash: var(--intel-accent-08);
--accent-hover: #0284C7; /* sky-600 — pressed/hover for filled buttons */
/* ── Color · Semantic / severity (FIXED — never remap) ──────── */
--intel-info: #0EA5E9; /* Medium · Info · Standard */
--sev-critical: var(--intel-danger);
--sev-high: var(--intel-warning);
--sev-medium: var(--intel-info);
--sev-low: var(--intel-success);
/* Severity text-on-dark (lighter; better contrast) */
--sev-critical-text: #FCA5A5;
--sev-high-text: #FCD34D;
--sev-medium-text: #7DD3FC;
--sev-low-text: #6EE7B7;
/* Severity fills */
--sev-critical-bg: rgba(239, 68, 68, 0.20);
--sev-high-bg: rgba(245, 158, 11, 0.20);
--sev-medium-bg: rgba(14, 165, 233, 0.20);
--sev-low-bg: rgba(16, 185, 129, 0.20);
/* ── Color · Group badges ───────────────────────────────────── */
--group-admin: #EF4444;
--group-standard: #38BDF8;
--group-leadership: #F59E0B;
--group-readonly: #94A3B8;
/* ── Type · Families ────────────────────────────────────────── */
--font-ui: 'Outfit', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
/* ── Type · Scale ───────────────────────────────────────────── */
--fs-display: 28px;
--fs-h1: 24px;
--fs-h2: 18px;
--fs-h3: 16px;
--fs-body: 14px;
--fs-sm: 13px;
--fs-xs: 12px;
--fs-tiny: 11px;
--lh-tight: 1.2;
--lh-normal: 1.4;
--lh-loose: 1.6;
--fw-regular: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-bold: 700;
--tracking-wide: 0.05em; /* mono buttons, badges */
--tracking-wider: 0.10em; /* uppercase headings */
/* ── Spacing (4-px grid) ────────────────────────────────────── */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 20px;
--sp-6: 24px;
--sp-8: 32px;
--sp-10: 40px;
--sp-12: 48px;
/* ── Radii ──────────────────────────────────────────────────── */
--r-xs: 3px;
--r-sm: 4px;
--r-md: 6px;
--r-lg: 8px;
--r-xl: 12px;
--r-pill: 999px;
/* ── Elevation (with sky-blue inner highlight) ──────────────── */
--shadow-rest: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-card: 0 4px 12px rgba(0, 0, 0, 0.4),
0 2px 6px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(14, 165, 233, 0.10);
--shadow-card-hover: 0 8px 24px rgba(14, 165, 233, 0.15),
0 4px 12px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(14, 165, 233, 0.20),
0 0 30px rgba(14, 165, 233, 0.10);
--shadow-popover: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.6),
0 10px 30px rgba(14, 165, 233, 0.10);
--shadow-focus: 0 0 0 2px rgba(14, 165, 233, 0.15);
/* Severity glow (used by status badge dots) */
--glow-danger: 0 0 12px rgba(239, 68, 68, 0.6),
0 0 6px rgba(239, 68, 68, 0.4);
--glow-warning: 0 0 12px rgba(245, 158, 11, 0.6),
0 0 6px rgba(245, 158, 11, 0.4);
--glow-info: 0 0 12px rgba(14, 165, 233, 0.6),
0 0 6px rgba(14, 165, 233, 0.4);
--glow-success: 0 0 12px rgba(16, 185, 129, 0.6),
0 0 6px rgba(16, 185, 129, 0.4);
/* Heading text-shadow glow */
--glow-heading: 0 0 16px rgba(14, 165, 233, 0.30),
0 0 32px rgba(14, 165, 233, 0.15);
/* ── Motion ─────────────────────────────────────────────────── */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--dur-fast: 150ms;
--dur-med: 200ms;
--dur-slow: 300ms;
/* ── Layout ─────────────────────────────────────────────────── */
--topbar-h: 64px;
--drawer-w: 240px;
--panel-w: 480px;
--content-max: 1600px;
--z-topbar: 50;
--z-drawer: 60;
--z-modal: 100;
--z-tooltip: 120;
}
body {
@@ -92,6 +252,65 @@ body {
opacity: 1;
}
/* ── Semantic type ───────────────────────────────────────────── */
.t-display {
font: var(--fw-bold) var(--fs-display)/var(--lh-tight) var(--font-mono);
color: var(--intel-accent-bright);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
text-shadow: var(--glow-heading);
}
.t-h1 {
font: var(--fw-bold) var(--fs-h1)/var(--lh-tight) var(--font-mono);
color: var(--intel-accent-bright);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
.t-h2 {
font: var(--fw-semibold) var(--fs-h2)/var(--lh-tight) var(--font-mono);
color: var(--text-primary);
text-transform: uppercase;
letter-spacing: var(--tracking-wide);
}
.t-h3 {
font: var(--fw-semibold) var(--fs-h3)/var(--lh-normal) var(--font-ui);
color: var(--text-primary);
}
.t-body {
font: var(--fw-regular) var(--fs-body)/var(--lh-normal) var(--font-ui);
color: var(--text-tertiary);
}
.t-sm {
font: var(--fw-regular) var(--fs-sm)/var(--lh-normal) var(--font-ui);
color: var(--text-tertiary);
}
.t-meta {
font: var(--fw-regular) var(--fs-xs)/var(--lh-normal) var(--font-ui);
color: var(--text-muted);
}
.t-label {
font: var(--fw-medium) var(--fs-xs)/var(--lh-normal) var(--font-mono);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: var(--tracking-wide);
}
.t-mono {
font: var(--fw-regular) var(--fs-sm)/var(--lh-normal) var(--font-mono);
color: var(--text-secondary);
}
.t-mono-sm {
font: var(--fw-regular) var(--fs-xs)/var(--lh-normal) var(--font-mono);
color: var(--text-muted);
}
.t-code {
font: var(--fw-medium) var(--fs-sm)/var(--lh-normal) var(--font-mono);
color: var(--intel-success);
background: var(--intel-darkest);
border: 1px solid var(--border-default);
padding: 1px 6px;
border-radius: var(--r-sm);
}
/* Scanning line animation */
.scan-line {
position: absolute;
@@ -123,14 +342,11 @@ body {
.intel-card {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%);
border: 1.5px solid rgba(14, 165, 233, 0.3);
border-radius: var(--r-lg);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.4),
0 2px 6px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(14, 165, 233, 0.1),
inset 0 -1px 0 rgba(14, 165, 233, 0.05);
box-shadow: var(--shadow-card);
}
.intel-card::after {
@@ -152,11 +368,7 @@ body {
.intel-card:hover {
border-color: rgba(14, 165, 233, 0.5);
transform: translateY(-2px);
box-shadow:
0 8px 24px rgba(14, 165, 233, 0.15),
0 4px 12px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(14, 165, 233, 0.2),
0 0 30px rgba(14, 165, 233, 0.1);
box-shadow: var(--shadow-card-hover);
}
.intel-card:hover::after {
@@ -166,13 +378,13 @@ body {
/* Status badges with STRONG glow and contrast */
.status-badge {
position: relative;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
padding: 0.375rem 0.875rem;
border-radius: 0.375rem;
border-radius: var(--r-md);
border: 2px solid;
display: inline-flex;
align-items: center;
@@ -239,13 +451,13 @@ body {
/* Button styles with depth and glow */
.intel-button {
position: relative;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
font-size: 0.875rem;
padding: 0.625rem 1.25rem;
border-radius: 0.375rem;
border-radius: var(--r-md);
transition: all 0.3s;
border: 1px solid;
overflow: hidden;
@@ -322,11 +534,11 @@ body {
/* Input fields with better contrast */
.intel-input {
background: rgba(30, 41, 59, 0.6);
border: 1px solid rgba(14, 165, 233, 0.25);
background: var(--bg-input);
border: 1px solid var(--border-subtle);
color: #F8FAFC;
padding: 0.625rem 1rem;
border-radius: 0.375rem;
border-radius: var(--r-md);
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
transition: all 0.3s;
@@ -337,11 +549,8 @@ body {
.intel-input:focus {
outline: none;
border-color: #0EA5E9;
box-shadow:
0 0 0 2px rgba(14, 165, 233, 0.15),
inset 0 2px 4px rgba(0, 0, 0, 0.15),
0 4px 12px rgba(14, 165, 233, 0.1);
border-color: var(--border-focus);
box-shadow: var(--shadow-focus);
background: rgba(30, 41, 59, 0.8);
}
@@ -353,25 +562,18 @@ body {
.stat-card {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 100%);
border: 1.5px solid rgba(14, 165, 233, 0.35);
border-radius: 0.5rem;
border-radius: var(--r-lg);
padding: 1rem;
position: relative;
overflow: hidden;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.4),
0 2px 6px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(14, 165, 233, 0.15);
box-shadow: var(--shadow-card);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
border-color: rgba(14, 165, 233, 0.5);
box-shadow:
0 8px 20px rgba(14, 165, 233, 0.15),
0 4px 12px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(14, 165, 233, 0.2),
0 0 24px rgba(14, 165, 233, 0.1);
box-shadow: var(--shadow-card-hover);
}
.stat-card::before {
@@ -388,16 +590,20 @@ body {
/* Modal overlay with proper backdrop */
.modal-overlay {
background: rgba(10, 14, 39, 0.97);
background: var(--bg-overlay);
backdrop-filter: blur(12px);
}
/* Modal card enhancements */
.intel-card.modal-card {
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.6),
0 10px 30px rgba(0, 217, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: var(--shadow-modal);
}
/* ── Focus ───────────────────────────────────────────────────── */
*:focus-visible {
outline: none;
border-color: var(--border-focus);
box-shadow: var(--shadow-focus);
}
/* Scrollbar styling */
@@ -407,12 +613,12 @@ body {
}
::-webkit-scrollbar-track {
background: #1E293B;
background: var(--intel-dark);
}
::-webkit-scrollbar-thumb {
background: rgba(14, 165, 233, 0.3);
border-radius: 4px;
border-radius: var(--r-sm);
}
::-webkit-scrollbar-thumb:hover {
@@ -447,7 +653,7 @@ body {
/* Data table styling */
.data-row {
border-bottom: 1px solid rgba(0, 217, 255, 0.1);
border-bottom: 1px solid var(--border-subtle);
transition: all 0.2s;
}
@@ -535,7 +741,7 @@ body {
/* Loading spinner */
.loading-spinner {
border: 2px solid rgba(14, 165, 233, 0.1);
border-top-color: #0EA5E9;
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -575,8 +781,8 @@ body {
pointer-events: none;
transition: opacity 0.3s;
margin-bottom: 0.5rem;
font-family: 'JetBrains Mono', monospace;
color: #F8FAFC;
font-family: var(--font-mono);
color: var(--fg-1);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.4),
0 0 16px rgba(14, 165, 233, 0.15);
@@ -620,8 +826,8 @@ h3.text-intel-accent {
/* Vendor Cards - nested depth */
.vendor-card {
background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%);
border: 1.5px solid rgba(14, 165, 233, 0.25);
border-radius: 0.5rem;
border: 1.5px solid var(--border-default);
border-radius: var(--r-lg);
padding: 1rem;
box-shadow:
0 3px 10px rgba(0, 0, 0, 0.4),
@@ -641,8 +847,8 @@ h3.text-intel-accent {
/* Document items - recessed appearance */
.document-item {
background: linear-gradient(135deg, rgba(15, 23, 42, 1) 0%, rgba(20, 28, 48, 0.98) 100%);
border: 1px solid rgba(14, 165, 233, 0.2);
border-radius: 0.375rem;
border: 1px solid var(--border-default);
border-radius: var(--r-md);
padding: 0.75rem;
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.3),
@@ -681,7 +887,7 @@ h3.text-intel-accent {
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid rgba(14, 165, 233, 0.3);
font-family: monospace;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.05em;
}
@@ -692,7 +898,7 @@ h3.text-intel-accent {
color: #10B981;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-family: monospace;
font-family: var(--font-mono);
}
.markdown-content h3 {
@@ -701,7 +907,7 @@ h3.text-intel-accent {
color: #F59E0B;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
font-family: monospace;
font-family: var(--font-mono);
}
.markdown-content h4,
@@ -747,7 +953,7 @@ h3.text-intel-accent {
border: 1px solid rgba(14, 165, 233, 0.2);
border-radius: 0.25rem;
padding: 0.125rem 0.375rem;
font-family: 'Courier New', monospace;
font-family: var(--font-mono);
font-size: 0.9em;
color: #10B981;
}
@@ -799,7 +1005,7 @@ h3.text-intel-accent {
background: rgba(14, 165, 233, 0.1);
color: #0EA5E9;
font-weight: 600;
font-family: monospace;
font-family: var(--font-mono);
}
.markdown-content td {

View File

@@ -32,110 +32,114 @@ const STYLES = {
position: 'relative',
overflow: 'hidden',
},
// Stat cards with refined borders
// Stat cards with Card_Surface gradient and top-edge rail
statCard: {
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 100%)',
border: '2px solid #0EA5E9',
borderRadius: '0.5rem',
padding: '1rem',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.15)',
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1.5px solid rgba(14,165,233,0.12)',
borderRadius: 8,
padding: 16,
boxShadow: 'var(--shadow-card)',
position: 'relative',
overflow: 'hidden',
},
// Intel card with refined glowing border
// Intel card with Card_Surface gradient
intelCard: {
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
border: '2px solid rgba(14, 165, 233, 0.4)',
borderRadius: '0.5rem',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12)',
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1.5px solid rgba(14,165,233,0.12)',
borderRadius: 8,
boxShadow: 'var(--shadow-card)',
position: 'relative',
overflow: 'hidden',
},
// Vendor card with depth
// Vendor card — nested Card_Surface gradient matching VendorEntry
vendorCard: {
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%)',
border: '1.5px solid rgba(14, 165, 233, 0.3)',
borderRadius: '0.5rem',
padding: '1rem',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(14, 165, 233, 0.08)',
marginBottom: '0.75rem',
background: 'linear-gradient(135deg, rgba(15,23,42,0.95) 0%, rgba(30,41,59,0.9) 100%)',
border: '1.5px solid rgba(14,165,233,0.30)',
borderRadius: 6,
padding: 16,
boxShadow: '0 4px 12px rgba(0,0,0,0.4), inset 0 1px 0 rgba(14,165,233,0.08)',
marginBottom: 12,
},
// CRITICAL severity badge - Modern red with refined glow
// CRITICAL severity badge — matching SeverityBadge in HomePrimitives
badgeCritical: {
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.25) 0%, rgba(239, 68, 68, 0.2) 100%)',
gap: 6,
background: 'linear-gradient(135deg, rgba(239,68,68,0.25), rgba(239,68,68,0.20))',
border: '2px solid #EF4444',
borderRadius: '0.375rem',
padding: '0.375rem 0.875rem',
borderRadius: 6,
padding: '4px 10px',
color: '#FCA5A5',
fontWeight: '700',
fontSize: '0.75rem',
fontWeight: 700,
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: '0.5px',
textShadow: '0 0 8px rgba(239, 68, 68, 0.5)',
boxShadow: '0 0 16px rgba(239, 68, 68, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4)',
letterSpacing: '0.05em',
fontFamily: 'var(--font-mono)',
textShadow: '0 0 8px rgba(239,68,68,0.5)',
boxShadow: '0 0 16px rgba(239,68,68,0.25), 0 4px 8px rgba(0,0,0,0.4)',
},
// HIGH severity badge - Amber with refined glow
// HIGH severity badge
badgeHigh: {
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
background: 'linear-gradient(135deg, rgba(245, 158, 11, 0.25) 0%, rgba(245, 158, 11, 0.2) 100%)',
gap: 6,
background: 'linear-gradient(135deg, rgba(245,158,11,0.25), rgba(245,158,11,0.20))',
border: '2px solid #F59E0B',
borderRadius: '0.375rem',
padding: '0.375rem 0.875rem',
borderRadius: 6,
padding: '4px 10px',
color: '#FCD34D',
fontWeight: '700',
fontSize: '0.75rem',
fontWeight: 700,
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: '0.5px',
textShadow: '0 0 8px rgba(245, 158, 11, 0.5)',
boxShadow: '0 0 16px rgba(245, 158, 11, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4)',
letterSpacing: '0.05em',
fontFamily: 'var(--font-mono)',
textShadow: '0 0 8px rgba(245,158,11,0.5)',
boxShadow: '0 0 16px rgba(245,158,11,0.25), 0 4px 8px rgba(0,0,0,0.4)',
},
// MEDIUM severity badge - Sky blue with refined glow
// MEDIUM severity badge
badgeMedium: {
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
background: 'linear-gradient(135deg, rgba(14, 165, 233, 0.25) 0%, rgba(14, 165, 233, 0.2) 100%)',
gap: 6,
background: 'linear-gradient(135deg, rgba(14,165,233,0.25), rgba(14,165,233,0.20))',
border: '2px solid #0EA5E9',
borderRadius: '0.375rem',
padding: '0.375rem 0.875rem',
borderRadius: 6,
padding: '4px 10px',
color: '#7DD3FC',
fontWeight: '700',
fontSize: '0.75rem',
fontWeight: 700,
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: '0.5px',
textShadow: '0 0 8px rgba(14, 165, 233, 0.5)',
boxShadow: '0 0 16px rgba(14, 165, 233, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4)',
letterSpacing: '0.05em',
fontFamily: 'var(--font-mono)',
textShadow: '0 0 8px rgba(14,165,233,0.5)',
boxShadow: '0 0 16px rgba(14,165,233,0.25), 0 4px 8px rgba(0,0,0,0.4)',
},
// LOW severity badge - Emerald with refined glow
// LOW severity badge
badgeLow: {
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.25) 0%, rgba(16, 185, 129, 0.2) 100%)',
gap: 6,
background: 'linear-gradient(135deg, rgba(16,185,129,0.25), rgba(16,185,129,0.20))',
border: '2px solid #10B981',
borderRadius: '0.375rem',
padding: '0.375rem 0.875rem',
borderRadius: 6,
padding: '4px 10px',
color: '#6EE7B7',
fontWeight: '700',
fontSize: '0.75rem',
fontWeight: 700,
fontSize: 11,
textTransform: 'uppercase',
letterSpacing: '0.5px',
textShadow: '0 0 8px rgba(16, 185, 129, 0.5)',
boxShadow: '0 0 16px rgba(16, 185, 129, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4)',
letterSpacing: '0.05em',
fontFamily: 'var(--font-mono)',
textShadow: '0 0 8px rgba(16,185,129,0.5)',
boxShadow: '0 0 16px rgba(16,185,129,0.25), 0 4px 8px rgba(0,0,0,0.4)',
},
// Glowing dot for badges
// Glowing dot for badges — matching SeverityBadge dot
glowDot: (color) => ({
width: '8px',
height: '8px',
display: 'inline-block',
width: 6,
height: 6,
borderRadius: '50%',
background: color,
boxShadow: `0 0 12px ${color}, 0 0 6px ${color}`,
animation: 'pulse 2s ease-in-out infinite',
boxShadow: `0 0 8px ${color}`,
}),
};
@@ -955,8 +959,98 @@ export default function App() {
const filteredGroupedCVEs = groupedCVEs;
// Navigation tab definitions for the top bar
const navTabs = [
{ id: 'home', label: 'Home' },
{ id: 'triage', label: 'Reporting' },
{ id: 'compliance', label: 'Compliance' },
{ id: 'knowledge-base', label: 'Knowledge Base' },
{ id: 'exports', label: 'Exports' },
];
return (
<div className="min-h-screen bg-intel-darkest grid-bg p-6 relative overflow-hidden fade-in">
<div className="min-h-screen bg-intel-darkest grid-bg relative overflow-hidden fade-in" style={{ paddingTop: 'var(--topbar-h)' }}>
{/* Top Bar */}
<header style={{
height: 'var(--topbar-h)', position: 'fixed', top: 0, left: 0, right: 0, zIndex: 'var(--z-topbar)',
background: 'linear-gradient(180deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%)',
backdropFilter: 'blur(12px)',
borderBottom: '1px solid var(--border-1)',
display: 'flex', alignItems: 'center', padding: '0 20px', gap: 16,
}}>
<button
onClick={() => setNavOpen(true)}
style={{
background: 'transparent', border: 'none', color: 'var(--fg-2)',
cursor: 'pointer', padding: 6, display: 'flex', alignItems: 'center',
}}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--accent)'; }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--fg-2)'; }}
title="Navigation"
>
<Menu size={20} />
</button>
{/* Brand mark — typographic stack */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Shield size={22} style={{ color: 'var(--accent)' }} />
<div>
<div style={{ fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 15, color: 'var(--fg-1)', letterSpacing: '0.02em', lineHeight: 1 }}>STEAM</div>
<div style={{ fontFamily: 'var(--font-ui)', fontWeight: 500, fontSize: 9, color: 'var(--fg-muted)', letterSpacing: '0.18em', marginTop: 2 }}>SECURITY</div>
</div>
</div>
{/* Navigation tabs */}
<nav style={{ display: 'flex', gap: 2, marginLeft: 24 }}>
{navTabs.map(tab => {
const active = currentPage === tab.id;
return (
<button
key={tab.id}
onClick={() => {
if (tab.id === 'triage') { setCalendarFilter(null); setReportingExcFilter(null); }
setCurrentPage(tab.id);
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--bg-elevated)'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
style={{
background: active ? 'rgba(14, 165, 233, 0.15)' : 'transparent',
color: active ? 'var(--accent)' : 'var(--fg-2)',
border: 'none', borderRadius: 6,
padding: '8px 12px', fontFamily: 'var(--font-ui)', fontWeight: active ? 600 : 500, fontSize: 13,
cursor: 'pointer', transition: 'background 150ms, color 150ms',
}}
>{tab.label}</button>
);
})}
</nav>
<div style={{ flex: 1 }} />
{/* Action buttons + User menu */}
<div className="flex items-center gap-3">
{canWrite() && (
<button
onClick={() => setShowNvdSync(true)}
className="intel-button intel-button-success relative z-10 flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" />
NVD Sync
</button>
)}
{canWrite() && (
<button
onClick={() => setShowAddCVE(true)}
className="intel-button intel-button-primary relative z-10 flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Add Entry
</button>
)}
<UserMenu onManageUsers={() => setShowUserManagement(true)} onAuditLog={() => setShowAuditLog(true)} />
</div>
</header>
<NavDrawer
isOpen={navOpen}
onClose={() => setNavOpen(false)}
@@ -970,71 +1064,31 @@ export default function App() {
{/* Scanning line effect */}
<div className="scan-line"></div>
<div className={`${currentPage === 'triage' ? 'w-full' : 'max-w-7xl mx-auto'} relative z-10`}>
{/* Header */}
<div className="mb-8">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-4 flex-1">
<button
onClick={() => setNavOpen(true)}
style={{ background: 'none', border: '1px solid rgba(14, 165, 233, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', cursor: 'pointer', color: '#64748B', flexShrink: 0 }}
onMouseEnter={e => { e.currentTarget.style.color = '#0EA5E9'; e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.6)'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#64748B'; e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.25)'; }}
title="Navigation"
>
<Menu className="w-5 h-5" />
</button>
<div>
<h1 className="text-4xl font-bold text-intel-accent mb-1 font-mono tracking-tight">
STEAM Security Dashboard
</h1>
<p className="text-gray-400 text-sm font-sans">NTS Threat Intelligence and Metric Aggregation</p>
</div>
</div>
<div className="flex items-center gap-3">
{canWrite() && (
<button
onClick={() => setShowNvdSync(true)}
className="intel-button intel-button-success relative z-10 flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" />
NVD Sync
</button>
)}
{canWrite() && (
<button
onClick={() => setShowAddCVE(true)}
className="intel-button intel-button-primary relative z-10 flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Add Entry
</button>
)}
<UserMenu onManageUsers={() => setShowUserManagement(true)} onAuditLog={() => setShowAuditLog(true)} />
</div>
</div>
<div className={`${currentPage === 'triage' ? 'w-full' : 'max-w-7xl mx-auto'} relative z-10 p-6`}>
{/* Header — Stats Bar area */}
<div className="mb-4">
{/* Stats Bar - only shown on Home page */}
{currentPage === 'home' && <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div style={STYLES.statCard}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, #0EA5E9, transparent)', boxShadow: '0 0 8px rgba(14, 165, 233, 0.5)' }}></div>
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#CBD5E1', marginBottom: '0.25rem' }}>Total CVEs</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#0EA5E9', textShadow: '0 0 16px rgba(14, 165, 233, 0.4)' }}>{Object.keys(filteredGroupedCVEs).length}</div>
<div style={{...STYLES.statCard, border: '2px solid #0EA5E9', boxShadow: '0 4px 16px rgba(0,0,0,0.5), 0 0 20px rgba(14,165,233,0.15), inset 0 1px 0 rgba(14,165,233,0.15)'}}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(90deg, transparent, #0EA5E9, transparent)', boxShadow: '0 0 8px rgba(14,165,233,0.50)' }}></div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em', color: 'var(--fg-2)', marginBottom: 4 }}>Total CVEs</div>
<div style={{ fontSize: 24, fontWeight: 700, fontFamily: 'var(--font-mono)', color: '#0EA5E9', textShadow: '0 0 16px rgba(14,165,233,0.4)', lineHeight: 1 }}>{Object.keys(filteredGroupedCVEs).length}</div>
</div>
<div style={STYLES.statCard}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, #0EA5E9, transparent)', boxShadow: '0 0 8px rgba(14, 165, 233, 0.5)' }}></div>
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#CBD5E1', marginBottom: '0.25rem' }}>Vendor Entries</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#E2E8F0' }}>{cves.length}</div>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(90deg, transparent, #0EA5E9, transparent)', boxShadow: '0 0 8px rgba(14,165,233,0.50)' }}></div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em', color: 'var(--fg-2)', marginBottom: 4 }}>Vendor Entries</div>
<div style={{ fontSize: 24, fontWeight: 700, fontFamily: 'var(--font-mono)', color: 'var(--fg-1)', lineHeight: 1 }}>{cves.length}</div>
</div>
<div style={{...STYLES.statCard, border: '2px solid #F59E0B', boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(245, 158, 11, 0.15), inset 0 1px 0 rgba(245, 158, 11, 0.15)'}}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, #F59E0B, transparent)', boxShadow: '0 0 8px rgba(245, 158, 11, 0.5)' }}></div>
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#CBD5E1', marginBottom: '0.25rem' }}>Open Tickets</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#F59E0B', textShadow: '0 0 16px rgba(245, 158, 11, 0.4)' }}>{jiraTickets.filter(t => t.status !== 'Closed').length}</div>
<div style={{...STYLES.statCard, border: '2px solid #F59E0B', boxShadow: '0 4px 16px rgba(0,0,0,0.5), 0 0 20px rgba(245,158,11,0.15), inset 0 1px 0 rgba(245,158,11,0.15)'}}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(90deg, transparent, #F59E0B, transparent)', boxShadow: '0 0 8px rgba(245,158,11,0.50)' }}></div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em', color: 'var(--fg-2)', marginBottom: 4 }}>Open Tickets</div>
<div style={{ fontSize: 24, fontWeight: 700, fontFamily: 'var(--font-mono)', color: '#F59E0B', textShadow: '0 0 16px rgba(245,158,11,0.4)', lineHeight: 1 }}>{jiraTickets.filter(t => t.status !== 'Closed').length}</div>
</div>
<div style={{...STYLES.statCard, border: '2px solid #EF4444', boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(239, 68, 68, 0.15), inset 0 1px 0 rgba(239, 68, 68, 0.15)'}}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, transparent, #EF4444, transparent)', boxShadow: '0 0 8px rgba(239, 68, 68, 0.5)' }}></div>
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#CBD5E1', marginBottom: '0.25rem' }}>Critical</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#EF4444', textShadow: '0 0 16px rgba(239, 68, 68, 0.4)' }}>{cves.filter(c => c.severity === 'Critical').length}</div>
<div style={{...STYLES.statCard, border: '2px solid #EF4444', boxShadow: '0 4px 16px rgba(0,0,0,0.5), 0 0 20px rgba(239,68,68,0.15), inset 0 1px 0 rgba(239,68,68,0.15)'}}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(90deg, transparent, #EF4444, transparent)', boxShadow: '0 0 8px rgba(239,68,68,0.50)' }}></div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em', color: 'var(--fg-2)', marginBottom: 4 }}>Critical</div>
<div style={{ fontSize: 24, fontWeight: 700, fontFamily: 'var(--font-mono)', color: '#EF4444', textShadow: '0 0 16px rgba(239,68,68,0.4)', lineHeight: 1 }}>{cves.filter(c => c.severity === 'Critical').length}</div>
</div>
</div>}
</div>
@@ -1659,9 +1713,9 @@ export default function App() {
<div className="col-span-12 lg:col-span-9 space-y-4">
<>
{/* Quick Check */}
<div style={{...STYLES.intelCard, padding: '1.5rem'}} className="rounded-lg">
<div style={{...STYLES.intelCard, padding: 24}} className="rounded-lg">
<div className="scan-line"></div>
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#0EA5E9', marginBottom: '0.75rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 16px rgba(14, 165, 233, 0.4)' }}>Quick CVE Lookup</h2>
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 700, color: '#0EA5E9', marginBottom: 12, textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 16px rgba(14,165,233,0.4)' }}>Quick CVE Lookup</h2>
<div className="flex gap-3">
<input
type="text"
@@ -1680,28 +1734,28 @@ export default function App() {
</div>
{quickCheckResult && (
<div className={`mt-4 p-4 rounded border ${quickCheckResult.exists ? 'bg-intel-success/10 border-intel-success/30' : 'bg-intel-warning/10 border-intel-warning/30'}`}>
<div style={{ marginTop: 16 }}>
{quickCheckResult.error ? (
<div className="flex items-start gap-3">
<XCircle className="w-5 h-5 text-intel-danger mt-0.5" />
<div>
<p className="font-medium text-intel-danger font-mono">Error</p>
<p className="text-sm text-gray-300">{quickCheckResult.error}</p>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, borderRadius: 6, background: 'rgba(239,68,68,0.10)', border: '1px solid rgba(239,68,68,0.30)' }}>
<XCircle style={{ width: 18, height: 18, color: '#EF4444', marginTop: 1, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: '#EF4444' }}>Error</div>
<p style={{ margin: '8px 0 0', fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.5 }}>{quickCheckResult.error}</p>
</div>
</div>
) : quickCheckResult.exists ? (
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-intel-success mt-0.5" />
<div className="flex-1">
<p className="font-medium text-intel-success font-mono"> CVE Addressed ({quickCheckResult.vendors.length} vendor{quickCheckResult.vendors.length > 1 ? 's' : ''})</p>
<div className="mt-3 space-y-3">
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, borderRadius: 6, background: 'rgba(16,185,129,0.10)', border: '1px solid rgba(16,185,129,0.30)' }}>
<CheckCircle style={{ width: 18, height: 18, color: '#10B981', marginTop: 1, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: '#10B981', marginBottom: 8 }}> CVE Addressed ({quickCheckResult.vendors.length} vendor{quickCheckResult.vendors.length > 1 ? 's' : ''})</div>
<div style={{ display: 'grid', gap: 10, marginTop: 8 }}>
{quickCheckResult.vendors.map((vendorInfo, idx) => (
<div key={idx} className="p-3 bg-intel-dark/70 rounded border border-intel-accent/30 shadow-lg">
<p className="font-semibold text-white mb-2 font-sans">{vendorInfo.vendor}</p>
<div className="grid grid-cols-2 gap-2 text-sm text-gray-300 mb-2 font-mono">
<p><strong className="text-white">Severity:</strong> {vendorInfo.severity}</p>
<p><strong className="text-white">Status:</strong> {vendorInfo.status}</p>
<p><strong className="text-white">Documents:</strong> {vendorInfo.total_documents} attached</p>
<div key={idx} style={{ padding: 12, background: 'rgba(15,23,42,0.7)', border: '1px solid rgba(14,165,233,0.30)', borderRadius: 6, boxShadow: '0 4px 8px rgba(0,0,0,0.3)' }}>
<p style={{ fontFamily: 'var(--font-ui)', fontSize: 13, fontWeight: 600, color: 'var(--fg-1)', marginBottom: 6 }}>{vendorInfo.vendor}</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>
<span><strong style={{ color: 'var(--fg-1)' }}>Severity:</strong> {vendorInfo.severity}</span>
<span><strong style={{ color: 'var(--fg-1)' }}>Status:</strong> {vendorInfo.status}</span>
<span><strong style={{ color: 'var(--fg-1)' }}>Documents:</strong> {vendorInfo.total_documents} attached</span>
</div>
</div>
))}
@@ -1709,11 +1763,11 @@ export default function App() {
</div>
</div>
) : (
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-intel-warning mt-0.5" />
<div>
<p className="font-medium text-intel-warning font-mono">Not Found</p>
<p className="text-sm text-gray-300">This CVE has not been addressed yet. No entry exists in the database.</p>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, borderRadius: 6, background: 'rgba(245,158,11,0.10)', border: '1px solid rgba(245,158,11,0.30)' }}>
<AlertCircle style={{ width: 18, height: 18, color: '#F59E0B', marginTop: 1, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: '#F59E0B' }}>Not Found</div>
<p style={{ margin: '8px 0 0', fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.5 }}>This CVE has not been addressed yet. No entry exists in the database.</p>
</div>
</div>
)}
@@ -1722,11 +1776,11 @@ export default function App() {
</div>
{/* Search and Filters */}
<div style={{...STYLES.intelCard, padding: '1.5rem'}} className="rounded-lg">
<div style={{...STYLES.intelCard, padding: 24}} className="rounded-lg">
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
<Search className="inline w-4 h-4 mr-1" />
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>
<Search className="inline w-4 h-4" />
Search CVEs
</label>
<input
@@ -1734,20 +1788,23 @@ export default function App() {
placeholder="CVE ID or description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="intel-input w-full"
style={{ width: '100%', boxSizing: 'border-box', background: 'rgba(15,23,42,0.85)', border: '1px solid rgba(14,165,233,0.25)', borderRadius: 6, padding: '9px 12px', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: 13, outline: 'none', transition: 'border-color 160ms ease' }}
className="intel-input"
onFocus={e => { e.target.style.borderColor = '#0EA5E9'; e.target.style.boxShadow = '0 0 0 3px rgba(14,165,233,0.15)'; }}
onBlur={e => { e.target.style.borderColor = 'rgba(14,165,233,0.25)'; e.target.style.boxShadow = 'none'; }}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
<Filter className="inline w-4 h-4 mr-1" />
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>
<Filter className="inline w-4 h-4" />
Vendor
</label>
<select
value={selectedVendor}
onChange={(e) => setSelectedVendor(e.target.value)}
className="intel-input w-full"
style={{ width: '100%', boxSizing: 'border-box', background: 'rgba(15,23,42,0.85)', border: '1px solid rgba(14,165,233,0.25)', borderRadius: 6, padding: '9px 12px', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: 13, outline: 'none', appearance: 'none', backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230EA5E9' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E")`, backgroundRepeat: 'no-repeat', backgroundPosition: 'right 12px center', paddingRight: 32 }}
>
{vendors.map(vendor => (
<option key={vendor} value={vendor}>{vendor}</option>
@@ -1756,14 +1813,14 @@ export default function App() {
</div>
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
<AlertCircle className="inline w-4 h-4 mr-1" />
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>
<AlertCircle className="inline w-4 h-4" />
Severity
</label>
<select
value={selectedSeverity}
onChange={(e) => setSelectedSeverity(e.target.value)}
className="intel-input w-full"
style={{ width: '100%', boxSizing: 'border-box', background: 'rgba(15,23,42,0.85)', border: '1px solid rgba(14,165,233,0.25)', borderRadius: 6, padding: '9px 12px', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: 13, outline: 'none', appearance: 'none', backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230EA5E9' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E")`, backgroundRepeat: 'no-repeat', backgroundPosition: 'right 12px center', paddingRight: 32 }}
>
{severityLevels.map(level => (
<option key={level} value={level}>{level}</option>
@@ -1824,36 +1881,37 @@ export default function App() {
const overallStatuses = [...new Set(vendorEntries.map(e => e.status))];
return (
<div key={cveId} style={STYLES.intelCard} className="rounded-lg">
<div key={cveId} style={{...STYLES.intelCard, borderRadius: 8, transition: 'border-color 200ms ease'}} className="rounded-lg">
{/* Clickable CVE Header */}
<div
style={{ padding: '1.5rem', cursor: 'pointer', transition: 'all 0.2s', userSelect: 'none' }}
style={{ padding: 24, cursor: 'pointer', transition: 'all 0.2s', userSelect: 'none' }}
onClick={() => toggleCVEExpand(cveId)}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-1">
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 6 }}>
<ChevronDown
className={`w-5 h-5 text-intel-accent transition-transform duration-200 flex-shrink-0 ${isCVEExpanded ? 'rotate-0' : '-rotate-90'}`}
style={{ display: 'inline-block', transform: isCVEExpanded ? 'rotate(0)' : 'rotate(-90deg)', transition: 'transform 200ms ease', color: '#0EA5E9', flexShrink: 0 }}
className="w-5 h-5"
/>
<h3 className="text-2xl font-bold text-intel-accent font-mono tracking-tight">{cveId}</h3>
<h3 style={{ margin: 0, fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700, color: '#0EA5E9', letterSpacing: '-0.01em' }}>{cveId}</h3>
</div>
{/* Collapsed: truncated description + summary row */}
{!isCVEExpanded && (
<div className="ml-8">
<p style={{ color: '#E4E8F1', fontSize: '0.875rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: '0.5rem' }}>{vendorEntries[0].description}</p>
<div className="flex items-center gap-3 flex-wrap">
<div style={{ marginLeft: 30 }}>
<p style={{ margin: '0 0 8px 0', color: 'var(--fg-1)', fontSize: 13, lineHeight: 1.5, fontFamily: 'var(--font-ui)', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical', overflow: 'hidden', textOverflow: 'ellipsis' }}>{vendorEntries[0].description}</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
<span style={getSeverityBadgeStyle(highestSeverity)}>
<span style={STYLES.glowDot(getSeverityDotColor(highestSeverity))}></span>
{highestSeverity}
</span>
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace' }}>{vendorEntries.length} vendor{vendorEntries.length > 1 ? 's' : ''}</span>
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>{vendorEntries.length} vendor{vendorEntries.length > 1 ? 's' : ''}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<FileText className="w-3 h-3" />
{totalDocCount} doc{totalDocCount !== 1 ? 's' : ''}
</span>
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>
{overallStatuses.join(', ')}
</span>
</div>
@@ -1898,15 +1956,15 @@ export default function App() {
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h4 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#FFFFFF' }}>{cve.vendor}</h4>
<h4 style={{ fontSize: 15, fontWeight: 600, color: 'var(--fg-1)', fontFamily: 'var(--font-ui)', margin: 0 }}>{cve.vendor}</h4>
<span style={getSeverityBadgeStyle(cve.severity)}>
<span style={STYLES.glowDot(getSeverityDotColor(cve.severity))}></span>
{cve.severity}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', fontSize: '0.875rem', color: '#E4E8F1', fontFamily: 'monospace' }}>
<span>Status: <span style={{ fontWeight: '500', color: '#FFFFFF' }}>{cve.status}</span></span>
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
<span>Status: <span style={{ fontWeight: 500, color: 'var(--fg-1)' }}>{cve.status}</span></span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<FileText className="w-4 h-4" />
{cve.document_count} doc{cve.document_count !== 1 ? 's' : ''}
</span>
@@ -2032,7 +2090,7 @@ export default function App() {
{vendorTickets.length > 0 ? (
<div className="space-y-2">
{vendorTickets.map(ticket => (
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)', border: '1px solid rgba(255, 184, 0, 0.3)', borderRadius: '0.375rem', padding: '0.75rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.04)' }} className="flex items-center justify-between">
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(19,25,55,0.85), rgba(30,39,73,0.75))', border: '1px solid rgba(245,158,11,0.30)', borderRadius: 6, padding: 10, boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.04)' }} className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
<a
href={ticket.url || '#'}
@@ -2042,7 +2100,7 @@ export default function App() {
>
{ticket.ticket_key}
</a>
{ticket.summary && <span style={{ fontSize: '0.875rem', color: '#E4E8F1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '20rem' }}>{ticket.summary}</span>}
{ticket.summary && <span style={{ fontSize: 12, color: 'var(--fg-1)', fontFamily: 'var(--font-ui)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '20rem' }}>{ticket.summary}</span>}
<span style={
ticket.status === 'Open' ? STYLES.badgeCritical :
ticket.status === 'In Progress' ? STYLES.badgeHigh :
@@ -2137,8 +2195,8 @@ export default function App() {
{/* RIGHT PANEL - Calendar & Open Tickets */}
<div className="col-span-12 lg:col-span-3 space-y-4">
{/* Calendar Widget */}
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '3px solid #0EA5E9'}} className="rounded-lg">
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#0EA5E9', marginBottom: '1rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(14, 165, 233, 0.4)' }}>
<div style={{...STYLES.intelCard, padding: 20, borderLeft: '3px solid #0EA5E9'}} className="rounded-lg">
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600, color: '#0EA5E9', marginBottom: 12, textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(14,165,233,0.4)', display: 'flex', alignItems: 'center', gap: 8 }}>
Calendar
</h2>
@@ -2151,9 +2209,9 @@ export default function App() {
</div>
{/* Open Vendor Tickets */}
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '3px solid #F59E0B'}} className="rounded-lg">
<div style={{...STYLES.intelCard, padding: 20, borderLeft: '3px solid #F59E0B'}} className="rounded-lg">
<div className="flex justify-between items-center mb-4">
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#F59E0B', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(245, 158, 11, 0.4)' }}>
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600, color: '#F59E0B', display: 'flex', alignItems: 'center', gap: 8, textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(245,158,11,0.4)' }}>
<AlertCircle className="w-5 h-5" />
Open Tickets
</h2>
@@ -2166,15 +2224,15 @@ export default function App() {
</button>
)}
</div>
<div className="text-center mb-3">
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#F59E0B', textShadow: '0 0 16px rgba(245, 158, 11, 0.4)' }}>
<div style={{ textAlign: 'center', marginBottom: 12 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 32, fontWeight: 700, color: '#F59E0B', textShadow: '0 0 16px rgba(245,158,11,0.4)', lineHeight: 1 }}>
{jiraTickets.filter(t => t.status !== 'Closed').length}
</div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Active</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.12em', marginTop: 6 }}>Active</div>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{jiraTickets.filter(t => t.status !== 'Closed').slice(0, 10).map(ticket => (
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)', border: '1px solid rgba(245, 158, 11, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)' }}>
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(30,41,59,0.85), rgba(51,65,85,0.75))', border: '1px solid rgba(245,158,11,0.25)', borderRadius: 6, padding: 10, boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03)' }}>
<div className="flex items-start justify-between gap-2 mb-1">
<a
href={ticket.url || '#'}
@@ -2218,9 +2276,9 @@ export default function App() {
</div>
{/* Archer Risk Acceptance Tickets */}
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '3px solid #8B5CF6'}} className="rounded-lg">
<div style={{...STYLES.intelCard, padding: 20, borderLeft: '3px solid #8B5CF6'}} className="rounded-lg">
<div className="flex justify-between items-center mb-4">
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#8B5CF6', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(139, 92, 246, 0.4)' }}>
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600, color: '#8B5CF6', display: 'flex', alignItems: 'center', gap: 8, textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(139,92,246,0.4)' }}>
<Shield className="w-5 h-5" />
Archer Risk Tickets
</h2>
@@ -2233,15 +2291,15 @@ export default function App() {
</button>
)}
</div>
<div className="text-center mb-3">
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#8B5CF6', textShadow: '0 0 16px rgba(139, 92, 246, 0.4)' }}>
<div style={{ textAlign: 'center', marginBottom: 12 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 32, fontWeight: 700, color: '#8B5CF6', textShadow: '0 0 16px rgba(139,92,246,0.4)', lineHeight: 1 }}>
{archerTickets.filter(t => t.status !== 'Accepted').length}
</div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Active</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.12em', marginTop: 6 }}>Active</div>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{archerTickets.filter(t => t.status !== 'Accepted').slice(0, 10).map(ticket => (
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)', border: '1px solid rgba(139, 92, 246, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)' }}>
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(30,41,59,0.85), rgba(51,65,85,0.75))', border: '1px solid rgba(139,92,246,0.25)', borderRadius: 6, padding: 10, boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03)' }}>
<div className="flex items-start justify-between gap-2 mb-1">
<a
href={ticket.archer_url || '#'}
@@ -2291,9 +2349,9 @@ export default function App() {
</div>
{/* Ivanti Workflows */}
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '3px solid #0D9488'}} className="rounded-lg">
<div style={{...STYLES.intelCard, padding: 20, borderLeft: '3px solid #0D9488'}} className="rounded-lg">
<div className="flex justify-between items-center mb-1">
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#0D9488', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(13, 148, 136, 0.4)' }}>
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600, color: '#0D9488', display: 'flex', alignItems: 'center', gap: 8, textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(13,148,136,0.4)' }}>
<Activity className="w-5 h-5" />
Ivanti Workflows
</h2>
@@ -2389,11 +2447,11 @@ export default function App() {
</div>
) : ivantiSyncStatus === 'error' ? (
<>
<div className="text-center mb-3">
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#0D9488', textShadow: '0 0 16px rgba(13, 148, 136, 0.4)' }}>
<div style={{ textAlign: 'center', marginBottom: 12 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 32, fontWeight: 700, color: '#0D9488', textShadow: '0 0 16px rgba(13,148,136,0.4)', lineHeight: 1 }}>
{ivantiTotal ?? '—'}
</div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Total Workflows</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.12em', marginTop: 6 }}>Total Workflows</div>
</div>
<div className="flex items-start gap-2 p-2 rounded" style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)' }}>
<AlertCircle className="w-4 h-4 text-intel-danger mt-0.5 shrink-0" />
@@ -2402,15 +2460,15 @@ export default function App() {
</>
) : (
<>
<div className="text-center mb-3">
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#0D9488', textShadow: '0 0 16px rgba(13, 148, 136, 0.4)' }}>
<div style={{ textAlign: 'center', marginBottom: 12 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 32, fontWeight: 700, color: '#0D9488', textShadow: '0 0 16px rgba(13,148,136,0.4)', lineHeight: 1 }}>
{ivantiSyncStatus === 'never' ? '—' : (ivantiTotal ?? '—')}
</div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Total Workflows</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.12em', marginTop: 6 }}>Total Workflows</div>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto">
{ivantiWorkflows.slice(0, 10).map((wf, idx) => (
<div key={wf.uuid ?? idx} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)', border: '1px solid rgba(13, 148, 136, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)' }}>
<div key={wf.uuid ?? idx} style={{ background: 'linear-gradient(135deg, rgba(30,41,59,0.85), rgba(51,65,85,0.75))', border: '1px solid rgba(13,148,136,0.25)', borderRadius: 6, padding: 10, boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03)' }}>
<div className="flex items-start justify-between gap-2 mb-1">
<span className="font-mono text-xs font-semibold text-teal-300">
{wf.id?.value || wf.uuid?.slice(0, 8)}

View File

@@ -4,19 +4,19 @@ import { X, Loader, AlertCircle, ChevronLeft, ChevronRight, Search } from 'lucid
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const ACTION_BADGES = {
login: { bg: 'bg-green-100', text: 'text-green-800' },
logout: { bg: 'bg-gray-100', text: 'text-gray-800' },
login_failed: { bg: 'bg-red-100', text: 'text-red-800' },
cve_create: { bg: 'bg-blue-100', text: 'text-blue-800' },
cve_update_status: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
document_upload: { bg: 'bg-purple-100', text: 'text-purple-800' },
document_delete: { bg: 'bg-red-100', text: 'text-red-800' },
user_create: { bg: 'bg-blue-100', text: 'text-blue-800' },
user_update: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
user_delete: { bg: 'bg-red-100', text: 'text-red-800' },
cve_edit: { bg: 'bg-orange-100', text: 'text-orange-800' },
cve_delete: { bg: 'bg-red-100', text: 'text-red-800' },
cve_nvd_sync: { bg: 'bg-green-100', text: 'text-green-800' },
login: { bg: 'rgba(16,185,129,0.15)', text: '#10B981' },
logout: { bg: 'rgba(148,163,184,0.15)', text: '#94A3B8' },
login_failed: { bg: 'rgba(239,68,68,0.15)', text: '#EF4444' },
cve_create: { bg: 'rgba(14,165,233,0.15)', text: '#0EA5E9' },
cve_update_status: { bg: 'rgba(245,158,11,0.15)', text: '#F59E0B' },
document_upload: { bg: 'rgba(139,92,246,0.15)', text: '#8B5CF6' },
document_delete: { bg: 'rgba(239,68,68,0.15)', text: '#EF4444' },
user_create: { bg: 'rgba(14,165,233,0.15)', text: '#0EA5E9' },
user_update: { bg: 'rgba(245,158,11,0.15)', text: '#F59E0B' },
user_delete: { bg: 'rgba(239,68,68,0.15)', text: '#EF4444' },
cve_edit: { bg: 'rgba(249,115,22,0.15)', text: '#F97316' },
cve_delete: { bg: 'rgba(239,68,68,0.15)', text: '#EF4444' },
cve_nvd_sync: { bg: 'rgba(16,185,129,0.15)', text: '#10B981' },
};
const ENTITY_TYPES = ['auth', 'cve', 'document', 'user'];
@@ -97,9 +97,9 @@ export default function AuditLog({ onClose }) {
};
const getActionBadge = (action) => {
const style = ACTION_BADGES[action] || { bg: 'bg-gray-100', text: 'text-gray-800' };
const style = ACTION_BADGES[action] || { bg: 'rgba(148,163,184,0.15)', text: '#94A3B8' };
return (
<span className={`px-2 py-1 rounded text-xs font-medium ${style.bg} ${style.text}`}>
<span style={{ padding: '0.125rem 0.5rem', borderRadius: 'var(--r-sm)', fontSize: '0.75rem', fontWeight: '600', fontFamily: 'var(--font-mono)', background: style.bg, color: style.text, textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
{action}
</span>
);
@@ -119,44 +119,48 @@ export default function AuditLog({ onClose }) {
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<div style={{ position: 'fixed', inset: 0, background: 'var(--bg-overlay)', backdropFilter: 'blur(12px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 'var(--z-modal)', padding: '1rem' }}>
<div style={{ background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%)', border: '1.5px solid rgba(14,165,233,0.3)', borderRadius: 'var(--r-xl)', boxShadow: 'var(--shadow-modal)', maxWidth: '72rem', width: '100%', maxHeight: '90vh', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<div className="p-6 border-b border-gray-200 flex justify-between items-center">
<div style={{ padding: '1.5rem', borderBottom: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2 className="text-2xl font-bold text-gray-900">Audit Log</h2>
<p className="text-gray-600">Track all user actions across the system</p>
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: '1.5rem', fontWeight: '700', color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)', textShadow: 'var(--glow-heading)' }}>Audit Log</h2>
<p style={{ color: 'var(--fg-muted)', fontSize: '0.875rem' }}>Track all user actions across the system</p>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 p-2"
style={{ color: 'var(--fg-disabled)', cursor: 'pointer', background: 'none', border: 'none', padding: '0.5rem' }}
>
<X className="w-6 h-6" />
</button>
</div>
{/* Filter Bar */}
<form onSubmit={handleFilter} className="p-4 border-b border-gray-200 bg-gray-50">
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<form onSubmit={handleFilter} style={{ padding: '1rem', borderBottom: '1px solid var(--border-subtle)', background: 'rgba(15,23,42,0.4)' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '0.75rem' }}>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Username</label>
<div className="relative">
<Search className="w-4 h-4 text-gray-400 absolute left-2 top-1/2 transform -translate-y-1/2" />
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Username</label>
<div style={{ position: 'relative' }}>
<Search className="w-4 h-4" style={{ position: 'absolute', left: '0.5rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-disabled)' }} />
<input
type="text"
placeholder="Search user..."
value={userFilter}
onChange={(e) => setUserFilter(e.target.value)}
className="w-full pl-8 pr-3 py-1.5 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
style={{ width: '100%', paddingLeft: '2rem', paddingRight: '0.75rem', paddingTop: '0.375rem', paddingBottom: '0.375rem', fontSize: '0.875rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', outline: 'none' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Action</label>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Action</label>
<select
value={actionFilter}
onChange={(e) => setActionFilter(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
style={{ width: '100%', padding: '0.375rem 0.75rem', fontSize: '0.875rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', outline: 'none', cursor: 'pointer' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
>
<option value="">All Actions</option>
{actions.map(a => (
@@ -165,11 +169,13 @@ export default function AuditLog({ onClose }) {
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Entity Type</label>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Entity Type</label>
<select
value={entityTypeFilter}
onChange={(e) => setEntityTypeFilter(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
style={{ width: '100%', padding: '0.375rem 0.75rem', fontSize: '0.875rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', outline: 'none', cursor: 'pointer' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
>
<option value="">All Types</option>
{ENTITY_TYPES.map(t => (
@@ -178,35 +184,39 @@ export default function AuditLog({ onClose }) {
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Start Date</label>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Start Date</label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
style={{ width: '100%', padding: '0.375rem 0.75rem', fontSize: '0.875rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', outline: 'none' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">End Date</label>
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>End Date</label>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
style={{ width: '100%', padding: '0.375rem 0.75rem', fontSize: '0.875rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', outline: 'none' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
/>
</div>
</div>
<div className="flex gap-2 mt-3">
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem' }}>
<button
type="submit"
className="px-4 py-1.5 text-sm bg-[#0476D9] text-white rounded hover:bg-[#0360B8] transition-colors"
style={{ padding: '0.375rem 1rem', fontSize: '0.8rem', background: 'rgba(14,165,233,0.08)', border: '1px solid #0EA5E9', color: '#38BDF8', borderRadius: 'var(--r-md)', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
Apply Filters
</button>
<button
type="button"
onClick={handleReset}
className="px-4 py-1.5 text-sm bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
style={{ padding: '0.375rem 1rem', fontSize: '0.8rem', background: 'rgba(148,163,184,0.08)', border: '1px solid rgba(148,163,184,0.3)', color: 'var(--fg-muted)', borderRadius: 'var(--r-md)', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
Reset
</button>
@@ -214,56 +224,59 @@ export default function AuditLog({ onClose }) {
</form>
{/* Content */}
<div className="p-4 overflow-y-auto flex-1">
<div style={{ padding: '1rem', overflowY: 'auto', flex: 1 }}>
{loading ? (
<div className="text-center py-12">
<Loader className="w-8 h-8 text-[#0476D9] mx-auto animate-spin" />
<p className="text-gray-600 mt-2">Loading audit logs...</p>
<div style={{ textAlign: 'center', padding: '3rem' }}>
<Loader className="w-8 h-8 mx-auto animate-spin" style={{ color: '#0EA5E9' }} />
<p style={{ color: 'var(--fg-muted)', marginTop: '0.5rem' }}>Loading audit logs...</p>
</div>
) : error ? (
<div className="text-center py-12">
<AlertCircle className="w-8 h-8 text-red-500 mx-auto" />
<p className="text-red-600 mt-2">{error}</p>
<div style={{ textAlign: 'center', padding: '3rem' }}>
<AlertCircle className="w-8 h-8 mx-auto" style={{ color: '#EF4444' }} />
<p style={{ color: '#EF4444', marginTop: '0.5rem' }}>{error}</p>
</div>
) : logs.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500">No audit log entries found.</p>
<div style={{ textAlign: 'center', padding: '3rem' }}>
<p style={{ color: 'var(--fg-muted)' }}>No audit log entries found.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', fontSize: '0.875rem' }}>
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 px-3 text-xs font-medium text-gray-600">Time</th>
<th className="text-left py-2 px-3 text-xs font-medium text-gray-600">User</th>
<th className="text-left py-2 px-3 text-xs font-medium text-gray-600">Action</th>
<th className="text-left py-2 px-3 text-xs font-medium text-gray-600">Entity</th>
<th className="text-left py-2 px-3 text-xs font-medium text-gray-600">Details</th>
<th className="text-left py-2 px-3 text-xs font-medium text-gray-600">IP Address</th>
<tr style={{ borderBottom: '1px solid var(--border-subtle)' }}>
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Time</th>
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>User</th>
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Action</th>
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Entity</th>
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Details</th>
<th style={{ textAlign: 'left', padding: '0.5rem 0.75rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>IP Address</th>
</tr>
</thead>
<tbody>
{logs.map((log) => (
<tr key={log.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-2 px-3 text-gray-600 whitespace-nowrap">
<tr key={log.id} style={{ borderBottom: '1px solid var(--border-subtle)', transition: 'background var(--dur-fast)' }}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(14,165,233,0.06)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
>
<td style={{ padding: '0.5rem 0.75rem', color: 'var(--fg-muted)', whiteSpace: 'nowrap', fontFamily: 'var(--font-mono)', fontSize: '0.8rem' }}>
{formatDate(log.created_at)}
</td>
<td className="py-2 px-3 font-medium text-gray-900">
<td style={{ padding: '0.5rem 0.75rem', fontWeight: '500', color: 'var(--fg-2)' }}>
{log.username}
</td>
<td className="py-2 px-3">
<td style={{ padding: '0.5rem 0.75rem' }}>
{getActionBadge(log.action)}
</td>
<td className="py-2 px-3 text-gray-700">
<span className="text-gray-500">{log.entity_type}</span>
<td style={{ padding: '0.5rem 0.75rem', color: 'var(--fg-3)' }}>
<span style={{ color: 'var(--fg-muted)' }}>{log.entity_type}</span>
{log.entity_id && (
<span className="ml-1 text-gray-900">{log.entity_id}</span>
<span style={{ marginLeft: '0.25rem', color: 'var(--fg-2)' }}>{log.entity_id}</span>
)}
</td>
<td className="py-2 px-3 text-gray-600 max-w-xs truncate" title={formatDetails(log.details)}>
<td style={{ padding: '0.5rem 0.75rem', color: 'var(--fg-muted)', maxWidth: '20rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={formatDetails(log.details)}>
{formatDetails(log.details)}
</td>
<td className="py-2 px-3 text-gray-500 font-mono text-xs">
<td style={{ padding: '0.5rem 0.75rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontSize: '0.75rem' }}>
{log.ip_address || '-'}
</td>
</tr>
@@ -276,25 +289,25 @@ export default function AuditLog({ onClose }) {
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className="p-4 border-t border-gray-200 flex items-center justify-between">
<p className="text-sm text-gray-600">
<div style={{ padding: '1rem', borderTop: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<p style={{ fontSize: '0.875rem', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>
Showing {((pagination.page - 1) * pagination.limit) + 1} - {Math.min(pagination.page * pagination.limit, pagination.total)} of {pagination.total} entries
</p>
<div className="flex items-center gap-2">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<button
onClick={() => fetchLogs(pagination.page - 1)}
disabled={pagination.page <= 1}
className="p-2 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
style={{ padding: '0.5rem', borderRadius: 'var(--r-sm)', background: 'none', border: 'none', cursor: pagination.page <= 1 ? 'not-allowed' : 'pointer', opacity: pagination.page <= 1 ? 0.5 : 1, color: 'var(--fg-muted)' }}
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm text-gray-700">
<span style={{ fontSize: '0.875rem', color: 'var(--fg-3)', fontFamily: 'var(--font-mono)' }}>
Page {pagination.page} of {pagination.totalPages}
</span>
<button
onClick={() => fetchLogs(pagination.page + 1)}
disabled={pagination.page >= pagination.totalPages}
className="p-2 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
style={{ padding: '0.5rem', borderRadius: 'var(--r-sm)', background: 'none', border: 'none', cursor: pagination.page >= pagination.totalPages ? 'not-allowed' : 'pointer', opacity: pagination.page >= pagination.totalPages ? 0.5 : 1, color: 'var(--fg-muted)' }}
>
<ChevronRight className="w-4 h-4" />
</button>

View File

@@ -73,22 +73,22 @@ export default function CalendarWidget({ onDateClick }) {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
<button
onClick={prevMonth}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748B', padding: '2px 4px', borderRadius: '4px', lineHeight: 1, transition: 'color 0.15s' }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#0EA5E9'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#64748B'; }}
style={{ background: 'none', border: '1px solid rgba(14,165,233,0.25)', cursor: 'pointer', color: 'var(--fg-disabled)', padding: '2px 4px', borderRadius: 'var(--r-sm)', lineHeight: 1, transition: 'color var(--dur-fast), border-color var(--dur-fast)' }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#0EA5E9'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.5)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--fg-disabled)'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.25)'; }}
>
<ChevronLeft style={{ width: '14px', height: '14px' }} />
</button>
<span style={{ color: '#E2E8F0', fontFamily: 'monospace', fontWeight: '600', fontSize: '0.85rem' }}>
<span style={{ color: 'var(--fg-2)', fontFamily: 'var(--font-mono)', fontWeight: '600', fontSize: '0.85rem' }}>
{MONTH_NAMES[calMonth]} {calYear}
</span>
<button
onClick={nextMonth}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748B', padding: '2px 4px', borderRadius: '4px', lineHeight: 1, transition: 'color 0.15s' }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#0EA5E9'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#64748B'; }}
style={{ background: 'none', border: '1px solid rgba(14,165,233,0.25)', cursor: 'pointer', color: 'var(--fg-disabled)', padding: '2px 4px', borderRadius: 'var(--r-sm)', lineHeight: 1, transition: 'color var(--dur-fast), border-color var(--dur-fast)' }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#0EA5E9'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.5)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--fg-disabled)'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.25)'; }}
>
<ChevronRight style={{ width: '14px', height: '14px' }} />
</button>
@@ -97,7 +97,7 @@ export default function CalendarWidget({ onDateClick }) {
{/* Day-of-week headers */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px', textAlign: 'center', marginBottom: '4px' }}>
{DAY_NAMES.map((d) => (
<div key={d} style={{ fontSize: '0.6rem', color: '#475569', fontFamily: 'monospace', fontWeight: '600', textTransform: 'uppercase' }}>
<div key={d} style={{ fontSize: '0.6rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontWeight: '600', textTransform: 'uppercase' }}>
{d}
</div>
))}
@@ -121,18 +121,18 @@ export default function CalendarWidget({ onDateClick }) {
style={{
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: '2px', padding: '3px 1px',
borderRadius: '4px',
borderRadius: 'var(--r-sm)',
background: isToday ? 'rgba(14,165,233,0.2)' : 'transparent',
border: isToday ? '1px solid rgba(14,165,233,0.5)' : '1px solid transparent',
cursor: hasDue ? 'pointer' : 'default',
transition: hasDue ? 'background 0.15s' : undefined,
transition: hasDue ? 'background var(--dur-fast)' : undefined,
}}
onMouseEnter={hasDue ? (e) => { e.currentTarget.style.background = isToday ? 'rgba(14,165,233,0.35)' : 'rgba(239,68,68,0.15)'; } : undefined}
onMouseLeave={hasDue ? (e) => { e.currentTarget.style.background = isToday ? 'rgba(14,165,233,0.2)' : 'transparent'; } : undefined}
>
<span style={{
fontSize: '0.7rem', fontFamily: 'monospace', lineHeight: 1,
color: isToday ? '#0EA5E9' : hasDue ? '#EF4444' : '#CBD5E1',
fontSize: '0.7rem', fontFamily: 'var(--font-mono)', lineHeight: 1,
color: isToday ? '#0EA5E9' : hasDue ? '#EF4444' : 'var(--fg-3)',
fontWeight: (isToday || hasDue) ? '700' : '400',
}}>
{day}
@@ -157,7 +157,7 @@ export default function CalendarWidget({ onDateClick }) {
{hasDueDatesThisMonth && (
<div style={{ marginTop: '0.75rem', paddingTop: '0.625rem', borderTop: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: '#EF4444', boxShadow: '0 0 4px rgba(239,68,68,0.5)', flexShrink: 0 }} />
<span style={{ fontSize: '0.62rem', color: '#64748B', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
<span style={{ fontSize: '0.62rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Ivanti finding due
</span>
</div>

View File

@@ -182,23 +182,23 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) {
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '700px' }}>
<div className="modal-overlay" onClick={onClose} style={{ background: 'var(--bg-overlay)', backdropFilter: 'blur(12px)' }}>
<div className="modal-content" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '700px', background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%)', border: '1.5px solid rgba(14,165,233,0.3)', borderRadius: 'var(--r-xl)', boxShadow: 'var(--shadow-modal)' }}>
{/* Header */}
<div className="modal-header">
<h2 className="modal-title">Knowledge Base</h2>
<button onClick={onClose} className="modal-close">
<div className="modal-header" style={{ borderBottom: '1px solid var(--border-subtle)', padding: '1.25rem 1.5rem' }}>
<h2 className="modal-title" style={{ fontFamily: 'var(--font-mono)', fontWeight: '700', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)', color: '#0EA5E9', textShadow: 'var(--glow-heading)' }}>Knowledge Base</h2>
<button onClick={onClose} className="modal-close" style={{ color: 'var(--fg-disabled)', background: 'none', border: 'none', cursor: 'pointer' }}>
<X className="w-5 h-5" />
</button>
</div>
{/* Body */}
<div className="modal-body">
<div className="modal-body" style={{ padding: '1.5rem' }}>
{/* Idle Phase - Upload Form */}
{phase === 'idle' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: '#94A3B8' }}>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Title *
</label>
<input
@@ -206,33 +206,39 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) {
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g., Inventory Management Policy"
className="intel-input w-full"
style={{ width: '100%', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', padding: '0.625rem 1rem', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', outline: 'none' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
maxLength={255}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: '#94A3B8' }}>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Description
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief description of this document..."
className="intel-input w-full"
style={{ width: '100%', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', padding: '0.625rem 1rem', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', outline: 'none', resize: 'vertical' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
rows={3}
maxLength={500}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: '#94A3B8' }}>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Category
</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="intel-input w-full"
style={{ width: '100%', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', padding: '0.625rem 1rem', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', outline: 'none', cursor: 'pointer' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
>
<option value="General">General</option>
<option value="Policy">Policy</option>
@@ -243,14 +249,14 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) {
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: '#94A3B8' }}>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Document File *
</label>
<input
type="file"
accept=".pdf,.md,.txt,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.html,.json,.yaml,.yml"
onChange={handleFileSelect}
className="intel-input w-full"
style={{ width: '100%', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', padding: '0.625rem 1rem', fontFamily: 'var(--font-mono)', fontSize: '0.875rem' }}
/>
{selectedFile && (
<p className="mt-2 text-sm" style={{ color: '#10B981' }}>

View File

@@ -152,29 +152,29 @@ export default function KnowledgeBaseViewer({ article, onClose }) {
<div
style={{
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
border: '2px solid rgba(14, 165, 233, 0.4)',
borderRadius: '0.5rem',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15)',
border: '1.5px solid rgba(14, 165, 233, 0.3)',
borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-card)',
padding: '1.5rem',
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<div className="flex items-start justify-between mb-4 pb-4" style={{ borderBottom: '1px solid rgba(14, 165, 233, 0.2)' }}>
<div className="flex items-start justify-between mb-4 pb-4" style={{ borderBottom: '1px solid var(--border-subtle)' }}>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<FileText className="w-5 h-5" style={{ color: getCategoryColor(article.category) }} />
<h2 className="text-xl font-semibold" style={{ color: '#E2E8F0', fontFamily: 'monospace' }}>
<h2 className="text-xl font-semibold" style={{ color: 'var(--fg-2)', fontFamily: 'var(--font-mono)' }}>
{article.title}
</h2>
</div>
{article.description && (
<p className="text-sm mb-2" style={{ color: '#94A3B8' }}>
<p className="text-sm mb-2" style={{ color: 'var(--fg-muted)' }}>
{article.description}
</p>
)}
<div className="flex items-center gap-3 text-xs" style={{ color: '#64748B' }}>
<div className="flex items-center gap-3 text-xs" style={{ color: 'var(--fg-disabled)' }}>
<span
className="px-2 py-1 rounded"
style={{
@@ -214,7 +214,7 @@ export default function KnowledgeBaseViewer({ article, onClose }) {
{loading && (
<div className="text-center py-12">
<Loader className="w-8 h-8 animate-spin mx-auto mb-4" style={{ color: '#0EA5E9' }} />
<p style={{ color: '#94A3B8' }}>Loading document...</p>
<p style={{ color: 'var(--fg-muted)' }}>Loading document...</p>
</div>
)}
@@ -223,7 +223,7 @@ export default function KnowledgeBaseViewer({ article, onClose }) {
<AlertCircle className="w-6 h-6 flex-shrink-0" style={{ color: '#EF4444' }} />
<div>
<p className="font-medium" style={{ color: '#FCA5A5' }}>Failed to Load Document</p>
<p className="text-sm mt-1" style={{ color: '#94A3B8' }}>{error}</p>
<p className="text-sm mt-1" style={{ color: 'var(--fg-muted)' }}>{error}</p>
</div>
</div>
)}
@@ -244,7 +244,7 @@ export default function KnowledgeBaseViewer({ article, onClose }) {
return (
<code
className={className}
style={inline ? { background: 'rgba(14,165,233,0.15)', padding: '0.1rem 0.3rem', borderRadius: '0.25rem', fontFamily: 'monospace', fontSize: '0.85em' } : {}}
style={inline ? { background: 'rgba(14,165,233,0.15)', padding: '0.1rem 0.3rem', borderRadius: 'var(--r-sm)', fontFamily: 'var(--font-mono)', fontSize: '0.85em' } : {}}
>
{children}
</code>
@@ -263,9 +263,9 @@ export default function KnowledgeBaseViewer({ article, onClose }) {
className="text-sm p-4 rounded overflow-auto"
style={{
background: 'rgba(15, 23, 42, 0.8)',
border: '1px solid rgba(14, 165, 233, 0.2)',
color: '#E2E8F0',
fontFamily: 'monospace',
border: '1px solid var(--border-subtle)',
color: 'var(--fg-2)',
fontFamily: 'var(--font-mono)',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
maxHeight: '600px'

View File

@@ -28,29 +28,29 @@ export default function LoginForm() {
{/* Scanning line effect */}
<div className="scan-line"></div>
<div className="intel-card rounded-lg shadow-2xl max-w-md w-full p-8 border-intel-accent relative z-10">
<div style={{ background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%)', border: '1.5px solid rgba(14,165,233,0.3)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-modal)', padding: '2rem', position: 'relative', zIndex: 10, maxWidth: '28rem', width: '100%' }}>
<div className="text-center mb-8">
<div className="w-16 h-16 bg-gradient-to-br from-intel-accent to-intel-accent-dim rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg" style={{boxShadow: '0 0 30px rgba(0, 217, 255, 0.4)'}}>
<Lock className="w-8 h-8 text-intel-darkest" />
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: 'linear-gradient(135deg, rgba(14,165,233,0.15) 0%, rgba(14,165,233,0.1) 100%)', boxShadow: '0 0 30px rgba(14, 165, 233, 0.4)' }}>
<Lock className="w-8 h-8" style={{ color: 'var(--accent)' }} />
</div>
<h1 className="text-3xl font-bold text-intel-accent font-mono tracking-tight">CVE INTEL</h1>
<p className="text-gray-400 mt-2 font-sans text-sm">Threat Intelligence Access Portal</p>
<h1 style={{ fontSize: '1.875rem', fontWeight: '700', color: 'var(--accent)', fontFamily: 'var(--font-mono)', letterSpacing: 'var(--tracking-wider)', textTransform: 'uppercase', textShadow: 'var(--glow-heading)' }}>CVE INTEL</h1>
<p style={{ color: 'var(--fg-muted)', marginTop: '0.5rem', fontFamily: 'var(--font-ui)', fontSize: '0.875rem' }}>Threat Intelligence Access Portal</p>
</div>
{error && (
<div className="mb-6 p-4 bg-intel-danger/10 border border-intel-danger/30 rounded flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-intel-danger flex-shrink-0 mt-0.5" />
<p className="text-sm text-gray-300">{error}</p>
<div className="mb-6 p-4 rounded flex items-start gap-3" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.3)' }}>
<AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5" style={{ color: '#EF4444' }} />
<p style={{ fontSize: '0.875rem', color: 'var(--fg-3)' }}>{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="username" className="block text-xs font-medium text-gray-400 mb-2 uppercase tracking-wider">
<label htmlFor="username" style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)', fontFamily: 'var(--font-mono)' }}>
Username
</label>
<div className="relative">
<User className="w-5 h-5 text-gray-500 absolute left-3 top-1/2 transform -translate-y-1/2" />
<User className="w-5 h-5 absolute left-3 top-1/2 transform -translate-y-1/2" style={{ color: 'var(--fg-disabled)' }} />
<input
id="username"
type="text"
@@ -65,11 +65,11 @@ export default function LoginForm() {
</div>
<div>
<label htmlFor="password" className="block text-xs font-medium text-gray-400 mb-2 uppercase tracking-wider">
<label htmlFor="password" style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)', fontFamily: 'var(--font-mono)' }}>
Password
</label>
<div className="relative">
<Lock className="w-5 h-5 text-gray-500 absolute left-3 top-1/2 transform -translate-y-1/2" />
<Lock className="w-5 h-5 absolute left-3 top-1/2 transform -translate-y-1/2" style={{ color: 'var(--fg-disabled)' }} />
<input
id="password"
type="password"
@@ -91,10 +91,10 @@ export default function LoginForm() {
{loading ? (
<>
<div className="loading-spinner w-5 h-5"></div>
<span className="font-mono uppercase tracking-wider">Authenticating...</span>
<span style={{ fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)' }}>Authenticating...</span>
</>
) : (
<span className="font-mono uppercase tracking-wider">Access System</span>
<span style={{ fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)' }}>Access System</span>
)}
</button>
</form>

View File

@@ -3,186 +3,110 @@ import { X, Home, BarChart2, BookOpen, Download, ShieldCheck, Settings, Ticket }
import { useAuth } from '../contexts/AuthContext';
const NAV_ITEMS = [
{ id: 'home', label: 'Home', icon: Home, color: '#0EA5E9', description: 'Main dashboard' },
{ id: 'triage', label: 'Vuln Triage', icon: BarChart2, color: '#F59E0B', description: 'Active findings & CVE triage' },
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck, color: '#14B8A6', description: 'AEO posture & metrics' },
{ id: 'knowledge-base', label: 'Knowledge Base', icon: BookOpen, color: '#10B981', description: 'Articles & documentation' },
{ id: 'exports', label: 'Exports', icon: Download, color: '#8B5CF6', description: 'Export data & reports' },
{ id: 'jira', label: 'Jira Tickets', icon: Ticket, color: '#6366F1', description: 'Jira issue tracking & sync' },
{ id: 'home', label: 'Home', icon: Home },
{ id: 'triage', label: 'Vuln Triage', icon: BarChart2 },
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck },
{ id: 'knowledge-base', label: 'Knowledge Base', icon: BookOpen },
{ id: 'exports', label: 'Exports', icon: Download },
{ id: 'jira', label: 'Jira Tickets', icon: Ticket },
];
const ADMIN_ITEM = { id: 'admin', label: 'Admin Panel', icon: Settings, color: '#EF4444', description: 'User management & audit' };
const ADMIN_ITEM = { id: 'admin', label: 'Admin Panel', icon: Settings };
export default function NavDrawer({ isOpen, onClose, currentPage, onNavigate }) {
const { isAdmin } = useAuth();
if (!isOpen) return null;
const allItems = [
...NAV_ITEMS,
...(isAdmin() ? [ADMIN_ITEM] : []),
];
return (
<>
{/* Backdrop */}
{/* Overlay */}
<div
onClick={onClose}
style={{
position: 'fixed', inset: 0,
background: 'rgba(0, 0, 0, 0.65)',
backdropFilter: 'blur(3px)',
zIndex: 50
background: 'var(--bg-overlay)',
backdropFilter: 'blur(4px)',
zIndex: 60,
}}
/>
{/* Drawer */}
<div style={{
position: 'fixed', top: 0, left: 0, bottom: 0, width: '280px',
background: 'linear-gradient(180deg, #0F1A2E 0%, #0A1628 100%)',
borderRight: '1px solid rgba(14, 165, 233, 0.2)',
boxShadow: '6px 0 32px rgba(0, 0, 0, 0.7)',
zIndex: 51,
display: 'flex', flexDirection: 'column',
padding: '1.5rem'
<aside style={{
position: 'fixed', left: 0, top: 0, bottom: 0,
width: 'var(--drawer-w)',
zIndex: 61,
background: 'var(--bg-surface)',
borderRight: '1px solid var(--border-1)',
padding: 16,
display: 'flex', flexDirection: 'column', gap: 4,
}}>
{/* Drawer header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '2rem' }}>
<div>
<div style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '700', color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.15em', textShadow: '0 0 12px rgba(14, 165, 233, 0.5)' }}>
STEAM
</div>
<div style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '0.1em', marginTop: '2px' }}>
Security Dashboard
</div>
</div>
{/* Navigation label + close button */}
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: 8, padding: '4px 6px',
}}>
<span style={{
fontFamily: 'var(--font-ui)', fontWeight: 600, fontSize: 11,
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em',
}}>Navigation</span>
<button
onClick={onClose}
style={{ color: '#475569', padding: '0.25rem', background: 'none', border: 'none', cursor: 'pointer', lineHeight: 1 }}
onMouseEnter={e => e.currentTarget.style.color = '#E2E8F0'}
onMouseLeave={e => e.currentTarget.style.color = '#475569'}
style={{
background: 'transparent', border: 'none',
color: 'var(--fg-muted)', cursor: 'pointer', display: 'flex',
}}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--fg-1)'; }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--fg-muted)'; }}
>
<X style={{ width: '20px', height: '20px' }} />
<X size={16} />
</button>
</div>
{/* Nav items */}
<nav style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
{NAV_ITEMS.map(({ id, label, icon: Icon, color, description }) => {
const active = currentPage === id;
return (
<button
key={id}
onClick={() => { onNavigate(id); onClose(); }}
style={{
display: 'flex', alignItems: 'center', gap: '0.875rem',
padding: '0.75rem 0.875rem',
borderRadius: '0.5rem',
border: active ? `1px solid ${color}50` : '1px solid transparent',
background: active ? `${color}18` : 'transparent',
cursor: 'pointer', textAlign: 'left', width: '100%',
transition: 'background 0.15s, border-color 0.15s'
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
>
{/* Icon box */}
<div style={{
width: '36px', height: '36px', flexShrink: 0,
borderRadius: '0.375rem',
background: `${color}18`,
border: `1px solid ${color}40`,
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<Icon style={{ width: '17px', height: '17px', color }} />
</div>
{/* Label + description */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '600',
color: active ? color : '#CBD5E1',
textTransform: 'uppercase', letterSpacing: '0.06em'
}}>
{label}
</div>
<div style={{ fontSize: '0.68rem', color: '#475569', marginTop: '1px' }}>
{description}
</div>
</div>
{/* Active indicator dot */}
{active && (
<div style={{
width: '6px', height: '6px', borderRadius: '50%',
background: color, boxShadow: `0 0 8px ${color}`, flexShrink: 0
}} />
)}
</button>
);
})}
{/* Admin panel link — visible only to Admin group */}
{isAdmin() && (() => {
const { id, label, icon: Icon, color, description } = ADMIN_ITEM;
const active = currentPage === id;
return (
<button
key={id}
onClick={() => { onNavigate(id); onClose(); }}
style={{
display: 'flex', alignItems: 'center', gap: '0.875rem',
padding: '0.75rem 0.875rem',
borderRadius: '0.5rem',
border: active ? `1px solid ${color}50` : '1px solid transparent',
background: active ? `${color}18` : 'transparent',
cursor: 'pointer', textAlign: 'left', width: '100%',
marginTop: '0.5rem',
borderTop: '1px solid rgba(255, 255, 255, 0.05)',
paddingTop: '1rem',
transition: 'background 0.15s, border-color 0.15s'
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
>
<div style={{
width: '36px', height: '36px', flexShrink: 0,
borderRadius: '0.375rem',
background: `${color}18`,
border: `1px solid ${color}40`,
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<Icon style={{ width: '17px', height: '17px', color }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '600',
color: active ? color : '#CBD5E1',
textTransform: 'uppercase', letterSpacing: '0.06em'
}}>
{label}
</div>
<div style={{ fontSize: '0.68rem', color: '#475569', marginTop: '1px' }}>
{description}
</div>
</div>
{active && (
<div style={{
width: '6px', height: '6px', borderRadius: '50%',
background: color, boxShadow: `0 0 8px ${color}`, flexShrink: 0
}} />
)}
</button>
);
})()}
</nav>
{allItems.map(({ id, label, icon: Icon }) => {
const active = currentPage === id;
return (
<button
key={id}
onClick={() => { onNavigate(id); onClose(); }}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--bg-elevated)'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
style={{
display: 'flex', alignItems: 'center', gap: 10,
background: active ? 'var(--accent-soft)' : 'transparent',
color: active ? 'var(--accent)' : 'var(--fg-2)',
border: 'none', borderRadius: 6, padding: '9px 10px',
fontFamily: 'var(--font-ui)', fontWeight: active ? 600 : 500, fontSize: 13,
cursor: 'pointer', textAlign: 'left', width: '100%',
transition: 'background 150ms, color 150ms',
}}
>
<Icon size={16} />
{label}
</button>
);
})}
{/* Footer */}
<div style={{
marginTop: 'auto', paddingTop: '1rem',
borderTop: '1px solid rgba(255, 255, 255, 0.05)',
textAlign: 'center'
borderTop: '1px solid var(--border-1)',
textAlign: 'center',
}}>
<div style={{ fontSize: '0.6rem', color: '#1E293B', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
<div style={{
fontSize: '0.6rem', color: 'var(--fg-muted)',
fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.1em',
}}>
NTS Threat Intelligence
</div>
</div>
</div>
</aside>
</>
);
}

View File

@@ -217,40 +217,40 @@ export default function NvdSyncModal({ onClose, onSyncComplete }) {
const errorCount = Object.values(results).filter(r => r.status === 'error').length;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] flex flex-col">
<div style={{ position: 'fixed', inset: 0, background: 'var(--bg-overlay)', backdropFilter: 'blur(12px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 'var(--z-modal)', padding: '1rem' }}>
<div style={{ background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%)', border: '1.5px solid rgba(14,165,233,0.3)', borderRadius: 'var(--r-xl)', boxShadow: 'var(--shadow-modal)', maxWidth: '64rem', width: '100%', maxHeight: '90vh', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<div className="p-6 border-b border-gray-200 flex justify-between items-center flex-shrink-0">
<div style={{ padding: '1.5rem', borderBottom: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0 }}>
<div>
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<RefreshCw className="w-6 h-6 text-green-600" />
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: '1.5rem', fontWeight: '700', color: '#10B981', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)', textShadow: '0 0 16px rgba(16,185,129,0.3)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<RefreshCw className="w-6 h-6" style={{ color: '#10B981' }} />
Sync with NVD
</h2>
<p className="text-sm text-gray-500 mt-1">Update existing CVE entries with data from the National Vulnerability Database</p>
<p style={{ fontSize: '0.875rem', color: 'var(--fg-muted)', marginTop: '0.25rem' }}>Update existing CVE entries with data from the National Vulnerability Database</p>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<button onClick={onClose} style={{ color: 'var(--fg-disabled)', cursor: 'pointer', background: 'none', border: 'none' }}>
<X className="w-6 h-6" />
</button>
</div>
{/* Body */}
<div className="p-6 overflow-y-auto flex-1">
<div style={{ padding: '1.5rem', overflowY: 'auto', flex: 1 }}>
{/* Idle Phase */}
{phase === 'idle' && (
<div className="text-center py-8">
<p className="text-lg text-gray-700 mb-2">
<div style={{ textAlign: 'center', padding: '2rem 0' }}>
<p style={{ fontSize: '1rem', color: 'var(--fg-2)', marginBottom: '0.5rem' }}>
{cveIds.length > 0
? <><strong>{cveIds.length}</strong> unique CVE{cveIds.length !== 1 ? 's' : ''} in database</>
: 'Loading CVE count...'}
</p>
<p className="text-sm text-gray-500 mb-6">
<p style={{ fontSize: '0.875rem', color: 'var(--fg-muted)', marginBottom: '1.5rem' }}>
This will fetch data from NVD for each CVE and let you review changes before applying.
Rate-limited to stay within NVD API limits.
</p>
<button
onClick={fetchNvdData}
disabled={cveIds.length === 0}
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium shadow-md disabled:opacity-50 flex items-center gap-2 mx-auto"
style={{ padding: '0.75rem 1.5rem', background: 'rgba(16,185,129,0.08)', border: '1px solid #10B981', color: '#34D399', borderRadius: 'var(--r-md)', cursor: cveIds.length === 0 ? 'not-allowed' : 'pointer', fontFamily: 'var(--font-mono)', fontWeight: '600', fontSize: '0.875rem', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', display: 'inline-flex', alignItems: 'center', gap: '0.5rem', opacity: cveIds.length === 0 ? 0.5 : 1 }}
>
<RefreshCw className="w-5 h-5" />
Fetch NVD Data
@@ -260,25 +260,24 @@ export default function NvdSyncModal({ onClose, onSyncComplete }) {
{/* Fetching Phase */}
{phase === 'fetching' && (
<div className="py-8">
<div className="text-center mb-6">
<Loader className="w-8 h-8 text-green-600 animate-spin mx-auto mb-3" />
<p className="text-lg text-gray-700">
<div style={{ padding: '2rem 0' }}>
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<Loader className="w-8 h-8 animate-spin" style={{ color: '#10B981', margin: '0 auto 0.75rem' }} />
<p style={{ fontSize: '1rem', color: 'var(--fg-2)' }}>
Fetching CVE {fetchProgress.current} of {fetchProgress.total}
</p>
<p className="text-sm text-gray-500 font-mono mt-1">{fetchProgress.currentId}</p>
<p style={{ fontSize: '0.875rem', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', marginTop: '0.25rem' }}>{fetchProgress.currentId}</p>
</div>
{/* Progress bar */}
<div className="w-full bg-gray-200 rounded-full h-3 mb-4">
<div style={{ width: '100%', background: 'rgba(255,255,255,0.1)', borderRadius: 'var(--r-pill)', height: '0.75rem', marginBottom: '1rem' }}>
<div
className="bg-green-600 h-3 rounded-full transition-all duration-300"
style={{ width: `${fetchProgress.total > 0 ? (fetchProgress.current / fetchProgress.total) * 100 : 0}%` }}
style={{ background: '#10B981', height: '0.75rem', borderRadius: 'var(--r-pill)', transition: 'width 0.3s', width: `${fetchProgress.total > 0 ? (fetchProgress.current / fetchProgress.total) * 100 : 0}%` }}
/>
</div>
<div className="text-center">
<div style={{ textAlign: 'center' }}>
<button
onClick={cancelFetch}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
style={{ padding: '0.5rem 1rem', background: 'rgba(148,163,184,0.08)', border: '1px solid rgba(148,163,184,0.3)', color: 'var(--fg-muted)', borderRadius: 'var(--r-md)', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
Cancel
</button>
@@ -290,31 +289,31 @@ export default function NvdSyncModal({ onClose, onSyncComplete }) {
{phase === 'review' && (
<div>
{/* Summary bar */}
<div className="flex flex-wrap gap-3 mb-4 p-3 bg-gray-50 rounded-lg text-sm">
<span className="font-medium">Found: <span className="text-green-700">{foundCount}</span></span>
<span>|</span>
<span>Up to date: <span className="text-gray-600">{noChangeCount}</span></span>
<span>|</span>
<span>Changes: <span className="text-blue-700">{foundCount}</span></span>
<span>|</span>
<span>Not in NVD: <span className="text-gray-400">{notFoundCount}</span></span>
<span>|</span>
<span>Errors: <span className="text-red-600">{errorCount}</span></span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', marginBottom: '1rem', padding: '0.75rem', background: 'rgba(15,23,42,0.4)', borderRadius: 'var(--r-md)', fontSize: '0.875rem', fontFamily: 'var(--font-mono)', border: '1px solid var(--border-subtle)' }}>
<span style={{ fontWeight: '500' }}>Found: <span style={{ color: '#10B981' }}>{foundCount}</span></span>
<span style={{ color: 'var(--fg-disabled)' }}>|</span>
<span>Up to date: <span style={{ color: 'var(--fg-muted)' }}>{noChangeCount}</span></span>
<span style={{ color: 'var(--fg-disabled)' }}>|</span>
<span>Changes: <span style={{ color: '#0EA5E9' }}>{foundCount}</span></span>
<span style={{ color: 'var(--fg-disabled)' }}>|</span>
<span>Not in NVD: <span style={{ color: 'var(--fg-disabled)' }}>{notFoundCount}</span></span>
<span style={{ color: 'var(--fg-disabled)' }}>|</span>
<span>Errors: <span style={{ color: '#EF4444' }}>{errorCount}</span></span>
</div>
{/* Bulk controls */}
{Object.keys(descriptionChoices).length > 0 && (
<div className="flex gap-2 mb-4">
<span className="text-sm text-gray-600 self-center">Descriptions:</span>
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem', alignItems: 'center' }}>
<span style={{ fontSize: '0.875rem', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>Descriptions:</span>
<button
onClick={() => setBulkDescriptionChoice('keep')}
className="px-3 py-1 text-xs rounded border border-gray-300 hover:bg-gray-100 transition-colors"
style={{ padding: '0.25rem 0.75rem', fontSize: '0.75rem', borderRadius: 'var(--r-sm)', border: '1px solid var(--border-subtle)', background: 'none', color: 'var(--fg-muted)', cursor: 'pointer', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
Keep All Existing
</button>
<button
onClick={() => setBulkDescriptionChoice('nvd')}
className="px-3 py-1 text-xs rounded border border-green-300 text-green-700 hover:bg-green-50 transition-colors"
style={{ padding: '0.25rem 0.75rem', fontSize: '0.75rem', borderRadius: 'var(--r-sm)', border: '1px solid rgba(16,185,129,0.3)', background: 'none', color: '#10B981', cursor: 'pointer', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
Use All NVD
</button>
@@ -322,34 +321,34 @@ export default function NvdSyncModal({ onClose, onSyncComplete }) {
)}
{/* Comparison table */}
<div className="space-y-2">
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{Object.entries(results).map(([cveId, r]) => {
if (r.status === 'no_change') {
return (
<div key={cveId} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg text-sm">
<span className="text-gray-400"></span>
<span className="font-mono font-medium text-gray-500">{cveId}</span>
<span className="text-gray-400">No changes needed</span>
<div key={cveId} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.75rem', background: 'rgba(15,23,42,0.4)', borderRadius: 'var(--r-md)', fontSize: '0.875rem', border: '1px solid var(--border-subtle)' }}>
<span style={{ color: 'var(--fg-disabled)' }}></span>
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: '500', color: 'var(--fg-muted)' }}>{cveId}</span>
<span style={{ color: 'var(--fg-disabled)' }}>No changes needed</span>
</div>
);
}
if (r.status === 'not_found') {
return (
<div key={cveId} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg text-sm">
<span className="text-gray-400"></span>
<span className="font-mono font-medium text-gray-400">{cveId}</span>
<span className="text-gray-400 italic">Not found in NVD</span>
<div key={cveId} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.75rem', background: 'rgba(15,23,42,0.4)', borderRadius: 'var(--r-md)', fontSize: '0.875rem', border: '1px solid var(--border-subtle)' }}>
<span style={{ color: 'var(--fg-disabled)' }}></span>
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: '500', color: 'var(--fg-disabled)' }}>{cveId}</span>
<span style={{ color: 'var(--fg-disabled)', fontStyle: 'italic' }}>Not found in NVD</span>
</div>
);
}
if (r.status === 'error') {
return (
<div key={cveId} className="flex items-center gap-3 p-3 bg-red-50 rounded-lg text-sm">
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
<span className="font-mono font-medium text-gray-700">{cveId}</span>
<span className="text-red-600">{r.error}</span>
<div key={cveId} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.75rem', background: 'rgba(239,68,68,0.08)', borderRadius: 'var(--r-md)', fontSize: '0.875rem', border: '1px solid rgba(239,68,68,0.2)' }}>
<AlertCircle className="w-4 h-4" style={{ color: '#EF4444', flexShrink: 0 }} />
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: '500', color: 'var(--fg-3)' }}>{cveId}</span>
<span style={{ color: '#EF4444' }}>{r.error}</span>
</div>
);
}
@@ -357,74 +356,72 @@ export default function NvdSyncModal({ onClose, onSyncComplete }) {
// status === 'found' — show changes
const isExpanded = expandedDesc[cveId];
return (
<div key={cveId} className="border border-gray-200 rounded-lg p-4 bg-white">
<div className="flex items-start gap-3">
<CheckCircle className="w-4 h-4 text-green-500 mt-1 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2 flex-wrap">
<span className="font-mono font-bold text-gray-900">{cveId}</span>
<div key={cveId} style={{ border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', padding: '1rem', background: 'rgba(15,23,42,0.4)' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.75rem' }}>
<CheckCircle className="w-4 h-4" style={{ color: '#10B981', marginTop: '0.25rem', flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem', flexWrap: 'wrap' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: '700', color: 'var(--fg-2)' }}>{cveId}</span>
{r.sevChanged && (
<span className="text-xs">
Severity: <span className="text-red-600">{r.current.severity}</span>
<span style={{ fontSize: '0.75rem' }}>
Severity: <span style={{ color: '#EF4444' }}>{r.current.severity}</span>
{' → '}
<span className="text-green-700">{r.nvd.severity}</span>
<span style={{ color: '#10B981' }}>{r.nvd.severity}</span>
</span>
)}
{r.dateChanged && (
<span className="text-xs">
Date: <span className="text-red-600">{r.current.published_date || '(none)'}</span>
<span style={{ fontSize: '0.75rem' }}>
Date: <span style={{ color: '#EF4444' }}>{r.current.published_date || '(none)'}</span>
{' → '}
<span className="text-green-700">{r.nvd.published_date}</span>
<span style={{ color: '#10B981' }}>{r.nvd.published_date}</span>
</span>
)}
</div>
{r.descChanged && (
<div className="mt-2">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-medium text-gray-600">Description:</span>
<div style={{ marginTop: '0.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
<span style={{ fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>Description:</span>
<button
onClick={() => setExpandedDesc(prev => ({ ...prev, [cveId]: !prev[cveId] }))}
className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1"
style={{ fontSize: '0.75rem', color: '#0EA5E9', background: 'none', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.25rem' }}
>
{isExpanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
{isExpanded ? 'Collapse' : 'Expand'}
</button>
</div>
{isExpanded ? (
<div className="space-y-2 text-xs">
<div className="p-2 bg-red-50 rounded border border-red-200">
<span className="font-medium text-red-700">Current: </span>
<span className="text-gray-700">{r.current.description || '(empty)'}</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', fontSize: '0.75rem' }}>
<div style={{ padding: '0.5rem', background: 'rgba(239,68,68,0.08)', borderRadius: 'var(--r-sm)', border: '1px solid rgba(239,68,68,0.2)' }}>
<span style={{ fontWeight: '500', color: '#EF4444' }}>Current: </span>
<span style={{ color: 'var(--fg-3)' }}>{r.current.description || '(empty)'}</span>
</div>
<div className="p-2 bg-green-50 rounded border border-green-200">
<span className="font-medium text-green-700">NVD: </span>
<span className="text-gray-700">{r.nvd.description}</span>
<div style={{ padding: '0.5rem', background: 'rgba(16,185,129,0.08)', borderRadius: 'var(--r-sm)', border: '1px solid rgba(16,185,129,0.2)' }}>
<span style={{ fontWeight: '500', color: '#10B981' }}>NVD: </span>
<span style={{ color: 'var(--fg-3)' }}>{r.nvd.description}</span>
</div>
</div>
) : (
<p className="text-xs text-gray-500">{truncate(r.nvd.description)}</p>
<p style={{ fontSize: '0.75rem', color: 'var(--fg-muted)' }}>{truncate(r.nvd.description)}</p>
)}
{/* Description choice */}
<div className="flex gap-4 mt-2">
<label className="flex items-center gap-1 text-xs cursor-pointer">
<div style={{ display: 'flex', gap: '1rem', marginTop: '0.5rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.75rem', cursor: 'pointer', color: 'var(--fg-3)' }}>
<input
type="radio"
name={`desc-${cveId}`}
checked={descriptionChoices[cveId] === 'keep'}
onChange={() => setDescriptionChoices(prev => ({ ...prev, [cveId]: 'keep' }))}
className="text-blue-600"
/>
Keep existing
</label>
<label className="flex items-center gap-1 text-xs cursor-pointer">
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem', fontSize: '0.75rem', cursor: 'pointer', color: 'var(--fg-3)' }}>
<input
type="radio"
name={`desc-${cveId}`}
checked={descriptionChoices[cveId] === 'nvd'}
onChange={() => setDescriptionChoices(prev => ({ ...prev, [cveId]: 'nvd' }))}
className="text-green-600"
/>
Use NVD
</label>
@@ -442,30 +439,30 @@ export default function NvdSyncModal({ onClose, onSyncComplete }) {
{/* Applying Phase */}
{phase === 'applying' && (
<div className="text-center py-12">
<Loader className="w-10 h-10 text-green-600 animate-spin mx-auto mb-4" />
<p className="text-lg text-gray-700">Applying changes...</p>
<div style={{ textAlign: 'center', padding: '3rem 0' }}>
<Loader className="w-10 h-10 animate-spin" style={{ color: '#10B981', margin: '0 auto 1rem' }} />
<p style={{ fontSize: '1rem', color: 'var(--fg-2)' }}>Applying changes...</p>
</div>
)}
{/* Done Phase */}
{phase === 'done' && applyResult && (
<div className="text-center py-8">
<div style={{ textAlign: 'center', padding: '2rem 0' }}>
{applyResult.error ? (
<>
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<p className="text-lg text-red-700 font-medium mb-2">Sync failed</p>
<p className="text-sm text-gray-600">{applyResult.error}</p>
<AlertCircle className="w-12 h-12" style={{ color: '#EF4444', margin: '0 auto 1rem' }} />
<p style={{ fontSize: '1rem', color: '#EF4444', fontWeight: '500', marginBottom: '0.5rem' }}>Sync failed</p>
<p style={{ fontSize: '0.875rem', color: 'var(--fg-muted)' }}>{applyResult.error}</p>
</>
) : (
<>
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-4" />
<p className="text-lg text-green-700 font-medium mb-2">Sync complete</p>
<p className="text-sm text-gray-600">
<CheckCircle className="w-12 h-12" style={{ color: '#10B981', margin: '0 auto 1rem' }} />
<p style={{ fontSize: '1rem', color: '#10B981', fontWeight: '500', marginBottom: '0.5rem' }}>Sync complete</p>
<p style={{ fontSize: '0.875rem', color: 'var(--fg-muted)' }}>
{applyResult.updated} row{applyResult.updated !== 1 ? 's' : ''} updated
</p>
{applyResult.errors && applyResult.errors.length > 0 && (
<p className="text-sm text-amber-600 mt-2">
<p style={{ fontSize: '0.875rem', color: '#F59E0B', marginTop: '0.5rem' }}>
{applyResult.errors.length} error{applyResult.errors.length !== 1 ? 's' : ''} occurred
</p>
)}
@@ -476,19 +473,19 @@ export default function NvdSyncModal({ onClose, onSyncComplete }) {
</div>
{/* Footer */}
<div className="p-6 border-t border-gray-200 flex justify-end gap-3 flex-shrink-0">
<div style={{ padding: '1.5rem', borderTop: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', flexShrink: 0 }}>
{phase === 'review' && (
<>
<button
onClick={onClose}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
style={{ padding: '0.5rem 1rem', background: 'rgba(148,163,184,0.08)', border: '1px solid rgba(148,163,184,0.3)', color: 'var(--fg-muted)', borderRadius: 'var(--r-md)', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
Cancel
</button>
<button
onClick={applyChanges}
disabled={getChangesCount() === 0}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium shadow-md disabled:opacity-50"
style={{ padding: '0.5rem 1.5rem', background: 'rgba(16,185,129,0.08)', border: '1px solid #10B981', color: '#34D399', borderRadius: 'var(--r-md)', cursor: getChangesCount() === 0 ? 'not-allowed' : 'pointer', fontFamily: 'var(--font-mono)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', opacity: getChangesCount() === 0 ? 0.5 : 1 }}
>
Apply {getChangesCount()} Change{getChangesCount() !== 1 ? 's' : ''}
</button>
@@ -497,7 +494,7 @@ export default function NvdSyncModal({ onClose, onSyncComplete }) {
{phase === 'done' && (
<button
onClick={onClose}
className="px-6 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors font-medium shadow-md"
style={{ padding: '0.5rem 1.5rem', background: 'rgba(14,165,233,0.08)', border: '1px solid #0EA5E9', color: '#38BDF8', borderRadius: 'var(--r-md)', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
Close
</button>

View File

@@ -18,10 +18,10 @@ const GROUP_LABELS = {
};
const GROUP_BADGE_STYLES = {
Admin: { backgroundColor: '#FEE2E2', color: '#991B1B' },
Standard_User: { backgroundColor: '#DBEAFE', color: '#1E40AF' },
Leadership: { backgroundColor: '#F3E8FF', color: '#6B21A8' },
Read_Only: { backgroundColor: '#F3F4F6', color: '#374151' }
Admin: { backgroundColor: 'rgba(239,68,68,0.15)', color: 'var(--group-admin)', border: '1px solid rgba(239,68,68,0.3)' },
Standard_User: { backgroundColor: 'rgba(56,189,248,0.15)', color: 'var(--group-standard)', border: '1px solid rgba(56,189,248,0.3)' },
Leadership: { backgroundColor: 'rgba(245,158,11,0.15)', color: 'var(--group-leadership)', border: '1px solid rgba(245,158,11,0.3)' },
Read_Only: { backgroundColor: 'rgba(148,163,184,0.15)', color: 'var(--group-readonly)', border: '1px solid rgba(148,163,184,0.3)' }
};
export default function UserManagement({ onClose }) {
@@ -196,22 +196,22 @@ export default function UserManagement({ onClose }) {
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-6 border-b border-gray-200 flex justify-between items-center">
<div style={{ position: 'fixed', inset: 0, background: 'var(--bg-overlay)', backdropFilter: 'blur(12px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 'var(--z-modal)', padding: '1rem' }}>
<div style={{ background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%)', border: '1.5px solid rgba(14,165,233,0.3)', borderRadius: 'var(--r-xl)', boxShadow: 'var(--shadow-modal)', maxWidth: '56rem', width: '100%', maxHeight: '90vh', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '1.5rem', borderBottom: '1px solid var(--border-subtle)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2 className="text-2xl font-bold text-gray-900">User Management</h2>
<p className="text-gray-600">Manage user accounts and permissions</p>
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: '1.5rem', fontWeight: '700', color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)', textShadow: 'var(--glow-heading)' }}>User Management</h2>
<p style={{ color: 'var(--fg-muted)', fontSize: '0.875rem' }}>Manage user accounts and permissions</p>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 p-2"
style={{ color: 'var(--fg-disabled)', cursor: 'pointer', background: 'none', border: 'none', padding: '0.5rem' }}
>
<X className="w-6 h-6" />
</button>
</div>
<div className="p-6 overflow-y-auto flex-1">
<div style={{ padding: '1.5rem', overflowY: 'auto', flex: 1 }}>
{!showAddUser && (
<button
onClick={() => {
@@ -221,7 +221,7 @@ export default function UserManagement({ onClose }) {
setFormError('');
setFormSuccess('');
}}
className="mb-6 px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors flex items-center gap-2 shadow-md"
style={{ marginBottom: '1.5rem', padding: '0.5rem 1rem', background: 'rgba(14,165,233,0.08)', border: '1px solid #0EA5E9', color: '#38BDF8', borderRadius: 'var(--r-md)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
<Plus className="w-5 h-5" />
Add User
@@ -229,61 +229,65 @@ export default function UserManagement({ onClose }) {
)}
{showAddUser && (
<div className="mb-6 p-6 bg-gray-50 rounded-lg border border-gray-200">
<h3 className="text-lg font-semibold mb-4">
<div style={{ marginBottom: '1.5rem', padding: '1.5rem', background: 'rgba(15,23,42,0.6)', borderRadius: 'var(--r-lg)', border: '1px solid var(--border-subtle)' }}>
<h3 style={{ fontSize: '1rem', fontWeight: '600', marginBottom: '1rem', color: 'var(--fg-2)', fontFamily: 'var(--font-mono)' }}>
{editingUser ? 'Edit User' : 'Add New User'}
</h3>
{formError && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-red-600" />
<span className="text-sm text-red-700">{formError}</span>
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.3)', borderRadius: 'var(--r-md)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<AlertCircle className="w-5 h-5" style={{ color: '#EF4444' }} />
<span style={{ fontSize: '0.875rem', color: '#FCA5A5' }}>{formError}</span>
</div>
)}
{formSuccess && (
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<span className="text-sm text-green-700">{formSuccess}</span>
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: 'rgba(16,185,129,0.08)', border: '1px solid rgba(16,185,129,0.3)', borderRadius: 'var(--r-md)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<CheckCircle className="w-5 h-5" style={{ color: '#10B981' }} />
<span style={{ fontSize: '0.875rem', color: '#6EE7B7' }}>{formSuccess}</span>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<form onSubmit={handleSubmit}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '1rem' }}>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Username *
</label>
<div className="relative">
<User className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
<div style={{ position: 'relative' }}>
<User className="w-5 h-5" style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-disabled)' }} />
<input
type="text"
required
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
style={{ width: '100%', paddingLeft: '2.5rem', paddingRight: '1rem', paddingTop: '0.5rem', paddingBottom: '0.5rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', outline: 'none' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Email *
</label>
<div className="relative">
<Mail className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
<div style={{ position: 'relative' }}>
<Mail className="w-5 h-5" style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-disabled)' }} />
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
style={{ width: '100%', paddingLeft: '2.5rem', paddingRight: '1rem', paddingTop: '0.5rem', paddingBottom: '0.5rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', outline: 'none' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Password {editingUser ? '(leave blank to keep current)' : '*'}
</label>
<input
@@ -291,21 +295,25 @@ export default function UserManagement({ onClose }) {
required={!editingUser}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
style={{ width: '100%', padding: '0.5rem 1rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', outline: 'none' }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label style={{ display: 'block', fontSize: '0.75rem', fontWeight: '500', color: 'var(--fg-muted)', marginBottom: '0.25rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
Group *
</label>
<div className="relative">
<Shield className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
<div style={{ position: 'relative' }}>
<Shield className="w-5 h-5" style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-disabled)' }} />
<select
value={formData.group}
onChange={(e) => setFormData({ ...formData, group: e.target.value })}
disabled={isGroupDropdownDisabled(editingUser)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
style={{ width: '100%', paddingLeft: '2.5rem', paddingRight: '1rem', paddingTop: '0.5rem', paddingBottom: '0.5rem', background: 'var(--bg-input)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--r-md)', color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', outline: 'none', cursor: isGroupDropdownDisabled(editingUser) ? 'not-allowed' : 'pointer', opacity: isGroupDropdownDisabled(editingUser) ? 0.5 : 1 }}
onFocus={e => { e.target.style.borderColor = 'var(--border-focus)'; e.target.style.boxShadow = 'var(--shadow-focus)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--border-subtle)'; e.target.style.boxShadow = 'none'; }}
title={isGroupDropdownDisabled(editingUser) ? 'Cannot change your own Admin group' : ''}
>
{VALID_GROUPS.map((g) => (
@@ -313,16 +321,16 @@ export default function UserManagement({ onClose }) {
))}
</select>
{isGroupDropdownDisabled(editingUser) && (
<p className="text-xs text-amber-600 mt-1">You cannot change your own Admin group.</p>
<p style={{ fontSize: '0.75rem', color: '#F59E0B', marginTop: '0.25rem' }}>You cannot change your own Admin group.</p>
)}
</div>
</div>
</div>
<div className="flex gap-3 pt-2">
<div style={{ display: 'flex', gap: '0.75rem', paddingTop: '0.5rem' }}>
<button
type="submit"
className="px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors"
style={{ padding: '0.5rem 1rem', background: 'rgba(14,165,233,0.08)', border: '1px solid #0EA5E9', color: '#38BDF8', borderRadius: 'var(--r-md)', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
{editingUser ? 'Update User' : 'Create User'}
</button>
@@ -332,7 +340,7 @@ export default function UserManagement({ onClose }) {
setShowAddUser(false);
setEditingUser(null);
}}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
style={{ padding: '0.5rem 1rem', background: 'rgba(148,163,184,0.08)', border: '1px solid rgba(148,163,184,0.3)', color: 'var(--fg-muted)', borderRadius: 'var(--r-md)', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', fontWeight: '600', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}
>
Cancel
</button>
@@ -342,73 +350,86 @@ export default function UserManagement({ onClose }) {
)}
{loading ? (
<div className="text-center py-12">
<Loader className="w-8 h-8 text-[#0476D9] mx-auto animate-spin" />
<p className="text-gray-600 mt-2">Loading users...</p>
<div style={{ textAlign: 'center', padding: '3rem' }}>
<Loader className="w-8 h-8 mx-auto animate-spin" style={{ color: '#0EA5E9' }} />
<p style={{ color: 'var(--fg-muted)', marginTop: '0.5rem' }}>Loading users...</p>
</div>
) : error ? (
<div className="text-center py-12">
<AlertCircle className="w-8 h-8 text-red-500 mx-auto" />
<p className="text-red-600 mt-2">{error}</p>
<div style={{ textAlign: 'center', padding: '3rem' }}>
<AlertCircle className="w-8 h-8 mx-auto" style={{ color: '#EF4444' }} />
<p style={{ color: '#EF4444', marginTop: '0.5rem' }}>{error}</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%' }}>
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">User</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Group</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Status</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Last Login</th>
<th className="text-right py-3 px-4 text-sm font-medium text-gray-700">Actions</th>
<tr style={{ borderBottom: '1px solid var(--border-subtle)' }}>
<th style={{ textAlign: 'left', padding: '0.75rem 1rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>User</th>
<th style={{ textAlign: 'left', padding: '0.75rem 1rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Group</th>
<th style={{ textAlign: 'left', padding: '0.75rem 1rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Status</th>
<th style={{ textAlign: 'left', padding: '0.75rem 1rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Last Login</th>
<th style={{ textAlign: 'right', padding: '0.75rem 1rem', fontSize: '0.75rem', fontWeight: '600', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>Actions</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-3 px-4">
<tr key={user.id} style={{ borderBottom: '1px solid var(--border-subtle)', transition: 'background var(--dur-fast)' }}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(14,165,233,0.06)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
>
<td style={{ padding: '0.75rem 1rem' }}>
<div>
<p className="font-medium text-gray-900">{user.username}</p>
<p className="text-sm text-gray-500">{user.email}</p>
<p style={{ fontWeight: '500', color: 'var(--fg-2)' }}>{user.username}</p>
<p style={{ fontSize: '0.875rem', color: 'var(--fg-muted)' }}>{user.email}</p>
</div>
</td>
<td className="py-3 px-4">
<td style={{ padding: '0.75rem 1rem' }}>
<span
style={{
...GROUP_BADGE_STYLES[user.group] || GROUP_BADGE_STYLES.Read_Only,
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: '500',
display: 'inline-block'
borderRadius: 'var(--r-pill)',
fontSize: '0.75rem',
fontWeight: '600',
fontFamily: 'var(--font-mono)',
display: 'inline-block',
textTransform: 'uppercase',
letterSpacing: 'var(--tracking-wide)',
}}
>
{user.group ? user.group.replace('_', ' ') : 'Read Only'}
</span>
</td>
<td className="py-3 px-4">
<td style={{ padding: '0.75rem 1rem' }}>
<button
onClick={() => handleToggleActive(user)}
disabled={user.id === currentUser.id}
className={`px-2 py-1 rounded text-xs font-medium ${
user.is_active
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
} ${user.id === currentUser.id ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:opacity-80'}`}
style={{
padding: '0.125rem 0.5rem',
borderRadius: 'var(--r-sm)',
fontSize: '0.75rem',
fontWeight: '600',
fontFamily: 'var(--font-mono)',
border: 'none',
cursor: user.id === currentUser.id ? 'not-allowed' : 'pointer',
opacity: user.id === currentUser.id ? 0.5 : 1,
background: user.is_active ? 'rgba(16,185,129,0.15)' : 'rgba(239,68,68,0.15)',
color: user.is_active ? '#10B981' : '#EF4444',
}}
>
{user.is_active ? 'Active' : 'Inactive'}
</button>
</td>
<td className="py-3 px-4 text-sm text-gray-500">
<td style={{ padding: '0.75rem 1rem', fontSize: '0.875rem', color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>
{user.last_login
? new Date(user.last_login).toLocaleString()
: 'Never'}
</td>
<td className="py-3 px-4">
<div className="flex justify-end gap-2">
<td style={{ padding: '0.75rem 1rem' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}>
<button
onClick={() => handleEdit(user)}
className="p-2 text-gray-600 hover:bg-gray-100 rounded"
style={{ padding: '0.5rem', color: 'var(--fg-muted)', background: 'none', border: 'none', cursor: 'pointer', borderRadius: 'var(--r-sm)' }}
title="Edit user"
>
<Edit2 className="w-4 h-4" />
@@ -416,9 +437,7 @@ export default function UserManagement({ onClose }) {
<button
onClick={() => handleDelete(user.id)}
disabled={user.id === currentUser.id}
className={`p-2 text-red-600 hover:bg-red-50 rounded ${
user.id === currentUser.id ? 'opacity-50 cursor-not-allowed' : ''
}`}
style={{ padding: '0.5rem', color: '#EF4444', background: 'none', border: 'none', cursor: user.id === currentUser.id ? 'not-allowed' : 'pointer', opacity: user.id === currentUser.id ? 0.5 : 1, borderRadius: 'var(--r-sm)' }}
title="Delete user"
>
<Trash2 className="w-4 h-4" />

View File

@@ -4,7 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
import UserProfilePanel from './UserProfilePanel';
// ============================================
// INLINE STYLES — Dark theme matching DESIGN_SYSTEM.md
// INLINE STYLES — Design-token based
// ============================================
const STYLES = {
container: {
@@ -13,47 +13,42 @@ const STYLES = {
menuButton: {
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 0.75rem',
borderRadius: '0.5rem',
gap: 10,
padding: '6px 10px',
borderRadius: 6,
background: 'transparent',
border: 'none',
border: '1px solid var(--border-1)',
cursor: 'pointer',
fontFamily: 'var(--font-ui)',
fontSize: 13,
color: 'var(--fg-1)',
transition: 'background 0.2s',
},
menuButtonHover: {
background: 'rgba(14, 165, 233, 0.1)',
background: 'var(--bg-elevated)',
},
avatar: {
width: '2rem',
height: '2rem',
backgroundColor: '#0476D9',
borderRadius: '9999px',
width: 26,
height: 26,
borderRadius: '50%',
background: 'var(--accent-soft)',
color: 'var(--accent)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
avatarIcon: {
color: '#FFFFFF',
},
userInfo: {
textAlign: 'left',
fontFamily: 'var(--font-ui)',
fontWeight: 700,
fontSize: 11,
},
username: {
fontSize: '0.875rem',
fontWeight: '500',
color: '#F8FAFC',
margin: 0,
lineHeight: 1.25,
},
groupLabel: {
fontSize: '0.75rem',
color: '#E2E8F0',
fontFamily: 'var(--font-ui)',
fontSize: 13,
color: 'var(--fg-1)',
margin: 0,
lineHeight: 1.25,
},
chevron: {
color: '#E2E8F0',
color: 'var(--fg-2)',
transition: 'transform 0.2s',
},
chevronOpen: {
@@ -63,40 +58,50 @@ const STYLES = {
dropdown: {
position: 'absolute',
right: 0,
marginTop: '0.5rem',
width: '16rem',
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
borderRadius: '0.5rem',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.6), 0 10px 30px rgba(14, 165, 233, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
border: '1.5px solid rgba(14, 165, 233, 0.3)',
padding: '0.5rem 0',
zIndex: 50,
top: '110%',
minWidth: 240,
background: 'var(--bg-surface)',
border: '1px solid var(--border-1)',
borderRadius: 8,
boxShadow: 'var(--shadow-popover)',
padding: 8,
zIndex: 60,
},
// Dropdown header section
dropdownHeader: {
padding: '0.75rem 1rem',
borderBottom: '1px solid rgba(14, 165, 233, 0.2)',
padding: '8px 10px',
borderBottom: '1px solid var(--border-1)',
marginBottom: 6,
},
dropdownHeaderName: {
fontSize: '0.875rem',
fontWeight: '500',
color: '#F8FAFC',
fontFamily: 'var(--font-ui)',
fontWeight: 600,
fontSize: 13,
color: 'var(--fg-1)',
margin: 0,
},
dropdownHeaderEmail: {
fontSize: '0.875rem',
color: '#94A3B8',
fontFamily: 'var(--font-mono)',
fontWeight: 400,
fontSize: 11,
color: 'var(--fg-muted)',
margin: 0,
marginTop: 2,
},
groupBadgeSection: {
marginTop: 8,
},
// Menu items
menuItem: {
width: '100%',
padding: '0.5rem 1rem',
padding: '7px 10px',
textAlign: 'left',
fontSize: '0.875rem',
color: '#F8FAFC',
fontFamily: 'var(--font-ui)',
fontSize: 13,
color: 'var(--fg-2)',
background: 'transparent',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
@@ -104,17 +109,19 @@ const STYLES = {
transition: 'background 0.15s',
},
menuItemHover: {
background: 'rgba(14, 165, 233, 0.1)',
background: 'var(--bg-elevated)',
},
// Sign out item
signOutItem: {
width: '100%',
padding: '0.5rem 1rem',
padding: '7px 10px',
textAlign: 'left',
fontSize: '0.875rem',
color: '#F87171',
fontFamily: 'var(--font-ui)',
fontSize: 13,
color: 'var(--sev-critical)',
background: 'transparent',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
@@ -122,7 +129,7 @@ const STYLES = {
transition: 'background 0.15s',
},
signOutItemHover: {
background: 'rgba(239, 68, 68, 0.1)',
background: 'var(--bg-elevated)',
},
};
@@ -140,7 +147,6 @@ function getGroupBadgeStyle(group) {
const c = colors[group] || colors.Read_Only;
return {
display: 'inline-block',
marginTop: '0.5rem',
padding: '0.125rem 0.5rem',
borderRadius: '0.25rem',
fontSize: '0.75rem',
@@ -151,6 +157,18 @@ function getGroupBadgeStyle(group) {
};
}
/**
* Get user initials from username (up to 2 characters).
*/
function getUserInitials(username) {
if (!username) return '';
const parts = username.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return username.slice(0, 2).toUpperCase();
}
export default function UserMenu({ onManageUsers, onAuditLog }) {
const { user, logout, isAdmin } = useAuth();
const [isOpen, setIsOpen] = useState(false);
@@ -213,15 +231,13 @@ export default function UserMenu({ onManageUsers, onAuditLog }) {
...(buttonHovered ? STYLES.menuButtonHover : {}),
}}
>
{/* Circular avatar with initials */}
<div style={STYLES.avatar}>
<User size={16} style={STYLES.avatarIcon} />
</div>
<div style={STYLES.userInfo} className="hidden sm:block">
<p style={STYLES.username}>{user.username}</p>
<p style={STYLES.groupLabel}>{formatGroupName(user.group)}</p>
{getUserInitials(user.username)}
</div>
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 13 }}>{user.username}</span>
<ChevronDown
size={16}
size={14}
style={{
...STYLES.chevron,
...(isOpen ? STYLES.chevronOpen : {}),
@@ -234,9 +250,11 @@ export default function UserMenu({ onManageUsers, onAuditLog }) {
<div style={STYLES.dropdownHeader}>
<p style={STYLES.dropdownHeaderName}>{user.username}</p>
<p style={STYLES.dropdownHeaderEmail}>{user.email}</p>
<span style={getGroupBadgeStyle(user.group)}>
{formatGroupName(user.group)}
</span>
<div style={STYLES.groupBadgeSection}>
<span style={getGroupBadgeStyle(user.group)}>
{formatGroupName(user.group)}
</span>
</div>
</div>
<button

View File

@@ -34,9 +34,9 @@ const ARCHER_STATUS_COLORS = {
// ---------------------------------------------------------------------------
// Shared style tokens
// ---------------------------------------------------------------------------
const AXIS_STYLE = { fontFamily: 'monospace', fontSize: '0.6rem', fill: '#475569' };
const AXIS_STYLE = { fontFamily: 'var(--font-mono)', fontSize: '0.6rem', fill: '#475569' };
const GRID_STYLE = { stroke: 'rgba(255,255,255,0.05)', strokeDasharray: '3 3' };
const LEGEND_STYLE = { fontFamily: 'monospace', fontSize: '0.62rem', color: '#64748B' };
const LEGEND_STYLE = { fontFamily: 'var(--font-mono)', fontSize: '0.62rem', color: '#64748B' };
// ---------------------------------------------------------------------------
// Custom dark tooltip
@@ -49,7 +49,7 @@ function DarkTooltip({ active, payload, label }) {
border: '1px solid rgba(20,184,166,0.3)',
borderRadius: '0.375rem',
padding: '0.5rem 0.75rem',
fontFamily: 'monospace',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
minWidth: '130px',
}}>
@@ -76,20 +76,20 @@ function DarkTooltip({ active, payload, label }) {
function ChartCard({ title, subtitle, children }) {
return (
<div style={{
background: 'linear-gradient(135deg,rgba(30,41,59,0.95) 0%,rgba(15,23,42,0.98) 100%)',
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1px solid rgba(20,184,166,0.15)',
borderRadius: '0.5rem',
borderRadius: 8,
padding: '1rem 1.125rem 0.875rem',
}}>
<div style={{ marginBottom: '0.75rem' }}>
<div style={{
fontFamily: 'monospace', fontSize: '0.72rem', fontWeight: '700',
color: '#CBD5E1', textTransform: 'uppercase', letterSpacing: '0.08em',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
}}>
{title}
</div>
{subtitle && (
<div style={{ fontSize: '0.62rem', color: '#334155', marginTop: '0.2rem', fontFamily: 'monospace' }}>
<div style={{ fontSize: 10, color: 'var(--fg-disabled)', marginTop: '0.2rem', fontFamily: 'var(--font-mono)' }}>
{subtitle}
</div>
)}
@@ -106,10 +106,10 @@ function NoData({ msg }) {
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
height: '160px', color: '#334155',
fontFamily: 'monospace', fontSize: '0.72rem',
height: '160px', color: 'var(--fg-disabled)',
fontFamily: 'var(--font-mono)', fontSize: 12,
border: '1px dashed rgba(20,184,166,0.1)',
borderRadius: '0.375rem',
borderRadius: 6,
}}>
{msg || 'No data yet — upload compliance reports to populate this chart'}
</div>
@@ -347,8 +347,8 @@ export default function ComplianceChartsPanel() {
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<TrendingUp style={{ width: '14px', height: '14px', color: TEAL }} />
<span style={{
fontFamily: 'monospace', fontSize: '0.65rem', fontWeight: '700',
color: '#334155', textTransform: 'uppercase', letterSpacing: '0.1em',
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em',
}}>
Historical Trends
</span>

View File

@@ -30,8 +30,8 @@ function MetricChip({ metricId, category, status }) {
padding: '0.2rem 0.5rem',
background: `${color}18`,
border: `1px solid ${color}50`,
borderRadius: '0.25rem',
color, fontSize: '0.7rem', fontFamily: 'monospace', fontWeight: '600',
borderRadius: 4,
color, fontSize: '0.7rem', fontFamily: 'var(--font-mono)', fontWeight: '600',
}}>
{metricId}
</span>
@@ -126,8 +126,8 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
{/* Panel */}
<div style={{
position: 'fixed', top: 0, right: 0, bottom: 0, width: '420px',
background: 'linear-gradient(180deg, #0F1A2E 0%, #0A1628 100%)',
borderLeft: `1px solid ${TEAL}30`,
background: 'var(--bg-surface)',
borderLeft: '1px solid var(--border-subtle)',
boxShadow: `-8px 0 32px rgba(0,0,0,0.6)`,
zIndex: 41,
display: 'flex', flexDirection: 'column',
@@ -136,29 +136,29 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
{/* Header */}
<div style={{
padding: '1.25rem 1.25rem 1rem',
borderBottom: '1px solid rgba(255,255,255,0.06)',
borderBottom: '1px solid var(--border-subtle)',
flexShrink: 0,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ minWidth: 0, flex: 1, marginRight: '0.75rem' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.9rem', fontWeight: '700', color: '#F8FAFC', wordBreak: 'break-all', lineHeight: 1.3 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.9rem', fontWeight: '700', color: '#F8FAFC', wordBreak: 'break-all', lineHeight: 1.3 }}>
{hostname}
</div>
{detail && (
<div style={{ marginTop: '0.4rem', display: 'flex', flexWrap: 'wrap', gap: '0.4rem' }}>
{detail.ip_address && (
<span style={{ fontSize: '0.72rem', fontFamily: 'monospace', color: '#64748B' }}>{detail.ip_address}</span>
<span style={{ fontSize: '0.72rem', fontFamily: 'var(--font-mono)', color: 'var(--fg-2)' }}>{detail.ip_address}</span>
)}
{detail.device_type && (
<span style={{ fontSize: '0.72rem', color: '#475569' }}>· {detail.device_type}</span>
<span style={{ fontSize: '0.72rem', color: 'var(--fg-disabled)' }}>· {detail.device_type}</span>
)}
<span style={{ fontSize: '0.72rem', color: TEAL }}>· {detail.team}</span>
</div>
)}
</div>
<button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#475569', flexShrink: 0, padding: '0.25rem' }}
onMouseEnter={e => e.currentTarget.style.color = '#E2E8F0'}
onMouseLeave={e => e.currentTarget.style.color = '#475569'}>
<button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fg-disabled)', flexShrink: 0, padding: '0.25rem' }}
onMouseEnter={e => e.currentTarget.style.color = 'var(--fg-1)'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--fg-disabled)'}>
<X style={{ width: '18px', height: '18px' }} />
</button>
</div>
@@ -203,7 +203,7 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
{activeMetrics.map(m => (
<div key={m.metric_id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.35rem' }}>
<MetricChip metricId={m.metric_id} category={m.category} />
<div style={{ fontSize: '0.72rem', color: '#64748B', fontFamily: 'monospace', textAlign: 'right' }}>
<div style={{ fontSize: '0.72rem', color: 'var(--fg-2)', fontFamily: 'var(--font-mono)', textAlign: 'right' }}>
<span style={{ color: m.seen_count > 2 ? '#F59E0B' : '#94A3B8' }}>
{m.seen_count}× seen
</span>
@@ -253,7 +253,7 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', flexShrink: 0, marginLeft: '0.5rem' }}>
<span style={{ fontSize: '0.68rem', color: '#334155', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>
<span style={{ fontSize: '0.68rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', whiteSpace: 'nowrap' }}>
{g.created_by && `${g.created_by} · `}{g.created_at?.slice(0, 10)}
</span>
<button
@@ -284,7 +284,7 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
return (
<div style={{ marginBottom: '0.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.35rem' }}>
<span style={{ fontSize: '0.68rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.05em', color: '#475569' }}>
<span style={{ fontSize: '0.68rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--fg-disabled)' }}>
Metrics
</span>
<button
@@ -297,7 +297,7 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
fontSize: '0.68rem', fontFamily: 'monospace',
fontSize: '0.68rem', fontFamily: 'var(--font-mono)',
color: TEAL, padding: 0,
transition: 'opacity 0.15s',
}}
@@ -328,9 +328,9 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
padding: '0.25rem 0.5rem',
background: isSelected ? `${color}25` : `${color}08`,
border: `1px solid ${isSelected ? `${color}90` : `${color}30`}`,
borderRadius: '0.25rem',
borderRadius: 4,
color: isSelected ? color : `${color}90`,
fontSize: '0.7rem', fontFamily: 'monospace', fontWeight: '600',
fontSize: '0.7rem', fontFamily: 'var(--font-mono)', fontWeight: '600',
cursor: (isSelected && selectedMetrics.length === 1) ? 'default' : 'pointer',
transition: 'all 0.15s',
opacity: (isSelected && selectedMetrics.length === 1) ? 0.85 : 1,
@@ -399,16 +399,16 @@ function Section({ title, icon, children, muted, grow }) {
return (
<div style={{
padding: '1rem 1.25rem',
borderBottom: '1px solid rgba(255,255,255,0.04)',
borderBottom: '1px solid var(--border-subtle)',
...(grow ? { flex: 1, display: 'flex', flexDirection: 'column' } : {}),
}}>
<div style={{
display: 'flex', alignItems: 'center', gap: '0.4rem',
fontSize: '0.68rem', fontFamily: 'monospace', textTransform: 'uppercase',
letterSpacing: '0.1em', color: muted ? '#334155' : '#475569',
fontSize: '0.68rem', fontFamily: 'var(--font-mono)', textTransform: 'uppercase',
letterSpacing: '0.1em', color: muted ? 'var(--fg-disabled)' : 'var(--fg-disabled)',
marginBottom: '0.75rem',
}}>
{icon && <span style={{ color: muted ? '#334155' : TEAL }}>{icon}</span>}
{icon && <span style={{ color: muted ? 'var(--fg-disabled)' : TEAL }}>{icon}</span>}
{title}
</div>
{children}
@@ -441,12 +441,12 @@ function MetricRow({ metric, resolved, onNavigate }) {
marginBottom: '0.625rem', padding: '0.625rem 0.75rem',
background: resolved ? 'transparent' : `${color}08`,
border: `1px solid ${color}25`,
borderRadius: '0.375rem',
borderRadius: 6,
opacity: resolved ? 0.5 : 1,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: highlights.length ? '0.4rem' : 0 }}>
<MetricChip metricId={metric.metric_id} category={metric.category} status={metric.status} />
{resolved && <span style={{ fontSize: '0.68rem', color: '#334155', fontFamily: 'monospace' }}>resolved {metric.resolved_on || ''}</span>}
{resolved && <span style={{ fontSize: '0.68rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)' }}>resolved {metric.resolved_on || ''}</span>}
</div>
{metric.metric_desc && (
<div style={{ fontSize: '0.72rem', color: '#475569', marginBottom: (highlights.length || ivantiId) ? '0.4rem' : 0, lineHeight: 1.4 }}>
@@ -456,8 +456,8 @@ function MetricRow({ metric, resolved, onNavigate }) {
{ivantiId && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: highlights.length ? '0.25rem' : 0 }}>
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', minWidth: 0 }}>
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', flexShrink: 0 }}>Ivanti ID</span>
<span style={{ fontSize: '0.68rem', color: '#94A3B8', fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ivantiId}</span>
<span style={{ fontSize: '0.68rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', flexShrink: 0 }}>Ivanti ID</span>
<span style={{ fontSize: '0.68rem', color: 'var(--fg-2)', fontFamily: 'var(--font-mono)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ivantiId}</span>
</div>
{onNavigate && (
<button
@@ -468,7 +468,7 @@ function MetricRow({ metric, resolved, onNavigate }) {
border: '1px solid rgba(14,165,233,0.4)',
borderRadius: '0.25rem',
color: '#0EA5E9',
fontSize: '0.65rem', fontFamily: 'monospace',
fontSize: '0.65rem', fontFamily: 'var(--font-mono)',
padding: '0.2rem 0.5rem',
cursor: 'pointer', whiteSpace: 'nowrap',
transition: 'all 0.15s',
@@ -483,8 +483,8 @@ function MetricRow({ metric, resolved, onNavigate }) {
)}
{highlights.map(h => (
<div key={h.label} style={{ display: 'flex', gap: '0.4rem', marginTop: '0.25rem' }}>
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', minWidth: '48px' }}>{h.label}</span>
<span style={{ fontSize: '0.68rem', color: '#94A3B8', fontFamily: 'monospace', wordBreak: 'break-all' }}>
<span style={{ fontSize: '0.68rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', minWidth: '48px' }}>{h.label}</span>
<span style={{ fontSize: '0.68rem', color: 'var(--fg-2)', fontFamily: 'var(--font-mono)', wordBreak: 'break-all' }}>
{String(h.value).length > 80 ? String(h.value).slice(0, 80) + '…' : h.value}
</span>
</div>

View File

@@ -97,25 +97,25 @@ function VariantPill({ entry, label }) {
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.3rem',
padding: '0.15rem 0.45rem',
gap: 4,
padding: '2px 7px',
background: `${color}1F`,
borderRadius: '0.2rem',
border: `1px solid ${color}25`,
fontSize: '0.62rem',
fontFamily: 'monospace',
color: '#CBD5E1',
borderRadius: 3,
border: `1px solid ${color}40`,
fontSize: 10,
fontFamily: 'var(--font-mono)',
color: 'var(--fg-2)',
whiteSpace: 'nowrap',
}}>
{!isOk && (
<span style={{
width: '4px', height: '4px', borderRadius: '50%',
display: 'inline-block', width: 4, height: 4, borderRadius: '50%',
background: color, flexShrink: 0,
boxShadow: `0 0 5px ${color}`,
}} />
)}
{label && <span style={{ color: '#94A3B8' }}>{label}</span>}
<span style={{ color, fontWeight: '600' }}>{pctDisplay(entry.compliance_pct)}</span>
{label && <span style={{ color: 'var(--fg-disabled)' }}>{label}</span>}
<span style={{ color, fontWeight: 600 }}>{pctDisplay(entry.compliance_pct)}</span>
</span>
);
}
@@ -128,18 +128,16 @@ function MetricHealthCard({ family, active, onClick, onInfoClick, definitionLook
<button
onClick={onClick}
style={{
position: 'relative', textAlign: 'left', cursor: 'pointer',
background: active
? `${color}18`
: 'linear-gradient(135deg,rgba(30,41,59,0.95) 0%,rgba(15,23,42,0.98) 100%)',
? `${color}26`
: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: `1.5px solid ${active ? color : color + '40'}`,
borderRadius: '0.5rem',
padding: '0.875rem 1rem',
cursor: 'pointer',
textAlign: 'left',
transition: 'all 0.15s',
minWidth: '160px',
borderRadius: 8,
padding: '14px 16px',
minWidth: 160,
flex: '1 1 0',
position: 'relative',
transition: 'all 160ms ease',
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.borderColor = color + '80'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.borderColor = active ? color : color + '40'; }}
@@ -149,37 +147,43 @@ function MetricHealthCard({ family, active, onClick, onInfoClick, definitionLook
onClick={(e) => { e.stopPropagation(); onInfoClick(family.metricId); }}
style={{
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
top: 8,
right: 8,
cursor: 'pointer',
color: '#475569',
display: 'flex',
color: 'var(--fg-disabled)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0.15rem',
borderRadius: '0.2rem',
transition: 'color 0.15s',
padding: 2,
borderRadius: 3,
transition: 'color 160ms ease',
}}
onMouseEnter={e => { e.currentTarget.style.color = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--fg-disabled)'; }}
>
<Info style={{ width: '13px', height: '13px' }} />
<Info style={{ width: 13, height: 13 }} />
</span>
{/* Metric ID */}
<div style={{ fontFamily: 'monospace', fontSize: '0.95rem', fontWeight: '700', color: active ? color : '#E2E8F0', marginBottom: '0.25rem', paddingRight: '1.25rem' }}>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700,
color: active ? color : 'var(--fg-1)', marginBottom: 4, paddingRight: 20,
}}>
{family.metricId}
</div>
{/* Category */}
<div style={{ fontSize: '0.65rem', color: '#475569', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 10,
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.05em',
marginBottom: 8,
}}>
{family.category}
</div>
{/* Variant pills */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', marginBottom: '0.5rem' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 8 }}>
{family.entries.map((entry, i) => {
// Only show a label when there are multiple variants to differentiate
let label = null;
if (family.entries.length > 1) {
label = entry.priority || `#${i + 1}`;
@@ -189,20 +193,24 @@ function MetricHealthCard({ family, active, onClick, onInfoClick, definitionLook
</div>
{/* Target */}
<div style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', marginBottom: '0.5rem' }}>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 10,
color: 'var(--fg-disabled)', marginBottom: 8,
}}>
target {pctDisplay(family.target)}
</div>
{/* Status pill */}
{/* Status ribbon */}
<div style={{
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
fontSize: '0.62rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.04em',
color, padding: '0.2rem 0.5rem',
background: `${color}12`, borderRadius: '999px',
border: `1px solid ${color}30`,
display: 'inline-flex', alignItems: 'center', gap: 5,
fontFamily: 'var(--font-mono)', fontSize: 10,
textTransform: 'uppercase', letterSpacing: '0.04em',
color, padding: '3px 9px',
background: `${color}1A`, borderRadius: 999,
border: `1px solid ${color}4D`,
}}>
<span style={{
width: '5px', height: '5px', borderRadius: '50%',
display: 'inline-block', width: 5, height: 5, borderRadius: '50%',
background: color, flexShrink: 0,
...(isOk ? {} : { boxShadow: `0 0 6px ${color}` }),
}} />
@@ -217,10 +225,10 @@ function MetricBadge({ metricId, category }) {
return (
<span style={{
display: 'inline-flex', alignItems: 'center',
padding: '0.15rem 0.45rem',
background: `${color}15`, border: `1px solid ${color}40`,
borderRadius: '0.2rem', color,
fontSize: '0.65rem', fontFamily: 'monospace', fontWeight: '600',
padding: '2px 7px',
background: `${color}1F`, border: `1px solid ${color}4D`,
borderRadius: 3, color,
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
whiteSpace: 'nowrap',
}}>
{metricId}
@@ -232,10 +240,11 @@ function SeenBadge({ count }) {
const color = count > 3 ? '#EF4444' : count > 1 ? '#F59E0B' : '#64748B';
return (
<span style={{
fontSize: '0.65rem', fontFamily: 'monospace', fontWeight: '700',
color, padding: '0.15rem 0.4rem',
background: `${color}12`, borderRadius: '0.2rem',
border: `1px solid ${color}30`, whiteSpace: 'nowrap',
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
color, padding: '2px 7px',
background: `${color}1A`,
border: `1px solid ${color}4D`,
borderRadius: 3, whiteSpace: 'nowrap',
}}>
{count}×
</span>
@@ -345,32 +354,33 @@ export default function CompliancePage({ onNavigate }) {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1.5rem' }}>
<div>
<h2 style={{
fontFamily: 'monospace', fontSize: '1.5rem', fontWeight: '700',
margin: '0 0 6px 0',
fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 700,
color: TEAL, textTransform: 'uppercase', letterSpacing: '0.1em',
textShadow: `0 0 16px ${TEAL}40`, marginBottom: '0.25rem',
textShadow: `0 0 16px rgba(20,184,166,0.4)`,
}}>
AEO Compliance
</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
{lastUpload ? (
<>
<span style={{ fontSize: '0.72rem', color: '#475569', fontFamily: 'monospace' }}>
Last report: <span style={{ color: '#64748B' }}>{lastUpload.report_date || lastUpload.uploaded_at?.slice(0, 10)}</span>
<span style={{ fontSize: '0.72rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)' }}>
Last report: <span style={{ color: 'var(--fg-2)' }}>{lastUpload.report_date || lastUpload.uploaded_at?.slice(0, 10)}</span>
</span>
{isAdmin() && (
<button
onClick={() => setRollbackConfirm(true)}
title="Rollback last upload"
style={{
background: 'none', border: '1px solid rgba(239,68,68,0.25)',
borderRadius: '0.25rem', padding: '0.15rem 0.4rem',
cursor: 'pointer', color: '#64748B',
display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
fontSize: '0.62rem', fontFamily: 'monospace',
transition: 'all 0.15s',
background: 'transparent', border: '1px solid rgba(239,68,68,0.25)',
borderRadius: 4, padding: '2px 6px',
cursor: 'pointer', color: 'var(--fg-2)',
display: 'inline-flex', alignItems: 'center', gap: 4,
fontSize: 10, fontFamily: 'var(--font-mono)',
transition: 'all 160ms ease',
}}
onMouseEnter={e => { e.currentTarget.style.color = '#EF4444'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.6)'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#64748B'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.25)'; }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--fg-2)'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.25)'; }}
>
<RotateCcw style={{ width: '10px', height: '10px' }} />
Rollback
@@ -378,15 +388,15 @@ export default function CompliancePage({ onNavigate }) {
)}
</>
) : (
<span style={{ fontSize: '0.72rem', color: '#334155', fontFamily: 'monospace' }}>No reports uploaded</span>
<span style={{ fontSize: '0.72rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)' }}>No reports uploaded</span>
)}
{summary.overall_scores?.customer_network != null && (
<span style={{ fontSize: '0.68rem', fontFamily: 'monospace', color: '#64748B' }}>
<span style={{ fontSize: '0.68rem', fontFamily: 'var(--font-mono)', color: 'var(--fg-2)' }}>
Network: <span style={{ color: TEAL }}>{pctDisplay(summary.overall_scores.customer_network)}</span>
</span>
)}
{summary.overall_scores?.vertical != null && (
<span style={{ fontSize: '0.68rem', fontFamily: 'monospace', color: '#64748B' }}>
<span style={{ fontSize: '0.68rem', fontFamily: 'var(--font-mono)', color: 'var(--fg-2)' }}>
Vertical: <span style={{ color: TEAL }}>{pctDisplay(summary.overall_scores.vertical)}</span>
</span>
)}
@@ -395,21 +405,26 @@ export default function CompliancePage({ onNavigate }) {
<div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0 }}>
<button onClick={refresh} title="Refresh"
style={{ background: 'none', border: '1px solid rgba(20,184,166,0.25)', borderRadius: '0.375rem', padding: '0.5rem', cursor: 'pointer', color: '#475569' }}
onMouseEnter={e => { e.currentTarget.style.color = TEAL; e.currentTarget.style.borderColor = `${TEAL}60`; }}
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.25)'; }}>
style={{
background: 'transparent', border: `1px solid rgba(20,184,166,0.25)`,
borderRadius: 6, padding: 8, cursor: 'pointer', color: 'var(--fg-2)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 160ms ease',
}}
onMouseEnter={e => { e.currentTarget.style.color = TEAL; e.currentTarget.style.borderColor = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--fg-2)'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.25)'; }}>
<RefreshCw style={{ width: '16px', height: '16px' }} />
</button>
{canWrite() && (
<button onClick={() => setShowUpload(true)}
className="intel-button"
style={{
background: `${TEAL}18`, border: `1px solid ${TEAL}`,
color: TEAL, padding: '0.5rem 1rem',
display: 'flex', alignItems: 'center', gap: '0.4rem',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
background: 'rgba(20,184,166,0.18)', border: `1px solid ${TEAL}`,
color: TEAL, padding: '8px 16px',
display: 'inline-flex', alignItems: 'center', gap: 6,
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em', cursor: 'pointer',
borderRadius: '0.375rem',
borderRadius: 6, transition: 'all 160ms ease', whiteSpace: 'nowrap',
}}>
<Upload style={{ width: '14px', height: '14px' }} />
Upload Report
@@ -419,23 +434,23 @@ export default function CompliancePage({ onNavigate }) {
</div>
{/* ── Team tabs ────────────────────────────────────────────── */}
<div style={{ display: 'flex', gap: '0.375rem', marginBottom: '1.5rem' }}>
<div style={{ display: 'flex', gap: 6, marginBottom: 24 }}>
{TEAMS.map(team => {
const isActive = activeTeam === team;
return (
<button key={team} onClick={() => setActiveTeam(team)}
style={{
padding: '0.5rem 1.25rem', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '600',
padding: '8px 18px', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.06em',
borderRadius: '0.375rem',
border: isActive ? `1px solid ${TEAL}` : '1px solid rgba(20,184,166,0.2)',
background: isActive ? `${TEAL}18` : 'transparent',
color: isActive ? TEAL : '#475569',
transition: 'all 0.15s',
borderRadius: 6,
border: isActive ? `1px solid ${TEAL}` : '1px solid rgba(20,184,166,0.20)',
background: isActive ? 'rgba(20,184,166,0.18)' : 'transparent',
color: isActive ? TEAL : 'var(--fg-disabled)',
transition: 'all 160ms ease',
}}
onMouseEnter={e => { if (!isActive) { e.currentTarget.style.color = '#94A3B8'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.4)'; }}}
onMouseLeave={e => { if (!isActive) { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.2)'; }}}>
onMouseLeave={e => { if (!isActive) { e.currentTarget.style.color = 'var(--fg-disabled)'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.20)'; }}}>
{team}
</button>
);
@@ -445,11 +460,11 @@ export default function CompliancePage({ onNavigate }) {
{/* ── Metric health cards ──────────────────────────────────── */}
{families.length > 0 ? (
<div style={{ marginBottom: '1.5rem' }}>
<div style={{ fontSize: '0.65rem', color: '#334155', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.625rem' }}>
<div style={{ fontSize: 10, color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 10 }}>
Metric Health click to filter
{metricFilter && (
<button onClick={() => setMetricFilter(null)}
style={{ marginLeft: '0.75rem', color: TEAL, background: 'none', border: 'none', cursor: 'pointer', fontSize: '0.65rem', fontFamily: 'monospace' }}>
style={{ marginLeft: 12, color: TEAL, background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, fontFamily: 'var(--font-mono)' }}>
× clear filter
</button>
)}
@@ -501,29 +516,29 @@ export default function CompliancePage({ onNavigate }) {
top: tooltipTop,
left: tooltipLeft,
zIndex: 50,
width: '300px',
width: 300,
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: '1px solid rgba(20,184,166,0.25)',
borderRadius: '0.5rem',
border: '1px solid rgba(20,184,166,0.30)',
borderRadius: 8,
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
padding: '0.75rem 0.875rem',
padding: '12px 14px',
pointerEvents: 'none',
}}>
<div style={{ fontFamily: 'monospace', fontSize: '0.82rem', fontWeight: '700', color: '#E2E8F0', marginBottom: '0.4rem', lineHeight: 1.3 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700, color: 'var(--fg-1)', marginBottom: 6, lineHeight: 1.3 }}>
{def ? def.metric_title : (family.entries[0]?.description || hoveredMetric)}
</div>
{def && def.business_justification && (
<div style={{ fontSize: '0.72rem', color: '#94A3B8', marginBottom: '0.3rem', lineHeight: 1.4 }}>
<div style={{ fontFamily: 'var(--font-display, var(--font-ui))', fontSize: 12, color: 'var(--fg-2)', marginBottom: 6, lineHeight: 1.4 }}>
{def.business_justification}
</div>
)}
{def && def.data_sources_required && (
<div style={{ fontSize: '0.65rem', color: '#475569', fontFamily: 'monospace' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)' }}>
Sources: {def.data_sources_required}
</div>
)}
{!def && family.entries[0]?.description && (
<div style={{ fontSize: '0.72rem', color: '#94A3B8', lineHeight: 1.4 }}>
<div style={{ fontSize: 12, color: 'var(--fg-2)', lineHeight: 1.4 }}>
{family.entries[0].description}
</div>
)}
@@ -534,10 +549,10 @@ export default function CompliancePage({ onNavigate }) {
) : lastUpload === null ? (
<div style={{
marginBottom: '1.5rem', padding: '2rem',
border: '1px dashed rgba(20,184,166,0.2)', borderRadius: '0.5rem',
border: '1px dashed rgba(20,184,166,0.2)', borderRadius: 8,
textAlign: 'center',
}}>
<div style={{ color: '#334155', fontFamily: 'monospace', fontSize: '0.8rem' }}>
<div style={{ color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>
No compliance data upload a report to get started
</div>
</div>
@@ -548,33 +563,33 @@ export default function CompliancePage({ onNavigate }) {
{/* ── Device table ─────────────────────────────────────────── */}
<div style={{
background: 'linear-gradient(135deg,rgba(30,41,59,0.95) 0%,rgba(15,23,42,0.98) 100%)',
border: '1px solid rgba(20,184,166,0.15)', borderRadius: '0.5rem',
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1px solid rgba(20,184,166,0.15)', borderRadius: 8,
overflow: 'hidden',
}}>
{/* Table toolbar */}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '0.875rem 1rem', borderBottom: '1px solid rgba(255,255,255,0.05)',
padding: '14px 16px', borderBottom: '1px solid rgba(255,255,255,0.05)',
}}>
{/* Active / Resolved tabs */}
<div style={{ display: 'flex', gap: '0.25rem' }}>
<div style={{ display: 'flex', gap: 4 }}>
{['active', 'resolved'].map(tab => {
const isActive = activeTab === tab;
return (
<button key={tab} onClick={() => setActiveTab(tab)}
style={{
padding: '0.35rem 0.875rem', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.72rem', fontWeight: '600',
padding: '6px 14px', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.04em',
borderRadius: '0.25rem',
border: isActive ? `1px solid ${TEAL}60` : '1px solid transparent',
background: isActive ? `${TEAL}12` : 'transparent',
color: isActive ? TEAL : '#475569',
borderRadius: 4,
border: `1px solid ${isActive ? 'rgba(20,184,166,0.40)' : 'transparent'}`,
background: isActive ? 'rgba(20,184,166,0.10)' : 'transparent',
color: isActive ? TEAL : 'var(--fg-disabled)',
}}>
{tab}
{isActive && (
<span style={{ marginLeft: '0.4rem', color: '#64748B' }}>
<span style={{ marginLeft: 6, color: 'var(--fg-2)' }}>
({loading ? '…' : filteredDevices.length})
</span>
)}
@@ -589,13 +604,14 @@ export default function CompliancePage({ onNavigate }) {
onChange={e => setHostSearch(e.target.value)}
placeholder="Search hostname…"
style={{
background: 'rgba(15,23,42,0.8)', border: '1px solid rgba(20,184,166,0.2)',
borderRadius: '0.25rem', color: '#E2E8F0', outline: 'none',
padding: '0.35rem 0.625rem', fontSize: '0.75rem', fontFamily: 'monospace',
width: '220px',
background: 'rgba(15,23,42,0.85)',
border: '1px solid rgba(20,184,166,0.20)',
borderRadius: 4, color: 'var(--fg-1)', outline: 'none',
padding: '6px 12px', fontSize: 12, fontFamily: 'var(--font-mono)',
width: 220, transition: 'border-color 160ms ease',
}}
onFocus={e => e.target.style.borderColor = `${TEAL}60`}
onBlur={e => e.target.style.borderColor = 'rgba(20,184,166,0.2)'}
onFocus={e => { e.target.style.borderColor = 'rgba(20,184,166,0.60)'; e.target.style.boxShadow = '0 0 0 3px rgba(20,184,166,0.10)'; }}
onBlur={e => { e.target.style.borderColor = 'rgba(20,184,166,0.20)'; e.target.style.boxShadow = 'none'; }}
/>
</div>
@@ -603,10 +619,10 @@ export default function CompliancePage({ onNavigate }) {
<div style={{
display: 'grid',
gridTemplateColumns: '2.5fr 1.1fr 1fr 2fr 0.5fr 0.4fr',
padding: '0.5rem 1rem',
padding: '8px 16px',
borderBottom: '1px solid rgba(255,255,255,0.05)',
fontSize: '0.62rem', color: '#334155',
fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.08em',
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.08em',
}}>
<span>Hostname</span>
<span>IP Address</span>
@@ -626,7 +642,7 @@ export default function CompliancePage({ onNavigate }) {
<AlertCircle style={{ width: '16px', height: '16px' }} />{error}
</div>
) : filteredDevices.length === 0 ? (
<div style={{ padding: '3rem', textAlign: 'center', color: '#334155', fontFamily: 'monospace', fontSize: '0.8rem' }}>
<div style={{ padding: 48, textAlign: 'center', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>
{lastUpload === null ? 'No reports uploaded yet' : activeTab === 'active' ? 'No non-compliant devices' : 'No resolved items'}
</div>
) : (
@@ -673,68 +689,78 @@ export default function CompliancePage({ onNavigate }) {
{rollbackConfirm && lastUpload && (
<div style={{
position: 'fixed', inset: 0, zIndex: 60,
background: 'rgba(10, 14, 39, 0.95)',
backdropFilter: 'blur(8px)',
background: 'rgba(10,14,39,0.92)', backdropFilter: 'blur(8px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '1rem',
padding: 16,
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: '1px solid rgba(239,68,68,0.3)',
borderRadius: '0.75rem',
border: '1px solid rgba(239,68,68,0.30)',
borderRadius: 12,
boxShadow: '0 20px 60px rgba(0,0,0,0.7)',
width: '100%', maxWidth: '420px',
padding: '2rem',
width: '100%', maxWidth: 420,
padding: 28,
}}>
<div style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '700', color: '#EF4444', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '1rem' }}>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700,
color: '#EF4444', textTransform: 'uppercase', letterSpacing: '0.1em',
marginBottom: 12,
}}>
Rollback Upload
</div>
<div style={{ fontSize: '0.8rem', color: '#CBD5E1', lineHeight: '1.5', marginBottom: '0.5rem' }}>
<div style={{
fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.5,
marginBottom: 8, fontFamily: 'var(--font-display, var(--font-ui))',
}}>
This will reverse the most recent upload:
</div>
<div style={{
fontFamily: 'monospace', fontSize: '0.75rem', color: '#94A3B8',
background: 'rgba(15,23,42,0.6)', borderRadius: '0.375rem',
padding: '0.625rem 0.75rem', marginBottom: '1.25rem',
fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)',
background: 'rgba(15,23,42,0.6)', borderRadius: 6,
padding: '10px 12px', marginBottom: 18,
border: '1px solid rgba(239,68,68,0.15)',
}}>
<div><span style={{ color: '#64748B' }}>File:</span> {lastUpload.report_date || 'unknown date'}</div>
<div style={{ marginTop: '0.25rem', fontSize: '0.68rem', color: '#475569' }}>
<div><span style={{ color: 'var(--fg-disabled)' }}>File:</span> {lastUpload.report_date || 'unknown date'}</div>
<div style={{ marginTop: 4, fontSize: 10, color: 'var(--fg-disabled)' }}>
New items will be deleted, resolved items will be reactivated, and the upload record will be removed.
</div>
</div>
<div style={{ display: 'flex', gap: '0.75rem' }}>
<div style={{ display: 'flex', gap: 10 }}>
<button
onClick={() => setRollbackConfirm(false)}
style={{
flex: 1, padding: '0.625rem', background: 'transparent',
border: '1px solid rgba(100,116,139,0.4)', borderRadius: '0.375rem',
color: '#64748B', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.8rem',
flex: 1, padding: 10, background: 'transparent',
border: '1px solid rgba(100,116,139,0.40)', borderRadius: 6,
color: 'var(--fg-2)', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 160ms ease',
}}
onMouseEnter={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.8)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.4)'}>
onMouseLeave={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.40)'}>
Cancel
</button>
<button
onClick={handleRollback}
disabled={rollbackLoading}
style={{
flex: 2, padding: '0.625rem',
background: rollbackLoading ? 'rgba(239,68,68,0.05)' : 'rgba(239,68,68,0.1)',
flex: 2, padding: 10,
background: rollbackLoading ? 'rgba(239,68,68,0.05)' : 'rgba(239,68,68,0.10)',
border: '1px solid #EF4444',
borderRadius: '0.375rem',
borderRadius: 6,
color: '#EF4444', cursor: rollbackLoading ? 'wait' : 'pointer',
fontFamily: 'monospace', fontSize: '0.8rem',
fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.4rem',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
opacity: rollbackLoading ? 0.6 : 1,
transition: 'all 160ms ease',
}}
onMouseEnter={e => { if (!rollbackLoading) e.currentTarget.style.background = 'rgba(239,68,68,0.18)'; }}
onMouseLeave={e => { if (!rollbackLoading) e.currentTarget.style.background = 'rgba(239,68,68,0.1)'; }}>
onMouseLeave={e => { if (!rollbackLoading) e.currentTarget.style.background = 'rgba(239,68,68,0.10)'; }}>
{rollbackLoading
? <><Loader style={{ width: '14px', height: '14px', animation: 'spin 1s linear infinite' }} /> Rolling back</>
: <><RotateCcw style={{ width: '14px', height: '14px' }} /> Confirm Rollback</>
? <><Loader style={{ width: 14, height: 14, animation: 'spin 1s linear infinite' }} /> Rolling back</>
: <><RotateCcw style={{ width: 13, height: 13 }} /> Confirm Rollback</>
}
</button>
</div>
@@ -745,34 +771,32 @@ export default function CompliancePage({ onNavigate }) {
{/* ── Rollback result toast ────────────────────────────────── */}
{rollbackResult && (
<div style={{
position: 'fixed', bottom: '1.5rem', right: '1.5rem', zIndex: 70,
background: rollbackResult.error
? 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)'
: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: `1px solid ${rollbackResult.error ? 'rgba(239,68,68,0.4)' : 'rgba(16,185,129,0.4)'}`,
borderRadius: '0.5rem',
position: 'fixed', bottom: 24, right: 24, zIndex: 70,
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: `1px solid ${rollbackResult.error ? 'rgba(239,68,68,0.40)' : 'rgba(16,185,129,0.40)'}`,
borderRadius: 8,
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
padding: '0.875rem 1.25rem',
maxWidth: '360px',
fontFamily: 'monospace', fontSize: '0.75rem',
padding: '14px 20px',
maxWidth: 360,
fontFamily: 'var(--font-mono)', fontSize: 12,
color: rollbackResult.error ? '#F87171' : '#10B981',
cursor: 'pointer',
}}
onClick={() => setRollbackResult(null)}
>
{rollbackResult.error ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<AlertCircle style={{ width: '16px', height: '16px', flexShrink: 0 }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<AlertCircle style={{ width: 14, height: 14, flexShrink: 0 }} />
{rollbackResult.error}
</div>
) : (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<RotateCcw style={{ width: '14px', height: '14px' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: rollbackResult.rolled_back ? 4 : 0 }}>
<RotateCcw style={{ width: 14, height: 14 }} />
{rollbackResult.message}
</div>
{rollbackResult.rolled_back && (
<div style={{ fontSize: '0.68rem', color: '#64748B' }}>
<div style={{ fontSize: 10, color: 'var(--fg-2)' }}>
{rollbackResult.rolled_back.items_deleted} items deleted, {rollbackResult.rolled_back.items_reactivated} reactivated
</div>
)}
@@ -791,34 +815,38 @@ function DeviceRow({ device, selected, onClick }) {
style={{
display: 'grid',
gridTemplateColumns: '2.5fr 1.1fr 1fr 2fr 0.5fr 0.4fr',
padding: '0.625rem 1rem',
padding: '10px 16px',
borderBottom: '1px solid rgba(255,255,255,0.04)',
cursor: 'pointer',
background: selected ? `${TEAL}08` : 'transparent',
background: selected ? 'rgba(20,184,166,0.08)' : 'transparent',
borderLeft: selected ? `2px solid ${TEAL}` : '2px solid transparent',
transition: 'all 0.15s',
transition: 'all 160ms ease',
alignItems: 'center',
}}
onMouseEnter={e => { if (!selected) e.currentTarget.style.background = 'rgba(255,255,255,0.025)'; }}
onMouseLeave={e => { if (!selected) e.currentTarget.style.background = 'transparent'; }}
>
{/* Hostname */}
<div style={{ fontFamily: 'monospace', fontSize: '0.78rem', color: selected ? TEAL : '#E2E8F0', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<div style={{
fontFamily: 'var(--font-mono)', fontSize: 12,
color: selected ? TEAL : 'var(--fg-1)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{device.hostname}
</div>
{/* IP */}
<div style={{ fontFamily: 'monospace', fontSize: '0.72rem', color: '#64748B' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>
{device.ip_address || '—'}
</div>
{/* Type */}
<div style={{ fontSize: '0.7rem', color: '#475569', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<div style={{ fontSize: 11, color: 'var(--fg-disabled)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{device.device_type || '—'}
</div>
{/* Failing metrics */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{device.failing_metrics.map(m => (
<MetricBadge key={m.metric_id} metricId={m.metric_id} category={m.category} />
))}
@@ -832,7 +860,7 @@ function DeviceRow({ device, selected, onClick }) {
{/* Notes indicator */}
<div style={{ display: 'flex', justifyContent: 'center' }}>
{device.has_notes && (
<MessageSquare style={{ width: '13px', height: '13px', color: TEAL, opacity: 0.7 }} />
<MessageSquare style={{ width: 13, height: 13, color: 'rgba(20,184,166,0.7)' }} />
)}
</div>
</div>

View File

@@ -264,7 +264,7 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
return (
<div style={{
position: 'fixed', inset: 0, zIndex: 60,
background: 'rgba(10, 14, 39, 0.97)',
background: 'var(--bg-overlay)',
backdropFilter: 'blur(12px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '1rem',
@@ -272,8 +272,8 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
<div style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: `1px solid ${TEAL}40`,
borderRadius: '0.75rem',
boxShadow: `0 20px 60px rgba(0,0,0,0.7), 0 0 40px ${TEAL}15`,
borderRadius: 12,
boxShadow: 'var(--shadow-modal)',
width: '100%', maxWidth: phase === 'drift-review' ? '560px' : '480px',
maxHeight: 'calc(100vh - 2rem)',
overflowY: 'auto',
@@ -283,14 +283,14 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.75rem' }}>
<div>
<div style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '700', color: TEAL, textTransform: 'uppercase', letterSpacing: '0.1em' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '1rem', fontWeight: '700', color: TEAL, textTransform: 'uppercase', letterSpacing: '0.1em' }}>
Upload Report
</div>
<div style={{ fontSize: '0.75rem', color: '#475569', marginTop: '2px' }}>NTS_AEO xlsx compliance report</div>
<div style={{ fontSize: '0.75rem', color: 'var(--fg-disabled)', marginTop: '2px' }}>NTS_AEO xlsx compliance report</div>
</div>
<button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#475569', padding: '0.25rem' }}
onMouseEnter={e => e.currentTarget.style.color = '#E2E8F0'}
onMouseLeave={e => e.currentTarget.style.color = '#475569'}>
<button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fg-disabled)', padding: '0.25rem' }}
onMouseEnter={e => e.currentTarget.style.color = 'var(--fg-1)'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--fg-disabled)'}>
<X style={{ width: '20px', height: '20px' }} />
</button>
</div>
@@ -332,7 +332,7 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
{(phase === 'uploading' || phase === 'committing') && (
<div style={{ textAlign: 'center', padding: '2rem 0' }}>
<Loader style={{ width: '40px', height: '40px', color: TEAL, margin: '0 auto 1rem', animation: 'spin 1s linear infinite' }} />
<div style={{ color: '#CBD5E1', fontFamily: 'monospace', fontSize: '0.875rem' }}>
<div style={{ color: '#CBD5E1', fontFamily: 'var(--font-mono)', fontSize: '0.875rem' }}>
{phase === 'uploading' ? 'Parsing report…' : 'Committing upload…'}
</div>
</div>
@@ -342,8 +342,8 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
{phase === 'drift-review' && driftReport && (
<>
<div style={{
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: '0.8rem', color: '#64748B',
fontFamily: 'var(--font-mono)',
fontSize: '0.8rem', color: 'var(--fg-disabled)',
textTransform: 'uppercase', letterSpacing: '0.05em',
marginBottom: '1rem',
}}>
@@ -369,11 +369,11 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
{/* Status message */}
{driftReport.breaking && driftReport.breaking.length > 0 && (
<div style={{
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem', color: '#EF4444',
background: 'rgba(239,68,68,0.08)',
border: '1px solid rgba(239,68,68,0.25)',
borderRadius: '0.375rem',
borderRadius: 6,
padding: '0.5rem 0.75rem',
marginBottom: '1rem',
lineHeight: '1.4',
@@ -386,11 +386,11 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
{(!driftReport.breaking || driftReport.breaking.length === 0) &&
driftReport.silent_miss && driftReport.silent_miss.length > 0 && (
<div style={{
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem', color: '#F59E0B',
background: 'rgba(245,158,11,0.08)',
border: '1px solid rgba(245,158,11,0.25)',
borderRadius: '0.375rem',
borderRadius: 6,
padding: '0.5rem 0.75rem',
marginBottom: '1rem',
lineHeight: '1.4',
@@ -403,11 +403,11 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
{/* Reconcile changes summary (shown after a successful reconcile) */}
{reconcileChanges && reconcileChanges.length > 0 && (
<div style={{
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem', color: '#10B981',
background: 'rgba(16,185,129,0.08)',
border: '1px solid rgba(16,185,129,0.25)',
borderRadius: '0.375rem',
borderRadius: 6,
padding: '0.5rem 0.75rem',
marginBottom: '1rem',
lineHeight: '1.6',
@@ -429,9 +429,10 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
<button onClick={() => { setPhase('idle'); setPreviewData(null); setDriftReport(null); setReconcileChanges(null); setLastFile(null); setLastSchema(null); }}
style={{
flex: 1, minWidth: '80px', padding: '0.625rem', background: 'transparent',
border: '1px solid rgba(100,116,139,0.4)', borderRadius: '0.375rem',
color: '#64748B', cursor: 'pointer',
fontFamily: "'JetBrains Mono', 'Fira Code', monospace", fontSize: '0.8rem',
border: '1px solid rgba(100,116,139,0.4)', borderRadius: 6,
color: 'var(--fg-2)', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: '0.8rem',
transition: 'all 160ms ease',
}}
onMouseEnter={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.8)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.4)'}>
@@ -447,13 +448,14 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
flex: 2, minWidth: '140px', padding: '0.625rem',
background: reconciling ? 'rgba(245,158,11,0.05)' : 'rgba(245,158,11,0.1)',
border: '1px solid rgba(245,158,11,0.5)',
borderRadius: '0.375rem',
borderRadius: 6,
color: '#F59E0B',
cursor: reconciling ? 'wait' : 'pointer',
fontFamily: "'JetBrains Mono', 'Fira Code', monospace", fontSize: '0.8rem',
fontFamily: 'var(--font-mono)', fontSize: '0.8rem',
fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.4rem',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '0.4rem',
opacity: reconciling ? 0.6 : 1,
transition: 'all 160ms ease',
}}
onMouseEnter={e => { if (!reconciling) e.currentTarget.style.background = 'rgba(245,158,11,0.18)'; }}
onMouseLeave={e => { if (!reconciling) e.currentTarget.style.background = 'rgba(245,158,11,0.1)'; }}>
@@ -472,12 +474,13 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
? 'rgba(100,116,139,0.08)'
: `${TEAL}18`,
border: `1px solid ${(driftReport.breaking && driftReport.breaking.length > 0) ? 'rgba(100,116,139,0.3)' : TEAL}`,
borderRadius: '0.375rem',
borderRadius: 6,
color: (driftReport.breaking && driftReport.breaking.length > 0) ? '#475569' : TEAL,
cursor: (driftReport.breaking && driftReport.breaking.length > 0) ? 'not-allowed' : 'pointer',
fontFamily: "'JetBrains Mono', 'Fira Code', monospace", fontSize: '0.8rem',
fontFamily: 'var(--font-mono)', fontSize: '0.8rem',
fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em',
opacity: (driftReport.breaking && driftReport.breaking.length > 0) ? 0.5 : 1,
transition: 'all 160ms ease',
}}
onMouseEnter={e => {
if (!(driftReport.breaking && driftReport.breaking.length > 0)) {
@@ -499,7 +502,7 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
{phase === 'preview' && previewData && (
<>
<div style={{ marginBottom: '1.5rem' }}>
<div style={{ fontSize: '0.8rem', color: '#64748B', fontFamily: 'monospace', marginBottom: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
<div style={{ fontSize: '0.8rem', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', marginBottom: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{previewData.filename}
{previewData.report_date && <span style={{ color: TEAL, marginLeft: '0.75rem' }}>{previewData.report_date}</span>}
</div>
@@ -519,20 +522,20 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
<span style={{ color, marginRight: '0.5rem', fontWeight: '700' }}>{icon}</span>
{label}
</span>
<span style={{ color, fontFamily: 'monospace', fontWeight: '700', fontSize: '1.1rem' }}>{count}</span>
<span style={{ color, fontFamily: 'var(--font-mono)', fontWeight: '700', fontSize: '1.1rem' }}>{count}</span>
</div>
))}
</div>
<div style={{ display: 'flex', gap: '0.75rem' }}>
<button onClick={() => { setPhase('idle'); setPreviewData(null); }}
style={{ flex: 1, padding: '0.625rem', background: 'transparent', border: '1px solid rgba(100,116,139,0.4)', borderRadius: '0.375rem', color: '#64748B', cursor: 'pointer', fontFamily: 'monospace', fontSize: '0.8rem' }}
style={{ flex: 1, padding: '0.625rem', background: 'transparent', border: '1px solid rgba(100,116,139,0.4)', borderRadius: 6, color: 'var(--fg-2)', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', transition: 'all 160ms ease' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.8)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.4)'}>
Cancel
</button>
<button onClick={handleCommit}
style={{ flex: 2, padding: '0.625rem', background: `${TEAL}18`, border: `1px solid ${TEAL}`, borderRadius: '0.375rem', color: TEAL, cursor: 'pointer', fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em' }}
style={{ flex: 2, padding: '0.625rem', background: `${TEAL}18`, border: `1px solid ${TEAL}`, borderRadius: 6, color: TEAL, cursor: 'pointer', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em', transition: 'all 160ms ease' }}
onMouseEnter={e => e.currentTarget.style.background = `${TEAL}28`}
onMouseLeave={e => e.currentTarget.style.background = `${TEAL}18`}>
Confirm Upload
@@ -545,7 +548,7 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
{phase === 'done' && (
<div style={{ textAlign: 'center', padding: '1.5rem 0' }}>
<CheckCircle style={{ width: '44px', height: '44px', color: '#10B981', margin: '0 auto 1rem' }} />
<div style={{ color: '#10B981', fontFamily: 'monospace', fontSize: '0.875rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
<div style={{ color: '#10B981', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Upload committed
</div>
</div>
@@ -557,7 +560,7 @@ export default function ComplianceUploadModal({ onClose, onUploadComplete }) {
<AlertCircle style={{ width: '36px', height: '36px', color: '#EF4444', margin: '0 auto 0.75rem' }} />
<div style={{ color: '#F87171', fontSize: '0.875rem', marginBottom: '1.25rem' }}>{error}</div>
<button onClick={() => { setPhase('idle'); setError(null); }}
style={{ padding: '0.5rem 1.25rem', background: 'rgba(239,68,68,0.1)', border: '1px solid #EF4444', borderRadius: '0.375rem', color: '#F87171', cursor: 'pointer', fontFamily: 'monospace', fontSize: '0.8rem' }}>
style={{ padding: '0.5rem 1.25rem', background: 'rgba(239,68,68,0.1)', border: '1px solid #EF4444', borderRadius: 6, color: '#F87171', cursor: 'pointer', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', transition: 'all 160ms ease' }}>
Try Again
</button>
</div>

View File

@@ -154,12 +154,12 @@ async function fetchAtlasAndFindings() {
function ExportCard({ color, colorRgb, icon: Icon, title, description, children }) {
return (
<div style={{
background: 'linear-gradient(135deg, rgba(15,26,46,0.95) 0%, rgba(10,22,40,0.9) 100%)',
border: `1px solid rgba(${colorRgb},0.2)`,
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%)',
border: `1.5px solid rgba(14,165,233,0.20)`,
borderLeft: `3px solid ${color}`,
borderRadius: '0.5rem',
borderRadius: 'var(--r-lg)',
padding: '1.5rem',
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
boxShadow: 'var(--shadow-card)',
display: 'flex',
flexDirection: 'column',
gap: '1rem',
@@ -167,14 +167,14 @@ function ExportCard({ color, colorRgb, icon: Icon, title, description, children
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
<Icon style={{ width: '18px', height: '18px', color, flexShrink: 0 }} />
<h3 style={{
fontFamily: 'monospace', fontSize: '0.9rem', fontWeight: '600',
color, textTransform: 'uppercase', letterSpacing: '0.1em',
fontFamily: 'var(--font-mono)', fontSize: '0.9rem', fontWeight: '600',
color, textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)',
textShadow: `0 0 12px rgba(${colorRgb},0.4)`, margin: 0,
}}>
{title}
</h3>
</div>
<p style={{ fontFamily: 'monospace', fontSize: '0.72rem', color: '#475569', margin: 0, lineHeight: 1.6 }}>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--fg-disabled)', margin: 0, lineHeight: 1.6 }}>
{description}
</p>
<div style={{ borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: '1rem' }}>
@@ -195,13 +195,14 @@ function ExportBtn({ label, exportKey, loading, color, colorRgb, onClick, disabl
padding: '0.45rem 0.875rem',
background: `rgba(${colorRgb},0.08)`,
border: `1px solid rgba(${colorRgb},0.25)`,
borderRadius: '0.375rem',
color: isLoading ? '#64748B' : color,
borderRadius: 'var(--r-md)',
color: isLoading ? 'var(--fg-disabled)' : color,
cursor: (!!loading || disabled) ? 'not-allowed' : 'pointer',
opacity: (!!loading && !isLoading) ? 0.45 : 1,
fontFamily: 'monospace', fontSize: '0.72rem', fontWeight: '600',
letterSpacing: '0.05em',
transition: 'opacity 0.15s, color 0.15s',
fontFamily: 'var(--font-mono)', fontSize: '0.72rem', fontWeight: '600',
letterSpacing: 'var(--tracking-wide)',
textTransform: 'uppercase',
transition: 'opacity var(--dur-fast), color var(--dur-fast)',
whiteSpace: 'nowrap',
}}
>
@@ -220,10 +221,10 @@ function Toggle({ label, checked, onChange, color, colorRgb }) {
<div
onClick={() => onChange(!checked)}
style={{
width: '32px', height: '18px', borderRadius: '9px',
width: '32px', height: '18px', borderRadius: 'var(--r-pill)',
background: checked ? color : 'rgba(255,255,255,0.1)',
border: `1px solid rgba(${colorRgb},0.4)`,
position: 'relative', transition: 'background 0.2s',
position: 'relative', transition: 'background var(--dur-med)',
cursor: 'pointer', flexShrink: 0,
}}
>
@@ -231,11 +232,11 @@ function Toggle({ label, checked, onChange, color, colorRgb }) {
position: 'absolute', top: '2px',
left: checked ? '14px' : '2px',
width: '12px', height: '12px', borderRadius: '50%',
background: '#E2E8F0',
transition: 'left 0.2s',
background: 'var(--fg-2)',
transition: 'left var(--dur-med)',
}} />
</div>
<span style={{ fontFamily: 'monospace', fontSize: '0.7rem', color: '#64748B' }}>{label}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--fg-disabled)' }}>{label}</span>
</label>
);
}
@@ -427,9 +428,9 @@ export default function ExportsPage() {
if (!canExport()) {
return (
<div style={{ textAlign: 'center', padding: '4rem 1rem', color: '#94A3B8' }}>
<div style={{ textAlign: 'center', padding: '4rem 1rem', color: 'var(--fg-muted)' }}>
<Shield style={{ width: '48px', height: '48px', margin: '0 auto 1rem', opacity: 0.5 }} />
<p style={{ fontFamily: 'monospace', fontSize: '0.9rem' }}>You do not have permission to export data.</p>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.9rem' }}>You do not have permission to export data.</p>
</div>
);
}
@@ -439,8 +440,8 @@ export default function ExportsPage() {
{/* Page header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
<Download style={{ width: '20px', height: '20px', color: '#8B5CF6' }} />
<h2 style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '600', color: '#8B5CF6', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(139,92,246,0.4)', margin: 0 }}>
<Download style={{ width: '20px', height: '20px', color: '#0EA5E9' }} />
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: '1.5rem', fontWeight: '700', color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wider)', textShadow: 'var(--glow-heading)', margin: 0 }}>
Exports
</h2>
</div>
@@ -451,10 +452,10 @@ export default function ExportsPage() {
display: 'flex', alignItems: 'center', gap: '0.625rem',
padding: '0.75rem 1rem',
background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.3)',
borderRadius: '0.375rem',
borderRadius: 'var(--r-md)',
}}>
<AlertCircle style={{ width: '14px', height: '14px', color: '#EF4444', flexShrink: 0 }} />
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#EF4444', flex: 1 }}>{error}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: '#EF4444', flex: 1 }}>{error}</span>
<button onClick={() => setError(null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#EF4444', padding: 0 }}>
<X style={{ width: '14px', height: '14px' }} />
</button>
@@ -477,7 +478,7 @@ export default function ExportsPage() {
<ExportBtn label="Overdue SLA" exportKey="ivanti-overdue" loading={loading} color="#F59E0B" colorRgb="245,158,11" onClick={exportOverdue} />
<ExportBtn label="By Business Unit" exportKey="ivanti-bu" loading={loading} color="#F59E0B" colorRgb="245,158,11" onClick={exportByBU} />
</div>
<p style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#334155', margin: '0.75rem 0 0', lineHeight: 1.5 }}>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--fg-disabled)', margin: '0.75rem 0 0', lineHeight: 1.5 }}>
"By Business Unit" creates one sheet per BU in a single workbook.
</p>
</ExportCard>
@@ -501,15 +502,15 @@ export default function ExportsPage() {
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em', whiteSpace: 'nowrap' }}>Status</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.68rem', color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', whiteSpace: 'nowrap' }}>Status</span>
<select
value={cveStatus}
onChange={e => setCveStatus(e.target.value)}
disabled={!!loading}
style={{
background: 'rgba(34,197,94,0.06)', border: '1px solid rgba(34,197,94,0.2)',
borderRadius: '0.25rem', color: '#CBD5E1', padding: '0.25rem 0.5rem',
fontFamily: 'monospace', fontSize: '0.72rem', cursor: 'pointer', outline: 'none',
background: 'var(--bg-input)', border: '1px solid var(--border-subtle)',
borderRadius: 'var(--r-sm)', color: 'var(--fg-3)', padding: '0.25rem 0.5rem',
fontFamily: 'var(--font-mono)', fontSize: '0.72rem', cursor: 'pointer', outline: 'none',
}}
>
<option value="">All Statuses</option>
@@ -569,7 +570,7 @@ export default function ExportsPage() {
<div style={{ marginTop: '0.5rem' }}>
<ExportBtn label="Full Report (multi-sheet)" exportKey="atlas-full" loading={loading} color="#A855F7" colorRgb="168,85,247" onClick={exportAtlasFull} />
</div>
<p style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#334155', margin: '0.75rem 0 0', lineHeight: 1.5 }}>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--fg-disabled)', margin: '0.75rem 0 0', lineHeight: 1.5 }}>
"Full Report" creates three sheets: Active Plans, No Plan, and History (overridden plans).
</p>
</ExportCard>

View File

@@ -2,42 +2,37 @@
// Full-page knowledge base library — browse, search, filter, and read
// articles inline. Upload and delete require editor/admin role.
// Reuses existing KnowledgeBaseViewer and KnowledgeBaseModal components.
// Layout: list-based rows inside a single Card container (design kit v2).
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
BookOpen, Search, Upload, RefreshCw, Loader,
AlertCircle, Trash2, X, // FileText and File available if needed later
BookOpen, Search, RefreshCw, Loader,
AlertCircle, Trash2, X, Download, FilePlus,
} from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import KnowledgeBaseModal from '../KnowledgeBaseModal';
import KnowledgeBaseViewer from '../KnowledgeBaseViewer';
import ConfirmModal from '../ConfirmModal'; // ⚠️ CONVENTION: ConfirmModal is imported but never used — either integrate it into handleDelete or remove this import
import ConfirmModal from '../ConfirmModal';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const GREEN = '#10B981';
// ---------------------------------------------------------------------------
// Static config
// ---------------------------------------------------------------------------
const CATEGORY_COLORS = {
General: '#94A3B8',
Policy: '#0EA5E9',
Procedure: GREEN,
Guide: '#F59E0B',
Reference: '#8B5CF6',
};
const FILE_EXT_COLORS = {
pdf: '#EF4444',
md: '#10B981',
md: '#38BDF8',
txt: '#94A3B8',
doc: '#0EA5E9',
docx: '#0EA5E9',
doc: '#7DD3FC',
docx: '#7DD3FC',
xls: '#10B981',
xlsx: '#10B981',
ppt: '#F97316',
pptx: '#F97316',
ppt: '#F59E0B',
pptx: '#F59E0B',
html: '#8B5CF6',
json: '#94A3B8',
yaml: '#94A3B8',
yml: '#94A3B8',
};
const CATEGORY_ORDER = ['Procedure', 'Guide', 'Policy', 'Reference', 'General'];
@@ -50,7 +45,7 @@ function extOf(filename) {
}
function extColor(filename) {
return FILE_EXT_COLORS[extOf(filename)] || '#64748B';
return FILE_EXT_COLORS[extOf(filename)] || '#94A3B8';
}
function fmtSize(bytes) {
@@ -65,138 +60,215 @@ function fmtDate(str) {
return new Date(str).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
}
function catColor(cat) {
return CATEGORY_COLORS[cat] || '#94A3B8';
// ---------------------------------------------------------------------------
// FileTypeChip — 36x36 square showing file extension
// ---------------------------------------------------------------------------
function FileTypeChip({ filename }) {
const ext = extOf(filename);
const color = extColor(filename);
const label = ext.toUpperCase();
return (
<span style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 36, height: 36, borderRadius: 6,
background: 'var(--bg-elevated)',
border: `1px solid ${color}`,
color: color,
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10,
flexShrink: 0,
}}>
{label}
</span>
);
}
// ---------------------------------------------------------------------------
// ArticleCard
// ArticleRow — horizontal row for each article
// ---------------------------------------------------------------------------
function ArticleCard({ article, selected, onSelect, onDelete, canDelete }) {
const color = catColor(article.category);
const fileColor = extColor(article.file_name);
const ext = extOf(article.file_name).toUpperCase();
function ArticleRow({ article, selected, onSelect, onDelete, canDelete }) {
const [hover, setHover] = useState(false);
return (
<div
onClick={() => onSelect(article)}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 18px',
background: selected
? `linear-gradient(135deg,rgba(16,185,129,0.1) 0%,rgba(15,23,42,0.98) 100%)`
: 'linear-gradient(135deg,rgba(30,41,59,0.95) 0%,rgba(15,23,42,0.98) 100%)',
border: `1.5px solid ${selected ? GREEN : 'rgba(16,185,129,0.12)'}`,
borderRadius: '0.5rem',
padding: '1rem',
? 'linear-gradient(90deg, rgba(14,165,233,0.14) 0%, rgba(14,165,233,0.06) 100%)'
: hover
? 'linear-gradient(90deg, rgba(14,165,233,0.10) 0%, rgba(14,165,233,0.04) 100%)'
: 'transparent',
borderBottom: '1px solid var(--border-subtle)',
boxShadow: selected
? 'inset 3px 0 0 var(--intel-accent)'
: hover
? 'inset 3px 0 0 var(--intel-accent)'
: 'none',
cursor: 'pointer',
transition: 'all 0.15s',
position: 'relative',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
transition: 'all 200ms cubic-bezier(0.4,0,0.2,1)',
}}
onMouseEnter={e => { if (!selected) e.currentTarget.style.borderColor = 'rgba(16,185,129,0.35)'; }}
onMouseLeave={e => { if (!selected) e.currentTarget.style.borderColor = 'rgba(16,185,129,0.12)'; }}
>
{/* File type badge + delete button */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{
fontFamily: 'monospace', fontSize: '0.58rem', fontWeight: '700',
color: fileColor, padding: '0.15rem 0.4rem',
background: `${fileColor}15`, borderRadius: '0.2rem',
border: `1px solid ${fileColor}30`,
{/* Col 1: File type chip */}
<FileTypeChip filename={article.file_name} />
{/* Col 2: Title + Description */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
font: '600 14px var(--font-ui)', color: 'var(--fg-1)',
marginBottom: 3,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>
{ext}
{article.title}
</div>
{article.description && (
<div style={{
font: '400 12px var(--font-ui)', color: 'var(--fg-muted)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>
{article.description}
</div>
)}
</div>
{/* Col 3: Category + Date */}
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'flex-end',
gap: 4, minWidth: 110, flexShrink: 0,
}}>
<span style={{
padding: '2px 8px', borderRadius: 4,
background: 'var(--bg-elevated)',
color: 'var(--fg-2)', font: '500 11px var(--font-ui)',
}}>
{article.category}
</span>
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)' }}>
{fmtDate(article.created_at)}
</span>
</div>
{/* Col 4: Size + Author */}
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'flex-end',
gap: 4, minWidth: 90, flexShrink: 0,
}}>
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)' }}>
{fmtSize(article.file_size)}
</span>
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-disabled)' }}>
{article.created_by_name || article.created_by || ''}
</span>
</div>
{/* Col 5: Action buttons */}
<div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
<button
onClick={e => {
e.stopPropagation();
// Download via API
window.open(`${API_BASE}/knowledge-base/${article.id}/download`, '_blank');
}}
title="Download"
style={{
background: 'transparent',
border: '1px solid var(--border-1)',
borderRadius: 6, padding: 7, cursor: 'pointer',
color: 'var(--fg-2)', display: 'flex',
transition: 'all 150ms',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--intel-accent)'; e.currentTarget.style.color = 'var(--intel-accent)'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-1)'; e.currentTarget.style.color = 'var(--fg-2)'; }}
>
<Download style={{ width: 14, height: 14 }} />
</button>
{canDelete && (
<button
onClick={e => { e.stopPropagation(); onDelete(article); }}
title="Delete article"
style={{
background: 'none', border: 'none', cursor: 'pointer',
color: '#334155', padding: '0.15rem',
borderRadius: '0.2rem', display: 'flex', alignItems: 'center',
background: 'transparent',
border: '1px solid var(--border-1)',
borderRadius: 6, padding: 7, cursor: 'pointer',
color: 'var(--fg-2)', display: 'flex',
transition: 'all 150ms',
}}
onMouseEnter={e => { e.currentTarget.style.color = '#EF4444'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#334155'; }}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#EF4444'; e.currentTarget.style.color = '#EF4444'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-1)'; e.currentTarget.style.color = 'var(--fg-2)'; }}
>
<Trash2 style={{ width: '12px', height: '12px' }} />
<Trash2 style={{ width: 14, height: 14 }} />
</button>
)}
</div>
{/* Title */}
<div style={{
fontFamily: 'monospace', fontSize: '0.82rem', fontWeight: '700',
color: selected ? GREEN : '#E2E8F0',
lineHeight: 1.3,
}}>
{article.title}
</div>
{/* Description */}
{article.description && (
<div style={{
fontSize: '0.7rem', color: '#475569',
lineHeight: 1.45, display: '-webkit-box',
WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}>
{article.description}
</div>
)}
{/* Footer — category + date */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 'auto', paddingTop: '0.375rem', borderTop: '1px solid rgba(255,255,255,0.04)' }}>
<span style={{
fontFamily: 'monospace', fontSize: '0.6rem', fontWeight: '600',
color, padding: '0.15rem 0.4rem',
background: `${color}12`, borderRadius: '0.2rem',
border: `1px solid ${color}25`,
textTransform: 'uppercase', letterSpacing: '0.04em',
}}>
{article.category}
</span>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
{article.file_size && (
<span style={{ fontFamily: 'monospace', fontSize: '0.6rem', color: '#334155' }}>
{fmtSize(article.file_size)}
</span>
)}
<span style={{ fontFamily: 'monospace', fontSize: '0.6rem', color: '#334155' }}>
{fmtDate(article.created_at)}
</span>
</div>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// CategoryPill — filter tab matching design kit
// ---------------------------------------------------------------------------
function CategoryPill({ label, active, count, onClick }) {
const [hover, setHover] = useState(false);
return (
<button
onClick={onClick}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '6px 14px', borderRadius: 6,
background: active
? 'linear-gradient(135deg, rgba(14,165,233,0.20) 0%, rgba(14,165,233,0.12) 100%)'
: (hover ? 'rgba(14,165,233,0.08)' : 'transparent'),
color: active ? 'var(--intel-accent-bright)' : 'var(--fg-2)',
border: `1px solid ${active ? 'var(--intel-accent)' : 'var(--border-default)'}`,
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 11,
textTransform: 'uppercase', letterSpacing: '0.5px',
textShadow: active ? '0 0 8px rgba(14,165,233,0.4)' : 'none',
boxShadow: active ? '0 0 16px rgba(14,165,233,0.20)' : 'none',
cursor: 'pointer',
transition: 'all 200ms cubic-bezier(0.4,0,0.2,1)',
}}
>
{label}
<span style={{
font: '700 10px var(--font-mono)',
color: active ? 'var(--intel-accent-bright)' : 'var(--fg-muted)',
padding: '1px 6px', borderRadius: 999,
background: active ? 'rgba(14,165,233,0.15)' : 'rgba(148,163,184,0.10)',
}}>
{count}
</span>
</button>
);
}
// ---------------------------------------------------------------------------
// Empty state
// ---------------------------------------------------------------------------
function EmptyState({ hasFilter, onClear }) {
return (
<div style={{
gridColumn: '1 / -1',
display: 'flex', flexDirection: 'column', alignItems: 'center',
justifyContent: 'center', padding: '4rem 2rem',
border: '1px dashed rgba(16,185,129,0.15)', borderRadius: '0.5rem',
color: '#334155',
color: 'var(--fg-disabled)',
}}>
<BookOpen style={{ width: '36px', height: '36px', marginBottom: '1rem', opacity: 0.4 }} />
<div style={{ fontFamily: 'monospace', fontSize: '0.8rem', marginBottom: '0.375rem' }}>
<BookOpen style={{ width: 36, height: 36, marginBottom: '1rem', opacity: 0.4 }} />
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', marginBottom: '0.375rem' }}>
{hasFilter ? 'No articles match your search' : 'No articles yet'}
</div>
{hasFilter ? (
<button onClick={onClear} style={{
background: 'none', border: 'none', cursor: 'pointer',
color: GREEN, fontFamily: 'monospace', fontSize: '0.72rem',
color: '#0EA5E9', fontFamily: 'var(--font-mono)', fontSize: '0.72rem',
marginTop: '0.375rem',
}}>
Clear filters
</button>
) : (
<div style={{ fontFamily: 'monospace', fontSize: '0.7rem', color: '#475569' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--fg-disabled)' }}>
Upload a document to get started
</div>
)}
@@ -208,12 +280,13 @@ function EmptyState({ hasFilter, onClear }) {
// Main page
// ---------------------------------------------------------------------------
export default function KnowledgeBasePage() {
const { canWrite } = useAuth();
const { canWrite, canDelete: canDeleteResource } = useAuth();
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState('newest');
const [activeCategory, setActiveCategory] = useState('All');
const [selected, setSelected] = useState(null);
const [showUpload, setShowUpload] = useState(false);
@@ -264,18 +337,33 @@ export default function KnowledgeBasePage() {
}, [selected]);
// -------------------------------------------------------------------------
// Filtering
// Filtering + sorting
// -------------------------------------------------------------------------
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
return articles.filter(a => {
let result = articles.filter(a => {
const matchesCat = activeCategory === 'All' || a.category === activeCategory;
const matchesSearch = !q ||
a.title.toLowerCase().includes(q) ||
(a.description || '').toLowerCase().includes(q);
return matchesCat && matchesSearch;
});
}, [articles, activeCategory, search]);
// Sort
result = [...result].sort((a, b) => {
switch (sortBy) {
case 'oldest':
return new Date(a.created_at) - new Date(b.created_at);
case 'title':
return (a.title || '').localeCompare(b.title || '');
case 'newest':
default:
return new Date(b.created_at) - new Date(a.created_at);
}
});
return result;
}, [articles, activeCategory, search, sortBy]);
// Category tab counts (always from full list, not filtered by search)
const categoryCounts = useMemo(() => {
@@ -296,128 +384,161 @@ export default function KnowledgeBasePage() {
// Render
// -------------------------------------------------------------------------
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem', paddingBottom: '2rem' }}>
<div style={{ padding: '24px 24px 48px', maxWidth: 1280, margin: '0 auto' }}>
{/* ── Page header ─────────────────────────────────────────── */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: 20,
}}>
<div>
<h2 style={{
fontFamily: 'monospace', fontSize: '1.5rem', fontWeight: '700',
color: GREEN, textTransform: 'uppercase', letterSpacing: '0.1em',
textShadow: `0 0 16px ${GREEN}40`, marginBottom: '0.25rem',
<h1 style={{
font: '700 24px var(--font-mono)',
color: 'var(--intel-accent-bright)',
margin: 0, textTransform: 'uppercase',
letterSpacing: '0.10em',
textShadow: 'var(--glow-heading)',
}}>
Knowledge Base
</h2>
<div style={{ fontSize: '0.72rem', color: '#475569', fontFamily: 'monospace' }}>
{loading ? '…' : `${articles.length} article${articles.length !== 1 ? 's' : ''}`}
{articles.length > 0 && activeCategory !== 'All' && (
<span style={{ marginLeft: '0.5rem', color: '#334155' }}>
· {categoryCounts[activeCategory] || 0} in {activeCategory}
</span>
)}
</h1>
<div style={{
font: '400 13px var(--font-ui)',
color: 'var(--fg-muted)', marginTop: 6,
}}>
Internal reference material runbooks, advisories, policies
</div>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0 }}>
<div style={{ display: 'flex', gap: 8 }}>
<button
onClick={fetchArticles}
title="Refresh"
style={{
background: 'none', border: `1px solid rgba(16,185,129,0.25)`,
borderRadius: '0.375rem', padding: '0.5rem', cursor: 'pointer', color: '#475569',
background: 'none', border: '1px solid rgba(14,165,233,0.25)',
borderRadius: 'var(--r-md)', padding: '0.5rem', cursor: 'pointer',
color: 'var(--fg-disabled)', display: 'flex', alignItems: 'center',
transition: 'all 150ms',
}}
onMouseEnter={e => { e.currentTarget.style.color = GREEN; e.currentTarget.style.borderColor = `${GREEN}60`; }}
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(16,185,129,0.25)'; }}
onMouseEnter={e => { e.currentTarget.style.color = '#0EA5E9'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.5)'; }}
onMouseLeave={e => { e.currentTarget.style.color = 'var(--fg-disabled)'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.25)'; }}
>
<RefreshCw style={{ width: '16px', height: '16px' }} />
<RefreshCw style={{ width: 16, height: 16 }} />
</button>
<button
onClick={() => {
// Export list as CSV
const header = 'Title,Category,File,Size,Date,Author\n';
const rows = filtered.map(a =>
`"${a.title}","${a.category}","${a.file_name}","${fmtSize(a.file_size)}","${fmtDate(a.created_at)}","${a.created_by_name || a.created_by || ''}"`
).join('\n');
const blob = new Blob([header + rows], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'knowledge-base-export.csv';
link.click();
URL.revokeObjectURL(url);
}}
style={{
display: 'flex', alignItems: 'center', gap: '0.4rem',
background: 'transparent',
border: '1px solid rgba(14,165,233,0.25)',
color: 'var(--fg-2)', padding: '0.5rem 1rem',
fontFamily: 'var(--font-mono)', fontSize: '0.75rem', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
cursor: 'pointer', borderRadius: 'var(--r-md)',
transition: 'all 150ms',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = 'rgba(14,165,233,0.5)'; e.currentTarget.style.color = '#0EA5E9'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'rgba(14,165,233,0.25)'; e.currentTarget.style.color = 'var(--fg-2)'; }}
>
<Download style={{ width: 14, height: 14 }} />
Export List
</button>
{canWrite() && (
<button
onClick={() => setShowUpload(true)}
style={{
display: 'flex', alignItems: 'center', gap: '0.4rem',
background: `${GREEN}18`, border: `1px solid ${GREEN}`,
color: GREEN, padding: '0.5rem 1rem',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
textTransform: 'uppercase', letterSpacing: '0.05em',
cursor: 'pointer', borderRadius: '0.375rem',
background: 'rgba(14,165,233,0.08)', border: '1px solid #0EA5E9',
color: '#38BDF8', padding: '0.5rem 1rem',
fontFamily: 'var(--font-mono)', fontSize: '0.75rem', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)',
cursor: 'pointer', borderRadius: 'var(--r-md)',
transition: 'all 150ms',
}}
>
<Upload style={{ width: '14px', height: '14px' }} />
<FilePlus style={{ width: 14, height: 14 }} />
Upload Article
</button>
)}
</div>
</div>
{/* ── Search + category tabs ───────────────────────────────── */}
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', flexWrap: 'wrap' }}>
{/* Search */}
<div style={{ position: 'relative', flexShrink: 0 }}>
{/* ── Search + Sort row ────────────────────────────────────── */}
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
<div style={{ flex: 1, maxWidth: 420, position: 'relative' }}>
<Search style={{
position: 'absolute', left: '0.625rem', top: '50%', transform: 'translateY(-50%)',
width: '13px', height: '13px', color: '#334155', pointerEvents: 'none',
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
width: 14, height: 14, color: 'var(--fg-disabled)', pointerEvents: 'none',
}} />
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search articles…"
placeholder="Search title or description…"
style={{
paddingLeft: '2rem', paddingRight: search ? '2rem' : '0.625rem',
paddingTop: '0.4rem', paddingBottom: '0.4rem',
background: 'rgba(15,23,42,0.8)',
border: '1px solid rgba(16,185,129,0.2)',
borderRadius: '0.375rem', color: '#E2E8F0',
outline: 'none', fontFamily: 'monospace', fontSize: '0.75rem',
width: '220px',
width: '100%', boxSizing: 'border-box',
paddingLeft: 34, paddingRight: search ? 32 : 10,
paddingTop: 8, paddingBottom: 8,
background: 'var(--bg-input)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--r-md)', color: 'var(--fg-2)',
outline: 'none', fontFamily: 'var(--font-mono)', fontSize: 13,
}}
onFocus={e => e.target.style.borderColor = `${GREEN}60`}
onBlur={e => e.target.style.borderColor = 'rgba(16,185,129,0.2)'}
onFocus={e => e.target.style.borderColor = 'var(--border-focus)'}
onBlur={e => e.target.style.borderColor = 'var(--border-subtle)'}
/>
{search && (
<button
onClick={() => setSearch('')}
style={{
position: 'absolute', right: '0.5rem', top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer', color: '#334155', padding: 0,
position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', cursor: 'pointer',
color: 'var(--fg-disabled)', padding: 0, display: 'flex',
}}
>
<X style={{ width: '12px', height: '12px' }} />
<X style={{ width: 12, height: 12 }} />
</button>
)}
</div>
<select
value={sortBy}
onChange={e => setSortBy(e.target.value)}
style={{
minWidth: 160, padding: '8px 12px',
background: 'var(--bg-input)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--r-md)', color: 'var(--fg-2)',
fontFamily: 'var(--font-mono)', fontSize: 13,
outline: 'none', cursor: 'pointer',
}}
>
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
<option value="title">Title AZ</option>
</select>
</div>
{/* Category tabs */}
<div style={{ display: 'flex', gap: '0.3rem', flexWrap: 'wrap' }}>
{activeTabs.map(cat => {
const isActive = activeCategory === cat;
const color = cat === 'All' ? GREEN : catColor(cat);
return (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
style={{
padding: '0.35rem 0.75rem',
fontFamily: 'monospace', fontSize: '0.7rem', fontWeight: '600',
textTransform: 'uppercase', letterSpacing: '0.05em',
cursor: 'pointer', borderRadius: '0.25rem',
border: isActive ? `1px solid ${color}` : '1px solid transparent',
background: isActive ? `${color}15` : 'transparent',
color: isActive ? color : '#475569',
transition: 'all 0.12s',
}}
onMouseEnter={e => { if (!isActive) { e.currentTarget.style.color = '#94A3B8'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; }}}
onMouseLeave={e => { if (!isActive) { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'transparent'; }}}
>
{cat}
<span style={{ marginLeft: '0.35rem', opacity: 0.6, fontWeight: '400' }}>
{categoryCounts[cat] ?? 0}
</span>
</button>
);
})}
</div>
{/* ── Category pills ───────────────────────────────────────── */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 16 }}>
{activeTabs.map(cat => (
<CategoryPill
key={cat}
label={cat}
count={categoryCounts[cat] ?? 0}
active={activeCategory === cat}
onClick={() => setActiveCategory(cat)}
/>
))}
</div>
{/* ── Error state ──────────────────────────────────────────── */}
@@ -426,14 +547,19 @@ export default function KnowledgeBasePage() {
display: 'flex', alignItems: 'center', gap: '0.5rem',
padding: '0.875rem 1rem',
background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.3)',
borderRadius: '0.5rem', color: '#F87171',
fontFamily: 'monospace', fontSize: '0.78rem',
borderRadius: 'var(--r-lg)', color: '#F87171',
fontFamily: 'var(--font-mono)', fontSize: '0.78rem',
marginBottom: 16,
}}>
<AlertCircle style={{ width: '15px', height: '15px', flexShrink: 0 }} />
<AlertCircle style={{ width: 15, height: 15, flexShrink: 0 }} />
{error}
<button
onClick={fetchArticles}
style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: '#F87171', fontFamily: 'monospace', fontSize: '0.72rem' }}
style={{
marginLeft: 'auto', background: 'none', border: 'none',
cursor: 'pointer', color: '#F87171',
fontFamily: 'var(--font-mono)', fontSize: '0.72rem',
}}
>
Retry
</button>
@@ -443,34 +569,51 @@ export default function KnowledgeBasePage() {
{/* ── Loading state ────────────────────────────────────────── */}
{loading && (
<div style={{ display: 'flex', justifyContent: 'center', padding: '3rem' }}>
<Loader style={{ width: '28px', height: '28px', color: GREEN, animation: 'spin 1s linear infinite' }} />
<Loader style={{ width: 28, height: 28, color: '#0EA5E9', animation: 'spin 1s linear infinite' }} />
</div>
)}
{/* ── Article grid ─────────────────────────────────────────── */}
{/* ── Article list (Card wrapper) ──────────────────────────── */}
{!loading && !error && (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
gap: '0.875rem',
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1.5px solid rgba(14,165,233,0.30)',
borderRadius: 8,
padding: 0,
boxShadow: 'var(--shadow-card)',
overflow: 'hidden',
}}>
{filtered.length === 0 ? (
<EmptyState hasFilter={hasFilter} onClear={clearFilters} />
) : (
filtered.map(article => (
<ArticleCard
<ArticleRow
key={article.id}
article={article}
selected={selected?.id === article.id}
onSelect={a => setSelected(selected?.id === a.id ? null : a)}
onDelete={handleDelete}
canDelete={canWrite()}
canDelete={canDeleteResource(article)}
/>
))
)}
</div>
)}
{/* ── Footer count ─────────────────────────────────────────── */}
{!loading && !error && (
<div style={{
marginTop: 12,
font: '400 12px var(--font-mono)',
color: 'var(--fg-muted)',
}}>
{filtered.length} article{filtered.length === 1 ? '' : 's'}
{activeCategory !== 'All' && (
<> · filtered to <span style={{ color: 'var(--accent)' }}>{activeCategory}</span></>
)}
</div>
)}
{/* ── Inline viewer ────────────────────────────────────────── */}
{selected && (
<div style={{ marginTop: '0.25rem' }}>
@@ -489,7 +632,7 @@ export default function KnowledgeBasePage() {
/>
)}
{/* Confirmation Modal */}
{/* ── Confirmation Modal ───────────────────────────────────── */}
<ConfirmModal
open={!!pendingConfirm}
title={pendingConfirm?.title}

View File

@@ -263,7 +263,7 @@ function StatusDonut({ open, closed, loading }) {
if (total === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No data click Sync to load</p>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: '#475569' }}>No data click Sync to load</p>
</div>
);
}
@@ -288,10 +288,10 @@ function StatusDonut({ open, closed, loading }) {
/>
))}
{/* Center total */}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{total.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
TOTAL
</text>
</svg>
@@ -302,10 +302,10 @@ function StatusDonut({ open, closed, loading }) {
<div key={seg.label} style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
<div style={{ width: '10px', height: '10px', borderRadius: '2px', background: seg.color, flexShrink: 0 }} />
<div>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
{seg.label}
</div>
<div style={{ fontFamily: 'monospace', fontSize: '0.9rem', fontWeight: '700', color: '#E2E8F0', lineHeight: 1.2 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.9rem', fontWeight: '700', color: '#E2E8F0', lineHeight: 1.2 }}>
{seg.count.toLocaleString()}
<span style={{ fontSize: '0.68rem', fontWeight: '400', color: '#64748B', marginLeft: '0.4rem' }}>
({((seg.count / total) * 100).toFixed(1)}%)
@@ -346,7 +346,7 @@ function ActionCoverageDonut({ findings, activeSegment, onSegmentClick }) {
if (total === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No data click Sync to load</p>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: '#475569' }}>No data click Sync to load</p>
</div>
);
}
@@ -381,10 +381,10 @@ function ActionCoverageDonut({ findings, activeSegment, onSegmentClick }) {
/>
);
})}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{total.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
TOTAL
</text>
</svg>
@@ -402,13 +402,13 @@ function ActionCoverageDonut({ findings, activeSegment, onSegmentClick }) {
>
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: seg.color, flexShrink: 0, outline: isActive ? `2px solid ${seg.color}` : 'none', outlineOffset: '1px' }} />
<div>
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
{seg.label}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
{seg.count}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
({total > 0 ? ((seg.count / total) * 100).toFixed(0) : 0}%)
</span>
</div>
@@ -418,7 +418,7 @@ function ActionCoverageDonut({ findings, activeSegment, onSegmentClick }) {
{hasActive && (
<button
onClick={() => onSegmentClick(null)}
style={{ marginTop: '0.25rem', background: 'none', border: 'none', fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', cursor: 'pointer', textAlign: 'left', padding: 0, textDecoration: 'underline' }}
style={{ marginTop: '0.25rem', background: 'none', border: 'none', fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: '#475569', cursor: 'pointer', textAlign: 'left', padding: 0, textDecoration: 'underline' }}
>
clear filter
</button>
@@ -451,7 +451,7 @@ function FPWorkflowDonut({ counts, total, centerLabel = 'FP TOTAL' }) {
if (total === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No FP workflows click Sync to load</p>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: '#475569' }}>No FP workflows click Sync to load</p>
</div>
);
}
@@ -477,10 +477,10 @@ function FPWorkflowDonut({ counts, total, centerLabel = 'FP TOTAL' }) {
opacity={0.88}
/>
))}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{total.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
{centerLabel}
</text>
</svg>
@@ -491,13 +491,13 @@ function FPWorkflowDonut({ counts, total, centerLabel = 'FP TOTAL' }) {
<div key={seg.key} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: seg.color, flexShrink: 0 }} />
<div>
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
{seg.label}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
{seg.count}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
({((seg.count / total) * 100).toFixed(0)}%)
</span>
</div>
@@ -536,7 +536,7 @@ function AtlasCoverageDonut({ hostsWithPlans, hostsWithoutPlans, totalHosts }) {
if (totalHosts === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No data run Atlas Sync</p>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: '#475569' }}>No data run Atlas Sync</p>
</div>
);
}
@@ -558,10 +558,10 @@ function AtlasCoverageDonut({ hostsWithPlans, hostsWithoutPlans, totalHosts }) {
opacity={0.88}
/>
))}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{totalHosts.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
HOSTS
</text>
</svg>
@@ -572,10 +572,10 @@ function AtlasCoverageDonut({ hostsWithPlans, hostsWithoutPlans, totalHosts }) {
<div key={seg.label} style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
<div style={{ width: '10px', height: '10px', borderRadius: '2px', background: seg.color, flexShrink: 0 }} />
<div>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
{seg.label}
</div>
<div style={{ fontFamily: 'monospace', fontSize: '0.9rem', fontWeight: '700', color: '#E2E8F0', lineHeight: 1.2 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.9rem', fontWeight: '700', color: '#E2E8F0', lineHeight: 1.2 }}>
{seg.count.toLocaleString()}
<span style={{ fontSize: '0.68rem', fontWeight: '400', color: '#64748B', marginLeft: '0.4rem' }}>
({((seg.count / totalHosts) * 100).toFixed(1)}%)
@@ -599,7 +599,7 @@ function AtlasPlanTypeDonut({ plansByType, totalPlans }) {
if (totalPlans === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No plans run Atlas Sync</p>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: '#475569' }}>No plans run Atlas Sync</p>
</div>
);
}
@@ -625,10 +625,10 @@ function AtlasPlanTypeDonut({ plansByType, totalPlans }) {
opacity={0.88}
/>
))}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{totalPlans.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
PLANS
</text>
</svg>
@@ -639,13 +639,13 @@ function AtlasPlanTypeDonut({ plansByType, totalPlans }) {
<div key={seg.key} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: seg.color, flexShrink: 0 }} />
<div>
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
{seg.label}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
{seg.count}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
({((seg.count / totalPlans) * 100).toFixed(0)}%)
</span>
</div>
@@ -666,7 +666,7 @@ function AtlasPlanStatusDonut({ plansByStatus, totalPlans }) {
if (totalPlans === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: `${SIZE}px` }}>
<p style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#475569' }}>No plans run Atlas Sync</p>
<p style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: '#475569' }}>No plans run Atlas Sync</p>
</div>
);
}
@@ -700,10 +700,10 @@ function AtlasPlanStatusDonut({ plansByStatus, totalPlans }) {
opacity={0.88}
/>
))}
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
<text x={CX} y={CY - 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '22px', fontWeight: '700', fill: '#E2E8F0' }}>
{totalPlans.toLocaleString()}
</text>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'monospace', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
<text x={CX} y={CY + 10} textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: '8.5px', fontWeight: '600', fill: '#475569', letterSpacing: '0.12em' }}>
STATUS
</text>
</svg>
@@ -714,13 +714,13 @@ function AtlasPlanStatusDonut({ plansByStatus, totalPlans }) {
<div key={seg.key} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: seg.color, flexShrink: 0 }} />
<div>
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
{seg.label}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', fontWeight: '700', color: '#E2E8F0', marginLeft: '0.4rem' }}>
{seg.count}
</span>
<span style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: '#475569', marginLeft: '0.25rem' }}>
({((seg.count / totalPlans) * 100).toFixed(0)}%)
</span>
</div>
@@ -952,13 +952,14 @@ function ColumnManager({ columnOrder, onChange }) {
onClick={() => setOpen((p) => !p)}
style={{
display: 'flex', alignItems: 'center', gap: '0.375rem',
padding: '0.375rem 0.75rem',
background: open ? 'rgba(14,165,233,0.15)' : 'rgba(14,165,233,0.07)',
border: `1px solid rgba(14,165,233,${open ? '0.5' : '0.25'})`,
borderRadius: '0.375rem',
padding: '8px 14px',
background: open ? 'rgba(14,165,233,0.16)' : 'rgba(14,165,233,0.08)',
border: `1px solid rgba(14,165,233,${open ? '0.55' : '0.25'})`,
borderRadius: 6,
color: '#0EA5E9', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
textTransform: 'uppercase', letterSpacing: '0.05em'
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
transition: 'all 150ms cubic-bezier(0.4,0,0.2,1)',
}}
>
<Settings2 style={{ width: '13px', height: '13px' }} />
@@ -1195,9 +1196,10 @@ function TableCell({ colKey, finding, canWrite, onCveMouseEnter, onCveMouseLeave
const sc = severityColor(finding.vrrGroup);
return (
<td style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.3rem', padding: '0.2rem 0.45rem', borderRadius: '0.25rem', background: sc.bg, border: `1px solid ${sc.border}40`, fontFamily: 'monospace', fontWeight: '700', color: sc.text, fontSize: '0.72rem' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: sc.text, letterSpacing: '0.04em' }}>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: sc.border, boxShadow: `0 0 6px ${sc.border}99`, flexShrink: 0 }} />
{finding.severity?.toFixed(2)}
<span style={{ fontSize: '0.6rem', opacity: 0.75 }}>{finding.vrrGroup}</span>
<span style={{ fontSize: 10, opacity: 0.75 }}>{finding.vrrGroup}</span>
</span>
</td>
);
@@ -1278,12 +1280,32 @@ function TableCell({ colKey, finding, canWrite, onCveMouseEnter, onCveMouseLeave
</td>
);
}
case 'slaStatus':
case 'slaStatus': {
const slc = slaColor(finding.slaStatus);
const slaMap = {
'#EF4444': 'rgba(239,68,68,0.16)',
'#F59E0B': 'rgba(245,158,11,0.16)',
'#10B981': 'rgba(16,185,129,0.16)',
'#64748B': 'rgba(100,116,139,0.16)',
};
return (
<td style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap', fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: slaColor(finding.slaStatus) }}>
{finding.slaStatus || '—'}
<td style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap' }}>
{finding.slaStatus ? (
<span style={{
padding: '2px 8px', borderRadius: 999,
background: slaMap[slc] || 'rgba(100,116,139,0.16)',
color: slc,
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10,
letterSpacing: '0.05em',
}}>
{finding.slaStatus.replace('_', ' ')}
</span>
) : (
<span style={{ color: '#475569', fontFamily: 'var(--font-mono)', fontSize: 11 }}></span>
)}
</td>
);
}
case 'buOwnership': {
const bu = finding.buOwnership || '';
const isSteam = bu.toUpperCase().includes('STEAM');
@@ -1319,14 +1341,15 @@ function TableCell({ colKey, finding, canWrite, onCveMouseEnter, onCveMouseLeave
title={`${wf.id} · ${wf.state || 'Unknown'} · ${wf.type || ''}`}
style={{
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
padding: '0.15rem 0.45rem', borderRadius: '0.25rem',
background: ws.bg, border: `1px solid ${ws.border}`,
fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600',
padding: '2px 8px', borderRadius: 4,
background: ws.bg, border: `1px solid ${ws.text}55`,
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
color: ws.text, cursor: 'default',
letterSpacing: '0.05em',
}}
>
{wf.id}
<span style={{ fontSize: '0.58rem', opacity: 0.8, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
<span style={{ fontSize: 9, opacity: 0.8, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
{wf.state}
</span>
</span>
@@ -3638,13 +3661,14 @@ function RowVisibilityManager({ hiddenRowIds, findings, onRestore, onRestoreAll
onClick={() => setOpen(p => !p)}
style={{
display: 'flex', alignItems: 'center', gap: '0.375rem',
padding: '0.35rem 0.75rem',
background: open ? 'rgba(14,165,233,0.15)' : 'rgba(14,165,233,0.08)',
border: `1px solid rgba(14,165,233,${open ? '0.5' : '0.2'})`,
borderRadius: '0.375rem',
color: '#94a3b8', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.65rem', fontWeight: '600',
padding: '8px 14px',
background: open ? 'rgba(14,165,233,0.16)' : 'rgba(14,165,233,0.08)',
border: `1px solid rgba(14,165,233,${open ? '0.55' : '0.25'})`,
borderRadius: 6,
color: '#0EA5E9', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
transition: 'all 150ms cubic-bezier(0.4,0,0.2,1)',
}}
>
<EyeOff style={{ width: '13px', height: '13px' }} />
@@ -4946,16 +4970,15 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
Panel 1 — Metrics placeholder
---------------------------------------------------------------- */}
<div style={{
background: 'linear-gradient(135deg, rgba(15,26,46,0.95) 0%, rgba(10,22,40,0.9) 100%)',
border: '1px solid rgba(245,158,11,0.2)',
borderLeft: '3px solid #F59E0B',
borderRadius: '0.5rem',
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1.5px solid rgba(14,165,233,0.12)',
borderRadius: 8,
padding: '1.5rem',
boxShadow: '0 4px 16px rgba(0,0,0,0.4)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem', marginBottom: '1rem' }}>
<PieChart style={{ width: '20px', height: '20px', color: '#F59E0B' }} />
<h2 style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '600', color: '#F59E0B', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(245,158,11,0.4)', margin: 0 }}>
<PieChart style={{ width: '20px', height: '20px', color: '#0EA5E9' }} />
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', margin: 0 }}>
Metric Graphs
</h2>
<div style={{ marginLeft: 'auto', display: 'flex', gap: '0.25rem' }} role="tablist">
@@ -4970,20 +4993,17 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
onClick={() => setMetricsTab(tab.key)}
onKeyDown={(e) => { if (e.key === 'Enter') setMetricsTab(tab.key); }}
style={{
background: 'transparent',
border: 'none',
borderBottom: isActive ? '2px solid #F59E0B' : '2px solid transparent',
color: isActive ? '#F59E0B' : '#64748B',
fontFamily: 'monospace',
fontSize: '0.7rem',
textTransform: 'uppercase',
letterSpacing: '0.08em',
padding: '0.375rem 0.75rem',
cursor: 'pointer',
transition: 'background 0.15s, color 0.15s'
padding: '6px 12px',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
cursor: 'pointer', borderRadius: 4,
border: isActive ? '1px solid #0EA5E9' : '1px solid transparent',
background: isActive ? 'rgba(14,165,233,0.15)' : 'transparent',
color: isActive ? '#0EA5E9' : 'var(--fg-muted)',
transition: 'all 120ms',
}}
onMouseEnter={(e) => { if (!isActive) e.currentTarget.style.background = 'rgba(245, 158, 11, 0.06)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
onMouseEnter={(e) => { if (!isActive) { e.currentTarget.style.border = '1px solid rgba(255,255,255,0.10)'; e.currentTarget.style.color = '#94A3B8'; } }}
onMouseLeave={(e) => { if (!isActive) { e.currentTarget.style.border = '1px solid transparent'; e.currentTarget.style.color = 'var(--fg-muted)'; } }}
>
{tab.label}
</button>
@@ -4996,7 +5016,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
<div style={{ display: 'flex', gap: '3rem', flexWrap: 'wrap', alignItems: 'flex-start' }}>
{/* Open vs Closed donut */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Open vs Closed
</div>
<StatusDonut
@@ -5011,7 +5031,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
{/* Action Coverage donut */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Action Coverage
{actionFilter && <span style={{ marginLeft: '0.5rem', color: ACTION_DEFS.find(d => d.key === actionFilter)?.color, fontSize: '0.6rem' }}> filtered</span>}
</div>
@@ -5030,7 +5050,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
{/* FP Finding Status donut — # of findings per FP workflow state */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
FP Finding Status
</div>
<FPWorkflowDonut counts={fpCounts.findingCounts} total={fpCounts.findingTotal} centerLabel="FINDINGS" />
@@ -5041,7 +5061,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
{/* FP Workflow Status donut — # of unique FP# ticket IDs per state */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
FP Workflow Status
</div>
<FPWorkflowDonut counts={fpCounts.idCounts} total={fpCounts.idTotal} centerLabel="FP TICKETS" />
@@ -5056,13 +5076,13 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
) : atlasMetricsError ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem', gap: '0.375rem' }}>
<AlertCircle style={{ width: '14px', height: '14px', color: '#EF4444', flexShrink: 0 }} />
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#FCA5A5' }}>{atlasMetricsError}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: '#FCA5A5' }}>{atlasMetricsError}</span>
</div>
) : (
<div style={{ display: 'flex', gap: '3rem', flexWrap: 'wrap', alignItems: 'flex-start' }}>
{/* Host Coverage donut */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Host Coverage
</div>
<AtlasCoverageDonut
@@ -5077,7 +5097,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
{/* Plan Types donut */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Plan Types
</div>
<AtlasPlanTypeDonut
@@ -5091,7 +5111,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
{/* Plan Status donut */}
<div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Plan Status
</div>
<AtlasPlanStatusDonut
@@ -5115,20 +5135,19 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
Panel 2 — Findings table
---------------------------------------------------------------- */}
<div style={{
background: 'linear-gradient(135deg, rgba(15,26,46,0.95) 0%, rgba(10,22,40,0.9) 100%)',
border: '1px solid rgba(14,165,233,0.2)',
borderLeft: '3px solid #0EA5E9',
borderRadius: '0.5rem',
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
border: '1.5px solid rgba(14,165,233,0.12)',
borderRadius: 8,
padding: '1.5rem',
boxShadow: '0 4px 16px rgba(0,0,0,0.4)'
}}>
{/* Panel header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
<div>
<h2 style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '600', color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(14,165,233,0.4)', margin: '0 0 4px 0' }}>
<h2 style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.1em', margin: '0 0 4px 0' }}>
Host Findings
</h2>
<div style={{ fontFamily: 'monospace', fontSize: '0.7rem', color: '#475569' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: '#475569' }}>
{syncedDisplay}
{syncStatus === 'success' && total !== null && (
<span style={{ marginLeft: '0.75rem', color: '#64748B' }}>
@@ -5151,12 +5170,12 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
onClick={() => setExcFilter(null)}
style={{
display: 'flex', alignItems: 'center', gap: '0.375rem',
padding: '0.375rem 0.75rem',
padding: '6px 12px',
background: 'rgba(245,158,11,0.08)',
border: '1px solid rgba(245,158,11,0.3)',
borderRadius: '0.375rem',
border: '1px solid rgba(245,158,11,0.30)',
borderRadius: 6,
color: '#F59E0B', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
letterSpacing: '0.05em'
}}
>
@@ -5170,13 +5189,13 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
onClick={() => setActionFilter(null)}
style={{
display: 'flex', alignItems: 'center', gap: '0.375rem',
padding: '0.375rem 0.75rem',
padding: '6px 12px',
background: actionFilter === 'fp' ? 'rgba(14,165,233,0.08)' : actionFilter === 'archer' ? 'rgba(245,158,11,0.08)' : 'rgba(239,68,68,0.08)',
border: `1px solid ${actionFilter === 'fp' ? 'rgba(14,165,233,0.3)' : actionFilter === 'archer' ? 'rgba(245,158,11,0.3)' : 'rgba(239,68,68,0.3)'}`,
borderRadius: '0.375rem',
border: `1px solid ${actionFilter === 'fp' ? 'rgba(14,165,233,0.30)' : actionFilter === 'archer' ? 'rgba(245,158,11,0.30)' : 'rgba(239,68,68,0.30)'}`,
borderRadius: 6,
color: actionFilter === 'fp' ? '#0EA5E9' : actionFilter === 'archer' ? '#F59E0B' : '#EF4444',
cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
letterSpacing: '0.05em'
}}
>
@@ -5189,12 +5208,12 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
onClick={() => setColumnFilters({})}
style={{
display: 'flex', alignItems: 'center', gap: '0.375rem',
padding: '0.375rem 0.75rem',
padding: '6px 12px',
background: 'rgba(245,158,11,0.08)',
border: '1px solid rgba(245,158,11,0.3)',
borderRadius: '0.375rem',
border: '1px solid rgba(245,158,11,0.30)',
borderRadius: 6,
color: '#F59E0B', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em'
}}
>
@@ -5209,14 +5228,15 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
disabled={sorted.length === 0}
style={{
display: 'flex', alignItems: 'center', gap: '0.375rem',
padding: '0.375rem 0.75rem',
background: 'rgba(16,185,129,0.08)',
border: '1px solid rgba(16,185,129,0.3)',
borderRadius: '0.375rem',
color: '#10B981', cursor: sorted.length === 0 ? 'not-allowed' : 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
padding: '8px 14px',
background: 'rgba(14,165,233,0.08)',
border: '1px solid rgba(14,165,233,0.25)',
borderRadius: 6,
color: '#0EA5E9', cursor: sorted.length === 0 ? 'not-allowed' : 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
opacity: sorted.length === 0 ? 0.4 : 1,
opacity: sorted.length === 0 ? 0.5 : 1,
transition: 'all 150ms cubic-bezier(0.4,0,0.2,1)',
}}
>
<Download style={{ width: '11px', height: '11px' }} />
@@ -5226,8 +5246,8 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
{exportMenuOpen && (
<div style={{
position: 'absolute', top: 'calc(100% + 4px)', right: 0, zIndex: 200,
background: 'rgb(12,22,40)', border: '1px solid rgba(16,185,129,0.3)',
borderRadius: '0.375rem', overflow: 'hidden',
background: 'rgb(12,22,40)', border: '1px solid rgba(14,165,233,0.25)',
borderRadius: 6, overflow: 'hidden',
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
minWidth: '120px',
}}>
@@ -5242,12 +5262,12 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
display: 'block', width: '100%', textAlign: 'left',
padding: '0.5rem 0.875rem',
background: 'none', border: 'none',
fontFamily: 'monospace', fontSize: '0.73rem', fontWeight: '600',
color: '#10B981', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
color: '#0EA5E9', cursor: 'pointer',
textTransform: 'uppercase', letterSpacing: '0.04em',
transition: 'background 0.1s',
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(16,185,129,0.1)'}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(14,165,233,0.1)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'none'}
>
{label}
@@ -5262,13 +5282,14 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
style={{
position: 'relative',
display: 'flex', alignItems: 'center', gap: '0.375rem',
padding: '0.375rem 0.75rem',
background: queueOpen ? 'rgba(14,165,233,0.15)' : 'rgba(14,165,233,0.08)',
border: `1px solid rgba(14,165,233,${queueOpen ? '0.5' : '0.25'})`,
borderRadius: '0.375rem',
padding: '8px 14px',
background: queueOpen ? 'rgba(14,165,233,0.16)' : 'rgba(14,165,233,0.08)',
border: `1px solid rgba(14,165,233,${queueOpen ? '0.55' : '0.25'})`,
borderRadius: 6,
color: '#0EA5E9', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
transition: 'all 150ms cubic-bezier(0.4,0,0.2,1)',
}}
>
<ListTodo style={{ width: '13px', height: '13px' }} />
@@ -5309,18 +5330,19 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
title={!canWrite() ? 'Insufficient permissions' : 'Sync Atlas action plan status'}
style={{
display: 'inline-flex', alignItems: 'center', gap: '0.4rem',
padding: '0.4rem 0.75rem',
background: atlasSyncing ? 'rgba(14,165,233,0.08)' : 'rgba(14,165,233,0.06)',
border: '1px solid rgba(14,165,233,0.2)',
borderRadius: '0.375rem',
padding: '8px 14px',
background: atlasSyncing ? 'rgba(14,165,233,0.08)' : 'rgba(14,165,233,0.08)',
border: '1px solid rgba(14,165,233,0.25)',
borderRadius: 6,
color: atlasSyncing ? '#475569' : '#0EA5E9',
fontSize: '0.72rem',
fontFamily: "'JetBrains Mono', monospace",
fontSize: 12,
fontFamily: 'var(--font-mono)',
fontWeight: 600,
cursor: atlasSyncing || !canWrite() ? 'not-allowed' : 'pointer',
opacity: !canWrite() ? 0.5 : 1,
textTransform: 'uppercase',
letterSpacing: '0.05em',
transition: 'all 150ms cubic-bezier(0.4,0,0.2,1)',
}}
>
{atlasSyncing
@@ -5332,15 +5354,16 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
onClick={syncFindings}
disabled={syncing || loading}
style={{
display: 'flex', alignItems: 'center', gap: '0.375rem',
padding: '0.375rem 0.75rem',
background: 'rgba(14,165,233,0.1)',
border: '1px solid rgba(14,165,233,0.35)',
borderRadius: '0.375rem',
color: '#0EA5E9', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 14px',
background: 'rgba(16,185,129,0.18)',
border: '1px solid #10B981',
borderRadius: 6,
color: '#10B981', cursor: 'pointer',
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em',
opacity: (syncing || loading) ? 0.6 : 1
opacity: (syncing || loading) ? 0.5 : 1,
transition: 'all 150ms cubic-bezier(0.4,0,0.2,1)',
}}
>
<RefreshCw style={{ width: '13px', height: '13px', animation: syncing ? 'spin 1s linear infinite' : 'none' }} />
@@ -5351,15 +5374,15 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
{/* Error banner */}
{syncStatus === 'error' && syncError && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem', padding: '0.625rem 0.875rem', background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.25)', borderRadius: '0.375rem', marginBottom: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, padding: '10px 14px', background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.25)', borderRadius: 8, marginBottom: '1rem' }}>
<AlertCircle style={{ width: '15px', height: '15px', color: '#EF4444', flexShrink: 0, marginTop: '1px' }} />
<span style={{ fontSize: '0.75rem', color: '#FCA5A5', fontFamily: 'monospace' }}>{syncError}</span>
<span style={{ fontSize: 12, color: '#FCA5A5', fontFamily: 'var(--font-mono)' }}>{syncError}</span>
</div>
)}
{atlasError && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem', padding: '0.625rem 0.875rem', background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.25)', borderRadius: '0.375rem', marginBottom: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, padding: '10px 14px', background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.25)', borderRadius: 8, marginBottom: '1rem' }}>
<AlertCircle style={{ width: '15px', height: '15px', color: '#EF4444', flexShrink: 0, marginTop: '1px' }} />
<span style={{ fontSize: '0.75rem', color: '#FCA5A5', fontFamily: 'monospace' }}>Atlas: {atlasError}</span>
<span style={{ fontSize: 12, color: '#FCA5A5', fontFamily: 'var(--font-mono)' }}>Atlas: {atlasError}</span>
</div>
)}
@@ -5399,14 +5422,14 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
)}
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.75rem', fontFamily: 'sans-serif' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(14,165,233,0.2)' }}>
<tr style={{ borderBottom: '1px solid rgba(14,165,233,0.15)' }}>
{/* Fixed selection checkbox column — row visibility feature */}
<th
style={{
width: '36px', minWidth: '36px', padding: '0.5rem 0.5rem',
background: 'rgb(10, 20, 36)',
position: 'sticky', top: 0, zIndex: 10,
boxShadow: '0 1px 0 rgba(14,165,233,0.2)',
boxShadow: '0 1px 0 rgba(14,165,233,0.15)',
textAlign: 'center',
cursor: 'pointer',
}}
@@ -5428,7 +5451,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
width: '36px', minWidth: '36px', padding: '0.5rem 0.5rem',
background: 'rgb(10, 20, 36)',
position: 'sticky', top: 0, zIndex: 10,
boxShadow: '0 1px 0 rgba(14,165,233,0.2)',
boxShadow: '0 1px 0 rgba(14,165,233,0.15)',
}}
/>
{/* Fixed checkbox column — not part of column manager */}
@@ -5437,7 +5460,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
width: '36px', minWidth: '36px', padding: '0.5rem 0.5rem',
background: 'rgb(10, 20, 36)',
position: 'sticky', top: 0, zIndex: 10,
boxShadow: '0 1px 0 rgba(14,165,233,0.2)',
boxShadow: '0 1px 0 rgba(14,165,233,0.15)',
textAlign: 'center',
}}
>
@@ -5473,15 +5496,15 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
onClick={def?.sortable ? () => toggleSort(col.key) : undefined}
style={{
padding: '0.5rem 0.75rem', textAlign: 'left',
fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600',
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700,
color: active ? '#0EA5E9' : '#64748B',
textTransform: 'uppercase', letterSpacing: '0.08em',
textTransform: 'uppercase', letterSpacing: '0.1em',
whiteSpace: 'nowrap',
cursor: def?.sortable ? 'pointer' : 'default',
userSelect: 'none',
background: 'rgb(10, 20, 36)',
position: 'sticky', top: 0, zIndex: 10,
boxShadow: '0 1px 0 rgba(14,165,233,0.2)',
boxShadow: '0 1px 0 rgba(14,165,233,0.15)',
}}
>
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
@@ -5520,9 +5543,9 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
return (
<tr
key={finding.id}
style={{ borderBottom: '1px solid rgba(255,255,255,0.04)', background: rowBg }}
onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = 'rgba(14,165,233,0.05)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = rowBg; }}
style={{ borderBottom: '1px solid rgba(14,165,233,0.15)', background: rowBg, transition: 'background 150ms, box-shadow 150ms' }}
onMouseEnter={(e) => { if (!isSelected) { e.currentTarget.style.background = 'rgba(0,217,255,0.06)'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,217,255,0.10)'; } }}
onMouseLeave={(e) => { e.currentTarget.style.background = rowBg; e.currentTarget.style.boxShadow = 'none'; }}
>
{/* Selection checkbox cell — row visibility feature */}
<td