From daeed88a7aca51500cc86063239ae151b90f0f99 Mon Sep 17 00:00:00 2001 From: Pranitrane Date: Sun, 9 Aug 2026 20:03:31 +0530 Subject: [PATCH] feat: implement live mission control event timeline (#172) --- backend/__pycache__/debug_log.cpython-314.pyc | Bin 0 -> 2308 bytes .../api/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 158 bytes .../api/__pycache__/router.cpython-314.pyc | Bin 0 -> 1527 bytes backend/api/router.py | 3 +- .../v1/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 161 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 171 bytes .../__pycache__/auth.cpython-314.pyc | Bin 0 -> 11685 bytes backend/api/v1/endpoints/events.py | 157 ++++++++ .../app/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 158 bytes backend/app/__pycache__/main.cpython-314.pyc | Bin 0 -> 9166 bytes .../core/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 163 bytes .../core/__pycache__/config.cpython-314.pyc | Bin 0 -> 3081 bytes .../error_handlers.cpython-314.pyc | Bin 0 -> 12106 bytes .../__pycache__/exceptions.cpython-314.pyc | Bin 0 -> 9668 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 162 bytes .../__pycache__/api_schemas.cpython-314.pyc | Bin 0 -> 18738 bytes frontend/src/App.tsx | 2 + .../src/components/layouts/MainLayout.tsx | 1 + .../components/timeline/EventTimelineCard.tsx | 356 ++++++++++++++++++ .../timeline/EventTimelineFilterBar.tsx | 202 ++++++++++ .../timeline/HighPriorityIncidentBanner.tsx | 154 ++++++++ frontend/src/hooks/useEventTimeline.ts | 149 ++++++++ frontend/src/pages/Dashboard.tsx | 9 +- frontend/src/pages/EventTimeline.tsx | 140 +++++++ frontend/src/services/api.ts | 9 + frontend/src/store/eventTimelineStore.ts | 340 +++++++++++++++++ frontend/src/types/events.ts | 65 ++++ frontend/tsconfig.app.json | 22 +- 28 files changed, 1600 insertions(+), 9 deletions(-) create mode 100644 backend/__pycache__/debug_log.cpython-314.pyc create mode 100644 backend/api/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/api/__pycache__/router.cpython-314.pyc create mode 100644 backend/api/v1/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/api/v1/endpoints/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/api/v1/endpoints/__pycache__/auth.cpython-314.pyc create mode 100644 backend/api/v1/endpoints/events.py create mode 100644 backend/app/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/__pycache__/main.cpython-314.pyc create mode 100644 backend/app/core/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/core/__pycache__/config.cpython-314.pyc create mode 100644 backend/app/core/__pycache__/error_handlers.cpython-314.pyc create mode 100644 backend/app/core/__pycache__/exceptions.cpython-314.pyc create mode 100644 backend/schemas/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/schemas/__pycache__/api_schemas.cpython-314.pyc create mode 100644 frontend/src/components/timeline/EventTimelineCard.tsx create mode 100644 frontend/src/components/timeline/EventTimelineFilterBar.tsx create mode 100644 frontend/src/components/timeline/HighPriorityIncidentBanner.tsx create mode 100644 frontend/src/hooks/useEventTimeline.ts create mode 100644 frontend/src/pages/EventTimeline.tsx create mode 100644 frontend/src/store/eventTimelineStore.ts create mode 100644 frontend/src/types/events.ts diff --git a/backend/__pycache__/debug_log.cpython-314.pyc b/backend/__pycache__/debug_log.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3880fcc68e9fc81772109408c59d3f18fc9c751d GIT binary patch literal 2308 zcmbVNTW=Fb6rR1B_XcpcT6h`o##&q|KOXW5Zqmd?7) zxE*&dnvtTd{NoLz59#u-P}_18-PMV7cPGlVJ6_!byux{(E{2fiT^u}RnGVTbEIJlB zkS1Adrg95v3STT{)SQ*E)qPppE@Cw_bW96t0`(iFX*q_IwM=^&*3ziCuo#%bg&Zbf z{#lY;UB4c=fd>bL^?qPVD28UmqhBDDg1sK}k#I@S1zpr7UDjQ?`=A6Sd7g?ON~OG= zp_1(o>P}~IE@cxIC+a5HDH4;))0T;s5g6c3TO@BdHoI{*J$h;+e$K|kjz@`MW*t>G zOdKD>_MBrC;*;FW_(dZ*2Y$x6xe4$xTv(uiM568*P9z8mYXJUsACMAyfxJTBb4IS& z&)8H>@@Xl>H`hT3alaYEh710FnAUS(R|&=WCL>Yi-KG&`(wl}ZG*Wn6*jqo#s4Qx| z|3{nH&{hy*Vnbg_VftdCt{j6KYB|v%2eIajsrQ(KE=N6^%!4H>te6n>zJ4-X-)ct< zu6er@=RjZr(U#Z0)ZOQ$;W{9Cq0mFf#5}R8Ti0SO(*du)=-^nB9<+ z68Z$4ZbRrQm_vdwApl5@i6deF4N3E;>|G|FW6IpLcE-7Ci&D`k6XlFNy;5-~9raqjQ7}u$b8>PvFA)P=$Au6YHMuxiZyh99r z>z={7H>95lF~YG`0H%bV_*-s_Tpzg+xgIHpD}Jr&A6xN{eXrlXuzcb6?DFg{6O{vF zrLjl;wwuE%-9z^}h8`-dk3208+B?2dZYir>qd)BVdGOBQPvJ_}Xr+DhnrlsIyU}vJ zm!!?HACR6Ihb;8$hQB`Ga4AqL>WTkazjg$!vSx<1eC!ZxR&Xu!tK zY5s6Hm{9La)@P(r4Iu@DrtlS{L&D@%~FNb&yX?{RsB4s5L3F=EE(nZ$t;0a@O zP2OT;7ZB=p77ENP4tOsBdI}_g!CidAj1;U-JCvhIjHs7gRjg_cu+S!Y=>- literal 0 HcmV?d00001 diff --git a/backend/api/__pycache__/__init__.cpython-314.pyc b/backend/api/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd90041c28dd99bbf2fe173e96ef5986f3b0ad14 GIT binary patch literal 158 zcmdPq@YUEVrw1QChqBq zt#@Si;z(EQa7T6&_jScKIcS1HiFw{QfC>}hG;zPeB z5LbjTm9}xA#3_sGB1lSnRhS;(G?#NZ^@QmZ!1LTf-V+*Gp?U5JBTKy1;Qr@`T<G@Y`KwikUmKn4>JS zOq>@qpUse>n_>QuC5vmqgz#yj4wHAG54tx1rh;~Q2R`tRPd3-8@y`qO9=jg2*$5!_ z;(bDYMnGiIWpYj85O~Q~7n6{_Sa9KxSr9(6kTi1POqV7sjUkQvAvG#Dgovn{cXFjH zecFt*=BTbv?#uEQ>sMznNe*9oS;&@h^g25Qp#t3tf!$RVMfr|S{6I_FXlV;A{frF0 zU#OTn$lOLFTWI9tnT^DHqI#=t&(*E@IvV+k7QXF+6Wg)GRxDAyTRUEh`rzX(!Lu6| z)-P1;+HlSEd9!U^d^>slQE literal 0 HcmV?d00001 diff --git a/backend/api/router.py b/backend/api/router.py index c6f2b70..294f5fe 100644 --- a/backend/api/router.py +++ b/backend/api/router.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from api.v1.endpoints import auth, satellites, collisions, agents, dashboard, catalog, weather +from api.v1.endpoints import auth, satellites, collisions, agents, dashboard, catalog, weather, events api_router = APIRouter() @@ -10,6 +10,7 @@ api_router.include_router(dashboard.router, prefix="/dashboard", tags=["Dashboard"]) api_router.include_router(catalog.router, prefix="/catalog", tags=["Orbital Catalog"]) api_router.include_router(weather.router, prefix="/weather", tags=["Space Weather"]) +api_router.include_router(events.router, prefix="/events", tags=["Mission Events"]) @api_router.get("/health", tags=["Health"]) diff --git a/backend/api/v1/__pycache__/__init__.cpython-314.pyc b/backend/api/v1/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b90c77a4994000df5b6f5c662fdb66be3153a6e GIT binary patch literal 161 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 literal 0 HcmV?d00001 diff --git a/backend/api/v1/endpoints/events.py b/backend/api/v1/endpoints/events.py new file mode 100644 index 0000000..40eae19 --- /dev/null +++ b/backend/api/v1/endpoints/events.py @@ -0,0 +1,157 @@ +""" +FastAPI Endpoint — Mission Control Live Event Stream API +========================================================== +Returns historical and live mission events (conjunctions, launches, +maneuvers, debris fragmentation, space weather alerts, system diagnostics). +""" + +from typing import List, Optional +from datetime import datetime, timezone, timedelta +from fastapi import APIRouter, Query, Depends +from sqlalchemy.orm import Session + +from database.session import get_db +from models.db_models import OrbitalEvent, CollisionPrediction, SpaceWeather + +router = APIRouter() + +# Seed mock events generator for instant deployment / fallback +STATIC_EVENTS = [ + { + "id": "evt_be_101", + "timestamp": (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat(), + "category": "CONJUNCTION", + "severity": "CRITICAL", + "title": "CRITICAL CONJUNCTION RISK: ISS (ZARYA) vs DEBRIS 2021-055A", + "description": "Close-approach predicted within key warning sphere. Collision probability exceeds emergency response threshold.", + "satellite_name": "ISS (ZARYA)", + "norad_id": "25544", + "cospar_id": "1998-067A", + "is_high_priority": True, + "acknowledged": False, + "telemetry": { + "miss_distance_m": 142.5, + "collision_probability": 0.0342, + "relative_velocity_kms": 14.2, + "orbit_altitude_km": 418.6 + }, + "external_references": [ + {"label": "Space-Track Conjunction Data", "url": "https://www.space-track.org"}, + {"label": "CelesTrak CDM", "url": "https://celestrak.org"} + ] + }, + { + "id": "evt_be_102", + "timestamp": (datetime.now(timezone.utc) - timedelta(minutes=18)).isoformat(), + "category": "SPACE_WEATHER", + "severity": "HIGH", + "title": "X1.2-CLASS SOLAR FLARE & GEOMAGNETIC STORM WARNING", + "description": "Active region AR3664 produced an X1.2 solar flare with an associated Earth-directed Coronal Mass Ejection (CME).", + "satellite_name": "GLOBAL WEATHER MONITOR", + "norad_id": "43012", + "is_high_priority": True, + "acknowledged": False, + "telemetry": { + "kp_index": 7.3, + "solar_flux_sfu": 245.8 + }, + "external_references": [ + {"label": "NOAA Space Weather Prediction Center", "url": "https://www.swpc.noaa.gov"} + ] + }, + { + "id": "evt_be_103", + "timestamp": (datetime.now(timezone.utc) - timedelta(minutes=45)).isoformat(), + "category": "MANEUVER", + "severity": "MEDIUM", + "title": "COLLISION AVOIDANCE MANEUVER EXECUTED — SENTINEL-6A", + "description": "Thrust duration 14.2s completed successfully. Perigee raised by +420m to clear debris field path.", + "satellite_name": "SENTINEL-6A", + "norad_id": "46984", + "cospar_id": "2020-086A", + "is_high_priority": False, + "acknowledged": True, + "telemetry": { + "delta_v_ms": 0.42, + "fuel_cost_kg": 1.84, + "orbit_altitude_km": 1336.2, + "velocity_kms": 7.21 + } + }, + { + "id": "evt_be_104", + "timestamp": (datetime.now(timezone.utc) - timedelta(minutes=85)).isoformat(), + "category": "LAUNCH", + "severity": "LOW", + "title": "ORBITAL INSERTION CONFIRMED — STARLINK-G8-12 BATCH", + "description": "Falcon 9 second stage deployment nominal. 23 spacecraft inserted into 290 km initial checkout orbit.", + "satellite_name": "STARLINK-G8-12", + "norad_id": "59102", + "cospar_id": "2026-014A", + "is_high_priority": False, + "acknowledged": True, + "telemetry": { + "orbit_altitude_km": 290.4, + "inclination_deg": 53.2, + "velocity_kms": 7.73 + } + }, + { + "id": "evt_be_105", + "timestamp": (datetime.now(timezone.utc) - timedelta(minutes=130)).isoformat(), + "category": "DEBRIS", + "severity": "HIGH", + "title": "NEW DEBRIS FRAGMENTATION EVENT DETECTED", + "description": "Breakup alert in LEO orbit (720 km). 48 new trackable object vectors registered by Ground Radar Network.", + "satellite_name": "COSMOS-1408 FRAGMENT CLUSTER", + "norad_id": "49812", + "is_high_priority": True, + "acknowledged": False, + "telemetry": { + "fragment_count": 48, + "orbit_altitude_km": 720.5 + } + } +] + +@router.get("", response_model=dict) +def get_mission_events( + category: Optional[str] = Query(None, description="Category filter (LAUNCH, CONJUNCTION, MANEUVER, DEBRIS, SPACE_WEATHER, SYSTEM)"), + severity: Optional[str] = Query(None, description="Severity filter (LOW, MEDIUM, HIGH, CRITICAL)"), + search: Optional[str] = Query(None, description="Search query across titles and descriptions"), + limit: int = Query(50, ge=1, le=200), + db: Session = Depends(get_db) +): + """ + Retrieve live mission control events stream with multi-field filtering. + """ + results = list(STATIC_EVENTS) + + # Category filter + if category and category.upper() != "ALL": + results = [e for e in results if e["category"].upper() == category.upper()] + + # Severity filter + if severity and severity.upper() != "ALL": + results = [e for e in results if e["severity"].upper() == severity.upper()] + + # Search filter + if search and search.strip(): + q = search.strip().lower() + results = [ + e for e in results + if q in e["title"].lower() or q in e["description"].lower() or q in (e.get("satellite_name") or "").lower() + ] + + # Limit + results = results[:limit] + + return { + "success": True, + "message": "Mission control events retrieved successfully", + "data": results, + "metadata": { + "total": len(results), + "source": "Kepler Strategic Command Event Engine" + } + } diff --git a/backend/app/__pycache__/__init__.cpython-314.pyc b/backend/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b31f9f6a666fde979e0febd80cd4af00633261e GIT binary patch literal 158 zcmdPq4HS1S*S<*DI*J g#bJ}1pHiBWYFESxGz(-~F^KVrnURsPh#ANN01*i#EdT%j literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9bf6dd624a6b73ae98204144b869aff547a0d73f GIT binary patch literal 9166 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 literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/config.cpython-314.pyc b/backend/app/core/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..432ef3a41485b7332745681edf284f23a88306de GIT binary patch literal 3081 zcmaJ@&2JmW6`v)S6eUxnsBhV_M4Pc?+7d~9e>iqcksF&9MbfjB5~H%WD{@I~O_B8M zP^pC;h_pxpw1=ih(&o?tITUDs{sBc#?I~BWEf95!qAhwTaucfnPH%m0MX8Kycz}KL zdvD%+ypMUaH$2^41lr9<-}!b7&_9tmZH^W=P64Qs$Ak(Aa!=sGIAUAEcF)f3_e3sQ zyglLIjsbFsP;rP*$0K3RelnjsA2~0Pxz=L5gW3Zm===itpkTUJRYQ|C&h>K92--|B zsT=z&^oVeQ5^j6!QffI6(+PH(-xs&$7UVYp} zMczpryo);dIoiS9)Wy5u^UzN2rCq!Sun)dodXD#Pl39Vehe*jqyMgxbe#;^cSmd3v zo$SsBEUg~Y;)B%3hc*cxrY^9m-*latvfsHTWs(y0M-%%UVYRA<_omG|k>&i%ycSuW zE2!#1PMw*bU&t*@FHJ8jY%i+I^9!@v%d>^W$ntVwp%5v|9+X-wEI`O?Utm~GC)0S1 zm4h~hIcD17G?0wWLWS+B-0W7ATuC*Iyq@EVV&II+b@O&?WoyIGn6Z^%YFX!gs+P5_ zxMn=zmFm`-6`-wcuum)Jw;(=SxeC*OlnZ(>RDEW;6-6zVD_rH8qL>4xnGJ3erir_WlU`c3Xnfz>~HGmXIN&(zO~zb}5Kf6)13 z;inz%cYM?d!iG0<;L7~x58jM{1^5?3QJ?tRejvG)FRku0#gE6VJiH`(g;Zmi?5Zf*qzyQOfErCR&%G>{jx%5@JZfr!}?I8u2PhL1zXp>1C~4 zfngWS2T@5gJvk`TilLPao$Gs=IRMuH5?9YDDhHo#*SKaV3LA%WvOXl1N1Z1JVR-y` z_zX9YcgbPj$a}8Ck&8d_oakcyeCyp_O%_Nk~X+_RbvrCp@RLWw_G%rMBF-ew{Yu1P-k_3v@1>(`(Ap&)%|=(FvZQR#q}fTOctWw* zLzbv@rgmW_D$8%CY23UZN3&8gnaD~C7_ga$OB6r?ZhZEk*&CCR5||KOQ_lAEq%%?~ zno!U{w5aE9I(;`ODY0ZiN@bNq-0V$7<){)*r`FE2f_B!8$G1b}U|7VU#(SV+ULdVf zZe=U;p|lr>6G#39_)~zE?;Sg zGfgitJtT0oAM!6aJ?a3YeoeWB?U4x{;~1l zZbQ7@T(?A2u;yI*w~Xw`EyTO4e+%xS{<~j+mqwT$1nd%$%SZx9f=J*&AnXPbDC2}p zA;A=3AtYfW_!cuPk8B3XEE0S!7?uIHfMgK~K5Y!|zv*jx_6oIf4yp-eIm*x8Uicj- zDN0Z{i5*tQJ13#*z-5qFp?eJe-p#9&UBrrzffsiub#4tmCT4akvQ5QM*y>whDc{z5!o aI;VxP_rCijfv&kD5O@C%SKe`1(f=>A$LHGs literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/error_handlers.cpython-314.pyc b/backend/app/core/__pycache__/error_handlers.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3391163db3b211c5eff250676a01560a36baa9c4 GIT binary patch literal 12106 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 AssI20 literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/exceptions.cpython-314.pyc b/backend/app/core/__pycache__/exceptions.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a419c2bc3a8d57bef45fbdb2fa3a005d07b7a8d GIT binary patch literal 9668 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 QKIz)}-Ph=LO=Kbe3jnvFO8@`> literal 0 HcmV?d00001 diff --git a/backend/schemas/__pycache__/__init__.cpython-314.pyc b/backend/schemas/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94506268607be4efa9ad32df57c902e9fea16598 GIT binary patch literal 162 zcmdPq k*DI*J#bJ}1pHiBWYFESxG!JBBF^KVrnURsPh#ANN0LvdH)Bpeg literal 0 HcmV?d00001 diff --git a/backend/schemas/__pycache__/api_schemas.cpython-314.pyc b/backend/schemas/__pycache__/api_schemas.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c634b80bc96d2d6d5c220db9859e2d7d7b22313 GIT binary patch literal 18738 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 aQM4bXXYMYxlOD&UZzlcx|B#-=G5-g(l?ZhJ literal 0 HcmV?d00001 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 38fa1f8..e65904a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,6 +19,7 @@ import { Debris } from '@/pages/Debris'; import { CollisionCenter } from '@/pages/CollisionCenter'; import { AIAgents } from '@/pages/AIAgents'; import { MissionPlanner } from '@/pages/MissionPlanner'; +import EventTimelinePage from '@/pages/EventTimeline'; import { Settings } from '@/pages/Settings'; import { Toaster } from 'sonner'; import { toastOptions } from './constants/toast'; @@ -89,6 +90,7 @@ function App() { }> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layouts/MainLayout.tsx b/frontend/src/components/layouts/MainLayout.tsx index 4889c3d..4a51ecf 100644 --- a/frontend/src/components/layouts/MainLayout.tsx +++ b/frontend/src/components/layouts/MainLayout.tsx @@ -45,6 +45,7 @@ export const MainLayout: React.FC = () => { const navItems = [ { name: 'Dashboard', path: '/dashboard', icon: 'dashboard' }, + { name: 'Event Timeline', path: '/dashboard/timeline', icon: 'timeline' }, { name: 'Space Traffic', path: '/dashboard/space-traffic', icon: 'language' }, { name: 'Space Weather', path: '/dashboard/space-weather', icon: 'wb_sunny' }, { name: 'Satellites', path: '/dashboard/satellites', icon: 'satellite_alt' }, diff --git a/frontend/src/components/timeline/EventTimelineCard.tsx b/frontend/src/components/timeline/EventTimelineCard.tsx new file mode 100644 index 0000000..58167ed --- /dev/null +++ b/frontend/src/components/timeline/EventTimelineCard.tsx @@ -0,0 +1,356 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useNavigate } from 'react-router-dom'; +import { MaterialIcon } from '@/components/MaterialIcon'; +import type { TimelineEvent, EventSeverity, TimelineEventCategory } from '@/types/events'; +import { logEvent } from '@/store/logbookStore'; + +interface EventTimelineCardProps { + event: TimelineEvent; + onAcknowledge: (id: string) => void; +} + +const severityConfig: Record< + EventSeverity, + { border: string; bg: string; text: string; glow: string; icon: string } +> = { + CRITICAL: { + border: 'border-status-emergency/70', + bg: 'bg-status-emergency/10', + text: 'text-status-emergency', + glow: 'shadow-[0_0_15px_rgba(255,59,48,0.2)]', + icon: 'error', + }, + HIGH: { + border: 'border-status-warning/70', + bg: 'bg-status-warning/10', + text: 'text-status-warning', + glow: 'shadow-[0_0_12px_rgba(255,149,0,0.15)]', + icon: 'warning', + }, + MEDIUM: { + border: 'border-amber-400/50', + bg: 'bg-amber-500/10', + text: 'text-amber-400', + glow: 'shadow-[0_0_10px_rgba(245,158,11,0.1)]', + icon: 'info', + }, + LOW: { + border: 'border-primary-container/40', + bg: 'bg-primary-container/10', + text: 'text-primary-container', + glow: '', + icon: 'check_circle', + }, +}; + +const categoryConfig: Record< + TimelineEventCategory, + { label: string; icon: string; color: string } +> = { + LAUNCH: { label: 'LAUNCH', icon: 'rocket_launch', color: 'text-cyan-400' }, + CONJUNCTION: { label: 'CONJUNCTION', icon: 'warning', color: 'text-status-emergency' }, + MANEUVER: { label: 'MANEUVER', icon: 'orbit', color: 'text-emerald-400' }, + DEBRIS: { label: 'DEBRIS', icon: 'delete_sweep', color: 'text-purple-400' }, + SPACE_WEATHER: { label: 'SPACE WEATHER', icon: 'wb_sunny', color: 'text-amber-400' }, + SYSTEM: { label: 'SYSTEM', icon: 'settings', color: 'text-blue-400' }, +}; + +function formatUtc(timestamp: string): string { + const d = new Date(timestamp); + return d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; +} + +function formatRelativeTime(timestamp: string): string { + const diffSec = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000); + if (diffSec < 60) return `${diffSec}s AGO`; + const min = Math.floor(diffSec / 60); + if (min < 60) return `${min}m AGO`; + const hr = Math.floor(min / 60); + if (hr < 24) return `${hr}h AGO`; + const days = Math.floor(hr / 24); + return `${days}d AGO`; +} + +export const EventTimelineCard: React.FC = ({ event, onAcknowledge }) => { + const [expanded, setExpanded] = useState(false); + const navigate = useNavigate(); + + const sev = severityConfig[event.severity]; + const cat = categoryConfig[event.category]; + + const handleManualLog = (e: React.MouseEvent) => { + e.stopPropagation(); + logEvent('MISSION', event.severity, event.title, event.description, { + NORAD_ID: event.norad_id || 'N/A', + CATEGORY: event.category, + SOURCE: 'User Timeline Card', + }); + }; + + return ( + + {/* Left severity color bar */} +
+ + {/* Main Collapsed Header Content */} +
setExpanded(!expanded)} + className="p-4 md:p-5 pl-5 md:pl-6 cursor-pointer flex flex-col md:flex-row md:items-center justify-between gap-4 select-none" + > +
+ {/* Category Icon */} +
+ +
+ +
+
+ {/* Category Badge */} + + {cat.label} + + + {/* Severity Badge */} + + {event.severity} + + + {/* Satellite / NORAD Badge */} + {event.satellite_name && ( + + {event.satellite_name}{' '} + {event.norad_id ? `(#${event.norad_id})` : ''} + + )} + + {/* Timestamp */} + + {formatUtc(event.timestamp)} + + + + {formatRelativeTime(event.timestamp)} + +
+ +

+ {event.title} +

+

+ {event.description} +

+
+
+ + {/* Right Status Badges & Chevron toggle */} +
+ {event.acknowledged ? ( + + + ACKNOWLEDGED + + ) : ( + + )} + +
+ +
+
+
+ + {/* Expanded Details Panel */} + + {expanded && ( + + {/* Full description */} +
+

+ EVENT SUMMARY & ANALYTICS +

+

+ {event.description} +

+
+ + {/* Telemetry Metrics Grid */} + {event.telemetry && Object.keys(event.telemetry).length > 0 && ( +
+

+ TELEMETRY PARAMETERS +

+
+ {event.telemetry.miss_distance_m !== undefined && ( +
+

MISS DISTANCE

+

+ {event.telemetry.miss_distance_m < 1000 + ? `${event.telemetry.miss_distance_m} m` + : `${(event.telemetry.miss_distance_m / 1000).toFixed(2)} km`} +

+
+ )} + + {event.telemetry.collision_probability !== undefined && ( +
+

PROBABILITY

+

+ {(event.telemetry.collision_probability * 100).toFixed(2)}% +

+
+ )} + + {event.telemetry.orbit_altitude_km !== undefined && ( +
+

ALTITUDE

+

+ {event.telemetry.orbit_altitude_km} km +

+
+ )} + + {event.telemetry.velocity_kms !== undefined && ( +
+

VELOCITY

+

+ {event.telemetry.velocity_kms} km/s +

+
+ )} + + {event.telemetry.delta_v_ms !== undefined && ( +
+

DELTA-V BURNT

+

+ {event.telemetry.delta_v_ms} m/s +

+
+ )} + + {event.telemetry.kp_index !== undefined && ( +
+

KP INDEX

+

+ {event.telemetry.kp_index} +

+
+ )} + + {event.telemetry.fragment_count !== undefined && ( +
+

TRACKED FRAGMENTS

+

+ {event.telemetry.fragment_count} +

+
+ )} +
+
+ )} + + {/* External References links */} + {event.external_references && event.external_references.length > 0 && ( +
+

+ EXTERNAL TELEMETRY SOURCES +

+
+ {event.external_references.map((ref, i) => ( + + + {ref.label} + + ))} +
+
+ )} + + {/* Action Bar */} +
+ + + {event.category === 'CONJUNCTION' && ( + + )} + + {event.category === 'SPACE_WEATHER' && ( + + )} + + +
+
+ )} +
+ + ); +}; diff --git a/frontend/src/components/timeline/EventTimelineFilterBar.tsx b/frontend/src/components/timeline/EventTimelineFilterBar.tsx new file mode 100644 index 0000000..0dae72f --- /dev/null +++ b/frontend/src/components/timeline/EventTimelineFilterBar.tsx @@ -0,0 +1,202 @@ +import React from 'react'; +import { MaterialIcon } from '@/components/MaterialIcon'; +import type { + EventFilterParams, + TimelineEventCategory, + EventSeverity, + TimeRangeFilter, +} from '@/types/events'; + +interface EventTimelineFilterBarProps { + filters: EventFilterParams; + streamStatus: 'LIVE' | 'PAUSED' | 'DISCONNECTED'; + onFilterChange: (key: K, value: EventFilterParams[K]) => void; + onResetFilters: () => void; + onTogglePauseStream: () => void; + onSimulateIncident: () => void; + filteredCount: number; + totalCount: number; +} + +const CATEGORIES: Array<{ label: string; value: TimelineEventCategory | 'ALL'; icon: string }> = [ + { label: 'ALL EVENTS', value: 'ALL', icon: 'apps' }, + { label: 'LAUNCHES', value: 'LAUNCH', icon: 'rocket_launch' }, + { label: 'CONJUNCTIONS', value: 'CONJUNCTION', icon: 'warning' }, + { label: 'MANEUVERS', value: 'MANEUVER', icon: 'orbit' }, + { label: 'DEBRIS', value: 'DEBRIS', icon: 'delete_sweep' }, + { label: 'SPACE WEATHER', value: 'SPACE_WEATHER', icon: 'wb_sunny' }, + { label: 'SYSTEM', value: 'SYSTEM', icon: 'settings' }, +]; + +const SEVERITIES: Array<{ label: string; value: EventSeverity | 'ALL'; color: string }> = [ + { label: 'ALL SEVERITIES', value: 'ALL', color: 'text-on-surface-variant' }, + { label: 'CRITICAL', value: 'CRITICAL', color: 'text-status-emergency' }, + { label: 'HIGH', value: 'HIGH', color: 'text-status-warning' }, + { label: 'MEDIUM', value: 'MEDIUM', color: 'text-amber-400' }, + { label: 'LOW', value: 'LOW', color: 'text-primary-container' }, +]; + +const TIME_RANGES: Array<{ label: string; value: TimeRangeFilter }> = [ + { label: 'ALL TIME', value: 'ALL' }, + { label: 'LAST 1 HOUR', value: '1H' }, + { label: 'LAST 24 HOURS', value: '24H' }, + { label: 'LAST 7 DAYS', value: '7D' }, +]; + +export const EventTimelineFilterBar: React.FC = ({ + filters, + streamStatus, + onFilterChange, + onResetFilters, + onTogglePauseStream, + onSimulateIncident, + filteredCount, + totalCount, +}) => { + const isLive = streamStatus === 'LIVE'; + + return ( +
+ {/* Top Controller Row: Stream Status & Global Action Bar */} +
+ {/* Stream Status indicator */} +
+ + + + SHOWING {filteredCount} OF{' '} + {totalCount} EVENTS + +
+ + {/* Action Controls */} +
+ + + + + {/* Sort order toggle */} + +
+
+ + {/* Category Pills Row */} +
+ {CATEGORIES.map((cat) => { + const isActive = filters.category === cat.value; + return ( + + ); + })} +
+ + {/* Search Input & Secondary Filters Grid */} +
+ {/* Real-time Search Input */} +
+ + onFilterChange('searchQuery', e.target.value)} + placeholder="SEARCH TITLE, NORAD ID, SATELLITE..." + className="w-full bg-surface-container-low border border-border-panel/80 rounded px-3 py-2 pl-9 text-xs font-technical-data text-on-surface placeholder:text-primary/30 focus:outline-none focus:border-primary-container transition-ui" + /> + {filters.searchQuery && ( + + )} +
+ + {/* Severity Selector Dropdown */} +
+ +
+ + {/* Time Range Selector Dropdown */} +
+ +
+
+
+ ); +}; diff --git a/frontend/src/components/timeline/HighPriorityIncidentBanner.tsx b/frontend/src/components/timeline/HighPriorityIncidentBanner.tsx new file mode 100644 index 0000000..d69ec3d --- /dev/null +++ b/frontend/src/components/timeline/HighPriorityIncidentBanner.tsx @@ -0,0 +1,154 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useNavigate } from 'react-router-dom'; +import { MaterialIcon } from '@/components/MaterialIcon'; +import type { TimelineEvent } from '@/types/events'; + +interface HighPriorityIncidentBannerProps { + incidents: TimelineEvent[]; + onAcknowledge: (id: string) => void; +} + +export const HighPriorityIncidentBanner: React.FC = ({ + incidents, + onAcknowledge, +}) => { + const navigate = useNavigate(); + + if (incidents.length === 0) return null; + + const currentIncident = incidents[0]; // Show top urgent incident + const isCritical = currentIncident.severity === 'CRITICAL'; + + return ( + + + {/* Background CRT pulse scan line effect */} +
+ +
+
+ {/* Pulsing hazard icon badge */} +
+ +
+ +
+
+ + HIGH PRIORITY {currentIncident.severity} INCIDENT + + + {incidents.length > 1 && ( + + +{incidents.length - 1} MORE ACTIVE ALERTS + + )} + + + {new Date(currentIncident.timestamp).toUTCString().replace('GMT', 'UTC')} + +
+ +

+ {currentIncident.title} +

+

+ {currentIncident.description} +

+ + {/* Telemetry highlights */} + {currentIncident.telemetry && ( +
+ {currentIncident.telemetry.miss_distance_m !== undefined && ( + + MISS DISTANCE:{' '} + + {currentIncident.telemetry.miss_distance_m < 1000 + ? `${currentIncident.telemetry.miss_distance_m}m` + : `${(currentIncident.telemetry.miss_distance_m / 1000).toFixed(2)}km`} + + + )} + {currentIncident.telemetry.collision_probability !== undefined && ( + + PROBABILITY:{' '} + + {(currentIncident.telemetry.collision_probability * 100).toFixed(2)}% + + + )} + {currentIncident.telemetry.kp_index !== undefined && ( + + KP INDEX: {currentIncident.telemetry.kp_index} + + )} +
+ )} +
+
+ + {/* Direct Action Buttons */} +
+ {currentIncident.category === 'CONJUNCTION' && ( + + )} + + {currentIncident.category === 'CONJUNCTION' && ( + + )} + + +
+
+ + + ); +}; diff --git a/frontend/src/hooks/useEventTimeline.ts b/frontend/src/hooks/useEventTimeline.ts new file mode 100644 index 0000000..a5c02f1 --- /dev/null +++ b/frontend/src/hooks/useEventTimeline.ts @@ -0,0 +1,149 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useEventTimelineStore } from '@/store/eventTimelineStore'; + +export function useEventTimeline() { + const events = useEventTimelineStore((s) => s.events); + const filters = useEventTimelineStore((s) => s.filters); + const streamStatus = useEventTimelineStore((s) => s.streamStatus); + const streamSpeed = useEventTimelineStore((s) => s.streamSpeed); + const selectedEventId = useEventTimelineStore((s) => s.selectedEventId); + + const addEvent = useEventTimelineStore((s) => s.addEvent); + const setFilter = useEventTimelineStore((s) => s.setFilter); + const resetFilters = useEventTimelineStore((s) => s.resetFilters); + const togglePauseStream = useEventTimelineStore((s) => s.togglePauseStream); + const setStreamSpeed = useEventTimelineStore((s) => s.setStreamSpeed); + const setSelectedEventId = useEventTimelineStore((s) => s.setSelectedEventId); + const acknowledgeEvent = useEventTimelineStore((s) => s.acknowledgeEvent); + const simulateIncident = useEventTimelineStore((s) => s.simulateIncident); + const clearAllEvents = useEventTimelineStore((s) => s.clearAllEvents); + const startLiveStreaming = useEventTimelineStore((s) => s.startLiveStreaming); + + const [currentTime, setCurrentTime] = useState(() => Date.now()); + + useEffect(() => { + const timer = setInterval(() => setCurrentTime(Date.now()), 10000); + return () => clearInterval(timer); + }, []); + + // Automatically start real-time event generator when hook is mounted + useEffect(() => { + const cleanup = startLiveStreaming(); + return () => cleanup(); + }, [startLiveStreaming]); + + // Compute filtered & sorted events + const filteredEvents = useMemo(() => { + return events.filter((evt) => { + // Category filter + if (filters.category !== 'ALL' && evt.category !== filters.category) { + return false; + } + + // Severity filter + if (filters.severity !== 'ALL' && evt.severity !== filters.severity) { + return false; + } + + // High priority filter + if (filters.highPriorityOnly && !evt.is_high_priority) { + return false; + } + + // NORAD ID filter + if (filters.noradId && evt.norad_id !== filters.noradId) { + return false; + } + + // Time Range filter + if (filters.timeRange !== 'ALL') { + const evtTime = new Date(evt.timestamp).getTime(); + const diffMs = currentTime - evtTime; + if (filters.timeRange === '1H' && diffMs > 60 * 60 * 1000) return false; + if (filters.timeRange === '24H' && diffMs > 24 * 60 * 60 * 1000) return false; + if (filters.timeRange === '7D' && diffMs > 7 * 24 * 60 * 60 * 1000) return false; + } + + // Search Query filter (matches title, description, satellite name, NORAD ID, category) + if (filters.searchQuery.trim()) { + const q = filters.searchQuery.toLowerCase().trim(); + const textToSearch = [ + evt.title, + evt.description, + evt.satellite_name, + evt.norad_id, + evt.cospar_id, + evt.category, + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); + + if (!textToSearch.includes(q)) return false; + } + + return true; + }).sort((a, b) => { + const tA = new Date(a.timestamp).getTime(); + const tB = new Date(b.timestamp).getTime(); + return filters.sortOrder === 'NEWEST_FIRST' ? tB - tA : tA - tB; + }); + }, [events, filters, currentTime]); + + // Compute stats + const stats = useMemo(() => { + const total = events.length; + const unacknowledged = events.filter((e) => !e.acknowledged).length; + const critical = events.filter((e) => e.severity === 'CRITICAL' && !e.acknowledged).length; + const high = events.filter((e) => e.severity === 'HIGH' && !e.acknowledged).length; + + const byCategory: Record = { + LAUNCH: 0, + CONJUNCTION: 0, + MANEUVER: 0, + DEBRIS: 0, + SPACE_WEATHER: 0, + SYSTEM: 0, + }; + + events.forEach((evt) => { + if (byCategory[evt.category] !== undefined) { + byCategory[evt.category] += 1; + } + }); + + return { + total, + unacknowledged, + critical, + high, + byCategory, + }; + }, [events]); + + const unacknowledgedCriticalEvents = useMemo(() => { + return events.filter((evt) => (evt.severity === 'CRITICAL' || evt.severity === 'HIGH') && !evt.acknowledged); + }, [events]); + + return { + events: filteredEvents, + rawEventsCount: events.length, + filters, + streamStatus, + streamSpeed, + selectedEventId, + stats, + unacknowledgedCriticalEvents, + + // Actions + addEvent, + setFilter, + resetFilters, + togglePauseStream, + setStreamSpeed, + setSelectedEventId, + acknowledgeEvent, + simulateIncident, + clearAllEvents, + }; +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 329c92a..4b9af3e 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,4 +1,5 @@ import React, { useRef, useEffect, useMemo } from 'react'; +import { Link } from 'react-router-dom'; import { motion } from 'framer-motion'; import { EarthTwin, type EarthTwinHandle } from '@/components/EarthTwin'; import { MaterialIcon } from '@/components/MaterialIcon'; @@ -215,7 +216,13 @@ export const Dashboard: React.FC = () => { LIVE COLLISION TIMELINE -
+
+ + FULL EVENT STREAM + {collisions.isFetching && ( UPDATING\u2026 )} diff --git a/frontend/src/pages/EventTimeline.tsx b/frontend/src/pages/EventTimeline.tsx new file mode 100644 index 0000000..b8f43be --- /dev/null +++ b/frontend/src/pages/EventTimeline.tsx @@ -0,0 +1,140 @@ +import React, { useState, useEffect } from 'react'; +import { AnimatePresence } from 'framer-motion'; +import { MaterialIcon } from '@/components/MaterialIcon'; +import { useEventTimeline } from '@/hooks/useEventTimeline'; +import { EventTimelineCard } from '@/components/timeline/EventTimelineCard'; +import { EventTimelineFilterBar } from '@/components/timeline/EventTimelineFilterBar'; +import { HighPriorityIncidentBanner } from '@/components/timeline/HighPriorityIncidentBanner'; + +export const EventTimelinePage: React.FC = () => { + const { + events, + rawEventsCount, + filters, + streamStatus, + stats, + unacknowledgedCriticalEvents, + setFilter, + resetFilters, + togglePauseStream, + acknowledgeEvent, + simulateIncident, + } = useEventTimeline(); + + const [utcClock, setUtcClock] = useState(''); + + useEffect(() => { + const updateClock = () => { + setUtcClock(new Date().toUTCString().replace('GMT', 'UTC')); + }; + updateClock(); + const interval = setInterval(updateClock, 1000); + return () => clearInterval(interval); + }, []); + + return ( +
+ {/* Top Page Header */} +
+
+
+ + + REAL-TIME MISSION STREAM + +
+ +

+ + LIVE EVENT TIMELINE +

+

+ Continuous orbital activity stream, conjunction alerts, maneuver tracking & space weather monitoring. +

+
+ + {/* Live Metrics Summary Cards */} +
+
+

TOTAL EVENTS

+

+ {stats.total} +

+
+ +
+

HIGH PRIORITY

+

0 ? 'text-status-emergency animate-pulse' : 'text-status-warning' + }`} + > + {stats.critical + stats.high} +

+
+ +
+

STREAM STATUS

+

+ {streamStatus} +

+
+ +
+

UTC TIME

+

+ {utcClock.substring(17, 25) || '18:20:00 UTC'} +

+
+
+
+ + {/* High-Priority Critical Alert Banner */} + + + {/* Toolbar Filter Controls */} + + + {/* Main Timeline Events Stream List */} +
+ {events.length === 0 ? ( +
+ +

+ NO EVENTS MATCH CURRENT FILTERS +

+

+ Try adjusting your category, severity, time range, or search query to display incoming orbital events. +

+ +
+ ) : ( + + {events.map((evt) => ( + + ))} + + )} +
+
+ ); +}; + +export default EventTimelinePage; diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index c850cbc..8dc50c1 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -183,6 +183,15 @@ export const api = { triggerCollisionEvaluation: () => // eslint-disable-next-line @typescript-eslint/no-explicit-any apiFetch('/collisions/evaluate', { method: 'POST' }), + + getEvents: (params: { category?: string; severity?: string; limit?: number; search?: string } = {}) => { + const q = new URLSearchParams(); + if (params.category) q.set('category', params.category); + if (params.severity) q.set('severity', params.severity); + if (params.limit) q.set('limit', String(params.limit)); + if (params.search) q.set('search', params.search); + return apiFetch(`/events?${q}`); + }, }; diff --git a/frontend/src/store/eventTimelineStore.ts b/frontend/src/store/eventTimelineStore.ts new file mode 100644 index 0000000..76c9cf3 --- /dev/null +++ b/frontend/src/store/eventTimelineStore.ts @@ -0,0 +1,340 @@ +import { create } from 'zustand'; +import type { + TimelineEvent, + EventFilterParams, + TimelineEventCategory, + EventSeverity, +} from '@/types/events'; +import { logEvent } from './logbookStore'; + +const INITIAL_EVENTS: TimelineEvent[] = [ + { + id: 'evt_init_101', + timestamp: new Date(Date.now() - 3 * 60 * 1000).toISOString(), + category: 'CONJUNCTION', + severity: 'CRITICAL', + title: 'CRITICAL CONJUNCTION RISK: ISS (ZARYA) vs DEBRIS 2021-055A', + description: 'Close-approach predicted within key warning sphere. Collision probability exceeds emergency response threshold.', + satellite_name: 'ISS (ZARYA)', + norad_id: '25544', + cospar_id: '1998-067A', + is_high_priority: true, + acknowledged: false, + telemetry: { + miss_distance_m: 142.5, + collision_probability: 0.0342, + relative_velocity_kms: 14.2, + orbit_altitude_km: 418.6, + }, + external_references: [ + { label: 'Space-Track Conjunction Data', url: 'https://www.space-track.org' }, + { label: 'CelesTrak CDM', url: 'https://celestrak.org' }, + ], + }, + { + id: 'evt_init_102', + timestamp: new Date(Date.now() - 15 * 60 * 1000).toISOString(), + category: 'SPACE_WEATHER', + severity: 'HIGH', + title: 'X1.2-CLASS SOLAR FLARE & GEOMAGNETIC STORM WARNING', + description: 'Active region AR3664 produced an X1.2 solar flare with an associated Earth-directed Coronal Mass Ejection (CME).', + satellite_name: 'GLOBAL WEATHER MONITOR', + norad_id: '43012', + is_high_priority: true, + acknowledged: false, + telemetry: { + kp_index: 7.3, + solar_flux_sfu: 245.8, + }, + external_references: [ + { label: 'NOAA Space Weather Prediction Center', url: 'https://www.swpc.noaa.gov' }, + ], + }, + { + id: 'evt_init_103', + timestamp: new Date(Date.now() - 42 * 60 * 1000).toISOString(), + category: 'MANEUVER', + severity: 'MEDIUM', + title: 'COLLISION AVOIDANCE MANEUVER EXECUTED — SENTINEL-6A', + description: 'Thrust duration 14.2s completed successfully. Perigee raised by +420m to clear debris field path.', + satellite_name: 'SENTINEL-6A', + norad_id: '46984', + cospar_id: '2020-086A', + is_high_priority: false, + acknowledged: true, + telemetry: { + delta_v_ms: 0.42, + fuel_cost_kg: 1.84, + orbit_altitude_km: 1336.2, + velocity_kms: 7.21, + }, + external_references: [ + { label: 'ESA Copernicus Operations', url: 'https://www.esa.int' }, + ], + }, + { + id: 'evt_init_104', + timestamp: new Date(Date.now() - 90 * 60 * 1000).toISOString(), + category: 'LAUNCH', + severity: 'LOW', + title: 'ORBITAL INSERTION CONFIRMED — STARLINK-G8-12 BATCH', + description: 'Falcon 9 second stage deployment nominal. 23 spacecraft inserted into 290 km initial checkout orbit.', + satellite_name: 'STARLINK-G8-12', + norad_id: '59102', + cospar_id: '2026-014A', + is_high_priority: false, + acknowledged: true, + telemetry: { + orbit_altitude_km: 290.4, + inclination_deg: 53.2, + velocity_kms: 7.73, + }, + }, + { + id: 'evt_init_105', + timestamp: new Date(Date.now() - 140 * 60 * 1000).toISOString(), + category: 'DEBRIS', + severity: 'HIGH', + title: 'NEW DEBRIS FRAGMENTATION EVENT DETECTED', + description: 'Breakup alert in LEO orbit (720 km). 48 new trackable object vectors registered by Ground Radar Network.', + satellite_name: 'COSMOS-1408 FRAGMENT CLUSTER', + norad_id: '49812', + is_high_priority: true, + acknowledged: false, + telemetry: { + fragment_count: 48, + orbit_altitude_km: 720.5, + }, + external_references: [ + { label: 'EU SST Tracking Alert', url: 'https://www.eusst.eu' }, + ], + }, + { + id: 'evt_init_106', + timestamp: new Date(Date.now() - 210 * 60 * 1000).toISOString(), + category: 'SYSTEM', + severity: 'LOW', + title: 'GROUND RADAR ALPHA TELEMETRY SYNCHRONIZATION', + description: 'Radar calibration completed. Orbital element propagation accuracy increased by 14.8%.', + is_high_priority: false, + acknowledged: true, + }, +]; + +const GENERATOR_TEMPLATES = [ + { + category: 'CONJUNCTION' as TimelineEventCategory, + severity: 'HIGH' as EventSeverity, + title: (sat: string, deb: string) => `CONJUNCTION ALERT: ${sat} vs ${deb}`, + description: 'Automated screening identified close approach within critical clearance distance.', + getTelemetry: () => ({ + miss_distance_m: Math.floor(Math.random() * 800 + 100), + collision_probability: Number((Math.random() * 0.02 + 0.001).toFixed(4)), + relative_velocity_kms: Number((Math.random() * 5 + 10).toFixed(1)), + orbit_altitude_km: Math.floor(Math.random() * 600 + 400), + }), + }, + { + category: 'MANEUVER' as TimelineEventCategory, + severity: 'MEDIUM' as EventSeverity, + title: (sat: string) => `STATION-KEEPING BURN COMPLETED — ${sat}`, + description: 'Electric propulsion thrusters fired for 180s to counteract atmospheric drag decay.', + getTelemetry: () => ({ + delta_v_ms: Number((Math.random() * 0.2 + 0.05).toFixed(2)), + fuel_cost_kg: Number((Math.random() * 0.5 + 0.1).toFixed(2)), + orbit_altitude_km: Math.floor(Math.random() * 300 + 500), + }), + }, + { + category: 'SPACE_WEATHER' as TimelineEventCategory, + severity: 'MEDIUM' as EventSeverity, + title: () => 'GEOMAGNETIC FLUCTUATION DETECTED (Kp 5.8)', + description: 'Increased ionospheric drag anticipated for satellites operating below 500km altitude.', + getTelemetry: () => ({ + kp_index: Number((Math.random() * 2 + 5).toFixed(1)), + solar_flux_sfu: Math.floor(Math.random() * 50 + 180), + }), + }, + { + category: 'DEBRIS' as TimelineEventCategory, + severity: 'LOW' as EventSeverity, + title: (sat: string) => `DEBRIS CATALOG RE-ENTRY UPDATED — ${sat}`, + description: 'Radar tracking confirmed atmospheric decay trajectory within nominal re-entry corridor.', + getTelemetry: () => ({ + orbit_altitude_km: Math.floor(Math.random() * 80 + 120), + }), + }, + { + category: 'LAUNCH' as TimelineEventCategory, + severity: 'LOW' as EventSeverity, + title: (sat: string) => `PAYLOAD SEPARATION CONFIRMED — ${sat}`, + description: 'Telemetry indicates solar array deployment sequence initiated successfully.', + getTelemetry: () => ({ + orbit_altitude_km: Math.floor(Math.random() * 200 + 500), + inclination_deg: Number((Math.random() * 40 + 45).toFixed(1)), + }), + }, +]; + +const SATELLITE_POOL = [ + 'HST (HUBBLE)', + 'NOAA-19', + 'TERRA (EOS AM-1)', + 'AQUA (EOS PM-1)', + 'LANDSAT-9', + 'ENVISAT', + 'CRYOSAT-2', + 'TIANGONG STATION', +]; + +const DEBRIS_POOL = [ + 'FENGYUN 1C DEBRIS', + 'COSMOS 2251 DEBRIS', + 'IRIDIUM 33 DEBRIS', + 'SL-16 R/B DEBRIS', + 'TITAN 3C DEBRIS', +]; + +export interface EventTimelineState { + events: TimelineEvent[]; + filters: EventFilterParams; + streamStatus: 'LIVE' | 'PAUSED' | 'DISCONNECTED'; + streamSpeed: number; // 1x, 2x, 5x + selectedEventId: string | null; + + // Actions + addEvent: (event: Omit & { timestamp?: string }) => void; + setFilter: (key: K, value: EventFilterParams[K]) => void; + resetFilters: () => void; + togglePauseStream: () => void; + setStreamSpeed: (speed: number) => void; + setSelectedEventId: (id: string | null) => void; + acknowledgeEvent: (id: string) => void; + simulateIncident: () => void; + clearAllEvents: () => void; + startLiveStreaming: () => () => void; +} + +export const DEFAULT_FILTERS: EventFilterParams = { + category: 'ALL', + severity: 'ALL', + timeRange: 'ALL', + searchQuery: '', + sortOrder: 'NEWEST_FIRST', +}; + +let eventCounter = 1000; + +export const useEventTimelineStore = create((set, get) => ({ + events: INITIAL_EVENTS, + filters: DEFAULT_FILTERS, + streamStatus: 'LIVE', + streamSpeed: 1, + selectedEventId: null, + + addEvent: (eventInput) => { + eventCounter += 1; + const newEvent: TimelineEvent = { + ...eventInput, + id: `evt_live_${Date.now()}_${eventCounter}`, + timestamp: eventInput.timestamp || new Date().toISOString(), + acknowledged: eventInput.acknowledged ?? false, + }; + + set((state) => { + const updated = [newEvent, ...state.events]; + // Keep up to 200 events in memory + return { events: updated.slice(0, 200) }; + }); + + // Record in central operational logbook if HIGH or CRITICAL + if (newEvent.severity === 'HIGH' || newEvent.severity === 'CRITICAL') { + logEvent( + 'ALERTS', + newEvent.severity, + newEvent.title, + newEvent.description, + { + CATEGORY: newEvent.category, + NORAD_ID: newEvent.norad_id || 'N/A', + SATELLITE: newEvent.satellite_name || 'N/A', + } + ); + } + }, + + setFilter: (key, value) => + set((state) => ({ + filters: { ...state.filters, [key]: value }, + })), + + resetFilters: () => set({ filters: DEFAULT_FILTERS }), + + togglePauseStream: () => + set((state) => ({ + streamStatus: state.streamStatus === 'LIVE' ? 'PAUSED' : 'LIVE', + })), + + setStreamSpeed: (speed) => set({ streamSpeed: speed }), + + setSelectedEventId: (id) => set({ selectedEventId: id }), + + acknowledgeEvent: (id) => + set((state) => ({ + events: state.events.map((evt) => + evt.id === id ? { ...evt, acknowledged: true } : evt + ), + })), + + simulateIncident: () => { + const sat = SATELLITE_POOL[Math.floor(Math.random() * SATELLITE_POOL.length)]; + const deb = DEBRIS_POOL[Math.floor(Math.random() * DEBRIS_POOL.length)]; + get().addEvent({ + category: 'CONJUNCTION', + severity: 'CRITICAL', + title: `EMERGENCY CONJUNCTION WARNING: ${sat} vs ${deb}`, + description: 'HIGH COLLISION PROBABILITY DETECTED! TCA estimated in less than 90 minutes. Immediate evasion calculation required.', + satellite_name: sat, + norad_id: String(Math.floor(Math.random() * 30000 + 20000)), + is_high_priority: true, + acknowledged: false, + telemetry: { + miss_distance_m: Math.floor(Math.random() * 80 + 20), + collision_probability: Number((Math.random() * 0.08 + 0.02).toFixed(4)), + relative_velocity_kms: 14.8, + orbit_altitude_km: 512, + }, + external_references: [ + { label: 'Space-Track Urgent CDM', url: 'https://www.space-track.org' }, + ], + }); + }, + + clearAllEvents: () => set({ events: [] }), + + startLiveStreaming: () => { + const intervalTime = 12000; // Generate event every 12 seconds when streaming + const timer = setInterval(() => { + const { streamStatus, addEvent } = get(); + if (streamStatus !== 'LIVE') return; + + const template = GENERATOR_TEMPLATES[Math.floor(Math.random() * GENERATOR_TEMPLATES.length)]; + const sat = SATELLITE_POOL[Math.floor(Math.random() * SATELLITE_POOL.length)]; + const deb = DEBRIS_POOL[Math.floor(Math.random() * DEBRIS_POOL.length)]; + + addEvent({ + category: template.category, + severity: template.severity, + title: template.title(sat, deb), + description: template.description, + satellite_name: sat, + norad_id: String(Math.floor(Math.random() * 40000 + 10000)), + is_high_priority: template.severity === 'HIGH' || template.severity === 'CRITICAL', + acknowledged: false, + telemetry: template.getTelemetry(), + }); + }, intervalTime); + + return () => clearInterval(timer); + }, +})); diff --git a/frontend/src/types/events.ts b/frontend/src/types/events.ts new file mode 100644 index 0000000..5705e1d --- /dev/null +++ b/frontend/src/types/events.ts @@ -0,0 +1,65 @@ +/** + * Types for the Live Mission Control Event Timeline. + * + * Tracks real-time mission & orbital activities including launches, + * conjunction predictions, maneuver executions, debris tracking, + * and space weather alerts. + */ + +export type TimelineEventCategory = + | 'LAUNCH' + | 'CONJUNCTION' + | 'MANEUVER' + | 'DEBRIS' + | 'SPACE_WEATHER' + | 'SYSTEM'; + +export type EventSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; + +export interface ExternalReference { + label: string; + url: string; +} + +export interface EventTelemetry { + miss_distance_m?: number; + collision_probability?: number; + relative_velocity_kms?: number; + orbit_altitude_km?: number; + velocity_kms?: number; + delta_v_ms?: number; + fuel_cost_kg?: number; + kp_index?: number; + solar_flux_sfu?: number; + fragment_count?: number; + inclination_deg?: number; + [key: string]: string | number | boolean | undefined; +} + +export interface TimelineEvent { + id: string; + timestamp: string; // ISO UTC string + category: TimelineEventCategory; + severity: EventSeverity; + title: string; + description: string; + satellite_name?: string; + norad_id?: string; + cospar_id?: string; + telemetry?: EventTelemetry; + external_references?: ExternalReference[]; + is_high_priority?: boolean; + acknowledged?: boolean; +} + +export type TimeRangeFilter = '1H' | '24H' | '7D' | 'ALL'; + +export interface EventFilterParams { + category: TimelineEventCategory | 'ALL'; + severity: EventSeverity | 'ALL'; + timeRange: TimeRangeFilter; + searchQuery: string; + noradId?: string; + sortOrder: 'NEWEST_FIRST' | 'OLDEST_FIRST'; + highPriorityOnly?: boolean; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 3f21f57..38694ff 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -2,14 +2,21 @@ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "es2023", - "lib": ["ES2023", "DOM"], + "lib": [ + "ES2023", + "DOM" + ], "module": "esnext", - "types": ["vite/client", "node"], + "types": [ + "vite/client", + "node" + ], "skipLibCheck": true, "baseUrl": ".", - "ignoreDeprecations": "6.0", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] }, /* Bundler mode */ "moduleResolution": "bundler", @@ -18,12 +25,13 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", - /* Linting */ "noUnusedLocals": false, "noUnusedParameters": false, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file