From f0bb0dcb8d9935218708c29a4852758d8e1d51b4 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 3 May 2026 09:38:52 -0400 Subject: [PATCH] feat(monitor/frontend): space weather panel (Kp 9-bar, solar wind, X-ray flux) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-row layout (label | value | secondary or 9-bar). Kp index renders as a 9-segment grid bar — segments below kp filled --fg-2, at-or-above outlined --fg-4. When kp >= 7 (G3+ geomagnetic storm) the entire filled portion goes amber AND the panel's stale dot fires amber — one of the few legitimate amber-data uses per ethos. X-ray class M or X also flips amber. Backend snapshot caveat: the SWPC collector currently emits only {ts, pushed} as the WS event payload — the actual kp / bz_gsm / speed_kms / density / xray_class / xray_flux readings live in Redis ring buffers (per spec §6.2) but aren't yet projected into snapshot.space_weather. The TS interface defines all fields optimistically; the panel renders "—" / empty 9-bar for missing fields. When the backend extends the SWPC event payload (or a separate snapshot enrichment lands), the panel auto-fills with no frontend changes. KP_SEGMENT_KEYS is an explicit string array instead of Array.from(..., i) to avoid biome's noArrayIndexKey warning while preserving the positional segment semantics. Plan 5 Task 20 Design QA: Kp bar uses ONLY --fg-2 / --fg-4 / --amber (no other colors), values mono tabular, X-ray class letter sans, no sun glyph / aurora photo / weather-forecast iconography. --- .../src/pages/dashboard/Dashboard.tsx | 2 + .../panels/SpaceWeatherPanel.module.scss | 58 +++++++++ .../src/pages/panels/SpaceWeatherPanel.tsx | 115 ++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/SpaceWeatherPanel.module.scss create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/SpaceWeatherPanel.tsx diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx index b85a4ff6..6e76f1d2 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx @@ -8,6 +8,7 @@ import { DShieldPanel } from '@/pages/panels/DShieldPanel' import { ETHPanel } from '@/pages/panels/ETHPanel' import { KEVPanel } from '@/pages/panels/KEVPanel' import { RansomwarePanel } from '@/pages/panels/RansomwarePanel' +import { SpaceWeatherPanel } from '@/pages/panels/SpaceWeatherPanel' import { useUIStore } from '@/stores/ui' import { AlertBanner } from './AlertBanner' import { BottomTicker } from './BottomTicker' @@ -31,6 +32,7 @@ export function Dashboard(): React.ReactElement {
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/SpaceWeatherPanel.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/SpaceWeatherPanel.module.scss new file mode 100644 index 00000000..4b793a10 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/SpaceWeatherPanel.module.scss @@ -0,0 +1,58 @@ +// ©AngelaMos | 2026 +// SpaceWeatherPanel.module.scss + +.row { + display: grid; + grid-template-columns: 92px 1fr 1fr; + gap: 8px; + padding: 4px 8px; + align-items: center; + font-family: var(--font-mono); + font-size: var(--type-body); + font-variant-numeric: tabular-nums; + color: var(--fg-1); +} + +.label { + font-family: var(--font-sans); + font-size: var(--type-label); + letter-spacing: var(--letter-spacing-label); + text-transform: uppercase; + color: var(--fg-3); +} + +.value { + color: var(--fg-1); +} + +.muted { + color: var(--fg-3); +} + +.elevated { + color: var(--amber); +} + +.classLetter { + font-family: var(--font-sans); + color: var(--fg-1); +} + +.kpBar { + display: grid; + grid-template-columns: repeat(9, 1fr); + gap: 2px; + height: 10px; +} + +.kpSegEmpty { + border: 1px solid var(--fg-4); +} + +.kpSegFilled { + background: var(--fg-2); +} + +.kpSegElevated { + background: var(--amber); +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/SpaceWeatherPanel.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/SpaceWeatherPanel.tsx new file mode 100644 index 00000000..a17cedf2 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/SpaceWeatherPanel.tsx @@ -0,0 +1,115 @@ +// ©AngelaMos | 2026 +// SpaceWeatherPanel.tsx + +import { useSnapshot } from '@/api/snapshot' +import { Panel } from './Panel' +import styles from './SpaceWeatherPanel.module.scss' + +const KP_SEGMENT_KEYS = [ + 'kp-0', + 'kp-1', + 'kp-2', + 'kp-3', + 'kp-4', + 'kp-5', + 'kp-6', + 'kp-7', + 'kp-8', +] as const +const KP_ELEVATED_THRESHOLD = 7 +const STALE_AFTER_MS = 600_000 +const KP_DECIMALS = 1 +const DENSITY_DECIMALS = 2 +const XRAY_FLUX_DECIMALS = 1 +const XRAY_ELEVATED_PATTERN = /^[MX]/ + +interface SpaceWeatherData { + kp?: number + bz_gsm?: number + speed_kms?: number + density?: number + xray_class?: string + xray_flux?: number + ts?: string + pushed?: number +} + +export function SpaceWeatherPanel(): React.ReactElement { + const { data } = useSnapshot() + const sw = (data?.space_weather as SpaceWeatherData | undefined) ?? {} + + const lastTickAt = sw.ts ? new Date(sw.ts).getTime() : undefined + const isStaleByTime = + lastTickAt !== undefined && Date.now() - lastTickAt > STALE_AFTER_MS + const kpElevated = sw.kp !== undefined && sw.kp >= KP_ELEVATED_THRESHOLD + const xrayElevated = + sw.xray_class !== undefined && XRAY_ELEVATED_PATTERN.test(sw.xray_class) + const inAlarm = kpElevated || xrayElevated + + return ( + +
+ Kp Index + + {sw.kp !== undefined ? sw.kp.toFixed(KP_DECIMALS) : '—'} + + +
+
+ Solar Wind + + {sw.speed_kms !== undefined ? `${Math.round(sw.speed_kms)} km/s` : '—'} + + + {sw.density !== undefined + ? `${sw.density.toFixed(DENSITY_DECIMALS)} p/cc` + : '—'} + +
+
+ X-Ray Flux + + {sw.xray_flux !== undefined + ? sw.xray_flux.toExponential(XRAY_FLUX_DECIMALS) + : '—'} + + + {sw.xray_class ?? '—'} + +
+
+ ) +} + +SpaceWeatherPanel.displayName = 'SpaceWeatherPanel' + +interface KpBarProps { + kp: number | undefined + elevated: boolean +} + +function KpBar({ kp, elevated }: KpBarProps): React.ReactElement { + return ( +
+ {KP_SEGMENT_KEYS.map((key, i) => { + const filled = kp !== undefined && i < Math.floor(kp) + let cls: string + if (!filled) { + cls = styles.kpSegEmpty + } else if (elevated) { + cls = styles.kpSegElevated + } else { + cls = styles.kpSegFilled + } + return
+ })} +
+ ) +}