diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.module.scss new file mode 100644 index 00000000..2dccaa66 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.module.scss @@ -0,0 +1,93 @@ +// =================== +// © AngelaMos | 2026 +// alert-feed.module.scss +// =================== + +@use '@/styles/tokens' as *; + +.feed { + display: flex; + flex-direction: column; + background: $bg-surface-100; + border: 1px solid $border-muted; + border-radius: $radius-lg; + overflow: hidden; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: $space-4 $space-5; + border-bottom: 1px solid $border-muted; +} + +.title { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-default; + margin: 0; +} + +.status { + width: 8px; + height: 8px; + border-radius: $radius-full; +} + +.connected { + background: $severity-low; +} + +.disconnected { + background: $severity-high; +} + +.list { + overflow-y: auto; + max-height: 400px; +} + +.row { + display: grid; + grid-template-columns: 80px 130px 1fr auto 60px; + gap: $space-3; + align-items: center; + padding: $space-2-5 $space-5; + border-bottom: 1px solid $border-muted; + font-size: $font-size-xs; + + &:last-child { + border-bottom: none; + } +} + +.time { + color: $text-lighter; + font-variant-numeric: tabular-nums; +} + +.ip { + color: $text-light; + font-family: monospace; +} + +.path { + color: $text-light; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.score { + color: $text-lighter; + text-align: right; + font-variant-numeric: tabular-nums; +} + +.empty { + padding: $space-10; + text-align: center; + color: $text-muted; + font-size: $font-size-sm; +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.tsx new file mode 100644 index 00000000..0aedde48 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.tsx @@ -0,0 +1,70 @@ +// =================== +// © AngelaMos | 2026 +// alert-feed.tsx +// =================== + +import { useEffect, useRef } from 'react' +import type { WebSocketAlert } from '@/api/types' +import { SeverityBadge } from './severity-badge' +import styles from './alert-feed.module.scss' + +interface AlertFeedProps { + alerts: WebSocketAlert[] + isConnected: boolean + maxHeight?: string +} + +function formatTime(timestamp: string): string { + return new Date(timestamp).toLocaleTimeString() +} + +export function AlertFeed({ + alerts, + isConnected, + maxHeight, +}: AlertFeedProps): React.ReactElement { + const listRef = useRef(null) + + useEffect(() => { + if (listRef.current) { + listRef.current.scrollTop = 0 + } + }, [alerts.length]) + + return ( +
+
+

Live Alerts

+ +
+ +
+ {alerts.length === 0 ? ( +
Waiting for alerts...
+ ) : ( + alerts.map((alert, i) => ( +
+ + {formatTime(alert.timestamp)} + + {alert.source_ip} + {alert.request_path} + + + {alert.threat_score.toFixed(2)} + +
+ )) + )} +
+
+ ) +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx index 9f7eeadd..5981c172 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx @@ -1,4 +1,9 @@ -/** - * ©AngelaMos | 2026 - * index.tsx - */ +// =================== +// © AngelaMos | 2026 +// index.tsx +// =================== + +export { SeverityBadge } from './severity-badge' +export { StatCard } from './stat-card' +export { AlertFeed } from './alert-feed' +export { ThreatDetail } from './threat-detail' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.module.scss new file mode 100644 index 00000000..338d2b01 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.module.scss @@ -0,0 +1,33 @@ +// =================== +// © AngelaMos | 2026 +// severity-badge.module.scss +// =================== + +@use '@/styles/tokens' as *; + +.badge { + display: inline-flex; + align-items: center; + padding: $space-0-5 $space-2; + border-radius: $radius-full; + font-size: $font-size-2xs; + font-weight: $font-weight-semibold; + letter-spacing: $tracking-wider; + line-height: $line-height-none; + text-transform: uppercase; +} + +.high { + color: $severity-high; + background: $severity-high-bg; +} + +.medium { + color: $severity-medium; + background: $severity-medium-bg; +} + +.low { + color: $severity-low; + background: $severity-low-bg; +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.tsx new file mode 100644 index 00000000..2389db84 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.tsx @@ -0,0 +1,20 @@ +// =================== +// © AngelaMos | 2026 +// severity-badge.tsx +// =================== + +import styles from './severity-badge.module.scss' + +interface SeverityBadgeProps { + severity: 'HIGH' | 'MEDIUM' | 'LOW' +} + +export function SeverityBadge({ + severity, +}: SeverityBadgeProps): React.ReactElement { + return ( + + {severity} + + ) +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.module.scss new file mode 100644 index 00000000..33b01cc7 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.module.scss @@ -0,0 +1,34 @@ +// =================== +// © AngelaMos | 2026 +// stat-card.module.scss +// =================== + +@use '@/styles/tokens' as *; + +.card { + display: flex; + flex-direction: column; + gap: $space-1; + padding: $space-5; + background: $bg-surface-100; + border: 1px solid $border-muted; + border-radius: $radius-lg; +} + +.value { + font-size: $font-size-3xl; + font-weight: $font-weight-semibold; + color: $text-default; + line-height: $line-height-none; + letter-spacing: $tracking-tight; +} + +.label { + font-size: $font-size-sm; + color: $text-lighter; +} + +.sublabel { + font-size: $font-size-xs; + color: $text-muted; +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.tsx new file mode 100644 index 00000000..cfd5da7e --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.tsx @@ -0,0 +1,26 @@ +// =================== +// © AngelaMos | 2026 +// stat-card.tsx +// =================== + +import styles from './stat-card.module.scss' + +interface StatCardProps { + label: string + value: string | number + sublabel?: string +} + +export function StatCard({ + label, + value, + sublabel, +}: StatCardProps): React.ReactElement { + return ( +
+ {value} + {label} + {sublabel && {sublabel}} +
+ ) +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.module.scss new file mode 100644 index 00000000..d6145de2 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.module.scss @@ -0,0 +1,170 @@ +// =================== +// © AngelaMos | 2026 +// threat-detail.module.scss +// =================== + +@use '@/styles/tokens' as *; + +.overlay { + position: fixed; + inset: 0; + background: rgb(0 0 0 / 60%); + z-index: $z-modal; + display: flex; + justify-content: flex-end; +} + +.panel { + width: 100%; + max-width: 520px; + height: 100%; + background: $bg-surface-100; + border-left: 1px solid $border-default; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: $space-5 $space-6; + border-bottom: 1px solid $border-muted; + position: sticky; + top: 0; + background: $bg-surface-100; + z-index: 1; +} + +.title { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: $text-default; + margin: 0; +} + +.closeBtn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: none; + border: 1px solid $border-default; + border-radius: $radius-md; + color: $text-light; + cursor: pointer; + transition: background $duration-fast $ease-out; + + &:hover { + background: $bg-surface-200; + } +} + +.body { + padding: $space-6; + display: flex; + flex-direction: column; + gap: $space-6; +} + +.section { + display: flex; + flex-direction: column; + gap: $space-3; +} + +.sectionTitle { + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: $text-lighter; + text-transform: uppercase; + letter-spacing: $tracking-wider; + margin: 0; +} + +.grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $space-3; +} + +.field { + display: flex; + flex-direction: column; + gap: $space-1; +} + +.fieldLabel { + font-size: $font-size-2xs; + color: $text-muted; +} + +.fieldValue { + font-size: $font-size-sm; + color: $text-default; +} + +.mono { + font-size: $font-size-xs; + color: $text-light; + font-family: monospace; + word-break: break-all; +} + +.scores { + display: flex; + flex-direction: column; + gap: $space-2; +} + +.scoreRow { + display: grid; + grid-template-columns: 40px 1fr 50px; + gap: $space-3; + align-items: center; +} + +.scoreLabel { + font-size: $font-size-xs; + color: $text-lighter; + text-transform: uppercase; + font-weight: $font-weight-medium; +} + +.scoreBar { + height: 6px; + background: $bg-surface-300; + border-radius: $radius-full; + overflow: hidden; +} + +.scoreFill { + height: 100%; + background: $accent; + border-radius: $radius-full; + transition: width $duration-slow $ease-out; +} + +.scoreValue { + font-size: $font-size-xs; + color: $text-light; + text-align: right; + font-variant-numeric: tabular-nums; +} + +.rules { + display: flex; + flex-wrap: wrap; + gap: $space-2; +} + +.rule { + padding: $space-1 $space-2-5; + background: $accent-muted; + color: $accent; + border-radius: $radius-sm; + font-size: $font-size-2xs; + font-family: monospace; +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.tsx new file mode 100644 index 00000000..48278be2 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.tsx @@ -0,0 +1,166 @@ +// =================== +// © AngelaMos | 2026 +// threat-detail.tsx +// =================== + +import { LuX } from 'react-icons/lu' +import type { ThreatEvent } from '@/api/types' +import { SeverityBadge } from './severity-badge' +import styles from './threat-detail.module.scss' + +interface ThreatDetailProps { + threat: ThreatEvent | null + onClose: () => void +} + +function formatDate(dateStr: string): string { + return new Date(dateStr).toLocaleString() +} + +export function ThreatDetail({ + threat, + onClose, +}: ThreatDetailProps): React.ReactElement | null { + if (!threat) return null + + return ( +
{}}> +
e.stopPropagation()} + onKeyDown={() => {}} + > +
+

Threat Details

+ +
+ +
+
+

Overview

+
+
+ Severity + +
+
+ Threat Score + + {threat.threat_score.toFixed(4)} + +
+
+ Detected + + {formatDate(threat.created_at)} + +
+
+ Reviewed + + {threat.reviewed ? 'Yes' : 'No'} + +
+
+
+ +
+

Request

+
+
+ Source IP + {threat.source_ip} +
+
+ Method + + {threat.request_method} + +
+
+ Path + {threat.request_path} +
+
+ Status + + {threat.status_code} + +
+
+ Response Size + + {threat.response_size} B + +
+
+ User Agent + {threat.user_agent} +
+
+
+ +
+

Component Scores

+
+ {Object.entries(threat.component_scores).map(([key, val]) => ( +
+ {key} +
+
+
+ {val.toFixed(3)} +
+ ))} +
+
+ + {threat.geo.country && ( +
+

Geolocation

+
+
+ Country + + {threat.geo.country} + +
+ {threat.geo.city && ( +
+ City + + {threat.geo.city} + +
+ )} +
+
+ )} + + {threat.matched_rules && threat.matched_rules.length > 0 && ( +
+

Matched Rules

+
+ {threat.matched_rules.map((rule) => ( + + {rule} + + ))} +
+
+ )} +
+
+
+ ) +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss index 53789f3c..0cbce729 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss @@ -299,6 +299,8 @@ $header-height: 56px; .content { flex: 1; overflow-y: auto; + background-image: radial-gradient(circle, $border-muted 1px, transparent 1px); + background-size: 24px 24px; } .loading { diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss index f3c63ba5..968f1c3f 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss @@ -109,6 +109,19 @@ $text-muted: hsl(0, 0%, 30.2%); $error-default: hsl(0, 72%, 51%); $error-light: hsl(0, 72%, 65%); +$severity-high: hsl(0, 72%, 51%); +$severity-high-bg: hsl(0, 72%, 15%); +$severity-medium: hsl(38, 92%, 50%); +$severity-medium-bg: hsl(38, 92%, 15%); +$severity-low: hsl(142, 71%, 45%); +$severity-low-bg: hsl(142, 71%, 15%); + +$accent: hsl(217, 91%, 60%); +$accent-muted: hsl(217, 91%, 20%); + +$success: hsl(142, 71%, 45%); +$success-bg: hsl(142, 71%, 15%); + // ============================================================================ // BORDER RADIUS // ============================================================================