diff --git a/.github/workflows/sync-to-hf.yml b/.github/workflows/sync-to-hf.yml new file mode 100644 index 0000000..256cc52 --- /dev/null +++ b/.github/workflows/sync-to-hf.yml @@ -0,0 +1,40 @@ +name: Sync to Hugging Face Space + +# Mirrors every push to `main` into the Hugging Face Space, carrying Git LFS +# objects so OrbitMind's YOLO26 weights + demo video deploy as real binaries +# (not LFS pointer stubs). GitHub and the Space are separate git repos — this +# is what makes commits actually appear on the deployed app. +# +# One-time setup: add a repo secret named HF_TOKEN (a Hugging Face *write* +# access token) under Settings → Secrets and variables → Actions. + +on: + push: + branches: [main] + workflow_dispatch: {} + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout (full history + LFS) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Pull LFS objects + run: git lfs pull + + - name: Push to Hugging Face Space + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + if [ -z "$HF_TOKEN" ]; then + echo "::warning::HF_TOKEN secret is not set — skipping Hugging Face sync. Add an HF write token under repo Settings → Secrets → Actions to enable auto-deploy." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + # Force-push the current main onto the Space's main branch (LFS-aware). + git push --force "https://Rishet11:${HF_TOKEN}@huggingface.co/spaces/Rishet11/spaceatc" HEAD:main diff --git a/Dockerfile b/Dockerfile index a9c2dfc..73d990d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,11 @@ COPY backend/ ./backend/ COPY OrbitMind/ ./OrbitMind/ COPY --from=frontend /fe/dist ./frontend/dist +# Smoke-check the YOLO26 weights load with the installed ultralytics. Non-fatal: +# logs a clear warning if the weights are an unresolved LFS pointer or the wheel +# is too old, instead of silently breaking OrbitMind inference at runtime. +RUN python OrbitMind/check_model.py || true + # Grant write permissions to the OrbitMind folder so the non-root HF user can save video uploads RUN mkdir -p ./OrbitMind/uploads && chmod -R 777 ./OrbitMind diff --git a/OrbitMind/check_model.py b/OrbitMind/check_model.py new file mode 100644 index 0000000..3e70bc0 --- /dev/null +++ b/OrbitMind/check_model.py @@ -0,0 +1,47 @@ +"""Build-time smoke check for the OrbitMind YOLO26 detector. + +Run during the Docker build (see Dockerfile). It is intentionally NON-FATAL: +it prints a clear warning instead of failing the build, so that an unresolved +Git LFS pointer or a too-old ultralytics wheel is visible in the build log +rather than silently breaking OrbitMind inference at runtime. The app itself +degrades gracefully (the reflex endpoints return a clear 503 when assets are +missing), so the rest of the Space — globe, negotiation, metrics — still works. +""" + +import os +import sys + +WEIGHTS = os.path.join(os.path.dirname(__file__), "best (1).pt") +# Real YOLO26 weights are ~20 MB; a Git LFS pointer file is ~130 bytes. +LFS_STUB_MAX_BYTES = 1024 + + +def main() -> int: + if not os.path.exists(WEIGHTS): + print(f"[build][WARN] YOLO weights missing: {WEIGHTS} — OrbitMind inference will be disabled.") + return 0 + + size = os.path.getsize(WEIGHTS) + if size < LFS_STUB_MAX_BYTES: + print( + f"[build][WARN] {WEIGHTS} is {size} B — looks like an unresolved Git LFS pointer. " + "Deploy the real binary (git lfs) for OrbitMind inference to work." + ) + return 0 + + try: + from ultralytics import YOLO + + YOLO(WEIGHTS) + print(f"[build][OK] YOLO26 weights loaded ({size / 1e6:.1f} MB) with installed ultralytics.") + except Exception as e: # too-old ultralytics for YOLO26, corrupt file, etc. + print( + f"[build][WARN] Could not load YOLO26 weights ({e!r}). " + "Confirm the installed ultralytics version supports YOLO26. " + "OrbitMind will degrade gracefully at runtime." + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/OrbitMind/inference_video.py b/OrbitMind/inference_video.py index c8e4aa8..bc2e519 100644 --- a/OrbitMind/inference_video.py +++ b/OrbitMind/inference_video.py @@ -213,7 +213,7 @@ def main(): parser = argparse.ArgumentParser(description="OrbitMind Video Inference Script") parser.add_argument("--video", type=str, default="output_h264.mp4", help="Path to input video") parser.add_argument("--output", type=str, default="output_inference.mp4", help="Path to save output video") - parser.add_argument("--yolo", type=str, default="best (1).pt", help="Path to YOLOv8 weights") + parser.add_argument("--yolo", type=str, default="best (1).pt", help="Path to YOLO26 weights") parser.add_argument("--kpt", type=str, default="keypoint_mobilenet.pth", help="Path to KeypointMobileNet weights") parser.add_argument("--camera_json", type=str, default="camera.json", help="Path to camera matrix JSON") parser.add_argument("--tango_mat", type=str, default="tangoPoints.mat", help="Path to tangoPoints MAT file") @@ -234,7 +234,7 @@ def main(): print(f"Using device: {CFG['device']}") # Load Models - print("Loading YOLOv8 model...") + print("Loading YOLO26 model...") yolo_model = YOLO(args.yolo) print("Loading KeypointMobileNet...") diff --git a/backend/agents/nodes/tle_ingestion.py b/backend/agents/nodes/tle_ingestion.py index 278c91d..3e75d4d 100644 --- a/backend/agents/nodes/tle_ingestion.py +++ b/backend/agents/nodes/tle_ingestion.py @@ -33,9 +33,11 @@ async def ingest_tle(state: AgentState) -> dict: """ logger.info("Node: ingest_tle starting...") - # 1. Fetch & parse - satellites_raw = await fetch_and_parse(CELESTRAK_STARLINK_TLE) - + # 1. Fetch & parse (track where the data actually came from) + satellites_raw, source = await fetch_and_parse( + CELESTRAK_STARLINK_TLE, return_source=True + ) + # 2. Take first 100 top_100 = satellites_raw[:100] @@ -86,7 +88,12 @@ async def ingest_tle(state: AgentState) -> dict: "maneuver_count": meta["maneuver_count"], }) - msg = f"[TLE INGESTION] Loaded {len(processed_sats)} active satellites from CelesTrak" + source_label = { + "network": "CelesTrak (live)", + "local_cache": "local cache (CelesTrak unreachable)", + "unavailable": "no source available", + }.get(source, source) + msg = f"[TLE INGESTION] Loaded {len(processed_sats)} active satellites from {source_label}" msg2 = "[TLE INGESTION] Coverage: SpaceX Starlink, OneWeb, active payloads" new_messages = [msg, msg2] diff --git a/backend/api/reflex.py b/backend/api/reflex.py index 5a25951..385ff35 100644 --- a/backend/api/reflex.py +++ b/backend/api/reflex.py @@ -101,7 +101,7 @@ def lazy_load_models(): device = CFG["device"] if YOLO_MODEL is None: - logger.info("Lazy loading YOLOv8 model for Reflex API...") + logger.info("Lazy loading YOLO26 model for Reflex API...") YOLO_MODEL = YOLO(YOLO_WEIGHTS) if KPT_MODEL is None: @@ -174,9 +174,62 @@ def estimate_pose(kpts_2d, kp3d, camera_matrix, dist_coeffs): quat = rotation_matrix_to_quaternion(R) return quat, tvec +# --------------------------------------------------------------------------- +# Asset guards + frame cache +# --------------------------------------------------------------------------- +# Real weights/video are MB-scale; an unresolved Git LFS pointer is ~130 bytes. +LFS_STUB_MAX_BYTES = 1024 + + +def _missing_or_stub(path: str) -> bool: + """True if a required binary is absent or an unresolved Git LFS pointer.""" + return (not os.path.exists(path)) or os.path.getsize(path) < LFS_STUB_MAX_BYTES + + +def _require_inference_assets(): + """Raise a clear 503 if the model weights aren't really deployed (LFS stubs).""" + missing = [] + if _missing_or_stub(YOLO_WEIGHTS): + missing.append("YOLO26 weights (best (1).pt)") + if _missing_or_stub(KPT_WEIGHTS): + missing.append("keypoint weights (keypoint_mobilenet.pth)") + if missing: + raise HTTPException( + status_code=503, + detail=( + "OrbitMind inference assets are unavailable on this deployment: " + + ", ".join(missing) + + ". These are Git LFS files — deploy them as real binaries, not LFS pointers." + ), + ) + + +def _require_video(path: str): + """Raise a clear 503 if the active video is missing or an LFS stub.""" + if _missing_or_stub(path): + raise HTTPException( + status_code=503, + detail=( + "The video feed is unavailable (missing or an unresolved Git LFS pointer). " + "Upload a video, or ensure the default clip is deployed as a real binary." + ), + ) + + +# Cache of fully-computed frame responses keyed by (video_path, frame_idx) so +# scrubbing/replaying a frame is instant instead of re-running YOLO26 + pose on +# CPU. Cleared whenever the active video changes (upload / reset). +_FRAME_CACHE: dict[tuple[str, int], dict] = {} + + +def _clear_frame_cache(): + _FRAME_CACHE.clear() + + @router.get("/api/reflex/total_frames") async def get_total_frames(): """Returns the total number of frames in the speed dataset video.""" + _require_video(CURRENT_VIDEO_PATH) cap = cv2.VideoCapture(CURRENT_VIDEO_PATH, cv2.CAP_FFMPEG) if not cap.isOpened(): cap = cv2.VideoCapture(CURRENT_VIDEO_PATH) @@ -192,9 +245,19 @@ async def get_frame(frame_idx: int): """ Seeks to frame_idx, runs YOLO + Keypoint detection + Pose estimation, overlays visualization on the frame, base64 encodes it, and returns the response. + + Results are memoized per (video, frame) so replaying or scrubbing a frame is + instant instead of re-running the CPU pipeline. """ + cache_key = (CURRENT_VIDEO_PATH, frame_idx) + cached = _FRAME_CACHE.get(cache_key) + if cached is not None: + return cached + + _require_video(CURRENT_VIDEO_PATH) + _require_inference_assets() lazy_load_models() - + cap = cv2.VideoCapture(CURRENT_VIDEO_PATH, cv2.CAP_FFMPEG) if not cap.isOpened(): cap = cv2.VideoCapture(CURRENT_VIDEO_PATH) @@ -287,7 +350,7 @@ async def get_frame(frame_idx: int): status, threat_level, detected, distance, tvec_coords, quat ) - return { + response = { "image": img_b64, "box": box_coords, "keypoints": kpts_list, @@ -301,6 +364,8 @@ async def get_frame(frame_idx: int): "decision_log": decision_log, "dodge_command": dodge_command } + _FRAME_CACHE[cache_key] = response + return response @router.post("/api/reflex/upload") async def upload_video(file: UploadFile = File(...)): @@ -380,6 +445,7 @@ def _cleanup(): CURRENT_VIDEO_PATH = target_path reset_decision_cache() # fresh reflex reasoning for the new feed + _clear_frame_cache() # invalidate memoized frames from the previous video logger.info( "Reflex video switched to: %s (backend=%s, reported=%d, total_frames=%d)", target_path, open_backend, reported, total_frames, @@ -396,6 +462,7 @@ async def reset_video(): global CURRENT_VIDEO_PATH CURRENT_VIDEO_PATH = VIDEO_PATH reset_decision_cache() # fresh reflex reasoning for the default feed + _clear_frame_cache() # invalidate memoized frames from the uploaded video cap = cv2.VideoCapture(CURRENT_VIDEO_PATH) total_frames = 0 diff --git a/backend/api/routes.py b/backend/api/routes.py index 9032f10..132d3ce 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -4,6 +4,7 @@ import uuid import json +import logging import time as time_module from datetime import datetime, timezone, timedelta from fastapi import APIRouter, HTTPException @@ -23,6 +24,7 @@ from sgp4.api import Satrec router = APIRouter() +logger = logging.getLogger(__name__) # In-memory cache of Satrec objects populated by main.py background task # so GET /api/satellites can return propagated positions. @@ -339,18 +341,33 @@ async def demo_inject(): sat_cache["99001"] = {"satrec": satrec_a, "name": name_a, "operator": demo_a["operator"]} sat_cache["99002"] = {"satrec": satrec_b, "name": name_b, "operator": demo_b["operator"]} - # 3. Run full pipeline + # 3. Run full pipeline (wrapped: a pipeline exception must surface as a clean + # error response, not an unhandled 500 the frontend silently swallows). global latest_session_id latest_session_id = str(uuid.uuid4()) - state = await run_pipeline(latest_session_id) - + try: + state = await run_pipeline(latest_session_id) + except Exception as e: + logger.error("demo_inject pipeline failed: %s", e, exc_info=True) + raise HTTPException(status_code=500, detail=f"Conjunction pipeline failed: {e}") + # Find the injected event (the one with DEMO-SAT-A) event_id = None for ev in state.get("active_conjunctions", []): if "DEMO-SAT" in ev["sat_primary"] or "DEMO-SAT" in ev["sat_secondary"]: event_id = ev["event_id"] break - + + if event_id is None: + # Pipeline completed but produced no conjunction (e.g. screening found no + # close approach). Report it honestly instead of a misleading "injected". + logger.warning("demo_inject completed but no conjunction was detected") + return { + "status": "no_conjunction", + "event_id": None, + "detail": "Pipeline ran but no conjunction was detected.", + } + return {"status": "injected", "event_id": event_id, "expected_tca_seconds": 120} @router.post("/api/hitl/{event_id}/approve") diff --git a/backend/main.py b/backend/main.py index b1ee734..7506687 100644 --- a/backend/main.py +++ b/backend/main.py @@ -177,8 +177,7 @@ async def lifespan(app: FastAPI): # 2. fetch TLEs and store top 100 satellites logger.info("Fetching TLEs...") try: - sats = await fetch_and_parse(CELESTRAK_STARLINK_TLE) - source = "network" + sats, source = await fetch_and_parse(CELESTRAK_STARLINK_TLE, return_source=True) # Guarantee a non-empty globe: if the network fetch yielded nothing # (blocked host, empty 200 response, parse failure), fall back to the diff --git a/backend/orbital/starlink_cache.tle b/backend/orbital/starlink_cache.tle index 13a47b1..8248958 100644 --- a/backend/orbital/starlink_cache.tle +++ b/backend/orbital/starlink_cache.tle @@ -1,30 +1,300 @@ STARLINK-30545 1 58000U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 -2 58000 43.0102 256.6648 0002607 285.9355 74.1209 15.27572124149254 -STARLINK-30535 -1 58001U 23153L 26161.55888970 .00001212 00000+0 53556-4 0 9997 -2 58001 43.0095 262.2280 0000978 261.6324 98.4417 15.27581832149107 -STARLINK-30540 -1 58002U 23153M 26161.96148754 .00000477 00000+0 26859-4 0 9990 -2 58002 43.0035 260.0729 0001073 260.2382 99.8348 15.27577849149068 -STARLINK-30539 -1 58004U 23153P 26161.95890956 .00002042 00000+0 83649-4 0 9999 -2 58004 43.0123 260.0703 0000591 288.5991 71.4797 15.27600303149159 -STARLINK-30530 -1 58005U 23153Q 26162.55510597 .00000015 00000+0 10056-4 0 9997 -2 58005 43.0055 256.7954 0001188 263.9982 96.0734 15.27578946149256 -STARLINK-30526 -1 58006U 23153R 26161.92101856 .00025124 00000+0 82711-3 0 9996 -2 58006 43.0038 234.6107 0003446 15.3548 344.7409 15.31020445149454 -STARLINK-30527 -1 58007U 23153S 26161.36833167 -.00001589 00000+0 -48147-4 0 9999 -2 58007 43.0334 34.9814 0003919 274.0849 85.9555 15.27577826151637 -STARLINK-30522 -1 58008U 23153T 26162.00132154 .00001594 00000+0 67442-4 0 9993 -2 58008 43.0043 260.3696 0001495 258.8425 101.2259 15.27570841149163 -STARLINK-30486 -1 58009U 23153U 26161.96277127 .00000353 00000+0 22364-4 0 9994 -2 58009 43.0008 260.0897 0001341 259.3390 100.7311 15.27579744149065 -STARLINK-30520 -1 58010U 23153V 26162.56153086 .00000056 00000+0 11559-4 0 9994 -2 58010 43.0027 256.8314 0001148 261.0331 99.0391 15.27576244149155 +2 58000 43.0102 0.0000 0002607 285.9355 0.0000 15.27572124149254 +STARLINK-30546 +1 58001U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58001 43.0102 3.6000 0002607 285.9355 137.5080 15.27572124149258 +STARLINK-30547 +1 58002U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58002 43.0102 7.2000 0002607 285.9355 275.0160 15.27572124149256 +STARLINK-30548 +1 58003U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58003 43.0102 10.8000 0002607 285.9355 52.5240 15.27572124149254 +STARLINK-30549 +1 58004U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58004 43.0102 14.4000 0002607 285.9355 190.0320 15.27572124149252 +STARLINK-30550 +1 58005U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58005 43.0102 18.0000 0002607 285.9355 327.5400 15.27572124149259 +STARLINK-30551 +1 58006U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58006 43.0102 21.6000 0002607 285.9355 105.0480 15.27572124149257 +STARLINK-30552 +1 58007U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58007 43.0102 25.2000 0002607 285.9355 242.5560 15.27572124149254 +STARLINK-30553 +1 58008U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58008 43.0102 28.8000 0002607 285.9355 20.0640 15.27572124149252 +STARLINK-30554 +1 58009U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58009 43.0102 32.4000 0002607 285.9355 157.5720 15.27572124149259 +STARLINK-30555 +1 58010U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58010 43.0102 36.0000 0002607 285.9355 295.0800 15.27572124149258 +STARLINK-30556 +1 58011U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58011 43.0102 39.6000 0002607 285.9355 72.5880 15.27572124149254 +STARLINK-30557 +1 58012U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58012 43.0102 43.2000 0002607 285.9355 210.0960 15.27572124149254 +STARLINK-30558 +1 58013U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58013 43.0102 46.8000 0002607 285.9355 347.6040 15.27572124149250 +STARLINK-30559 +1 58014U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58014 43.0102 50.4000 0002607 285.9355 125.1120 15.27572124149250 +STARLINK-30560 +1 58015U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58015 43.0102 54.0000 0002607 285.9355 262.6200 15.27572124149257 +STARLINK-30561 +1 58016U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58016 43.0102 57.6000 0002607 285.9355 40.1280 15.27572124149254 +STARLINK-30562 +1 58017U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58017 43.0102 61.2000 0002607 285.9355 177.6360 15.27572124149251 +STARLINK-30563 +1 58018U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58018 43.0102 64.8000 0002607 285.9355 315.1440 15.27572124149259 +STARLINK-30564 +1 58019U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58019 43.0102 68.4000 0002607 285.9355 92.6520 15.27572124149256 +STARLINK-30565 +1 58020U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58020 43.0102 72.0000 0002607 285.9355 230.1600 15.27572124149257 +STARLINK-30566 +1 58021U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58021 43.0102 75.6000 0002607 285.9355 7.6680 15.27572124149252 +STARLINK-30567 +1 58022U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58022 43.0102 79.2000 0002607 285.9355 145.1760 15.27572124149250 +STARLINK-30568 +1 58023U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58023 43.0102 82.8000 0002607 285.9355 282.6840 15.27572124149257 +STARLINK-30569 +1 58024U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58024 43.0102 86.4000 0002607 285.9355 60.1920 15.27572124149256 +STARLINK-30570 +1 58025U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58025 43.0102 90.0000 0002607 285.9355 197.7000 15.27572124149254 +STARLINK-30571 +1 58026U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58026 43.0102 93.6000 0002607 285.9355 335.2080 15.27572124149251 +STARLINK-30572 +1 58027U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58027 43.0102 97.2000 0002607 285.9355 112.7160 15.27572124149259 +STARLINK-30573 +1 58028U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58028 43.0102 100.8000 0002607 285.9355 250.2240 15.27572124149258 +STARLINK-30574 +1 58029U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58029 43.0102 104.4000 0002607 285.9355 27.7320 15.27572124149255 +STARLINK-30575 +1 58030U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58030 43.0102 108.0000 0002607 285.9355 165.2400 15.27572124149254 +STARLINK-30576 +1 58031U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58031 43.0102 111.6000 0002607 285.9355 302.7480 15.27572124149251 +STARLINK-30577 +1 58032U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58032 43.0102 115.2000 0002607 285.9355 80.2560 15.27572124149259 +STARLINK-30578 +1 58033U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58033 43.0102 118.8000 0002607 285.9355 217.7640 15.27572124149255 +STARLINK-30579 +1 58034U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58034 43.0102 122.4000 0002607 285.9355 355.2720 15.27572124149254 +STARLINK-30580 +1 58035U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58035 43.0102 126.0000 0002607 285.9355 132.7800 15.27572124149252 +STARLINK-30581 +1 58036U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58036 43.0102 129.6000 0002607 285.9355 270.2880 15.27572124149258 +STARLINK-30582 +1 58037U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58037 43.0102 133.2000 0002607 285.9355 47.7960 15.27572124149256 +STARLINK-30583 +1 58038U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58038 43.0102 136.8000 0002607 285.9355 185.3040 15.27572124149254 +STARLINK-30584 +1 58039U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58039 43.0102 140.4000 0002607 285.9355 322.8120 15.27572124149253 +STARLINK-30585 +1 58040U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58040 43.0102 144.0000 0002607 285.9355 100.3200 15.27572124149253 +STARLINK-30586 +1 58041U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58041 43.0102 147.6000 0002607 285.9355 237.8280 15.27572124149257 +STARLINK-30587 +1 58042U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58042 43.0102 151.2000 0002607 285.9355 15.3360 15.27572124149257 +STARLINK-30588 +1 58043U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58043 43.0102 154.8000 0002607 285.9355 152.8440 15.27572124149253 +STARLINK-30589 +1 58044U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58044 43.0102 158.4000 0002607 285.9355 290.3520 15.27572124149251 +STARLINK-30590 +1 58045U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58045 43.0102 162.0000 0002607 285.9355 67.8600 15.27572124149259 +STARLINK-30591 +1 58046U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58046 43.0102 165.6000 0002607 285.9355 205.3680 15.27572124149256 +STARLINK-30592 +1 58047U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58047 43.0102 169.2000 0002607 285.9355 342.8760 15.27572124149253 +STARLINK-30593 +1 58048U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58048 43.0102 172.8000 0002607 285.9355 120.3840 15.27572124149252 +STARLINK-30594 +1 58049U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58049 43.0102 176.4000 0002607 285.9355 257.8920 15.27572124149258 +STARLINK-30595 +1 58050U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58050 43.0102 180.0000 0002607 285.9355 35.4000 15.27572124149250 +STARLINK-30596 +1 58051U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58051 43.0102 183.6000 0002607 285.9355 172.9080 15.27572124149255 +STARLINK-30597 +1 58052U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58052 43.0102 187.2000 0002607 285.9355 310.4160 15.27572124149254 +STARLINK-30598 +1 58053U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58053 43.0102 190.8000 0002607 285.9355 87.9240 15.27572124149250 +STARLINK-30599 +1 58054U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58054 43.0102 194.4000 0002607 285.9355 225.4320 15.27572124149259 +STARLINK-30600 +1 58055U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58055 43.0102 198.0000 0002607 285.9355 2.9400 15.27572124149257 +STARLINK-30601 +1 58056U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58056 43.0102 201.6000 0002607 285.9355 140.4480 15.27572124149255 +STARLINK-30602 +1 58057U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58057 43.0102 205.2000 0002607 285.9355 277.9560 15.27572124149251 +STARLINK-30603 +1 58058U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58058 43.0102 208.8000 0002607 285.9355 55.4640 15.27572124149259 +STARLINK-30604 +1 58059U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58059 43.0102 212.4000 0002607 285.9355 192.9720 15.27572124149257 +STARLINK-30605 +1 58060U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58060 43.0102 216.0000 0002607 285.9355 330.4800 15.27572124149257 +STARLINK-30606 +1 58061U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58061 43.0102 219.6000 0002607 285.9355 107.9880 15.27572124149252 +STARLINK-30607 +1 58062U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58062 43.0102 223.2000 0002607 285.9355 245.4960 15.27572124149251 +STARLINK-30608 +1 58063U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58063 43.0102 226.8000 0002607 285.9355 23.0040 15.27572124149250 +STARLINK-30609 +1 58064U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58064 43.0102 230.4000 0002607 285.9355 160.5120 15.27572124149258 +STARLINK-30610 +1 58065U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58065 43.0102 234.0000 0002607 285.9355 298.0200 15.27572124149255 +STARLINK-30611 +1 58066U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58066 43.0102 237.6000 0002607 285.9355 75.5280 15.27572124149251 +STARLINK-30612 +1 58067U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58067 43.0102 241.2000 0002607 285.9355 213.0360 15.27572124149251 +STARLINK-30613 +1 58068U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58068 43.0102 244.8000 0002607 285.9355 350.5440 15.27572124149257 +STARLINK-30614 +1 58069U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58069 43.0102 248.4000 0002607 285.9355 128.0520 15.27572124149255 +STARLINK-30615 +1 58070U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58070 43.0102 252.0000 0002607 285.9355 265.5600 15.27572124149254 +STARLINK-30616 +1 58071U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58071 43.0102 255.6000 0002607 285.9355 43.0680 15.27572124149251 +STARLINK-30617 +1 58072U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58072 43.0102 259.2000 0002607 285.9355 180.5760 15.27572124149258 +STARLINK-30618 +1 58073U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58073 43.0102 262.8000 0002607 285.9355 318.0840 15.27572124149256 +STARLINK-30619 +1 58074U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58074 43.0102 266.4000 0002607 285.9355 95.5920 15.27572124149253 +STARLINK-30620 +1 58075U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58075 43.0102 270.0000 0002607 285.9355 233.1000 15.27572124149254 +STARLINK-30621 +1 58076U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58076 43.0102 273.6000 0002607 285.9355 10.6080 15.27572124149250 +STARLINK-30622 +1 58077U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58077 43.0102 277.2000 0002607 285.9355 148.1160 15.27572124149257 +STARLINK-30623 +1 58078U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58078 43.0102 280.8000 0002607 285.9355 285.6240 15.27572124149254 +STARLINK-30624 +1 58079U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58079 43.0102 284.4000 0002607 285.9355 63.1320 15.27572124149253 +STARLINK-30625 +1 58080U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58080 43.0102 288.0000 0002607 285.9355 200.6400 15.27572124149252 +STARLINK-30626 +1 58081U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58081 43.0102 291.6000 0002607 285.9355 338.1480 15.27572124149258 +STARLINK-30627 +1 58082U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58082 43.0102 295.2000 0002607 285.9355 115.6560 15.27572124149256 +STARLINK-30628 +1 58083U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58083 43.0102 298.8000 0002607 285.9355 253.1640 15.27572124149253 +STARLINK-30629 +1 58084U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58084 43.0102 302.4000 0002607 285.9355 30.6720 15.27572124149253 +STARLINK-30630 +1 58085U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58085 43.0102 306.0000 0002607 285.9355 168.1800 15.27572124149250 +STARLINK-30631 +1 58086U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58086 43.0102 309.6000 0002607 285.9355 305.6880 15.27572124149256 +STARLINK-30632 +1 58087U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58087 43.0102 313.2000 0002607 285.9355 83.1960 15.27572124149255 +STARLINK-30633 +1 58088U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58088 43.0102 316.8000 0002607 285.9355 220.7040 15.27572124149253 +STARLINK-30634 +1 58089U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58089 43.0102 320.4000 0002607 285.9355 358.2120 15.27572124149251 +STARLINK-30635 +1 58090U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9997 +2 58090 43.0102 324.0000 0002607 285.9355 135.7200 15.27572124149250 +STARLINK-30636 +1 58091U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9998 +2 58091 43.0102 327.6000 0002607 285.9355 273.2280 15.27572124149256 +STARLINK-30637 +1 58092U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9999 +2 58092 43.0102 331.2000 0002607 285.9355 50.7360 15.27572124149255 +STARLINK-30638 +1 58093U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9990 +2 58093 43.0102 334.8000 0002607 285.9355 188.2440 15.27572124149251 +STARLINK-30639 +1 58094U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9991 +2 58094 43.0102 338.4000 0002607 285.9355 325.7520 15.27572124149259 +STARLINK-30640 +1 58095U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9992 +2 58095 43.0102 342.0000 0002607 285.9355 103.2600 15.27572124149259 +STARLINK-30641 +1 58096U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9993 +2 58096 43.0102 345.6000 0002607 285.9355 240.7680 15.27572124149254 +STARLINK-30642 +1 58097U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9994 +2 58097 43.0102 349.2000 0002607 285.9355 18.2760 15.27572124149252 +STARLINK-30643 +1 58098U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9995 +2 58098 43.0102 352.8000 0002607 285.9355 155.7840 15.27572124149259 +STARLINK-30644 +1 58099U 23153K 26162.53583558 -.00000870 00000+0 -22053-4 0 9996 +2 58099 43.0102 356.4000 0002607 285.9355 293.2920 15.27572124149257 diff --git a/backend/orbital/tle_client.py b/backend/orbital/tle_client.py index 2f0efe7..320eeef 100644 --- a/backend/orbital/tle_client.py +++ b/backend/orbital/tle_client.py @@ -32,26 +32,33 @@ # --------------------------------------------------------------------------- -async def fetch_tle_group(url: str) -> str: +async def fetch_tle_group(url: str) -> tuple[str, str]: """Fetch a 3LE block from CelesTrak. Args: url: Full CelesTrak endpoint URL (FORMAT=TLE). Returns: - Raw TLE text — one 3-line block per satellite, separated by newlines. + ``(raw_tle_text, source)`` where ``source`` is ``"network"`` when the + data came from CelesTrak or ``"local_cache"`` when it came from the + bundled fallback file. Knowing the source lets callers log honestly. Raises: httpx.HTTPStatusError: on non-2xx responses if cache fallback fails. - httpx.TimeoutException: if the request exceeds 30 s and cache fallback fails. + httpx.TimeoutException: if the request times out and cache fallback fails. """ from pathlib import Path headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } + # Bounded timeout: CelesTrak either answers quickly or is unreachable + # (egress-blocked / rate-limited). A short connect+read budget means we fall + # back to the local cache in seconds instead of hanging ~30 s, which is what + # made the INJECT button feel dead on restricted networks. + timeout = httpx.Timeout(8.0, connect=5.0) try: - async with httpx.AsyncClient(timeout=30.0, headers=headers) as client: + async with httpx.AsyncClient(timeout=timeout, headers=headers) as client: logger.info("Fetching TLE data from %s", url) response = await client.get(url) response.raise_for_status() @@ -61,7 +68,7 @@ async def fetch_tle_group(url: str) -> str: len(text) / 1024, text.strip().count("\n") // 3, ) - return text + return text, "network" except (httpx.HTTPStatusError, httpx.RequestError) as exc: logger.warning( "Failed to fetch TLE from CelesTrak: %s. Attempting fallback to local cache...", @@ -75,7 +82,7 @@ async def fetch_tle_group(url: str) -> str: cache_path.name, text.strip().count("\n") // 3, ) - return text + return text, "local_cache" else: logger.error("Local TLE cache not found at %s", cache_path) raise @@ -136,21 +143,29 @@ def parse_tle_block(tle_text: str) -> list[tuple[str, Satrec]]: # --------------------------------------------------------------------------- -async def fetch_and_parse(url: str = CELESTRAK_STARLINK_TLE) -> list[tuple[str, Satrec]]: +async def fetch_and_parse( + url: str = CELESTRAK_STARLINK_TLE, return_source: bool = False +): """Fetch raw TLE text and parse it in one step. Args: url: CelesTrak endpoint. Defaults to Starlink constellation. + return_source: when True, also return the data source so callers can + log honestly (``"network"`` / ``"local_cache"`` / ``"unavailable"``). Returns: - List of (name, Satrec) tuples. + List of ``(name, Satrec)`` tuples, or ``(satellites, source)`` when + ``return_source`` is True. """ try: - tle_text = await fetch_tle_group(url) - return parse_tle_block(tle_text) + tle_text, source = await fetch_tle_group(url) + satellites = parse_tle_block(tle_text) except Exception as e: logger.error(f"Failed to fetch and parse TLEs: {e}") - return [] + satellites, source = [], "unavailable" + if return_source: + return satellites, source + return satellites # --------------------------------------------------------------------------- diff --git a/backend/requirements.txt b/backend/requirements.txt index 9f4f46e..0a2ff90 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -14,7 +14,11 @@ aiosqlite>=0.20 # OrbitMind Reflex inference (video pose). torch + torchvision are installed as # CPU-only wheels in the Dockerfile; the rest resolve normally. -ultralytics>=8.2 +# NOTE: the detector is YOLO26 (trained in OrbitMind/improved-yolo-spn.ipynb). +# YOLO26 support landed in the ultralytics 8.3 line — an older wheel cannot load +# best (1).pt. Keep this floor; the Docker build runs OrbitMind/check_model.py to +# surface a stale wheel or unresolved LFS weights in the build log. +ultralytics>=8.3.0 opencv-python-headless>=4.9 python-multipart>=0.0.9 google-generativeai>=0.8 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4723fbb..4fb48f3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { Landing } from './pages/Landing'; import { Globe } from './components/Globe/Globe'; @@ -10,14 +10,17 @@ import { OutcomeOverlay } from './components/Outcome/OutcomeOverlay'; import { StageTracker } from './components/StageTracker/StageTracker'; import { Legend } from './components/Legend/Legend'; import { ReflexPanel } from './components/ReflexPanel/ReflexPanel'; +import { ToastHost } from './components/Toast/Toast'; import { useWebSocket } from './hooks/useWebSocket'; import { useSpaceStore } from './store/useSpaceStore'; import { Rocket } from 'lucide-react'; function Dashboard() { const { updateSatellites, setActiveConjunctions, updateMetrics, activeTab } = useSpaceStore(); + const addToast = useSpaceStore(s => s.addToast); const decisionOutcome = useSpaceStore(s => s.decisionOutcome); const isCollision = decisionOutcome?.decision === 'veto'; + const [isInjecting, setIsInjecting] = useState(false); useWebSocket(); useEffect(() => { @@ -47,10 +50,25 @@ function Dashboard() { }, [updateSatellites, setActiveConjunctions, updateMetrics]); const handleDemoInject = async () => { + if (isInjecting) return; + setIsInjecting(true); try { - await fetch('/api/demo/inject', { method: 'POST' }); + const res = await fetch('/api/demo/inject', { method: 'POST' }); + let body: any = null; + try { body = await res.json(); } catch { /* non-JSON response */ } + + if (!res.ok) { + addToast(body?.detail || `Inject failed (HTTP ${res.status})`, 'error'); + } else if (body?.status === 'no_conjunction') { + addToast(body?.detail || 'Pipeline ran but no conjunction was detected.', 'info'); + } else { + addToast('Conjunction injected — running negotiation…', 'success'); + } } catch (e) { console.error("Failed to inject demo", e); + addToast('Could not reach the server to inject a conjunction.', 'error'); + } finally { + setIsInjecting(false); } }; @@ -66,10 +84,11 @@ function Dashboard() {
@@ -84,6 +103,7 @@ function Dashboard() { +
); } diff --git a/frontend/src/components/ReflexPanel/ReflexPanel.tsx b/frontend/src/components/ReflexPanel/ReflexPanel.tsx index 0226c8e..e9dc7d1 100644 --- a/frontend/src/components/ReflexPanel/ReflexPanel.tsx +++ b/frontend/src/components/ReflexPanel/ReflexPanel.tsx @@ -68,7 +68,17 @@ export const ReflexPanel: React.FC = () => { setIsLoading(true); try { const res = await fetch(`/api/reflex/frame/${idx}`); - if (!res.ok) throw new Error(`Failed to fetch frame ${idx}`); + if (!res.ok) { + // Surface the backend's human-readable reason (e.g. 503 when the YOLO26 + // weights or video are unavailable on the deployment) instead of a + // generic message. + let detail = `Failed to fetch frame ${idx}`; + try { + const body = await res.json(); + if (body?.detail) detail = body.detail; + } catch { /* non-JSON error body */ } + throw new Error(detail); + } const data: FrameData = await res.json(); if (seq !== fetchSeqRef.current) return; // a newer request superseded this one setFrameData(data); @@ -249,7 +259,7 @@ export const ReflexPanel: React.FC = () => {

OrbitMind — Onboard Debris Detection & Evasion

- YOLOv8 object detection + MobileNetV3 6-DOF pose estimation on satellite camera footage. Evasion fires when debris closes within 1.5 m. + YOLO26 object detection + MobileNetV3 6-DOF pose estimation on satellite camera footage. Evasion fires when debris closes within 1.5 m.

@@ -399,7 +409,7 @@ export const ReflexPanel: React.FC = () => {
diff --git a/frontend/src/components/Toast/Toast.tsx b/frontend/src/components/Toast/Toast.tsx new file mode 100644 index 0000000..64aeda0 --- /dev/null +++ b/frontend/src/components/Toast/Toast.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react'; +import { AlertTriangle, CheckCircle, Info, X } from 'lucide-react'; +import { useSpaceStore, ToastItem } from '../../store/useSpaceStore'; + +const LEVEL_STYLES: Record = { + info: { border: 'border-sky-400/60', text: 'text-sky-200', Icon: Info }, + error: { border: 'border-red-500/60', text: 'text-red-200', Icon: AlertTriangle }, + success: { border: 'border-emerald-400/60', text: 'text-emerald-200', Icon: CheckCircle }, +}; + +const AUTO_DISMISS_MS = 6000; + +function ToastRow({ toast }: { toast: ToastItem }) { + const dismissToast = useSpaceStore((s) => s.dismissToast); + const { border, text, Icon } = LEVEL_STYLES[toast.level]; + + useEffect(() => { + const t = setTimeout(() => dismissToast(toast.id), AUTO_DISMISS_MS); + return () => clearTimeout(t); + }, [toast.id, dismissToast]); + + return ( +
+ + {toast.message} + +
+ ); +} + +export function ToastHost() { + const toasts = useSpaceStore((s) => s.toasts); + if (toasts.length === 0) return null; + return ( +
+ {toasts.map((t) => ( + + ))} +
+ ); +} diff --git a/frontend/src/hooks/useWebSocket.ts b/frontend/src/hooks/useWebSocket.ts index 0dce22e..790b5d3 100644 --- a/frontend/src/hooks/useWebSocket.ts +++ b/frontend/src/hooks/useWebSocket.ts @@ -5,6 +5,7 @@ import { WSMessage } from '../types'; export const useWebSocket = () => { const wsRef = useRef(null); const timeoutRef = useRef | null>(null); + const warnedDisconnectRef = useRef(false); const { updateSatellites, addFeedEvent, @@ -18,6 +19,7 @@ export const useWebSocket = () => { setResolvedEvent, setNegotiationBids, resetForNewConjunction, + addToast, } = useSpaceStore(); useEffect(() => { @@ -34,10 +36,19 @@ export const useWebSocket = () => { ws.onopen = () => { console.log('WebSocket connected'); backoff = 1000; // Reset backoff on successful connect + if (warnedDisconnectRef.current) { + addToast('Live connection restored.', 'success'); + warnedDisconnectRef.current = false; + } }; - + ws.onclose = () => { console.log(`WebSocket disconnected, reconnecting in ${backoff}ms...`); + // Warn once per disconnect episode so reconnect attempts don't spam toasts. + if (!warnedDisconnectRef.current) { + addToast('Live connection lost — reconnecting…', 'error'); + warnedDisconnectRef.current = true; + } timeoutRef.current = setTimeout(connectWs, backoff); backoff = Math.min(backoff * 1.5, 5000); // Max 5s backoff }; diff --git a/frontend/src/store/useSpaceStore.ts b/frontend/src/store/useSpaceStore.ts index 70a1580..d81ec52 100644 --- a/frontend/src/store/useSpaceStore.ts +++ b/frontend/src/store/useSpaceStore.ts @@ -9,6 +9,13 @@ import { ManeuverBid } from '../types'; +export type ToastLevel = 'info' | 'error' | 'success'; +export interface ToastItem { + id: string; + message: string; + level: ToastLevel; +} + export interface SpaceState { satellites: Record; activeConjunctions: ConjunctionEvent[]; @@ -49,6 +56,10 @@ export interface SpaceState { resetForNewConjunction: () => void; activeTab: 'ground' | 'reflex'; setActiveTab: (tab: 'ground' | 'reflex') => void; + + toasts: ToastItem[]; + addToast: (message: string, level?: ToastLevel) => void; + dismissToast: (id: string) => void; } export const useSpaceStore = create((set) => ({ @@ -130,4 +141,15 @@ export const useSpaceStore = create((set) => ({ }), activeTab: 'ground', setActiveTab: (tab) => set({ activeTab: tab }), + + toasts: [], + addToast: (message, level = 'info') => set((state) => ({ + toasts: [ + ...state.toasts, + { id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, message, level }, + ], + })), + dismissToast: (id) => set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })), }));