diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss index ec05e16e..60cf8d62 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss @@ -6,147 +6,178 @@ @use '@/styles' as *; .page { - min-height: calc(100vh - 56px); padding: $space-6; - background-color: $bg-default; -} - -.container { - max-width: 800px; - margin: 0 auto; -} - -.header { - margin-bottom: $space-6; -} - -.title { - font-size: $font-size-2xl; - font-weight: $font-weight-semibold; - color: $text-default; - margin-bottom: $space-2; -} - -.subtitle { - font-size: $font-size-sm; - color: $text-lighter; -} - -.userCard { display: flex; - align-items: center; + flex-direction: column; + gap: $space-6; +} + +.loading { + @include flex-center; + height: 300px; + color: $text-muted; + font-size: $font-size-sm; +} + +.statRow { + display: grid; + grid-template-columns: repeat(4, 1fr); gap: $space-4; + + @include breakpoint-down('lg') { + grid-template-columns: repeat(2, 1fr); + } + + @include breakpoint-down('sm') { + grid-template-columns: 1fr; + } +} + +.severitySection { + display: flex; + flex-direction: column; + gap: $space-3; padding: $space-5; background: $bg-surface-100; - border: 1px solid $border-default; + border: 1px solid $border-muted; border-radius: $radius-lg; - margin-bottom: $space-8; } -.avatar { - width: 56px; - height: 56px; +.severityBar { + display: flex; + height: 12px; + border-radius: $radius-full; + overflow: hidden; + background: $bg-surface-300; +} + +.severityBarEmpty { + height: 12px; border-radius: $radius-full; background: $bg-surface-300; - color: $text-default; - font-size: $font-size-xl; - font-weight: $font-weight-semibold; @include flex-center; - flex-shrink: 0; + font-size: $font-size-2xs; + color: $text-muted; } -.userInfo { - @include flex-column; - gap: $space-1; - min-width: 0; +.severityHigh { + background: $severity-high; + transition: width $duration-slow $ease-out; } -.userName { - font-size: $font-size-base; - font-weight: $font-weight-medium; - color: $text-default; - @include truncate; +.severityMedium { + background: $severity-medium; + transition: width $duration-slow $ease-out; } -.userEmail { - font-size: $font-size-sm; - color: $text-light; - @include truncate; +.severityLow { + background: $severity-low; + transition: width $duration-slow $ease-out; } -.userRole { - display: inline-flex; - align-self: flex-start; - padding: $space-0-5 $space-2; - background: $bg-surface-200; - border-radius: $radius-full; +.legend { + display: flex; + gap: $space-5; +} + +.legendItem { + display: flex; + align-items: center; + gap: $space-2; font-size: $font-size-xs; color: $text-lighter; - text-transform: uppercase; - letter-spacing: $tracking-wide; } -.section { - margin-bottom: $space-8; +.legendDot { + width: 8px; + height: 8px; + border-radius: $radius-full; } -.sectionTitle { - font-size: $font-size-sm; - font-weight: $font-weight-medium; - color: $text-light; - text-transform: uppercase; - letter-spacing: $tracking-wide; - margin-bottom: $space-4; +.dotHigh { + background: $severity-high; } -.grid { +.dotMedium { + background: $severity-medium; +} + +.dotLow { + background: $severity-low; +} + +.bottomRow { display: grid; + grid-template-columns: 1fr 1fr; gap: $space-4; - @include breakpoint-up('md') { - grid-template-columns: repeat(3, 1fr); + @include breakpoint-down('lg') { + grid-template-columns: 1fr; } } -.card { - padding: $space-4; +.lists { + display: flex; + flex-direction: column; + gap: $space-4; +} + +.rankedList { + padding: $space-5; background: $bg-surface-100; - border: 1px solid $border-default; + border: 1px solid $border-muted; border-radius: $radius-lg; } -.hookName { - display: inline-block; +.rankedTitle { font-size: $font-size-sm; font-weight: $font-weight-medium; color: $text-default; - background: $bg-surface-200; - padding: $space-1 $space-2; - border-radius: $radius-sm; - margin-bottom: $space-2; + margin: 0 0 $space-3; } -.description { - font-size: $font-size-sm; +.rankedItems { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: $space-1; +} + +.rankedItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: $space-1-5 0; + font-size: $font-size-xs; +} + +.rankedLabel { color: $text-light; - margin-bottom: $space-2; - line-height: $line-height-relaxed; + font-family: monospace; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.file { +.rankedCount { + color: $text-lighter; + font-variant-numeric: tabular-nums; + flex-shrink: 0; + margin-left: $space-3; +} + +.emptyText { font-size: $font-size-xs; color: $text-muted; - font-family: $font-mono; } -.list { - @include flex-column; - gap: $space-2; - padding-left: $space-5; - list-style: disc; - - li { - font-size: $font-size-sm; - color: $text-light; - } +.wsError { + padding: $space-3 $space-4; + background: $severity-high-bg; + border: 1px solid $severity-high; + border-radius: $radius-md; + font-size: $font-size-xs; + color: $severity-high; } diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx index 4b1fca5d..01d6ef1b 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx @@ -3,8 +3,166 @@ // index.tsx // =================== +import { useStats, useModelStatus, useAlerts } from '@/api/hooks' +import { StatCard, AlertFeed } from '@/components' +import styles from './dashboard.module.scss' + +function SeverityBar({ + high, + medium, + low, +}: { + high: number + medium: number + low: number +}): React.ReactElement { + const total = high + medium + low + if (total === 0) { + return
No threats detected
+ } + + return ( +
+
+
+
+
+ ) +} + +function SeverityLegend({ + high, + medium, + low, +}: { + high: number + medium: number + low: number +}): React.ReactElement { + return ( +
+ + + High: {high} + + + + Medium: {medium} + + + + Low: {low} + +
+ ) +} + +function RankedList({ + title, + items, +}: { + title: string + items: { label: string; count: number }[] +}): React.ReactElement { + return ( +
+

{title}

+ {items.length === 0 ? ( + None + ) : ( +
    + {items.map((item) => ( +
  1. + {item.label} + {item.count} +
  2. + ))} +
+ )} +
+ ) +} + export function Component(): React.ReactElement { - return
Dashboard
+ const { data: stats, isLoading: statsLoading } = useStats() + const { data: modelStatus } = useModelStatus() + const { alerts, isConnected, connectionError } = useAlerts() + + if (statsLoading || !stats) { + return
Loading dashboard...
+ } + + const { severity_breakdown: sb } = stats + + return ( +
+
+ + + + +
+ +
+ + +
+ +
+ + +
+ ({ + label: ip.source_ip, + count: ip.count, + }))} + /> + ({ + label: p.path, + count: p.count, + }))} + /> +
+
+ + {connectionError && ( +
{connectionError}
+ )} +
+ ) } Component.displayName = 'DashboardPage' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx index 9f0c013d..b8884b27 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx @@ -3,8 +3,100 @@ // index.tsx // =================== +import { LuRefreshCw } from 'react-icons/lu' +import { useModelStatus, useRetrain } from '@/api/hooks' +import type { ActiveModel } from '@/api/types' +import styles from './models.module.scss' + +function ModelCard({ model }: { model: ActiveModel }): React.ReactElement { + return ( +
+
+ {model.model_type} + v{model.version} +
+ +
+
+ Training Samples + + {model.training_samples.toLocaleString()} + +
+ {model.threshold !== null && ( +
+ Threshold + + {model.threshold.toFixed(4)} + +
+ )} +
+ + {Object.keys(model.metrics).length > 0 && ( +
+ Metrics + {Object.entries(model.metrics).map(([key, val]) => ( +
+ {key} + {val.toFixed(4)} +
+ ))} +
+ )} +
+ ) +} + export function Component(): React.ReactElement { - return
Models
+ const { data: status, isLoading } = useModelStatus() + const retrain = useRetrain() + + if (isLoading || !status) { + return
Loading model status...
+ } + + const loaded = status.models_loaded + + return ( +
+
+ + {loaded + ? `Models Loaded — ${status.detection_mode} mode` + : `Models Not Loaded — ${status.detection_mode} mode`} + +
+ +
+ +
+ + {status.active_models.length === 0 ? ( +
+ No trained models found. Click Retrain to start training. +
+ ) : ( +
+ {status.active_models.map((model) => ( + + ))} +
+ )} +
+ ) } Component.displayName = 'ModelsPage' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/models.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/models.module.scss new file mode 100644 index 00000000..a2e9eb4b --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/models.module.scss @@ -0,0 +1,198 @@ +// =================== +// © AngelaMos | 2026 +// models.module.scss +// =================== + +@use '@/styles' as *; + +.page { + padding: $space-6; + display: flex; + flex-direction: column; + gap: $space-6; +} + +.loading { + @include flex-center; + height: 300px; + color: $text-muted; + font-size: $font-size-sm; +} + +.banner { + padding: $space-4 $space-5; + border-radius: $radius-lg; + font-size: $font-size-sm; + font-weight: $font-weight-medium; +} + +.bannerLoaded { + background: $severity-low-bg; + border: 1px solid $severity-low; + color: $severity-low; +} + +.bannerNotLoaded { + background: $severity-medium-bg; + border: 1px solid $severity-medium; + color: $severity-medium; +} + +.bannerText { + text-transform: capitalize; +} + +.actions { + display: flex; +} + +.retrainBtn { + display: inline-flex; + align-items: center; + gap: $space-2; + padding: $space-2-5 $space-4; + background: $accent-muted; + border: 1px solid $accent; + border-radius: $radius-md; + color: $accent; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + cursor: pointer; + @include transition-fast; + + svg { + width: 16px; + height: 16px; + } + + @include hover { + background: $accent; + color: $white; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +.empty { + @include flex-center; + height: 200px; + background: $bg-surface-100; + border: 1px solid $border-muted; + border-radius: $radius-lg; + color: $text-muted; + font-size: $font-size-sm; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: $space-4; + + @include breakpoint-down('lg') { + grid-template-columns: repeat(2, 1fr); + } + + @include breakpoint-down('sm') { + grid-template-columns: 1fr; + } +} + +.modelCard { + padding: $space-5; + background: $bg-surface-100; + border: 1px solid $border-muted; + border-radius: $radius-lg; + display: flex; + flex-direction: column; + gap: $space-4; +} + +.modelHeader { + display: flex; + align-items: center; + justify-content: space-between; +} + +.modelType { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-default; + text-transform: uppercase; +} + +.modelVersion { + font-size: $font-size-xs; + color: $text-lighter; + font-family: monospace; +} + +.modelStats { + display: flex; + gap: $space-5; +} + +.stat { + display: flex; + flex-direction: column; + gap: $space-0-5; +} + +.statLabel { + font-size: $font-size-2xs; + color: $text-muted; +} + +.statValue { + font-size: $font-size-sm; + color: $text-default; + font-variant-numeric: tabular-nums; +} + +.metrics { + display: flex; + flex-direction: column; + gap: $space-1-5; + padding-top: $space-3; + border-top: 1px solid $border-muted; +} + +.metricsTitle { + font-size: $font-size-2xs; + color: $text-muted; + text-transform: uppercase; + letter-spacing: $tracking-wider; + margin-bottom: $space-1; +} + +.metricRow { + display: flex; + justify-content: space-between; + font-size: $font-size-xs; +} + +.metricKey { + color: $text-lighter; +} + +.metricVal { + color: $text-light; + font-variant-numeric: tabular-nums; + font-family: monospace; +} diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx index 3dd9c9ac..819d40fb 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx @@ -3,8 +3,154 @@ // index.tsx // =================== +import { useState } from 'react' +import { useThreats } from '@/api/hooks' +import type { ThreatEvent } from '@/api/types' +import { SeverityBadge, ThreatDetail } from '@/components' +import { PAGINATION } from '@/config' +import styles from './threats.module.scss' + +type SeverityFilter = 'ALL' | 'HIGH' | 'MEDIUM' | 'LOW' + +function formatTime(dateStr: string): string { + return new Date(dateStr).toLocaleString() +} + export function Component(): React.ReactElement { - return
Threats
+ const [offset, setOffset] = useState(0) + const [severity, setSeverity] = useState('ALL') + const [sourceIp, setSourceIp] = useState('') + const [selectedThreat, setSelectedThreat] = useState(null) + + const params = { + limit: PAGINATION.DEFAULT_LIMIT, + offset, + ...(severity !== 'ALL' && { severity }), + ...(sourceIp && { source_ip: sourceIp }), + } + + const { data, isLoading } = useThreats(params) + + const total = data?.total ?? 0 + const items = data?.items ?? [] + const showing = Math.min(offset + PAGINATION.DEFAULT_LIMIT, total) + const hasPrev = offset > 0 + const hasNext = offset + PAGINATION.DEFAULT_LIMIT < total + + return ( +
+
+ + + { + setSourceIp(e.target.value) + setOffset(0) + }} + /> +
+ +
+ + + + + + + + + + + + + + {isLoading ? ( + + + + ) : items.length === 0 ? ( + + + + ) : ( + items.map((threat) => ( + setSelectedThreat(threat)} + > + + + + + + + + + )) + )} + +
TimeSource IPMethodPathScoreSeverityStatus
+ Loading threats... +
+ No threats found matching your filters +
+ {formatTime(threat.created_at)} + {threat.source_ip}{threat.request_method}{threat.request_path} + {threat.threat_score.toFixed(3)} + + + {threat.status_code}
+
+ +
+ + {total === 0 + ? 'No results' + : `${offset + 1}–${showing} of ${total}`} + +
+ + +
+
+ + setSelectedThreat(null)} + /> +
+ ) } Component.displayName = 'ThreatsPage' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/threats.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/threats.module.scss new file mode 100644 index 00000000..8f4a79a4 --- /dev/null +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/threats.module.scss @@ -0,0 +1,158 @@ +// =================== +// © AngelaMos | 2026 +// threats.module.scss +// =================== + +@use '@/styles' as *; + +.page { + padding: $space-6; + display: flex; + flex-direction: column; + gap: $space-4; +} + +.filters { + display: flex; + gap: $space-3; + flex-wrap: wrap; +} + +.select, +.input { + padding: $space-2 $space-3; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-md; + color: $text-default; + font-size: $font-size-sm; + outline: none; + @include transition-fast; + + &:focus { + border-color: $accent; + } +} + +.select { + min-width: 160px; +} + +.input { + flex: 1; + min-width: 200px; +} + +.tableWrapper { + overflow-x: auto; + background: $bg-surface-100; + border: 1px solid $border-muted; + border-radius: $radius-lg; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: $font-size-sm; + + th { + text-align: left; + padding: $space-3 $space-4; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: $text-lighter; + text-transform: uppercase; + letter-spacing: $tracking-wider; + border-bottom: 1px solid $border-default; + white-space: nowrap; + } + + td { + padding: $space-2-5 $space-4; + border-bottom: 1px solid $border-muted; + color: $text-light; + } +} + +.row { + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + } +} + +.timeCell { + white-space: nowrap; + font-variant-numeric: tabular-nums; + font-size: $font-size-xs; + color: $text-lighter; +} + +.monoCell { + font-family: monospace; + font-size: $font-size-xs; +} + +.pathCell { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: monospace; + font-size: $font-size-xs; +} + +.scoreCell { + font-variant-numeric: tabular-nums; +} + +.loadingCell, +.emptyCell { + text-align: center; + padding: $space-10 $space-4; + color: $text-muted; +} + +.pagination { + display: flex; + align-items: center; + justify-content: space-between; +} + +.paginationInfo { + font-size: $font-size-sm; + color: $text-lighter; +} + +.paginationButtons { + display: flex; + gap: $space-2; +} + +.pageBtn { + padding: $space-2 $space-4; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-md; + color: $text-light; + font-size: $font-size-sm; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + + &:hover { + background: $bg-surface-100; + color: $text-light; + } + } +}