diff --git a/.DS_Store b/.DS_Store
index 77e2b45..569b668 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.claude/launch.json b/.claude/launch.json
new file mode 100644
index 0000000..c1a117e
--- /dev/null
+++ b/.claude/launch.json
@@ -0,0 +1,12 @@
+{
+ "version": "0.0.1",
+ "configurations": [
+ {
+ "name": "frontend",
+ "runtimeExecutable": "yarn",
+ "runtimeArgs": ["--cwd", "frontend", "dev"],
+ "port": 5173,
+ "autoPort": true
+ }
+ ]
+}
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index c7a9b98..889a6cf 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -71,7 +71,12 @@
"Bash(recreate_resources_table.py )",
"Bash(reset_positions.py )",
"Bash(test_pluto.py )",
- "Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend backend/venv/bin/python:*)"
+ "Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend backend/venv/bin/python:*)",
+ "Bash(cat vite.config.ts)",
+ "Bash(ls -la .env*)",
+ "Bash(cat .env*)",
+ "Bash(node -e \"console.log\\('fiber', require\\('@react-three/fiber/package.json'\\).version\\); console.log\\('three', require\\('three/package.json'\\).version\\); console.log\\('drei', require\\('@react-three/drei/package.json'\\).version\\)\")",
+ "Bash(npx tsc *)"
],
"deny": [],
"ask": []
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..a85f662
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1 @@
+- tools
\ No newline at end of file
diff --git a/backend/app/api/celestial_body.py b/backend/app/api/celestial_body.py
index b1ac63a..e47c9a4 100644
--- a/backend/app/api/celestial_body.py
+++ b/backend/app/api/celestial_body.py
@@ -11,7 +11,8 @@ from typing import Optional, Dict, Any
from app.database import get_db
from app.models.celestial import BodyInfo
from app.services.horizons import horizons_service
-from app.services.db_service import celestial_body_service, resource_service
+from app.services.celestial_body_service import celestial_body_service
+from app.services.resource_service import resource_service
logger = logging.getLogger(__name__)
diff --git a/backend/app/api/celestial_orbit.py b/backend/app/api/celestial_orbit.py
index b989350..c1a4db8 100644
--- a/backend/app/api/celestial_orbit.py
+++ b/backend/app/api/celestial_orbit.py
@@ -9,7 +9,7 @@ from typing import Optional
from app.database import get_db
from app.services.horizons import horizons_service
-from app.services.db_service import celestial_body_service
+from app.services.celestial_body_service import celestial_body_service
from app.services.orbit_service import orbit_service
from app.services.task_service import task_service
from app.services.nasa_worker import generate_orbits_task
diff --git a/backend/app/api/celestial_position.py b/backend/app/api/celestial_position.py
index be1746d..78ca866 100644
--- a/backend/app/api/celestial_position.py
+++ b/backend/app/api/celestial_position.py
@@ -14,11 +14,9 @@ from app.services.horizons import horizons_service
from app.services.cache import cache_service
from app.services.redis_cache import redis_cache, make_cache_key, get_ttl_seconds
from app.services.system_settings_service import system_settings_service
-from app.services.db_service import (
- celestial_body_service,
- position_service,
- nasa_cache_service,
-)
+from app.services.celestial_body_service import celestial_body_service
+from app.services.nasa_cache_service import nasa_cache_service
+from app.services.position_service import position_service
logger = logging.getLogger(__name__)
diff --git a/backend/app/api/celestial_resource.py b/backend/app/api/celestial_resource.py
index 9e06012..25b8370 100644
--- a/backend/app/api/celestial_resource.py
+++ b/backend/app/api/celestial_resource.py
@@ -15,7 +15,8 @@ from typing import Optional, Dict, Any
from app.database import get_db
from app.models.db import Resource
-from app.services.db_service import celestial_body_service, resource_service
+from app.services.celestial_body_service import celestial_body_service
+from app.services.resource_service import resource_service
logger = logging.getLogger(__name__)
diff --git a/backend/app/api/celestial_static.py b/backend/app/api/celestial_static.py
index fb30eca..9979fb8 100644
--- a/backend/app/api/celestial_static.py
+++ b/backend/app/api/celestial_static.py
@@ -8,7 +8,7 @@ from pydantic import BaseModel
from typing import Optional, Dict, Any
from app.database import get_db
-from app.services.db_service import static_data_service
+from app.services.static_data_service import static_data_service
router = APIRouter(prefix="/celestial/static", tags=["celestial-static"])
diff --git a/backend/app/api/nasa_download.py b/backend/app/api/nasa_download.py
index a0636fe..5a58724 100644
--- a/backend/app/api/nasa_download.py
+++ b/backend/app/api/nasa_download.py
@@ -10,7 +10,8 @@ from pydantic import BaseModel
from app.database import get_db
from app.services.horizons import horizons_service
-from app.services.db_service import celestial_body_service, position_service
+from app.services.celestial_body_service import celestial_body_service
+from app.services.position_service import position_service
from app.services.task_service import task_service
from app.services.nasa_worker import download_positions_task
diff --git a/backend/app/api/rocket.py b/backend/app/api/rocket.py
new file mode 100644
index 0000000..064cbf8
--- /dev/null
+++ b/backend/app/api/rocket.py
@@ -0,0 +1,222 @@
+"""Rocket simulator configuration APIs."""
+from datetime import datetime
+from typing import Annotated
+
+from fastapi import APIRouter, Depends, HTTPException, Query, status
+from pydantic import BaseModel, ConfigDict, Field, StringConstraints, field_validator
+from sqlalchemy import func, or_, select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database import get_db
+from app.models.db import RocketConfig, User
+from app.services.auth_deps import require_admin
+
+
+router = APIRouter(prefix="/rockets", tags=["rockets"])
+RocketCode = Annotated[str, StringConstraints(pattern=r"^[a-z0-9][a-z0-9-]*$")]
+
+
+class RocketStage(BaseModel):
+ name: str = Field(min_length=1, max_length=100)
+ dry_mass_kg: float = Field(gt=0)
+ fuel_mass_kg: float = Field(gt=0)
+ max_thrust_n: float = Field(gt=0)
+ specific_impulse_s: float = Field(gt=0)
+ engine_count: int = Field(gt=0)
+ length_ratio: float = Field(gt=0, le=1)
+
+
+class RocketBase(BaseModel):
+ code: RocketCode
+ name: str = Field(min_length=1, max_length=100)
+ name_zh: str | None = Field(default=None, max_length=100)
+ manufacturer: str | None = Field(default=None, max_length=100)
+ country: str | None = Field(default=None, max_length=100)
+ launch_site_name: str = Field(default="Equatorial Launch Site", min_length=1, max_length=120)
+ launch_latitude_deg: float = Field(default=0, ge=-90, le=90)
+ launch_longitude_deg: float = Field(default=0, ge=-180, le=180)
+ description: str | None = None
+ color: str = Field(default="#f8fafc", pattern=r"^#[0-9a-fA-F]{6}$")
+ height_m: float = Field(gt=0)
+ diameter_m: float = Field(gt=0)
+ payload_mass_kg: float = Field(ge=0, default=0)
+ drag_coefficient: float = Field(gt=0, le=2, default=0.4)
+ reference_area_m2: float = Field(gt=0)
+ target_orbit_km: float = Field(gt=0, default=200)
+ target_velocity_mps: float = Field(gt=0, default=7800)
+ separation_delay_seconds: float = Field(ge=0, le=30, default=2)
+ second_stage_ignition_delay_seconds: float = Field(ge=0, le=30, default=1)
+ stage_1: RocketStage
+ stage_2: RocketStage
+ is_active: bool = True
+ sort_order: int = 0
+
+ @field_validator("code", mode="before")
+ @classmethod
+ def normalize_code(cls, value: str) -> str:
+ return value.strip().lower()
+
+
+class RocketCreate(RocketBase):
+ pass
+
+
+class RocketUpdate(BaseModel):
+ code: RocketCode | None = None
+ name: str | None = Field(default=None, min_length=1, max_length=100)
+ name_zh: str | None = Field(default=None, max_length=100)
+ manufacturer: str | None = Field(default=None, max_length=100)
+ country: str | None = Field(default=None, max_length=100)
+ launch_site_name: str | None = Field(default=None, min_length=1, max_length=120)
+ launch_latitude_deg: float | None = Field(default=None, ge=-90, le=90)
+ launch_longitude_deg: float | None = Field(default=None, ge=-180, le=180)
+ description: str | None = None
+ color: str | None = Field(default=None, pattern=r"^#[0-9a-fA-F]{6}$")
+ height_m: float | None = Field(default=None, gt=0)
+ diameter_m: float | None = Field(default=None, gt=0)
+ payload_mass_kg: float | None = Field(default=None, ge=0)
+ drag_coefficient: float | None = Field(default=None, gt=0, le=2)
+ reference_area_m2: float | None = Field(default=None, gt=0)
+ target_orbit_km: float | None = Field(default=None, gt=0)
+ target_velocity_mps: float | None = Field(default=None, gt=0)
+ separation_delay_seconds: float | None = Field(default=None, ge=0, le=30)
+ second_stage_ignition_delay_seconds: float | None = Field(default=None, ge=0, le=30)
+ stage_1: RocketStage | None = None
+ stage_2: RocketStage | None = None
+ is_active: bool | None = None
+ sort_order: int | None = None
+
+ @field_validator("code", mode="before")
+ @classmethod
+ def normalize_code(cls, value: str | None) -> str | None:
+ return value.strip().lower() if value else value
+
+
+class RocketResponse(RocketBase):
+ id: int
+ created_at: datetime | None = None
+ updated_at: datetime | None = None
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+def rocket_values(data: RocketCreate | RocketUpdate, *, exclude_unset: bool = False) -> dict:
+ values = data.model_dump(exclude_unset=exclude_unset)
+ for key in ("stage_1", "stage_2"):
+ if key in values and values[key] is not None:
+ values[key] = dict(values[key])
+ return values
+
+
+async def ensure_unique_code(db: AsyncSession, code: str, exclude_id: int | None = None) -> None:
+ query = select(RocketConfig.id).where(RocketConfig.code == code)
+ if exclude_id is not None:
+ query = query.where(RocketConfig.id != exclude_id)
+ if (await db.execute(query)).scalar_one_or_none() is not None:
+ raise HTTPException(status_code=400, detail="Rocket code already exists")
+
+
+@router.get("", response_model=list[RocketResponse])
+async def list_active_rockets(db: AsyncSession = Depends(get_db)):
+ result = await db.execute(
+ select(RocketConfig)
+ .where(RocketConfig.is_active.is_(True))
+ .order_by(RocketConfig.sort_order, RocketConfig.id)
+ )
+ return result.scalars().all()
+
+
+@router.get("/admin")
+async def list_rockets_for_admin(
+ skip: int = Query(0, ge=0),
+ limit: int = Query(20, ge=1, le=100),
+ search: str | None = None,
+ db: AsyncSession = Depends(get_db),
+ _: User = Depends(require_admin),
+):
+ filters = []
+ if search:
+ keyword = f"%{search.strip()}%"
+ filters.append(or_(
+ RocketConfig.code.ilike(keyword),
+ RocketConfig.name.ilike(keyword),
+ RocketConfig.name_zh.ilike(keyword),
+ ))
+
+ total = await db.scalar(select(func.count(RocketConfig.id)).where(*filters))
+ result = await db.execute(
+ select(RocketConfig)
+ .where(*filters)
+ .order_by(RocketConfig.sort_order, RocketConfig.id)
+ .offset(skip)
+ .limit(limit)
+ )
+ return {
+ "rockets": [RocketResponse.model_validate(item) for item in result.scalars().all()],
+ "total": total or 0,
+ }
+
+
+@router.get("/{code}", response_model=RocketResponse)
+async def get_active_rocket(code: str, db: AsyncSession = Depends(get_db)):
+ result = await db.execute(
+ select(RocketConfig).where(
+ RocketConfig.code == code.lower(),
+ RocketConfig.is_active.is_(True),
+ )
+ )
+ rocket = result.scalar_one_or_none()
+ if not rocket:
+ raise HTTPException(status_code=404, detail="Rocket not found")
+ return rocket
+
+
+@router.post("/admin", response_model=RocketResponse, status_code=status.HTTP_201_CREATED)
+async def create_rocket(
+ data: RocketCreate,
+ db: AsyncSession = Depends(get_db),
+ _: User = Depends(require_admin),
+):
+ await ensure_unique_code(db, data.code)
+ rocket = RocketConfig(**rocket_values(data))
+ db.add(rocket)
+ await db.commit()
+ await db.refresh(rocket)
+ return rocket
+
+
+@router.put("/admin/{rocket_id}", response_model=RocketResponse)
+async def update_rocket(
+ rocket_id: int,
+ data: RocketUpdate,
+ db: AsyncSession = Depends(get_db),
+ _: User = Depends(require_admin),
+):
+ rocket = await db.get(RocketConfig, rocket_id)
+ if not rocket:
+ raise HTTPException(status_code=404, detail="Rocket not found")
+
+ values = rocket_values(data, exclude_unset=True)
+ if not values:
+ raise HTTPException(status_code=400, detail="No fields to update")
+ if "code" in values:
+ await ensure_unique_code(db, values["code"], rocket_id)
+
+ for key, value in values.items():
+ setattr(rocket, key, value)
+ await db.commit()
+ await db.refresh(rocket)
+ return rocket
+
+
+@router.delete("/admin/{rocket_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_rocket(
+ rocket_id: int,
+ db: AsyncSession = Depends(get_db),
+ _: User = Depends(require_admin),
+):
+ rocket = await db.get(RocketConfig, rocket_id)
+ if not rocket:
+ raise HTTPException(status_code=404, detail="Rocket not found")
+ await db.delete(rocket)
+ await db.commit()
diff --git a/backend/app/main.py b/backend/app/main.py
index 1d80019..5525429 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -34,6 +34,7 @@ from app.api.star_system import router as star_system_router
from app.api.scheduled_job import router as scheduled_job_router
from app.api.social import router as social_router # Import social_router
from app.api.event import router as event_router # Import event_router
+from app.api.rocket import router as rocket_router
from app.services.redis_cache import redis_cache
from app.services.cache_preheat import preheat_all_caches
from app.services.scheduler_service import scheduler_service
@@ -187,6 +188,7 @@ app.include_router(task_router, prefix=settings.api_prefix)
app.include_router(scheduled_job_router, prefix=settings.api_prefix)
app.include_router(social_router, prefix=settings.api_prefix)
app.include_router(event_router, prefix=settings.api_prefix) # Added event_router
+app.include_router(rocket_router, prefix=settings.api_prefix)
# Mount static files for uploaded resources
upload_dir = Path(__file__).parent.parent / "upload"
@@ -235,4 +237,4 @@ if __name__ == "__main__":
port=8000,
reload=True,
log_level="info",
- )
\ No newline at end of file
+ )
diff --git a/backend/app/models/db/__init__.py b/backend/app/models/db/__init__.py
index b8c34d6..4a00d2c 100644
--- a/backend/app/models/db/__init__.py
+++ b/backend/app/models/db/__init__.py
@@ -15,6 +15,7 @@ from .system_settings import SystemSettings
from .task import Task
from .user_follow import UserFollow
from .celestial_event import CelestialEvent
+from .rocket import RocketConfig
__all__ = [
"CelestialBody",
@@ -33,4 +34,5 @@ __all__ = [
"Task",
"UserFollow",
"CelestialEvent",
+ "RocketConfig",
]
diff --git a/backend/app/models/db/rocket.py b/backend/app/models/db/rocket.py
new file mode 100644
index 0000000..c24d9f2
--- /dev/null
+++ b/backend/app/models/db/rocket.py
@@ -0,0 +1,38 @@
+"""Rocket configuration ORM model."""
+from sqlalchemy import Boolean, Column, Double, Integer, String, Text, TIMESTAMP, func
+from sqlalchemy.dialects.postgresql import JSONB
+
+from app.database import Base
+
+
+class RocketConfig(Base):
+ """Two-stage launch vehicle configuration used by the simulator."""
+
+ __tablename__ = "rocket_configs"
+
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ code = Column(String(50), unique=True, nullable=False, index=True)
+ name = Column(String(100), nullable=False)
+ name_zh = Column(String(100), nullable=True)
+ manufacturer = Column(String(100), nullable=True)
+ country = Column(String(100), nullable=True)
+ launch_site_name = Column(String(120), nullable=False, default="Equatorial Launch Site")
+ launch_latitude_deg = Column(Double, nullable=False, default=0)
+ launch_longitude_deg = Column(Double, nullable=False, default=0)
+ description = Column(Text, nullable=True)
+ color = Column(String(20), nullable=False, default="#f8fafc")
+ height_m = Column(Double, nullable=False)
+ diameter_m = Column(Double, nullable=False)
+ payload_mass_kg = Column(Double, nullable=False, default=0)
+ drag_coefficient = Column(Double, nullable=False, default=0.4)
+ reference_area_m2 = Column(Double, nullable=False)
+ target_orbit_km = Column(Double, nullable=False, default=200)
+ target_velocity_mps = Column(Double, nullable=False, default=7800)
+ separation_delay_seconds = Column(Double, nullable=False, default=2)
+ second_stage_ignition_delay_seconds = Column(Double, nullable=False, default=1)
+ stage_1 = Column(JSONB, nullable=False)
+ stage_2 = Column(JSONB, nullable=False)
+ is_active = Column(Boolean, nullable=False, default=True, index=True)
+ sort_order = Column(Integer, nullable=False, default=0)
+ created_at = Column(TIMESTAMP, server_default=func.now())
+ updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
diff --git a/backend/app/services/cache_preheat.py b/backend/app/services/cache_preheat.py
index 5709e87..f417804 100644
--- a/backend/app/services/cache_preheat.py
+++ b/backend/app/services/cache_preheat.py
@@ -8,7 +8,8 @@ from typing import List, Dict, Any
from app.database import get_db
from app.services.redis_cache import redis_cache, make_cache_key, get_ttl_seconds
-from app.services.db_service import celestial_body_service, position_service
+from app.services.celestial_body_service import celestial_body_service
+from app.services.position_service import position_service
logger = logging.getLogger(__name__)
diff --git a/backend/app/services/celestial_body_service.py b/backend/app/services/celestial_body_service.py
new file mode 100644
index 0000000..59464ff
--- /dev/null
+++ b/backend/app/services/celestial_body_service.py
@@ -0,0 +1,118 @@
+"""Persistence operations for celestial bodies."""
+
+from typing import Any, Dict, List, Optional
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database import AsyncSessionLocal
+from app.models.db import CelestialBody
+
+
+class CelestialBodyService:
+ """Create, read, update, and delete celestial bodies."""
+
+ @staticmethod
+ async def get_all_bodies(
+ session: Optional[AsyncSession] = None,
+ body_type: Optional[str] = None,
+ system_id: Optional[int] = None,
+ ) -> List[CelestialBody]:
+ async def query(active_session: AsyncSession) -> List[CelestialBody]:
+ statement = select(CelestialBody)
+ if body_type:
+ statement = statement.where(CelestialBody.type == body_type)
+ if system_id is not None:
+ statement = statement.where(CelestialBody.system_id == system_id)
+ result = await active_session.execute(statement.order_by(CelestialBody.name))
+ return list(result.scalars().all())
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+ @staticmethod
+ async def get_body_by_id(
+ body_id: str,
+ session: Optional[AsyncSession] = None,
+ ) -> Optional[CelestialBody]:
+ async def query(active_session: AsyncSession) -> Optional[CelestialBody]:
+ result = await active_session.execute(
+ select(CelestialBody).where(CelestialBody.id == body_id)
+ )
+ return result.scalar_one_or_none()
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+ @staticmethod
+ async def create_body(
+ body_data: Dict[str, Any],
+ session: Optional[AsyncSession] = None,
+ ) -> CelestialBody:
+ async def create(active_session: AsyncSession) -> CelestialBody:
+ body = CelestialBody(**body_data)
+ active_session.add(body)
+ await active_session.commit()
+ await active_session.refresh(body)
+ return body
+
+ if session is not None:
+ return await create(session)
+ async with AsyncSessionLocal() as active_session:
+ return await create(active_session)
+
+ @staticmethod
+ async def update_body(
+ body_id: str,
+ update_data: Dict[str, Any],
+ session: Optional[AsyncSession] = None,
+ ) -> Optional[CelestialBody]:
+ async def update(active_session: AsyncSession) -> Optional[CelestialBody]:
+ result = await active_session.execute(
+ select(CelestialBody).where(CelestialBody.id == body_id)
+ )
+ body = result.scalar_one_or_none()
+ if body is None:
+ return None
+
+ for key, value in update_data.items():
+ if hasattr(body, key):
+ setattr(body, key, value)
+
+ await active_session.commit()
+ await active_session.refresh(body)
+ return body
+
+ if session is not None:
+ return await update(session)
+ async with AsyncSessionLocal() as active_session:
+ return await update(active_session)
+
+ @staticmethod
+ async def delete_body(
+ body_id: str,
+ session: Optional[AsyncSession] = None,
+ ) -> bool:
+ async def delete(active_session: AsyncSession) -> bool:
+ result = await active_session.execute(
+ select(CelestialBody).where(CelestialBody.id == body_id)
+ )
+ body = result.scalar_one_or_none()
+ if body is None:
+ return False
+
+ await active_session.delete(body)
+ await active_session.commit()
+ return True
+
+ if session is not None:
+ return await delete(session)
+ async with AsyncSessionLocal() as active_session:
+ return await delete(active_session)
+
+
+celestial_body_service = CelestialBodyService()
diff --git a/backend/app/services/db_service.py b/backend/app/services/db_service.py
index 92bcc5d..1279dde 100644
--- a/backend/app/services/db_service.py
+++ b/backend/app/services/db_service.py
@@ -1,689 +1,27 @@
+"""Backward-compatible exports for domain-specific persistence services.
+
+New code should import from the individual service modules. This facade keeps
+existing API routes, jobs, and maintenance scripts working during migration.
"""
-Database service layer for celestial data operations
-"""
-from typing import List, Optional, Dict, Any
-from datetime import datetime
-from sqlalchemy import select, and_, delete
-from sqlalchemy.ext.asyncio import AsyncSession
-import logging
-from app.models.db import CelestialBody, Position, StaticData, NasaCache, Resource
-from app.database import AsyncSessionLocal
-
-logger = logging.getLogger(__name__)
-
-
-class CelestialBodyService:
- """Service for celestial body operations"""
-
- @staticmethod
- async def get_all_bodies(
- session: Optional[AsyncSession] = None,
- body_type: Optional[str] = None,
- system_id: Optional[int] = None
- ) -> List[CelestialBody]:
- """
- Get all celestial bodies, optionally filtered by type and star system
-
- Args:
- session: Database session
- body_type: Filter by body type (star, planet, dwarf_planet, etc.)
- system_id: Filter by star system ID (1=Solar System, 2+=Exoplanets)
- """
- async def _query(s: AsyncSession):
- query = select(CelestialBody)
- if body_type:
- query = query.where(CelestialBody.type == body_type)
- if system_id is not None:
- query = query.where(CelestialBody.system_id == system_id)
- result = await s.execute(query.order_by(CelestialBody.name))
- return result.scalars().all()
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
- @staticmethod
- async def get_body_by_id(
- body_id: str,
- session: Optional[AsyncSession] = None
- ) -> Optional[CelestialBody]:
- """Get a celestial body by ID"""
- async def _query(s: AsyncSession):
- result = await s.execute(
- select(CelestialBody).where(CelestialBody.id == body_id)
- )
- return result.scalar_one_or_none()
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
- @staticmethod
- async def create_body(
- body_data: Dict[str, Any],
- session: Optional[AsyncSession] = None
- ) -> CelestialBody:
- """Create a new celestial body"""
- async def _create(s: AsyncSession):
- body = CelestialBody(**body_data)
- s.add(body)
- await s.commit()
- await s.refresh(body)
- return body
-
- if session:
- return await _create(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _create(s)
-
- @staticmethod
- async def update_body(
- body_id: str,
- update_data: Dict[str, Any],
- session: Optional[AsyncSession] = None
- ) -> Optional[CelestialBody]:
- """Update a celestial body"""
- async def _update(s: AsyncSession):
- # Query the body
- result = await s.execute(
- select(CelestialBody).where(CelestialBody.id == body_id)
- )
- body = result.scalar_one_or_none()
-
- if not body:
- return None
-
- # Update fields
- for key, value in update_data.items():
- if hasattr(body, key):
- setattr(body, key, value)
-
- await s.commit()
- await s.refresh(body)
- return body
-
- if session:
- return await _update(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _update(s)
-
- @staticmethod
- async def delete_body(
- body_id: str,
- session: Optional[AsyncSession] = None
- ) -> bool:
- """Delete a celestial body"""
- async def _delete(s: AsyncSession):
- result = await s.execute(
- select(CelestialBody).where(CelestialBody.id == body_id)
- )
- body = result.scalar_one_or_none()
-
- if not body:
- return False
-
- await s.delete(body)
- await s.commit()
- return True
-
- if session:
- return await _delete(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _delete(s)
-
-
-class PositionService:
- """Service for position data operations"""
-
- @staticmethod
- async def save_positions(
- body_id: str,
- positions: List[Dict[str, Any]],
- source: str = "nasa_horizons",
- session: Optional[AsyncSession] = None
- ) -> int:
- """Save multiple position records for a celestial body (upsert: insert or update if exists)"""
- async def _save(s: AsyncSession):
- from sqlalchemy.dialects.postgresql import insert
-
- count = 0
- for pos_data in positions:
- # Use PostgreSQL's INSERT ... ON CONFLICT to handle duplicates
- stmt = insert(Position).values(
- body_id=body_id,
- time=pos_data["time"],
- x=pos_data["x"],
- y=pos_data["y"],
- z=pos_data["z"],
- vx=pos_data.get("vx"),
- vy=pos_data.get("vy"),
- vz=pos_data.get("vz"),
- source=source
- )
-
- # On conflict (body_id, time), update the existing record
- stmt = stmt.on_conflict_do_update(
- index_elements=['body_id', 'time'],
- set_={
- 'x': pos_data["x"],
- 'y': pos_data["y"],
- 'z': pos_data["z"],
- 'vx': pos_data.get("vx"),
- 'vy': pos_data.get("vy"),
- 'vz': pos_data.get("vz"),
- 'source': source
- }
- )
-
- await s.execute(stmt)
- count += 1
-
- await s.commit()
- return count
-
- if session:
- return await _save(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _save(s)
-
- @staticmethod
- async def get_positions(
- body_id: str,
- start_time: Optional[datetime] = None,
- end_time: Optional[datetime] = None,
- session: Optional[AsyncSession] = None
- ) -> List[Position]:
- """Get positions for a celestial body within a time range"""
- async def _query(s: AsyncSession):
- query = select(Position).where(Position.body_id == body_id)
-
- if start_time and end_time:
- query = query.where(
- and_(
- Position.time >= start_time,
- Position.time <= end_time
- )
- )
- elif start_time:
- query = query.where(Position.time >= start_time)
- elif end_time:
- query = query.where(Position.time <= end_time)
-
- query = query.order_by(Position.time)
- result = await s.execute(query)
- return result.scalars().all()
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
- @staticmethod
- async def get_positions_in_range(
- body_id: str,
- start_time: datetime,
- end_time: datetime,
- session: Optional[AsyncSession] = None
- ) -> List[Position]:
- """Alias for get_positions with required time range"""
- return await PositionService.get_positions(body_id, start_time, end_time, session)
-
- @staticmethod
- async def save_position(
- body_id: str,
- time: datetime,
- x: float,
- y: float,
- z: float,
- source: str = "nasa_horizons",
- vx: Optional[float] = None,
- vy: Optional[float] = None,
- vz: Optional[float] = None,
- session: Optional[AsyncSession] = None
- ) -> Position:
- """Save a single position record"""
- async def _save(s: AsyncSession):
- # Check if position already exists
- existing = await s.execute(
- select(Position).where(
- and_(
- Position.body_id == body_id,
- Position.time == time
- )
- )
- )
- existing_pos = existing.scalar_one_or_none()
-
- if existing_pos:
- # Update existing position
- existing_pos.x = x
- existing_pos.y = y
- existing_pos.z = z
- existing_pos.vx = vx
- existing_pos.vy = vy
- existing_pos.vz = vz
- existing_pos.source = source
- await s.commit()
- await s.refresh(existing_pos)
- return existing_pos
- else:
- # Create new position
- position = Position(
- body_id=body_id,
- time=time,
- x=x,
- y=y,
- z=z,
- vx=vx,
- vy=vy,
- vz=vz,
- source=source
- )
- s.add(position)
- await s.commit()
- await s.refresh(position)
- return position
-
- if session:
- return await _save(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _save(s)
-
- @staticmethod
- async def delete_old_positions(
- before_time: datetime,
- session: Optional[AsyncSession] = None
- ) -> int:
- """Delete position records older than specified time"""
- async def _delete(s: AsyncSession):
- result = await s.execute(
- delete(Position).where(Position.time < before_time)
- )
- await s.commit()
- return result.rowcount
-
- if session:
- return await _delete(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _delete(s)
-
- @staticmethod
- async def get_available_dates(
- body_id: str,
- start_time: datetime,
- end_time: datetime,
- session: Optional[AsyncSession] = None
- ) -> List[datetime]:
- """Get all dates that have position data for a specific body within a time range"""
- async def _query(s: AsyncSession):
- from sqlalchemy import func, Date
-
- # Query distinct dates (truncate to date)
- query = select(func.date(Position.time)).where(
- and_(
- Position.body_id == body_id,
- Position.time >= start_time,
- Position.time <= end_time
- )
- ).distinct().order_by(func.date(Position.time))
-
- result = await s.execute(query)
- dates = [row[0] for row in result]
- return dates
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
-
-class NasaCacheService:
- """Service for NASA API response caching"""
-
- @staticmethod
- async def get_cached_response(
- body_id: str,
- start_time: Optional[datetime],
- end_time: Optional[datetime],
- step: str,
- session: Optional[AsyncSession] = None
- ) -> Optional[Dict[str, Any]]:
- """Get cached NASA API response"""
- async def _query(s: AsyncSession):
- # Remove timezone info for comparison with database TIMESTAMP WITHOUT TIME ZONE
- start_naive = start_time.replace(tzinfo=None) if start_time else None
- end_naive = end_time.replace(tzinfo=None) if end_time else None
- now_naive = datetime.utcnow()
-
- result = await s.execute(
- select(NasaCache).where(
- and_(
- NasaCache.body_id == body_id,
- NasaCache.start_time == start_naive,
- NasaCache.end_time == end_naive,
- NasaCache.step == step,
- NasaCache.expires_at > now_naive
- )
- )
- )
- cache = result.scalar_one_or_none()
- return cache.data if cache else None
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
- @staticmethod
- async def save_response(
- body_id: str,
- start_time: Optional[datetime],
- end_time: Optional[datetime],
- step: str,
- response_data: Dict[str, Any],
- ttl_days: int = 7,
- session: Optional[AsyncSession] = None
- ) -> NasaCache:
- """Save NASA API response to cache (upsert: insert or update if exists)"""
- async def _save(s: AsyncSession):
- from datetime import timedelta
- from sqlalchemy.dialects.postgresql import insert
-
- # Remove timezone info for database storage (TIMESTAMP WITHOUT TIME ZONE)
- start_naive = start_time.replace(tzinfo=None) if start_time else None
- end_naive = end_time.replace(tzinfo=None) if end_time else None
- now_naive = datetime.utcnow()
-
- # Generate cache key
- start_str = start_time.isoformat() if start_time else "null"
- end_str = end_time.isoformat() if end_time else "null"
- cache_key = f"{body_id}:{start_str}:{end_str}:{step}"
-
- # Use PostgreSQL's INSERT ... ON CONFLICT to handle duplicates atomically
- stmt = insert(NasaCache).values(
- cache_key=cache_key,
- body_id=body_id,
- start_time=start_naive,
- end_time=end_naive,
- step=step,
- data=response_data,
- expires_at=now_naive + timedelta(days=ttl_days)
- )
-
- # On conflict, update the existing record
- stmt = stmt.on_conflict_do_update(
- index_elements=['cache_key'],
- set_={
- 'data': response_data,
- 'created_at': now_naive,
- 'expires_at': now_naive + timedelta(days=ttl_days)
- }
- ).returning(NasaCache)
-
- result = await s.execute(stmt)
- cache = result.scalar_one()
-
- await s.commit()
- await s.refresh(cache)
- return cache
-
- if session:
- return await _save(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _save(s)
-
-
-class StaticDataService:
- """Service for static data operations"""
-
- @staticmethod
- async def get_all_items(
- session: Optional[AsyncSession] = None
- ) -> List[StaticData]:
- """Get all static data items"""
- async def _query(s: AsyncSession):
- result = await s.execute(
- select(StaticData).order_by(StaticData.category, StaticData.name)
- )
- return result.scalars().all()
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
- @staticmethod
- async def create_static(
- data: Dict[str, Any],
- session: Optional[AsyncSession] = None
- ) -> StaticData:
- """Create new static data"""
- async def _create(s: AsyncSession):
- item = StaticData(**data)
- s.add(item)
- await s.commit()
- await s.refresh(item)
- return item
-
- if session:
- return await _create(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _create(s)
-
- @staticmethod
- async def update_static(
- item_id: int,
- update_data: Dict[str, Any],
- session: Optional[AsyncSession] = None
- ) -> Optional[StaticData]:
- """Update static data"""
- async def _update(s: AsyncSession):
- result = await s.execute(
- select(StaticData).where(StaticData.id == item_id)
- )
- item = result.scalar_one_or_none()
- if not item:
- return None
-
- for key, value in update_data.items():
- if hasattr(item, key):
- setattr(item, key, value)
-
- await s.commit()
- await s.refresh(item)
- return item
-
- if session:
- return await _update(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _update(s)
-
- @staticmethod
- async def delete_static(
- item_id: int,
- session: Optional[AsyncSession] = None
- ) -> bool:
- """Delete static data"""
- async def _delete(s: AsyncSession):
- result = await s.execute(
- select(StaticData).where(StaticData.id == item_id)
- )
- item = result.scalar_one_or_none()
- if not item:
- return False
-
- await s.delete(item)
- await s.commit()
- return True
-
- if session:
- return await _delete(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _delete(s)
-
- @staticmethod
- async def get_by_category(
- category: str,
- session: Optional[AsyncSession] = None
- ) -> List[StaticData]:
- """Get all static data items for a category"""
- async def _query(s: AsyncSession):
- result = await s.execute(
- select(StaticData)
- .where(StaticData.category == category)
- .order_by(StaticData.name)
- )
- return result.scalars().all()
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
- @staticmethod
- async def get_all_categories(
- session: Optional[AsyncSession] = None
- ) -> List[str]:
- """Get all available categories"""
- async def _query(s: AsyncSession):
- result = await s.execute(
- select(StaticData.category).distinct()
- )
- return [row[0] for row in result]
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
-
-class ResourceService:
- """Service for resource file management"""
-
- @staticmethod
- async def create_resource(
- resource_data: Dict[str, Any],
- session: Optional[AsyncSession] = None
- ) -> Resource:
- """Create a new resource record"""
- async def _create(s: AsyncSession):
- resource = Resource(**resource_data)
- s.add(resource)
- await s.commit()
- await s.refresh(resource)
- return resource
-
- if session:
- return await _create(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _create(s)
-
- @staticmethod
- async def get_resources_by_body(
- body_id: str,
- resource_type: Optional[str] = None,
- session: Optional[AsyncSession] = None
- ) -> List[Resource]:
- """Get all resources for a celestial body"""
- async def _query(s: AsyncSession):
- query = select(Resource).where(Resource.body_id == body_id)
- if resource_type:
- query = query.where(Resource.resource_type == resource_type)
- result = await s.execute(query.order_by(Resource.created_at))
- return result.scalars().all()
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
- @staticmethod
- async def get_all_resources_grouped_by_body(
- body_ids: Optional[List[str]] = None,
- session: Optional[AsyncSession] = None
- ) -> Dict[str, List[Resource]]:
- """
- Get all resources grouped by body_id (optimized for bulk loading)
-
- Args:
- body_ids: Optional list of body IDs to filter by
- session: Database session
-
- Returns:
- Dictionary mapping body_id to list of resources
- """
- async def _query(s: AsyncSession):
- query = select(Resource).order_by(Resource.body_id, Resource.created_at)
- if body_ids:
- query = query.where(Resource.body_id.in_(body_ids))
- result = await s.execute(query)
- resources = result.scalars().all()
-
- # Group by body_id
- grouped = {}
- for resource in resources:
- if resource.body_id not in grouped:
- grouped[resource.body_id] = []
- grouped[resource.body_id].append(resource)
-
- return grouped
-
- if session:
- return await _query(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _query(s)
-
- @staticmethod
- async def delete_resource(
- resource_id: int,
- session: Optional[AsyncSession] = None
- ) -> bool:
- """Delete a resource record"""
- async def _delete(s: AsyncSession):
- result = await s.execute(
- select(Resource).where(Resource.id == resource_id)
- )
- resource = result.scalar_one_or_none()
- if resource:
- await s.delete(resource)
- await s.commit()
- return True
- return False
-
- if session:
- return await _delete(session)
- else:
- async with AsyncSessionLocal() as s:
- return await _delete(s)
-
-
-# Export service instances
-celestial_body_service = CelestialBodyService()
-position_service = PositionService()
-nasa_cache_service = NasaCacheService()
-static_data_service = StaticDataService()
-resource_service = ResourceService()
+from app.services.celestial_body_service import (
+ CelestialBodyService,
+ celestial_body_service,
+)
+from app.services.nasa_cache_service import NasaCacheService, nasa_cache_service
+from app.services.position_service import PositionService, position_service
+from app.services.resource_service import ResourceService, resource_service
+from app.services.static_data_service import StaticDataService, static_data_service
+
+__all__ = [
+ "CelestialBodyService",
+ "NasaCacheService",
+ "PositionService",
+ "ResourceService",
+ "StaticDataService",
+ "celestial_body_service",
+ "nasa_cache_service",
+ "position_service",
+ "resource_service",
+ "static_data_service",
+]
diff --git a/backend/app/services/nasa_cache_service.py b/backend/app/services/nasa_cache_service.py
new file mode 100644
index 0000000..2da4aa3
--- /dev/null
+++ b/backend/app/services/nasa_cache_service.py
@@ -0,0 +1,95 @@
+"""Persistence operations for NASA API response caching."""
+
+from datetime import datetime, timedelta
+from typing import Any, Dict, Optional
+
+from sqlalchemy import and_, select
+from sqlalchemy.dialects.postgresql import insert
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database import AsyncSessionLocal
+from app.models.db import NasaCache
+
+
+class NasaCacheService:
+ """Read and write cached NASA API responses."""
+
+ @staticmethod
+ async def get_cached_response(
+ body_id: str,
+ start_time: Optional[datetime],
+ end_time: Optional[datetime],
+ step: str,
+ session: Optional[AsyncSession] = None,
+ ) -> Optional[Dict[str, Any]]:
+ async def query(active_session: AsyncSession) -> Optional[Dict[str, Any]]:
+ start_naive = start_time.replace(tzinfo=None) if start_time else None
+ end_naive = end_time.replace(tzinfo=None) if end_time else None
+ result = await active_session.execute(
+ select(NasaCache).where(
+ and_(
+ NasaCache.body_id == body_id,
+ NasaCache.start_time == start_naive,
+ NasaCache.end_time == end_naive,
+ NasaCache.step == step,
+ NasaCache.expires_at > datetime.utcnow(),
+ )
+ )
+ )
+ cache = result.scalar_one_or_none()
+ return cache.data if cache else None
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+ @staticmethod
+ async def save_response(
+ body_id: str,
+ start_time: Optional[datetime],
+ end_time: Optional[datetime],
+ step: str,
+ response_data: Dict[str, Any],
+ ttl_days: int = 7,
+ session: Optional[AsyncSession] = None,
+ ) -> NasaCache:
+ async def save(active_session: AsyncSession) -> NasaCache:
+ start_naive = start_time.replace(tzinfo=None) if start_time else None
+ end_naive = end_time.replace(tzinfo=None) if end_time else None
+ now = datetime.utcnow()
+ start_key = start_time.isoformat() if start_time else "null"
+ end_key = end_time.isoformat() if end_time else "null"
+ cache_key = f"{body_id}:{start_key}:{end_key}:{step}"
+
+ statement = insert(NasaCache).values(
+ cache_key=cache_key,
+ body_id=body_id,
+ start_time=start_naive,
+ end_time=end_naive,
+ step=step,
+ data=response_data,
+ expires_at=now + timedelta(days=ttl_days),
+ )
+ statement = statement.on_conflict_do_update(
+ index_elements=["cache_key"],
+ set_={
+ "data": response_data,
+ "created_at": now,
+ "expires_at": now + timedelta(days=ttl_days),
+ },
+ ).returning(NasaCache)
+
+ result = await active_session.execute(statement)
+ cache = result.scalar_one()
+ await active_session.commit()
+ await active_session.refresh(cache)
+ return cache
+
+ if session is not None:
+ return await save(session)
+ async with AsyncSessionLocal() as active_session:
+ return await save(active_session)
+
+
+nasa_cache_service = NasaCacheService()
diff --git a/backend/app/services/nasa_worker.py b/backend/app/services/nasa_worker.py
index b7f74cb..94be8cb 100644
--- a/backend/app/services/nasa_worker.py
+++ b/backend/app/services/nasa_worker.py
@@ -10,7 +10,8 @@ from typing import List, Optional
from app.database import AsyncSessionLocal
from app.services.task_service import task_service
-from app.services.db_service import celestial_body_service, position_service
+from app.services.celestial_body_service import celestial_body_service
+from app.services.position_service import position_service
from app.services.horizons import horizons_service
from app.services.orbit_service import orbit_service
from app.services.event_service import event_service
diff --git a/backend/app/services/position_service.py b/backend/app/services/position_service.py
new file mode 100644
index 0000000..e523a73
--- /dev/null
+++ b/backend/app/services/position_service.py
@@ -0,0 +1,183 @@
+"""Persistence operations for celestial position data."""
+
+from datetime import datetime
+from typing import Any, Dict, List, Optional
+
+from sqlalchemy import and_, delete, func, select
+from sqlalchemy.dialects.postgresql import insert
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database import AsyncSessionLocal
+from app.models.db import Position
+
+
+class PositionService:
+ """Store and query time-series position data."""
+
+ @staticmethod
+ async def save_positions(
+ body_id: str,
+ positions: List[Dict[str, Any]],
+ source: str = "nasa_horizons",
+ session: Optional[AsyncSession] = None,
+ ) -> int:
+ async def save(active_session: AsyncSession) -> int:
+ for position_data in positions:
+ statement = insert(Position).values(
+ body_id=body_id,
+ time=position_data["time"],
+ x=position_data["x"],
+ y=position_data["y"],
+ z=position_data["z"],
+ vx=position_data.get("vx"),
+ vy=position_data.get("vy"),
+ vz=position_data.get("vz"),
+ source=source,
+ )
+ statement = statement.on_conflict_do_update(
+ index_elements=["body_id", "time"],
+ set_={
+ "x": position_data["x"],
+ "y": position_data["y"],
+ "z": position_data["z"],
+ "vx": position_data.get("vx"),
+ "vy": position_data.get("vy"),
+ "vz": position_data.get("vz"),
+ "source": source,
+ },
+ )
+ await active_session.execute(statement)
+
+ await active_session.commit()
+ return len(positions)
+
+ if session is not None:
+ return await save(session)
+ async with AsyncSessionLocal() as active_session:
+ return await save(active_session)
+
+ @staticmethod
+ async def get_positions(
+ body_id: str,
+ start_time: Optional[datetime] = None,
+ end_time: Optional[datetime] = None,
+ session: Optional[AsyncSession] = None,
+ ) -> List[Position]:
+ async def query(active_session: AsyncSession) -> List[Position]:
+ statement = select(Position).where(Position.body_id == body_id)
+ if start_time and end_time:
+ statement = statement.where(
+ and_(Position.time >= start_time, Position.time <= end_time)
+ )
+ elif start_time:
+ statement = statement.where(Position.time >= start_time)
+ elif end_time:
+ statement = statement.where(Position.time <= end_time)
+
+ result = await active_session.execute(statement.order_by(Position.time))
+ return list(result.scalars().all())
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+ @staticmethod
+ async def get_positions_in_range(
+ body_id: str,
+ start_time: datetime,
+ end_time: datetime,
+ session: Optional[AsyncSession] = None,
+ ) -> List[Position]:
+ return await PositionService.get_positions(
+ body_id, start_time, end_time, session
+ )
+
+ @staticmethod
+ async def save_position(
+ body_id: str,
+ time: datetime,
+ x: float,
+ y: float,
+ z: float,
+ source: str = "nasa_horizons",
+ vx: Optional[float] = None,
+ vy: Optional[float] = None,
+ vz: Optional[float] = None,
+ session: Optional[AsyncSession] = None,
+ ) -> Position:
+ async def save(active_session: AsyncSession) -> Position:
+ result = await active_session.execute(
+ select(Position).where(
+ and_(Position.body_id == body_id, Position.time == time)
+ )
+ )
+ position = result.scalar_one_or_none()
+ if position is None:
+ position = Position(body_id=body_id, time=time, x=x, y=y, z=z)
+ active_session.add(position)
+
+ position.x = x
+ position.y = y
+ position.z = z
+ position.vx = vx
+ position.vy = vy
+ position.vz = vz
+ position.source = source
+ await active_session.commit()
+ await active_session.refresh(position)
+ return position
+
+ if session is not None:
+ return await save(session)
+ async with AsyncSessionLocal() as active_session:
+ return await save(active_session)
+
+ @staticmethod
+ async def delete_old_positions(
+ before_time: datetime,
+ session: Optional[AsyncSession] = None,
+ ) -> int:
+ async def remove(active_session: AsyncSession) -> int:
+ result = await active_session.execute(
+ delete(Position).where(Position.time < before_time)
+ )
+ await active_session.commit()
+ return result.rowcount
+
+ if session is not None:
+ return await remove(session)
+ async with AsyncSessionLocal() as active_session:
+ return await remove(active_session)
+
+ @staticmethod
+ async def get_available_dates(
+ body_id: str,
+ start_time: datetime,
+ end_time: datetime,
+ session: Optional[AsyncSession] = None,
+ ) -> List[datetime]:
+ async def query(active_session: AsyncSession) -> List[datetime]:
+ date_expression = func.date(Position.time)
+ statement = (
+ select(date_expression)
+ .where(
+ and_(
+ Position.body_id == body_id,
+ Position.time >= start_time,
+ Position.time <= end_time,
+ )
+ )
+ .distinct()
+ .order_by(date_expression)
+ )
+ result = await active_session.execute(statement)
+ return [row[0] for row in result]
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+
+position_service = PositionService()
diff --git a/backend/app/services/resource_service.py b/backend/app/services/resource_service.py
new file mode 100644
index 0000000..a60602f
--- /dev/null
+++ b/backend/app/services/resource_service.py
@@ -0,0 +1,99 @@
+"""Persistence operations for celestial resource metadata."""
+
+from collections import defaultdict
+from typing import Any, Dict, List, Optional
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database import AsyncSessionLocal
+from app.models.db import Resource
+
+
+class ResourceService:
+ """Create, query, and delete celestial resource records."""
+
+ @staticmethod
+ async def create_resource(
+ resource_data: Dict[str, Any],
+ session: Optional[AsyncSession] = None,
+ ) -> Resource:
+ async def create(active_session: AsyncSession) -> Resource:
+ resource = Resource(**resource_data)
+ active_session.add(resource)
+ await active_session.commit()
+ await active_session.refresh(resource)
+ return resource
+
+ if session is not None:
+ return await create(session)
+ async with AsyncSessionLocal() as active_session:
+ return await create(active_session)
+
+ @staticmethod
+ async def get_resources_by_body(
+ body_id: str,
+ resource_type: Optional[str] = None,
+ session: Optional[AsyncSession] = None,
+ ) -> List[Resource]:
+ async def query(active_session: AsyncSession) -> List[Resource]:
+ statement = select(Resource).where(Resource.body_id == body_id)
+ if resource_type:
+ statement = statement.where(Resource.resource_type == resource_type)
+ result = await active_session.execute(
+ statement.order_by(Resource.created_at)
+ )
+ return list(result.scalars().all())
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+ @staticmethod
+ async def get_all_resources_grouped_by_body(
+ body_ids: Optional[List[str]] = None,
+ session: Optional[AsyncSession] = None,
+ ) -> Dict[str, List[Resource]]:
+ async def query(active_session: AsyncSession) -> Dict[str, List[Resource]]:
+ statement = select(Resource).order_by(
+ Resource.body_id, Resource.created_at
+ )
+ if body_ids:
+ statement = statement.where(Resource.body_id.in_(body_ids))
+
+ result = await active_session.execute(statement)
+ grouped: Dict[str, List[Resource]] = defaultdict(list)
+ for resource in result.scalars().all():
+ grouped[resource.body_id].append(resource)
+ return dict(grouped)
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+ @staticmethod
+ async def delete_resource(
+ resource_id: int,
+ session: Optional[AsyncSession] = None,
+ ) -> bool:
+ async def delete(active_session: AsyncSession) -> bool:
+ result = await active_session.execute(
+ select(Resource).where(Resource.id == resource_id)
+ )
+ resource = result.scalar_one_or_none()
+ if resource is None:
+ return False
+
+ await active_session.delete(resource)
+ await active_session.commit()
+ return True
+
+ if session is not None:
+ return await delete(session)
+ async with AsyncSessionLocal() as active_session:
+ return await delete(active_session)
+
+
+resource_service = ResourceService()
diff --git a/backend/app/services/static_data_service.py b/backend/app/services/static_data_service.py
new file mode 100644
index 0000000..05d27b9
--- /dev/null
+++ b/backend/app/services/static_data_service.py
@@ -0,0 +1,130 @@
+"""Persistence operations for system static data."""
+
+from typing import Any, Dict, List, Optional
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database import AsyncSessionLocal
+from app.models.db import StaticData
+
+
+class StaticDataService:
+ """Create, read, update, and delete static data entries."""
+
+ @staticmethod
+ async def get_all_items(
+ session: Optional[AsyncSession] = None,
+ ) -> List[StaticData]:
+ async def query(active_session: AsyncSession) -> List[StaticData]:
+ result = await active_session.execute(
+ select(StaticData).order_by(StaticData.category, StaticData.name)
+ )
+ return list(result.scalars().all())
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+ @staticmethod
+ async def create_static(
+ data: Dict[str, Any],
+ session: Optional[AsyncSession] = None,
+ ) -> StaticData:
+ async def create(active_session: AsyncSession) -> StaticData:
+ item = StaticData(**data)
+ active_session.add(item)
+ await active_session.commit()
+ await active_session.refresh(item)
+ return item
+
+ if session is not None:
+ return await create(session)
+ async with AsyncSessionLocal() as active_session:
+ return await create(active_session)
+
+ @staticmethod
+ async def update_static(
+ item_id: int,
+ update_data: Dict[str, Any],
+ session: Optional[AsyncSession] = None,
+ ) -> Optional[StaticData]:
+ async def update(active_session: AsyncSession) -> Optional[StaticData]:
+ result = await active_session.execute(
+ select(StaticData).where(StaticData.id == item_id)
+ )
+ item = result.scalar_one_or_none()
+ if item is None:
+ return None
+
+ for key, value in update_data.items():
+ if hasattr(item, key):
+ setattr(item, key, value)
+
+ await active_session.commit()
+ await active_session.refresh(item)
+ return item
+
+ if session is not None:
+ return await update(session)
+ async with AsyncSessionLocal() as active_session:
+ return await update(active_session)
+
+ @staticmethod
+ async def delete_static(
+ item_id: int,
+ session: Optional[AsyncSession] = None,
+ ) -> bool:
+ async def delete(active_session: AsyncSession) -> bool:
+ result = await active_session.execute(
+ select(StaticData).where(StaticData.id == item_id)
+ )
+ item = result.scalar_one_or_none()
+ if item is None:
+ return False
+
+ await active_session.delete(item)
+ await active_session.commit()
+ return True
+
+ if session is not None:
+ return await delete(session)
+ async with AsyncSessionLocal() as active_session:
+ return await delete(active_session)
+
+ @staticmethod
+ async def get_by_category(
+ category: str,
+ session: Optional[AsyncSession] = None,
+ ) -> List[StaticData]:
+ async def query(active_session: AsyncSession) -> List[StaticData]:
+ result = await active_session.execute(
+ select(StaticData)
+ .where(StaticData.category == category)
+ .order_by(StaticData.name)
+ )
+ return list(result.scalars().all())
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+ @staticmethod
+ async def get_all_categories(
+ session: Optional[AsyncSession] = None,
+ ) -> List[str]:
+ async def query(active_session: AsyncSession) -> List[str]:
+ result = await active_session.execute(
+ select(StaticData.category).distinct()
+ )
+ return [row[0] for row in result]
+
+ if session is not None:
+ return await query(session)
+ async with AsyncSessionLocal() as active_session:
+ return await query(active_session)
+
+
+static_data_service = StaticDataService()
diff --git a/backend/scripts/add_rocket_simulator.sql b/backend/scripts/add_rocket_simulator.sql
new file mode 100644
index 0000000..ee570b4
--- /dev/null
+++ b/backend/scripts/add_rocket_simulator.sql
@@ -0,0 +1,94 @@
+CREATE TABLE IF NOT EXISTS rocket_configs (
+ id serial PRIMARY KEY,
+ code varchar(50) NOT NULL UNIQUE,
+ name varchar(100) NOT NULL,
+ name_zh varchar(100),
+ manufacturer varchar(100),
+ country varchar(100),
+ launch_site_name varchar(120) NOT NULL DEFAULT 'Equatorial Launch Site',
+ launch_latitude_deg double precision NOT NULL DEFAULT 0 CHECK (launch_latitude_deg BETWEEN -90 AND 90),
+ launch_longitude_deg double precision NOT NULL DEFAULT 0 CHECK (launch_longitude_deg BETWEEN -180 AND 180),
+ description text,
+ color varchar(20) NOT NULL DEFAULT '#f8fafc',
+ height_m double precision NOT NULL CHECK (height_m > 0),
+ diameter_m double precision NOT NULL CHECK (diameter_m > 0),
+ payload_mass_kg double precision NOT NULL DEFAULT 0 CHECK (payload_mass_kg >= 0),
+ drag_coefficient double precision NOT NULL DEFAULT 0.4 CHECK (drag_coefficient > 0),
+ reference_area_m2 double precision NOT NULL CHECK (reference_area_m2 > 0),
+ target_orbit_km double precision NOT NULL DEFAULT 200 CHECK (target_orbit_km > 0),
+ target_velocity_mps double precision NOT NULL DEFAULT 7800 CHECK (target_velocity_mps > 0),
+ separation_delay_seconds double precision NOT NULL DEFAULT 2 CHECK (separation_delay_seconds >= 0),
+ second_stage_ignition_delay_seconds double precision NOT NULL DEFAULT 1 CHECK (second_stage_ignition_delay_seconds >= 0),
+ stage_1 jsonb NOT NULL,
+ stage_2 jsonb NOT NULL,
+ is_active boolean NOT NULL DEFAULT true,
+ sort_order integer NOT NULL DEFAULT 0,
+ created_at timestamp DEFAULT now(),
+ updated_at timestamp DEFAULT now()
+);
+
+ALTER TABLE rocket_configs ADD COLUMN IF NOT EXISTS launch_site_name varchar(120) NOT NULL DEFAULT 'Equatorial Launch Site';
+ALTER TABLE rocket_configs ADD COLUMN IF NOT EXISTS launch_latitude_deg double precision NOT NULL DEFAULT 0;
+ALTER TABLE rocket_configs ADD COLUMN IF NOT EXISTS launch_longitude_deg double precision NOT NULL DEFAULT 0;
+
+CREATE INDEX IF NOT EXISTS ix_rocket_configs_code ON rocket_configs (code);
+CREATE INDEX IF NOT EXISTS ix_rocket_configs_is_active ON rocket_configs (is_active);
+
+INSERT INTO rocket_configs (
+ code, name, name_zh, manufacturer, country,
+ launch_site_name, launch_latitude_deg, launch_longitude_deg, description, color,
+ height_m, diameter_m, payload_mass_kg, drag_coefficient, reference_area_m2,
+ target_orbit_km, target_velocity_mps, separation_delay_seconds,
+ second_stage_ignition_delay_seconds, stage_1, stage_2, is_active, sort_order
+) VALUES
+(
+ 'falcon-9', 'Falcon 9', '猎鹰9号', 'SpaceX', '美国',
+ 'Cape Canaveral', 28.5623, -80.5774,
+ '两级可重复使用运载火箭,用于近地轨道发射任务模拟。', '#f8fafc',
+ 70, 3.7, 15000, 0.4, 10.75, 200, 7800, 2.2, 1.0,
+ jsonb_build_object('name', '一级推进器', 'dry_mass_kg', 25600, 'fuel_mass_kg', 411000, 'max_thrust_n', 7607000, 'specific_impulse_s', 282, 'engine_count', 9, 'length_ratio', 0.65),
+ jsonb_build_object('name', '二级推进器', 'dry_mass_kg', 4000, 'fuel_mass_kg', 107500, 'max_thrust_n', 981000, 'specific_impulse_s', 348, 'engine_count', 1, 'length_ratio', 0.35),
+ true, 1
+),
+(
+ 'long-march-5', 'Long March 5', '长征五号', '中国运载火箭技术研究院', '中国',
+ '文昌航天发射场', 19.6145, 110.951,
+ '中国大型两级液体运载火箭,用于重型载荷与深空任务模拟。', '#e8edf2',
+ 56.97, 5, 18000, 0.42, 19.63, 200, 7800, 2.5, 1.2,
+ jsonb_build_object('name', '芯一级', 'dry_mass_kg', 38000, 'fuel_mass_kg', 175000, 'max_thrust_n', 10600000, 'specific_impulse_s', 310, 'engine_count', 10, 'length_ratio', 0.62),
+ jsonb_build_object('name', '芯二级', 'dry_mass_kg', 12000, 'fuel_mass_kg', 26000, 'max_thrust_n', 768000, 'specific_impulse_s', 442, 'engine_count', 2, 'length_ratio', 0.38),
+ true, 2
+)
+ON CONFLICT (code) DO NOTHING;
+
+UPDATE rocket_configs
+SET launch_site_name = 'Cape Canaveral', launch_latitude_deg = 28.5623, launch_longitude_deg = -80.5774
+WHERE code = 'falcon-9' AND launch_site_name = 'Equatorial Launch Site';
+
+UPDATE rocket_configs
+SET launch_site_name = '文昌航天发射场', launch_latitude_deg = 19.6145, launch_longitude_deg = 110.951
+WHERE code = 'long-march-5' AND launch_site_name = 'Equatorial Launch Site';
+
+INSERT INTO menus (
+ parent_id, name, title, icon, path, component, sort_order,
+ is_active, description
+)
+SELECT
+ parent.id, 'rocket_configs', '火箭数据管理', 'rocket',
+ '/admin/rockets', 'admin/Rockets', 5, true,
+ '管理火箭发射模拟使用的两级运载火箭数据'
+FROM menus AS parent
+WHERE parent.name = 'data_management'
+ AND NOT EXISTS (SELECT 1 FROM menus WHERE name = 'rocket_configs')
+LIMIT 1;
+
+INSERT INTO role_menus (role_id, menu_id)
+SELECT roles.id, menus.id
+FROM roles
+JOIN menus ON menus.name = 'rocket_configs'
+WHERE roles.name = 'admin'
+ AND NOT EXISTS (
+ SELECT 1 FROM role_menus
+ WHERE role_menus.role_id = roles.id
+ AND role_menus.menu_id = menus.id
+ );
diff --git a/backend/scripts/fetch_and_cache.py b/backend/scripts/fetch_and_cache.py
index 3a8008e..3bf6b13 100755
--- a/backend/scripts/fetch_and_cache.py
+++ b/backend/scripts/fetch_and_cache.py
@@ -23,11 +23,9 @@ import logging
sys.path.insert(0, str(Path(__file__).parent.parent))
from app.services.horizons import horizons_service
-from app.services.db_service import (
- celestial_body_service,
- position_service,
- nasa_cache_service
-)
+from app.services.celestial_body_service import celestial_body_service
+from app.services.nasa_cache_service import nasa_cache_service
+from app.services.position_service import position_service
from app.services.redis_cache import redis_cache, cache_nasa_response
from app.config import settings
diff --git a/backend/scripts/init_db.sql b/backend/scripts/init_db.sql
index 7658de7..8d5b590 100644
--- a/backend/scripts/init_db.sql
+++ b/backend/scripts/init_db.sql
@@ -5420,7 +5420,8 @@ CREATE TABLE "public"."users" (
"is_active" bool NOT NULL,
"created_at" timestamp(6) DEFAULT now(),
"updated_at" timestamp(6) DEFAULT now(),
- "last_login_at" timestamp(6)
+ "last_login_at" timestamp(6),
+ "avatar_url" varchar(255) COLLATE "pg_catalog"."default"
)
;
ALTER TABLE "public"."users" OWNER TO "postgres";
@@ -5430,6 +5431,7 @@ COMMENT ON COLUMN "public"."users"."email" IS 'Email address';
COMMENT ON COLUMN "public"."users"."full_name" IS 'Full name';
COMMENT ON COLUMN "public"."users"."is_active" IS 'Account active status';
COMMENT ON COLUMN "public"."users"."last_login_at" IS 'Last login time';
+COMMENT ON COLUMN "public"."users"."avatar_url" IS 'User avatar file path';
-- ----------------------------
-- Records of users
diff --git a/backend/scripts/prefetch_historical_data.py b/backend/scripts/prefetch_historical_data.py
index 9fb587e..bffe15c 100644
--- a/backend/scripts/prefetch_historical_data.py
+++ b/backend/scripts/prefetch_historical_data.py
@@ -28,7 +28,8 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')
from app.database import get_db
from app.services.horizons import horizons_service
-from app.services.db_service import position_service, celestial_body_service
+from app.services.celestial_body_service import celestial_body_service
+from app.services.position_service import position_service
async def prefetch_month(year: int, month: int, session):
diff --git a/backend/scripts/seed_celestial_bodies.py b/backend/scripts/seed_celestial_bodies.py
index 74298dc..67ff213 100755
--- a/backend/scripts/seed_celestial_bodies.py
+++ b/backend/scripts/seed_celestial_bodies.py
@@ -19,7 +19,8 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')
from app.database import get_db
from app.services.horizons import horizons_service
-from app.services.db_service import celestial_body_service, position_service
+from app.services.celestial_body_service import celestial_body_service
+from app.services.position_service import position_service
from app.models.celestial import CELESTIAL_BODIES
diff --git a/backend/scripts/update_static_data.py b/backend/scripts/update_static_data.py
index 227e249..ce74507 100644
--- a/backend/scripts/update_static_data.py
+++ b/backend/scripts/update_static_data.py
@@ -10,7 +10,7 @@ import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.database import get_db
-from app.services.db_service import static_data_service
+from app.services.static_data_service import static_data_service
from app.models.db import StaticData
from sqlalchemy import select, update, insert
from sqlalchemy.dialects.postgresql import insert as pg_insert
diff --git a/backend/upload/texture/Europa_2k.jpeg b/backend/upload/texture/Europa_2k.jpeg
new file mode 100644
index 0000000..989504a
Binary files /dev/null and b/backend/upload/texture/Europa_2k.jpeg differ
diff --git a/backend/upload/texture/charon_2k.jpeg b/backend/upload/texture/charon_2k.jpeg
new file mode 100644
index 0000000..b4e0ba6
Binary files /dev/null and b/backend/upload/texture/charon_2k.jpeg differ
diff --git a/backend/upload/texture/pluto_rgb_8k.jpeg b/backend/upload/texture/pluto_rgb_8k.jpeg
new file mode 100644
index 0000000..0bcf618
Binary files /dev/null and b/backend/upload/texture/pluto_rgb_8k.jpeg differ
diff --git a/backend/upload/texture/sedna_2k.jpeg b/backend/upload/texture/sedna_2k.jpeg
new file mode 100644
index 0000000..23dfac6
Binary files /dev/null and b/backend/upload/texture/sedna_2k.jpeg differ
diff --git a/backend/upload/texture/vesta_8K.jpeg b/backend/upload/texture/vesta_8K.jpeg
new file mode 100644
index 0000000..fd7d731
Binary files /dev/null and b/backend/upload/texture/vesta_8K.jpeg differ
diff --git a/data/.DS_Store b/data/.DS_Store
deleted file mode 100644
index 2629684..0000000
Binary files a/data/.DS_Store and /dev/null differ
diff --git a/data/backups/.DS_Store b/data/backups/.DS_Store
deleted file mode 100644
index 4734352..0000000
Binary files a/data/backups/.DS_Store and /dev/null differ
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..6d55305
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,15 @@
+# Cosmo 文档
+
+项目入口文档保留在仓库根目录:
+
+- [README](../README.md)
+- [项目说明](../PROJECT.md)
+- [快速开始](../QUICKSTART.md)
+- [部署指南](../DEPLOYMENT.md)
+
+其余文档按用途归档:
+
+- `architecture/`:架构、数据策略、算法和数据库设计
+- `guides/`:配置、运维和功能使用指南
+- `plans/`:路线图及阶段计划
+- `reports/`:阶段完成报告、问题分析和修复记录
diff --git a/CACHE_ARCHITECTURE.md b/docs/architecture/CACHE_ARCHITECTURE.md
similarity index 100%
rename from CACHE_ARCHITECTURE.md
rename to docs/architecture/CACHE_ARCHITECTURE.md
diff --git a/CAMERA_FOCUS_ALGORITHMS.md b/docs/architecture/CAMERA_FOCUS_ALGORITHMS.md
similarity index 100%
rename from CAMERA_FOCUS_ALGORITHMS.md
rename to docs/architecture/CAMERA_FOCUS_ALGORITHMS.md
diff --git a/backend/DATABASE_SCHEMA.md b/docs/architecture/DATABASE_SCHEMA.md
similarity index 100%
rename from backend/DATABASE_SCHEMA.md
rename to docs/architecture/DATABASE_SCHEMA.md
diff --git a/NASA_DATA_STRATEGY.md b/docs/architecture/NASA_DATA_STRATEGY.md
similarity index 100%
rename from NASA_DATA_STRATEGY.md
rename to docs/architecture/NASA_DATA_STRATEGY.md
diff --git a/ORBIT_GENERATION_SYSTEM.md b/docs/architecture/ORBIT_GENERATION_SYSTEM.md
similarity index 100%
rename from ORBIT_GENERATION_SYSTEM.md
rename to docs/architecture/ORBIT_GENERATION_SYSTEM.md
diff --git a/ORBIT_INCLINATION_SOLUTION.md b/docs/architecture/ORBIT_INCLINATION_SOLUTION.md
similarity index 100%
rename from ORBIT_INCLINATION_SOLUTION.md
rename to docs/architecture/ORBIT_INCLINATION_SOLUTION.md
diff --git a/REDIS.md b/docs/architecture/REDIS.md
similarity index 98%
rename from REDIS.md
rename to docs/architecture/REDIS.md
index 851107e..ef6d0b1 100644
--- a/REDIS.md
+++ b/docs/architecture/REDIS.md
@@ -37,4 +37,4 @@
|------|--------------|-------------|------|
| user:tokens:{uid} | Set | 存该用户所有活动的 JWT Token,用于多端登录和注销。 | 随 Token 过期 |
| positions:{args} | String (JSON) | API 响应缓存。存前端需要的天体坐标数组。 | 1小时 (实时) / 7天 (历史) |
-| cosmo:danmaku:stream | ZSet | 留言板消息流。按时间排序,自动过期。 | 24 小时 (可配) |
\ No newline at end of file
+| cosmo:danmaku:stream | ZSet | 留言板消息流。按时间排序,自动过期。 | 24 小时 (可配) |
diff --git a/backend/CONFIG.md b/docs/guides/BACKEND_CONFIG.md
similarity index 100%
rename from backend/CONFIG.md
rename to docs/guides/BACKEND_CONFIG.md
diff --git a/CACHE_PREHEAT_GUIDE.md b/docs/guides/CACHE_PREHEAT_GUIDE.md
similarity index 100%
rename from CACHE_PREHEAT_GUIDE.md
rename to docs/guides/CACHE_PREHEAT_GUIDE.md
diff --git a/backend/scripts/UPGRADE_GUIDE_FINAL.md b/docs/guides/DATABASE_UPGRADE.md
similarity index 100%
rename from backend/scripts/UPGRADE_GUIDE_FINAL.md
rename to docs/guides/DATABASE_UPGRADE.md
diff --git a/ORBIT_GENERATION_USER_GUIDE.md b/docs/guides/ORBIT_GENERATION_USER_GUIDE.md
similarity index 98%
rename from ORBIT_GENERATION_USER_GUIDE.md
rename to docs/guides/ORBIT_GENERATION_USER_GUIDE.md
index 9358973..f9ebda0 100644
--- a/ORBIT_GENERATION_USER_GUIDE.md
+++ b/docs/guides/ORBIT_GENERATION_USER_GUIDE.md
@@ -307,7 +307,7 @@ GET /celestial/orbits?body_type=planet
## 相关文档
-- [天体轨道生成系统技术文档](./ORBIT_GENERATION_SYSTEM.md)
+- [天体轨道生成系统技术文档](../architecture/ORBIT_GENERATION_SYSTEM.md)
- [NASA JPL Horizons API文档](https://ssd.jpl.nasa.gov/horizons/)
---
diff --git a/frontend/PERFORMANCE_OPTIMIZATION.md b/docs/guides/PERFORMANCE_OPTIMIZATION.md
similarity index 100%
rename from frontend/PERFORMANCE_OPTIMIZATION.md
rename to docs/guides/PERFORMANCE_OPTIMIZATION.md
diff --git a/PROXY_SETUP.md b/docs/guides/PROXY_SETUP.md
similarity index 100%
rename from PROXY_SETUP.md
rename to docs/guides/PROXY_SETUP.md
diff --git a/PHASE4_PLAN.md b/docs/plans/PHASE4_PLAN.md
similarity index 100%
rename from PHASE4_PLAN.md
rename to docs/plans/PHASE4_PLAN.md
diff --git a/PHASE5_PLAN.md b/docs/plans/PHASE5_PLAN.md
similarity index 99%
rename from PHASE5_PLAN.md
rename to docs/plans/PHASE5_PLAN.md
index 20babfd..3185d81 100644
--- a/PHASE5_PLAN.md
+++ b/docs/plans/PHASE5_PLAN.md
@@ -87,4 +87,4 @@
3. **验证**:
* 测试关注/取关。
* 测试频道消息发送与接收(验证 Redis 存储)。
- * 测试 Horizons 缓存生效。
\ No newline at end of file
+ * 测试 Horizons 缓存生效。
diff --git a/RAODMAP.md b/docs/plans/ROADMAP.md
similarity index 99%
rename from RAODMAP.md
rename to docs/plans/ROADMAP.md
index 3caaa1d..275885a 100644
--- a/RAODMAP.md
+++ b/docs/plans/ROADMAP.md
@@ -328,4 +328,4 @@ function getPlanetTexture(radius, temp) {
// 4. 宜居带/岩石行星 (默认)
return textureLoader.load(PATH + 'rocky_atmosphere.jpg');
}
-```
\ No newline at end of file
+```
diff --git a/backend/ADMIN_PROGRESS.md b/docs/reports/ADMIN_PROGRESS.md
similarity index 100%
rename from backend/ADMIN_PROGRESS.md
rename to docs/reports/ADMIN_PROGRESS.md
diff --git a/frontend/ASSET_LOADING_ANALYSIS.md b/docs/reports/ASSET_LOADING_ANALYSIS.md
similarity index 99%
rename from frontend/ASSET_LOADING_ANALYSIS.md
rename to docs/reports/ASSET_LOADING_ANALYSIS.md
index 4487fe9..bfe163e 100644
--- a/frontend/ASSET_LOADING_ANALYSIS.md
+++ b/docs/reports/ASSET_LOADING_ANALYSIS.md
@@ -364,7 +364,7 @@ POSITIVE:
- **Model Source**: `/Users/jiliu/WorkSpace/cosmo/backend/upload/model/`
- **Texture Build Output**: `/Users/jiliu/WorkSpace/cosmo/frontend/dist/textures/`
- **Model Build Output**: `/Users/jiliu/WorkSpace/cosmo/frontend/dist/models/`
-- **Documentation**: `/Users/jiliu/WorkSpace/cosmo/frontend/PERFORMANCE_OPTIMIZATION.md`
+- **Documentation**: `../guides/PERFORMANCE_OPTIMIZATION.md`
---
@@ -387,4 +387,3 @@ POSITIVE:
2. Add KTX2/Basis texture compression
3. Implement virtual scrolling for star catalog
4. Add service worker for persistent caching
-
diff --git a/BINARY_SYSTEMS_COMPLETION.md b/docs/reports/BINARY_SYSTEMS_COMPLETION.md
similarity index 100%
rename from BINARY_SYSTEMS_COMPLETION.md
rename to docs/reports/BINARY_SYSTEMS_COMPLETION.md
diff --git a/CELESTIAL_BODY_FIX_SUMMARY.md b/docs/reports/CELESTIAL_BODY_FIX_SUMMARY.md
similarity index 100%
rename from CELESTIAL_BODY_FIX_SUMMARY.md
rename to docs/reports/CELESTIAL_BODY_FIX_SUMMARY.md
diff --git a/backend/DATABASE_SCHEMA_MISSING.md b/docs/reports/DATABASE_SCHEMA_MISSING.md
similarity index 100%
rename from backend/DATABASE_SCHEMA_MISSING.md
rename to docs/reports/DATABASE_SCHEMA_MISSING.md
diff --git a/DATA_STRATEGY_OPTIMIZATION.md b/docs/reports/DATA_STRATEGY_OPTIMIZATION.md
similarity index 100%
rename from DATA_STRATEGY_OPTIMIZATION.md
rename to docs/reports/DATA_STRATEGY_OPTIMIZATION.md
diff --git a/backend/scripts/FIX_PARAMETER_TYPES.md b/docs/reports/FIX_PARAMETER_TYPES.md
similarity index 100%
rename from backend/scripts/FIX_PARAMETER_TYPES.md
rename to docs/reports/FIX_PARAMETER_TYPES.md
diff --git a/LOGIN_REDESIGN.md b/docs/reports/LOGIN_REDESIGN.md
similarity index 100%
rename from LOGIN_REDESIGN.md
rename to docs/reports/LOGIN_REDESIGN.md
diff --git a/MULTI_STAR_SYSTEMS_ANALYSIS.md b/docs/reports/MULTI_STAR_SYSTEMS_ANALYSIS.md
similarity index 100%
rename from MULTI_STAR_SYSTEMS_ANALYSIS.md
rename to docs/reports/MULTI_STAR_SYSTEMS_ANALYSIS.md
diff --git a/PHASE_3_COMPLETION_SUMMARY.md b/docs/reports/PHASE_3_COMPLETION_SUMMARY.md
similarity index 100%
rename from PHASE_3_COMPLETION_SUMMARY.md
rename to docs/reports/PHASE_3_COMPLETION_SUMMARY.md
diff --git a/frontend/REMOVE_ASTEROID_COLUMN.md b/docs/reports/REMOVE_ASTEROID_COLUMN.md
similarity index 100%
rename from frontend/REMOVE_ASTEROID_COLUMN.md
rename to docs/reports/REMOVE_ASTEROID_COLUMN.md
diff --git a/docs/火箭发送模拟/.env.example b/docs/火箭发送模拟/.env.example
new file mode 100644
index 0000000..7a550fe
--- /dev/null
+++ b/docs/火箭发送模拟/.env.example
@@ -0,0 +1,9 @@
+# GEMINI_API_KEY: Required for Gemini AI API calls.
+# AI Studio automatically injects this at runtime from user secrets.
+# Users configure this via the Secrets panel in the AI Studio UI.
+GEMINI_API_KEY="MY_GEMINI_API_KEY"
+
+# APP_URL: The URL where this applet is hosted.
+# AI Studio automatically injects this at runtime with the Cloud Run service URL.
+# Used for self-referential links, OAuth callbacks, and API endpoints.
+APP_URL="MY_APP_URL"
diff --git a/docs/火箭发送模拟/.gitignore b/docs/火箭发送模拟/.gitignore
new file mode 100644
index 0000000..5a86d2a
--- /dev/null
+++ b/docs/火箭发送模拟/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+build/
+dist/
+coverage/
+.DS_Store
+*.log
+.env*
+!.env.example
diff --git a/docs/火箭发送模拟/README.md b/docs/火箭发送模拟/README.md
new file mode 100644
index 0000000..770beb0
--- /dev/null
+++ b/docs/火箭发送模拟/README.md
@@ -0,0 +1,20 @@
+
+

+
+
+# Run and deploy your AI Studio app
+
+This contains everything you need to run your app locally.
+
+View your app in AI Studio: https://ai.studio/apps/60f28484-41b5-453e-beeb-3d312fb598db
+
+## Run Locally
+
+**Prerequisites:** Node.js
+
+
+1. Install dependencies:
+ `npm install`
+2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
+3. Run the app:
+ `npm run dev`
diff --git a/docs/火箭发送模拟/index.html b/docs/火箭发送模拟/index.html
new file mode 100644
index 0000000..21dfe69
--- /dev/null
+++ b/docs/火箭发送模拟/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ My Google AI Studio App
+
+
+
+
+
+
+
diff --git a/docs/火箭发送模拟/metadata.json b/docs/火箭发送模拟/metadata.json
new file mode 100644
index 0000000..3e62aee
--- /dev/null
+++ b/docs/火箭发送模拟/metadata.json
@@ -0,0 +1,6 @@
+{
+ "name": "",
+ "description": "",
+ "requestFramePermissions": [],
+ "majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
+}
diff --git a/docs/火箭发送模拟/package-lock.json b/docs/火箭发送模拟/package-lock.json
new file mode 100644
index 0000000..76a7e9f
--- /dev/null
+++ b/docs/火箭发送模拟/package-lock.json
@@ -0,0 +1,4696 @@
+{
+ "name": "react-example",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "react-example",
+ "version": "0.0.0",
+ "dependencies": {
+ "@google/genai": "^2.4.0",
+ "@tailwindcss/vite": "^4.1.14",
+ "@vitejs/plugin-react": "^5.0.4",
+ "clsx": "^2.1.1",
+ "dotenv": "^17.2.3",
+ "express": "^4.21.2",
+ "lucide-react": "^0.546.0",
+ "motion": "^12.23.24",
+ "react": "^19.0.1",
+ "react-dom": "^19.0.1",
+ "recharts": "^3.8.1",
+ "tailwind-merge": "^3.6.0",
+ "vite": "^6.2.3"
+ },
+ "devDependencies": {
+ "@types/express": "^4.17.21",
+ "@types/node": "^22.14.0",
+ "autoprefixer": "^10.4.21",
+ "esbuild": "^0.25.0",
+ "tailwindcss": "^4.1.14",
+ "tsx": "^4.21.0",
+ "typescript": "~5.8.2",
+ "vite": "^6.2.3"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@google/genai": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.8.0.tgz",
+ "integrity": "sha512-pc2ayxqO5+O7AvnHBqpNHIk7PAZkHZgL31tbyx0gJZBSS9qPYiQoqwK7oYOw/ePmG6QY4EMSu+304vD5QlhXAw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "google-auth-library": "^10.3.0",
+ "p-retry": "^4.6.2",
+ "protobufjs": "^7.5.4",
+ "ws": "^8.18.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/sdk": "^1.25.2"
+ },
+ "peerDependenciesMeta": {
+ "@modelcontextprotocol/sdk": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
+ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
+ "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.8",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz",
+ "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
+ "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz",
+ "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz",
+ "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz",
+ "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz",
+ "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz",
+ "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz",
+ "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz",
+ "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz",
+ "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz",
+ "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz",
+ "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz",
+ "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz",
+ "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz",
+ "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz",
+ "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz",
+ "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz",
+ "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz",
+ "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz",
+ "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz",
+ "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz",
+ "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz",
+ "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz",
+ "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz",
+ "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz",
+ "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
+ "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
+ "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-x64": "4.3.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
+ "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
+ "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
+ "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
+ "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
+ "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
+ "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
+ "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
+ "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
+ "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
+ "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
+ "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
+ "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
+ "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.1",
+ "@tailwindcss/oxide": "4.3.1",
+ "tailwindcss": "4.3.1"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "^1"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.8",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz",
+ "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.3",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.2",
+ "caniuse-lite": "^1.0.30001787",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.37",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
+ "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bignumber.js": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+ "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001799",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.375",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
+ "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
+ "license": "ISC"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-toolkit": {
+ "version": "1.47.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz",
+ "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/framer-motion": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz",
+ "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.40.0",
+ "motion-utils": "^12.39.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gaxios": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz",
+ "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "node-fetch": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
+ "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "gaxios": "^7.0.0",
+ "google-logging-utils": "^1.0.0",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/google-auth-library": {
+ "version": "10.7.0",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz",
+ "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "gaxios": "^7.1.4",
+ "gcp-metadata": "8.1.2",
+ "google-logging-utils": "1.1.3",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/google-logging-utils": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
+ "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.546.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz",
+ "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/motion": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.40.0.tgz",
+ "integrity": "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.40.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.40.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz",
+ "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.39.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.39.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
+ "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.47",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/protobufjs": {
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz",
+ "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.1",
+ "@protobufjs/fetch": "^1.1.1",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.3.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
+ "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-redux": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
+ "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/recharts": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
+ "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz",
+ "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.0",
+ "@rollup/rollup-android-arm64": "4.62.0",
+ "@rollup/rollup-darwin-arm64": "4.62.0",
+ "@rollup/rollup-darwin-x64": "4.62.0",
+ "@rollup/rollup-freebsd-arm64": "4.62.0",
+ "@rollup/rollup-freebsd-x64": "4.62.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.0",
+ "@rollup/rollup-linux-arm64-musl": "4.62.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.0",
+ "@rollup/rollup-linux-loong64-musl": "4.62.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.0",
+ "@rollup/rollup-linux-x64-gnu": "4.62.0",
+ "@rollup/rollup-linux-x64-musl": "4.62.0",
+ "@rollup/rollup-openbsd-x64": "4.62.0",
+ "@rollup/rollup-openharmony-arm64": "4.62.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.0",
+ "@rollup/rollup-win32-x64-gnu": "4.62.0",
+ "@rollup/rollup-win32-x64-msvc": "4.62.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
+ "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tsx": {
+ "version": "4.22.4",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
+ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/docs/火箭发送模拟/package.json b/docs/火箭发送模拟/package.json
new file mode 100644
index 0000000..54ab744
--- /dev/null
+++ b/docs/火箭发送模拟/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "react-example",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3000 --host=0.0.0.0",
+ "build": "vite build",
+ "preview": "vite preview",
+ "clean": "rm -rf dist server.js",
+ "lint": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@google/genai": "^2.4.0",
+ "@tailwindcss/vite": "^4.1.14",
+ "@vitejs/plugin-react": "^5.0.4",
+ "clsx": "^2.1.1",
+ "dotenv": "^17.2.3",
+ "express": "^4.21.2",
+ "lucide-react": "^0.546.0",
+ "motion": "^12.23.24",
+ "react": "^19.0.1",
+ "react-dom": "^19.0.1",
+ "recharts": "^3.8.1",
+ "tailwind-merge": "^3.6.0",
+ "vite": "^6.2.3"
+ },
+ "devDependencies": {
+ "@types/express": "^4.17.21",
+ "@types/node": "^22.14.0",
+ "autoprefixer": "^10.4.21",
+ "esbuild": "^0.25.0",
+ "tailwindcss": "^4.1.14",
+ "tsx": "^4.21.0",
+ "typescript": "~5.8.2",
+ "vite": "^6.2.3"
+ }
+}
diff --git a/docs/火箭发送模拟/simtest.js b/docs/火箭发送模拟/simtest.js
new file mode 100644
index 0000000..bacd125
--- /dev/null
+++ b/docs/火箭发送模拟/simtest.js
@@ -0,0 +1,118 @@
+import { CONSTANTS, PRESETS } from "./src/types";
+
+let h = 0, x = 0, vx = 0, vy = 0, v = 0, pitch = 90, t = 0;
+let currentStage = 0;
+let fuel = PRESETS[0].stages[0].fuelMass;
+const currentEvents = [];
+
+let maxQ = 0;
+let isRunning = true;
+
+for (let i = 0; i < 50000; i++) {
+ const dt = 0.1;
+ const rocket = PRESETS[0];
+ const { area, cd, stages } = rocket;
+ const stage = stages[currentStage];
+ const { dryMass, maxThrust, isp } = stage;
+
+ let upperStagesMass = 0;
+ for (let j = currentStage + 1; j < stages.length; j++) {
+ upperStagesMass += stages[j].dryMass + stages[j].fuelMass;
+ }
+
+ const m = dryMass + fuel + upperStagesMass;
+ const mDotMax = maxThrust / (isp * CONSTANTS.g0);
+ let actualThrottle = 1;
+
+ if (fuel <= 0 && currentStage < stages.length - 1) {
+ currentStage++;
+ fuel = stages[currentStage].fuelMass;
+ if (!currentEvents.find((e) => e.name === `stageSep${currentStage}`)) {
+ currentEvents.push({ name: `stageSep${currentStage}`, time: t });
+ }
+ } else if (fuel <= 0) {
+ actualThrottle = 0;
+ fuel = 0;
+ }
+
+ const currentThrust = maxThrust * actualThrottle;
+ const mDot = mDotMax * actualThrottle;
+
+ if (actualThrottle > 0) {
+ fuel -= mDot * dt;
+ if (fuel < 0) fuel = 0;
+ }
+
+ v = Math.sqrt(vx * vx + vy * vy);
+
+ const pitching = h > 500;
+ if (pitching) {
+ pitch = 90 * Math.exp(-h / 30000);
+ }
+ if (h <= 500) {
+ pitch = 90;
+ }
+
+ const pitchRad = (pitch * Math.PI) / 180;
+ const fg = (CONSTANTS.G * (CONSTANTS.M_EARTH * m)) / Math.pow(CONSTANTS.R_EARTH + h, 2);
+ const centrifugalY = (m * vx * vx) / (CONSTANTS.R_EARTH + h);
+ const rho = h > 100000 ? 0 : CONSTANTS.RHO_0 * Math.exp(-h / CONSTANTS.H);
+
+ const currentQ = 0.5 * rho * v * v;
+ const fd = currentQ * cd * area;
+
+ const thrustX = currentThrust * Math.cos(pitchRad);
+ const thrustY = currentThrust * Math.sin(pitchRad);
+
+ const vDirX = v > 0 ? vx / v : 0;
+ const vDirY = v > 0 ? vy / v : 1;
+
+ const dragX = fd * vDirX;
+ const dragY = fd * vDirY;
+
+ const netForceX = thrustX - dragX;
+ const netForceY = thrustY - dragY - fg + centrifugalY;
+
+ let ax = netForceX / m;
+ let ay = netForceY / m;
+
+ if (h <= 0 && vy <= 0 && netForceY <= 0) {
+ ay = 0; ax = 0; vx = 0; vy = 0; h = 0; pitch = 90;
+ }
+
+ vx += ax * dt;
+ vy += ay * dt;
+ x += vx * dt;
+ h += vy * dt;
+ if (h < 0) h = 0;
+ t += dt;
+
+ v = Math.sqrt(vx * vx + vy * vy);
+
+ if (h > 10 && v > 1 && !currentEvents.find((e) => e.name === "liftoff")) {
+ currentEvents.push({ name: "liftoff", time: t });
+ }
+ if (currentQ > maxQ) maxQ = currentQ;
+ if (maxQ > 20000 && currentQ < maxQ * 0.95 && !currentEvents.find((e) => e.name === "maxq")) {
+ currentEvents.push({ name: "maxq", time: t });
+ }
+ if (currentStage === stages.length - 1 && fuel <= 0 && t > 10 && !currentEvents.find((e) => e.name === "meco")) {
+ currentEvents.push({ name: "meco", time: t });
+ }
+ if (h >= 100000 && !currentEvents.find((e) => e.name === "karman")) {
+ currentEvents.push({ name: "karman", time: t });
+ }
+ if (vx >= 7800 && !currentEvents.find((e) => e.name === "orbit")) {
+ currentEvents.push({ name: "orbit", time: t });
+ }
+
+ if (i % 500 === 0) {
+ console.log(`t: ${t.toFixed(1)}, stage: ${currentStage}, h: ${(h/1000).toFixed(1)}km, v: ${v.toFixed(1)}m/s, vx: ${vx.toFixed(1)}, pitch: ${pitch.toFixed(1)}, fuel: ${fuel.toFixed(0)}, FnetY: ${netForceY.toFixed(0)}`);
+ }
+
+ if (currentEvents.find((e) => e.name === "meco") && t > currentEvents.find((e) => e.name === "meco").time + 3) {
+ console.log("SIM ENDED");
+ break;
+ }
+}
+console.log(currentEvents);
diff --git a/docs/火箭发送模拟/src/App.tsx b/docs/火箭发送模拟/src/App.tsx
new file mode 100644
index 0000000..e8a7d40
--- /dev/null
+++ b/docs/火箭发送模拟/src/App.tsx
@@ -0,0 +1,466 @@
+import { useState, useRef, useEffect } from "react";
+import {
+ Rocket,
+ Play,
+ Pause,
+ RotateCcw,
+ Activity,
+ Globe,
+ Camera,
+ Volume2,
+ VolumeX,
+ Music,
+} from "lucide-react";
+import { useSimulation } from "./useSimulation";
+import { PRESETS } from "./types";
+import { DashboardChart } from "./components/DashboardChart";
+import { FlightVisualizer } from "./components/FlightVisualizer";
+import { TelemetryGauge } from "./components/TelemetryGauge";
+import { Navball } from "./components/Navball";
+import { i18n, Lang } from "./i18n";
+
+export default function App() {
+ const [lang, setLang] = useState("en");
+ const [cameraMode, setCameraMode] = useState<"dynamic" | "follow">("dynamic");
+ const [audioOn, setAudioOn] = useState(false);
+ const [bgmUrl, setBgmUrl] = useState("");
+ const { rocket, state, setThrottle, changePreset, toggleSimulation, reset } =
+ useSimulation(PRESETS[0]);
+
+ const t = i18n[lang];
+
+ const formatNumber = (num: number, decimals: number = 1) =>
+ num.toLocaleString("en-US", {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals,
+ });
+
+ let totalDryMass = 0;
+ let totalFuelMass = 0;
+ let maxPossibleThrust = 0;
+ rocket.stages.forEach((s) => {
+ totalDryMass += s.dryMass;
+ totalFuelMass += s.fuelMass;
+ if (s.maxThrust > maxPossibleThrust) maxPossibleThrust = s.maxThrust;
+ });
+
+ const totalMass = totalDryMass + state.fuel; // Simplified for TWR calc
+ const currentThrust =
+ rocket.stages[state.currentStage].maxThrust * state.throttle;
+ const tWR = currentThrust / (totalMass * 9.80665);
+
+ const drag = state.q * rocket.cd * rocket.area;
+ let tempC = 15 - 0.0065 * Math.min(state.altitude, 11000);
+ if (state.altitude > 11000) tempC = -56.5;
+ const speedOfSound = Math.sqrt(1.4 * 287 * (tempC + 273.15));
+ const mach = state.velocity / speedOfSound;
+
+ const gForce =
+ state.altitude <= 0 && currentThrust < totalMass * 9.80665
+ ? 1
+ : Math.max(0, currentThrust - drag) / (totalMass * 9.80665);
+
+ const formatTime = (totalSeconds: number) => {
+ const mins = Math.floor(totalSeconds / 60);
+ const secs = Math.floor(totalSeconds % 60);
+ return `T+ ${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
+ };
+
+ const isFinished = state.events.some((e) => e.name === "simEnded");
+
+ return (
+
+ {bgmUrl &&
}
+
+ {/* Absolute Background Visualizer */}
+
+
+ {/* Top Left: Logo / Mission Info Overlay */}
+
+
+
+
+
+
+
+ {t.appTitle}
+
+
+ {t.appSub}
+
+
+
+
+
+ {/* Top Right: Preset Selector & Lang Toggle */}
+
+
+
+
+
+
+
+
+ {/* Control Panel (Floating Glass Panel Left) */}
+
+
+
+
+
+
+ {/* Rocket Info */}
+
+
+ {t.vehicleParams}
+
+
+
{t.dryMass}
+
+ {formatNumber(totalDryMass / 1000, 1)} t
+
+
{t.fuelMass}
+
+ {formatNumber(totalFuelMass / 1000, 1)} t
+
+
{t.maxThrust}
+
+ {formatNumber(maxPossibleThrust / 1000000, 1)} MN
+
+
{t.isp}
+
+ {rocket.stages[0].isp} s
+
+
{t.engines}
+
+ {rocket.stages[0].engines}
+
+
+
+
+
+
+
+
+
+
{
+ const file = e.target.files?.[0];
+ if (file) {
+ const url = URL.createObjectURL(file);
+ setBgmUrl(url);
+ setAudioOn(true); // Automatically turn on global audio so they can hear it
+ }
+ }}
+ />
+
+
+
+
+
+
+
+ {Math.round(state.throttle * 100)}%
+
+
+
+
setThrottle(parseFloat(e.target.value))}
+ className="w-full h-2 bg-white/10 rounded-full appearance-none cursor-pointer accent-white"
+ />
+
+
+
+
+
+ {t.twr}
+
+ 1 ? "text-emerald-400 drop-shadow-md" : "text-white/80"
+ }
+ >
+ {formatNumber(tWR, 2)}
+
+
+
+
+ {t.mach}
+
+ 1 ? "text-sky-400 drop-shadow-md" : "text-white/80"
+ }
+ >
+ {formatNumber(mach, 1)}
+
+
+
+
+ {t.gforce}
+
+ 3 ? "text-rose-400 drop-shadow-md" : "text-white/80"
+ }
+ >
+ {formatNumber(gForce, 1)}
+
+
+
+
+ {t.maxT}
+
+
+ {formatNumber(
+ rocket.stages[state.currentStage].maxThrust / 1000000,
+ 1,
+ )}
+ MN
+
+
+
+
+
+ {/* Dashboards (Floating Glass Panel Right) */}
+
+
+ {/* Timeline (Top Center) */}
+
+ {["liftoff", "maxq", "stageSep1", "karman", "orbit"].map(
+ (evName, idx, arr) => {
+ const ev = state.events.find((e) => e.name === evName);
+ const isReached = !!ev;
+ return (
+
+
+ {/* Light */}
+
+ {/* Label */}
+
+ {t[evName as keyof typeof t]}
+
+ {/* Time */}
+
+ {isReached ? formatTime(ev.time) : "--:--"}
+
+
+ {idx < arr.length - 1 && (
+
+ )}
+
+ );
+ },
+ )}
+
+
+ {/* Bottom Center: Broadcast Telemetry Style HUD */}
+
+ {/* Speed */}
+
+
+ {/* Center Stage Info */}
+
+
+ {formatTime(state.time)}
+
+
+
+ {/* Throttle Bar */}
+
+
+ {t.hudThrottle}
+
+ {Math.round(state.throttle * 100)}%
+
+
+
+
+
+ {/* Fuel Bar */}
+
+
+
+ {t.hudProp} (S{state.currentStage + 1})
+
+
+ {Math.round(
+ (state.fuel / rocket.stages[state.currentStage].fuelMass) *
+ 100,
+ )}
+ %
+
+
+
+
+
+
+
+ {/* Altitude */}
+
+
+ {/* Navball */}
+
+
+
+ );
+}
diff --git a/docs/火箭发送模拟/src/components/DashboardChart.tsx b/docs/火箭发送模拟/src/components/DashboardChart.tsx
new file mode 100644
index 0000000..c48c542
--- /dev/null
+++ b/docs/火箭发送模拟/src/components/DashboardChart.tsx
@@ -0,0 +1,103 @@
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+} from "recharts";
+import { DataPoint } from "../types";
+
+interface ChartProps {
+ data: DataPoint[];
+ dataKey: keyof DataPoint;
+ color: string;
+ unit: string;
+ title: string;
+}
+
+export function DashboardChart({
+ data,
+ dataKey,
+ color,
+ unit,
+ title,
+}: ChartProps) {
+ // Simple formatter for Y Axis
+ const formatYAxis = (tickItem: number) => {
+ if (tickItem > 1000) {
+ return `${(tickItem / 1000).toFixed(1)}k`;
+ }
+ return tickItem.toFixed(0);
+ };
+
+ return (
+
+
+ {title}
+
+
+
+
+
+ `${t.toFixed(0)}s`}
+ stroke="rgba(255,255,255,0.4)"
+ fontSize={10}
+ tickMargin={8}
+ tickLine={false}
+ axisLine={false}
+ />
+
+ [
+ `${value.toFixed(1)} ${unit}`,
+ title,
+ ]}
+ labelFormatter={(label: number) => `T+ ${label.toFixed(1)}s`}
+ />
+
+
+
+
+
+ );
+}
diff --git a/docs/火箭发送模拟/src/components/FlightVisualizer.tsx b/docs/火箭发送模拟/src/components/FlightVisualizer.tsx
new file mode 100644
index 0000000..a9ac789
--- /dev/null
+++ b/docs/火箭发送模拟/src/components/FlightVisualizer.tsx
@@ -0,0 +1,428 @@
+import { ReactNode } from "react";
+import { RocketPreset, SimulationState } from "../types";
+
+interface FlightVisualizerProps {
+ rocket: RocketPreset;
+ state: SimulationState;
+ cameraMode: "dynamic" | "follow";
+}
+
+export function FlightVisualizer({
+ rocket,
+ state,
+ cameraMode,
+}: FlightVisualizerProps) {
+ // Atmosphere gradient based on altitude: fades to black at 80km
+ const getAtmosphereOpacity = () => {
+ return Math.max(0, 1 - state.altitude / 80000);
+ };
+
+ // Map physical coords to SVG viewbox
+ const aspect =
+ typeof window !== "undefined" ? window.innerWidth / window.innerHeight : 2;
+ const minVHeight = cameraMode === "dynamic" ? 2500 : 500;
+ const viewHeight =
+ cameraMode === "dynamic"
+ ? Math.max(minVHeight, state.altitude * 0.8 + 500)
+ : minVHeight;
+ const viewWidth = viewHeight * aspect;
+
+ const physX = state.downrange || 0;
+ const physY = state.altitude || 0;
+
+ // Center camera on rocket
+ const minY =
+ cameraMode === "dynamic"
+ ? -physY - viewHeight * 0.35
+ : -physY - viewHeight * 0.6;
+ const minX = physX - viewWidth * 0.5;
+
+ const viewBox = `${minX} ${minY} ${viewWidth} ${viewHeight}`;
+
+ const trailPoints = (state.history || [])
+ .map((p) => `${p.downrange},${-p.altitude}`)
+ .join(" ");
+
+ // visualScale controls the visual size of the rocket.
+ const visualScale =
+ cameraMode === "dynamic"
+ ? Math.max(1, Math.pow(viewHeight / minVHeight, 0.95)) * 1.5
+ : 3.5;
+
+ const getStageOffset = (stageIndex: number) => {
+ if (state.currentStage <= stageIndex) return 0;
+ const sepEventName = `stageSep${stageIndex + 1}`;
+ const sepEvent = state.events.find((e) => e.name === sepEventName);
+ if (!sepEvent) return 0;
+ const dt = state.time - sepEvent.time;
+ if (dt < 0) return 0;
+ // Quadratic falling back effect
+ return (dt * 15 + 0.5 * 10 * Math.pow(Math.min(dt, 20), 2)) * visualScale;
+ };
+
+ // Custom render for each rocket type
+ const renderRocketSvg = (id: string, scale: number) => {
+ let activeEngineY = 0;
+ let nodes: ReactNode = null;
+
+ if (id === "saturnv") {
+ const w = 10 * scale;
+ const l = 111 * scale;
+
+ const s3Bottom = l * 0.3;
+ const s2Bottom = l * 0.7;
+ const s1Bottom = l;
+
+ if (state.currentStage === 0) activeEngineY = s1Bottom;
+ else if (state.currentStage === 1) activeEngineY = s2Bottom;
+ else activeEngineY = s3Bottom;
+
+ const offset1 = getStageOffset(0);
+ const offset2 = getStageOffset(1);
+
+ nodes = (
+ <>
+ {/* Stage 3 + Apollo */}
+
+
+ {/* Decal */}
+
+
+
+ {/* Stage 2 */}
+
+
+
+
+
+ {/* Stage 1 */}
+
+
+
+
+ >
+ );
+ } else if (id === "starship") {
+ const w = 9 * scale;
+ const l = 120 * scale;
+
+ const s2Bottom = l * 0.4;
+ const s1Bottom = l;
+
+ if (state.currentStage === 0) activeEngineY = s1Bottom;
+ else activeEngineY = s2Bottom;
+
+ const offset1 = getStageOffset(0);
+
+ nodes = (
+ <>
+ {/* Stage 2 (Starship) */}
+
+
+
+
+
+
+
+
+ {/* Stage 1 (Super Heavy) */}
+
+
+
+
+ >
+ );
+ } else {
+ // Falcon 9 default
+ const w = 3.7 * scale;
+ const l = 70 * scale;
+
+ const s2Bottom = l * 0.35;
+ const s1Bottom = l;
+
+ if (state.currentStage === 0) activeEngineY = s1Bottom;
+ else activeEngineY = s2Bottom;
+
+ const offset1 = getStageOffset(0);
+
+ nodes = (
+ <>
+ {/* Stage 2 + Fairing */}
+
+
+
+
+ {/* Stage 1 */}
+
+
+ {/* Interstage (top of Stage 1) */}
+
+ {/* Landing Legs */}
+
+
+
+ >
+ );
+ }
+
+ return { activeEngineY, nodes };
+ };
+
+ const getFlameScale = () => {
+ if (rocket.id === "saturnv") return 11 * visualScale;
+ if (rocket.id === "starship") return 10 * visualScale;
+ return 4 * visualScale; // falcon 9
+ };
+
+ const flameWidth = getFlameScale();
+ const { activeEngineY, nodes } = renderRocketSvg(rocket.id, visualScale);
+
+ return (
+
+ {/* Atmosphere background */}
+
+
+ {/* Stars - Deep layer */}
+
+ {/* Stars - Mid layer */}
+
+ {/* Stars - Close layer */}
+
+
+ {/* Screen Effects (Cinematic Vignette) */}
+
+
+
+
+ );
+}
diff --git a/docs/火箭发送模拟/src/components/Navball.tsx b/docs/火箭发送模拟/src/components/Navball.tsx
new file mode 100644
index 0000000..b501f06
--- /dev/null
+++ b/docs/火箭发送模拟/src/components/Navball.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+
+interface NavballProps {
+ pitch: number; // 90 is up, 0 is horizontal, -90 is down
+}
+
+export function Navball({ pitch }: NavballProps) {
+ // For SpaceX broadcast style, we show a rocket silhouette that rotates based on pitch.
+ // pitch 90 = pointing straight up = 0 deg rotation
+ // pitch 0 = pointing horizontal = 90 deg rotation
+ const rotation = 90 - pitch;
+
+ return (
+
+
+ {/* Horizontal reference line */}
+
+ {/* Vertical reference line */}
+
+
+ {/* Rocket outline rotating */}
+
+ {/* Simple rocket silhouette */}
+
+
+
+
+ PITCH {Math.round(pitch)}°
+
+
+ );
+}
diff --git a/docs/火箭发送模拟/src/components/TelemetryGauge.tsx b/docs/火箭发送模拟/src/components/TelemetryGauge.tsx
new file mode 100644
index 0000000..d5aaa41
--- /dev/null
+++ b/docs/火箭发送模拟/src/components/TelemetryGauge.tsx
@@ -0,0 +1,68 @@
+import React from "react";
+
+interface TelemetryGaugeProps {
+ label: string;
+ value: number;
+ unit: string;
+ maxValue: number;
+}
+
+export function TelemetryGauge({
+ label,
+ value,
+ unit,
+ maxValue,
+}: TelemetryGaugeProps) {
+ const radius = 32;
+ const stroke = 3;
+ const normalizedRadius = radius - stroke * 2;
+ const circumference = normalizedRadius * 2 * Math.PI;
+ const fillRatio = Math.min(Math.max(value / maxValue, 0), 1);
+ const strokeDashoffset = circumference - fillRatio * circumference;
+
+ return (
+
+
+
+
+
+ {Math.floor(value).toLocaleString()}
+
+
+ {unit}
+
+
+
+
+ {label}
+
+
+ );
+}
diff --git a/docs/火箭发送模拟/src/i18n.ts b/docs/火箭发送模拟/src/i18n.ts
new file mode 100644
index 0000000..a80f361
--- /dev/null
+++ b/docs/火箭发送模拟/src/i18n.ts
@@ -0,0 +1,78 @@
+export const i18n = {
+ en: {
+ appTitle: "Orbital Ascent",
+ appSub: "Orbital Launch Telemetry",
+ directives: "Flight Directives",
+ launch: "LAUNCH",
+ pause: "PAUSE",
+ throttle: "Engine Throttle",
+ twr: "TWR",
+ mach: "MACH",
+ gforce: "G-FORCE",
+ maxT: "MAX T",
+ altProfile: "Altitude Profile",
+ velProfile: "Velocity Profile",
+ qProfile: "Dynamic Pressure (Q)",
+ speed: "SPEED",
+ altitude: "ALTITUDE",
+ hudThrottle: "THROTTLE",
+ hudProp: "PROPELLANT",
+ liftoff: "Liftoff",
+ maxq: "Max-Q",
+ meco: "MECO",
+ karman: "Karman Line",
+ stageSep1: "Stage 1 Sep",
+ stageSep2: "Stage 2 Sep",
+ stageSep3: "Stage 3 Sep",
+ orbit: "Orbital Insertion",
+ simEnded: "Simulation Ended",
+ vehicleParams: "Vehicle Parameters",
+ dryMass: "Dry Mass",
+ fuelMass: "Propellant",
+ maxThrust: "Max Thrust",
+ isp: "Vacuum ISP",
+ engines: "Engines",
+ cameraDynamic: "Dynamic Cam",
+ cameraFollow: "Follow Cam",
+ ended: "ENDED",
+ },
+ zh: {
+ appTitle: "火箭发射模拟器",
+ appSub: "轨道发射实时遥测",
+ directives: "飞行控制",
+ launch: "发射",
+ pause: "暂停",
+ throttle: "主引擎推力调节",
+ twr: "推重比 (TWR)",
+ mach: "马赫数 (MACH)",
+ gforce: "G力 (G-FORCE)",
+ maxT: "最大推力",
+ altProfile: "高度-时间曲线",
+ velProfile: "速度-时间曲线",
+ qProfile: "空气动压 (Q)",
+ speed: "速度",
+ altitude: "高度",
+ hudThrottle: "当前推力",
+ hudProp: "推进剂",
+ liftoff: "起飞",
+ maxq: "最大动压",
+ meco: "引擎熄火",
+ karman: "卡门线",
+ stageSep1: "一级分离",
+ stageSep2: "二级分离",
+ stageSep3: "三级分离",
+ orbit: "入轨",
+ simEnded: "结束",
+ vehicleParams: "载具参数",
+ dryMass: "干质量",
+ fuelMass: "燃料质量",
+ maxThrust: "最大推力",
+ isp: "真空比冲",
+ engines: "发动机数量",
+ cameraDynamic: "动态视角",
+ cameraFollow: "跟随视角",
+ ended: "已结束",
+ },
+};
+
+export type Lang = "en" | "zh";
diff --git a/docs/火箭发送模拟/src/index.css b/docs/火箭发送模拟/src/index.css
new file mode 100644
index 0000000..c395a53
--- /dev/null
+++ b/docs/火箭发送模拟/src/index.css
@@ -0,0 +1,20 @@
+@import "tailwindcss";
+
+@layer utilities {
+ .custom-scrollbar::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ .custom-scrollbar::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ .custom-scrollbar::-webkit-scrollbar-thumb {
+ background: #334155;
+ border-radius: 10px;
+ }
+
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
+ background: #475569;
+ }
+}
diff --git a/docs/火箭发送模拟/src/lib/utils.ts b/docs/火箭发送模拟/src/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/docs/火箭发送模拟/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/docs/火箭发送模拟/src/main.tsx b/docs/火箭发送模拟/src/main.tsx
new file mode 100644
index 0000000..ef474bf
--- /dev/null
+++ b/docs/火箭发送模拟/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App.tsx";
+import "./index.css";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/docs/火箭发送模拟/src/types.ts b/docs/火箭发送模拟/src/types.ts
new file mode 100644
index 0000000..5c888ba
--- /dev/null
+++ b/docs/火箭发送模拟/src/types.ts
@@ -0,0 +1,140 @@
+export interface RocketStage {
+ name: string;
+ dryMass: number; // kg
+ fuelMass: number; // kg
+ maxThrust: number; // N
+ isp: number; // seconds
+ engines: number;
+}
+
+export interface RocketPreset {
+ id: string;
+ name: string;
+ stages: RocketStage[];
+ area: number; // m^2
+ cd: number; // drag coefficient
+ color: string;
+}
+
+export interface SimulationState {
+ time: number; // s
+ altitude: number; // m
+ downrange: number; // m
+ velocity: number; // m/s
+ vx: number;
+ vy: number;
+ pitch: number; // degrees, 90 is straight up
+ acceleration: number; // m/s^2
+ q: number; // dynamic pressure (Pa)
+ currentStage: number; // index of active stage
+ fuel: number; // kg of current active stage
+ throttle: number; // 0 to 1
+ isRunning: boolean;
+ history: DataPoint[]; // For charts
+ events: { name: string; time: number }[];
+}
+
+export interface DataPoint {
+ time: number;
+ altitude: number; // m
+ downrange: number; // m
+ velocity: number; // m/s
+ acceleration: number; // m/s^2
+ thrust: number; // N
+ q: number; // Pa
+}
+
+export const CONSTANTS = {
+ G: 6.6743e-11,
+ M_EARTH: 5.972e24,
+ R_EARTH: 6371000,
+ RHO_0: 1.225, // Sea level density kg/m^3
+ H: 8500, // Scale height m
+ g0: 9.80665, // Standard gravity
+};
+
+export const PRESETS: RocketPreset[] = [
+ {
+ id: "falcon9",
+ name: "Falcon 9",
+ stages: [
+ {
+ name: "Stage 1",
+ dryMass: 25600,
+ fuelMass: 411000,
+ maxThrust: 7607000,
+ isp: 282,
+ engines: 9,
+ },
+ {
+ name: "Stage 2",
+ dryMass: 4000,
+ fuelMass: 107500,
+ maxThrust: 981000,
+ isp: 348,
+ engines: 1,
+ },
+ ],
+ area: 10.75,
+ cd: 0.4,
+ color: "#ffffff", // falcon 9 white
+ },
+ {
+ id: "saturnv",
+ name: "Saturn V",
+ stages: [
+ {
+ name: "S-IC (Stage 1)",
+ dryMass: 131000,
+ fuelMass: 2149000,
+ maxThrust: 35100000,
+ isp: 263,
+ engines: 5,
+ },
+ {
+ name: "S-II (Stage 2)",
+ dryMass: 36000,
+ fuelMass: 444000,
+ maxThrust: 5100000,
+ isp: 421,
+ engines: 5,
+ },
+ {
+ name: "S-IVB (Stage 3)",
+ dryMass: 10000,
+ fuelMass: 109000,
+ maxThrust: 1000000,
+ isp: 421,
+ engines: 1,
+ },
+ ],
+ area: 80,
+ cd: 0.5,
+ color: "#f59e0b", // amber-500
+ },
+ {
+ id: "starship",
+ name: "Starship",
+ stages: [
+ {
+ name: "Super Heavy",
+ dryMass: 200000,
+ fuelMass: 3400000,
+ maxThrust: 74000000,
+ isp: 330,
+ engines: 33,
+ },
+ {
+ name: "Starship",
+ dryMass: 100000,
+ fuelMass: 1200000,
+ maxThrust: 15000000,
+ isp: 380,
+ engines: 6,
+ },
+ ],
+ area: 63.6,
+ cd: 0.4,
+ color: "#a8a29e", // stone-400
+ },
+];
diff --git a/docs/火箭发送模拟/src/useSimulation.ts b/docs/火箭发送模拟/src/useSimulation.ts
new file mode 100644
index 0000000..ab84b45
--- /dev/null
+++ b/docs/火箭发送模拟/src/useSimulation.ts
@@ -0,0 +1,332 @@
+import { useState, useEffect, useRef, useCallback } from "react";
+import { RocketPreset, SimulationState, DataPoint, CONSTANTS } from "./types";
+
+export function useSimulation(initialPreset: RocketPreset) {
+ const [rocket, setRocket] = useState(initialPreset);
+ const [state, setState] = useState({
+ time: 0,
+ altitude: 0,
+ downrange: 0,
+ velocity: 0,
+ vx: 0,
+ vy: 0,
+ pitch: 90,
+ acceleration: 0,
+ q: 0,
+ fuel: initialPreset.stages[0].fuelMass,
+ currentStage: 0,
+ throttle: 0,
+ isRunning: false,
+ history: [],
+ events: [],
+ });
+
+ const stateRef = useRef(state);
+ const rocketRef = useRef(rocket);
+
+ const lastTimeRef = useRef(0);
+ const frameRef = useRef(0);
+ const maxQRef = useRef(0);
+
+ const setThrottle = useCallback((val: number) => {
+ setState((prev) => {
+ const newState = { ...prev, throttle: Math.max(0, Math.min(1, val)) };
+ stateRef.current = newState;
+ return newState;
+ });
+ }, []);
+
+ const changePreset = useCallback((preset: RocketPreset) => {
+ maxQRef.current = 0;
+ setRocket(preset);
+ rocketRef.current = preset;
+ const newState = {
+ time: 0,
+ altitude: 0,
+ downrange: 0,
+ velocity: 0,
+ vx: 0,
+ vy: 0,
+ pitch: 90,
+ acceleration: 0,
+ q: 0,
+ currentStage: 0,
+ fuel: preset.stages[0].fuelMass,
+ throttle: 0,
+ isRunning: false,
+ history: [],
+ events: [],
+ };
+ stateRef.current = newState;
+ setState(newState);
+ }, []);
+
+ const reset = useCallback(() => {
+ maxQRef.current = 0;
+ const newState = {
+ time: 0,
+ altitude: 0,
+ downrange: 0,
+ velocity: 0,
+ vx: 0,
+ vy: 0,
+ pitch: 90,
+ acceleration: 0,
+ q: 0,
+ currentStage: 0,
+ fuel: rocketRef.current.stages[0].fuelMass,
+ throttle: 0,
+ isRunning: false,
+ history: [],
+ events: [],
+ };
+ stateRef.current = newState;
+ setState(newState);
+ }, []);
+
+ const toggleSimulation = useCallback(() => {
+ setState((prev) => {
+ const isFinished = prev.events.some((e) => e.name === "simEnded");
+ if (isFinished) return prev;
+ const isRunning = !prev.isRunning;
+ if (isRunning) {
+ lastTimeRef.current = performance.now();
+ }
+ const newState = {
+ ...prev,
+ isRunning,
+ throttle: isRunning && prev.throttle === 0 ? 1 : prev.throttle,
+ };
+ stateRef.current = newState;
+ return newState;
+ });
+ }, []);
+
+ useEffect(() => {
+ const loop = (time: number) => {
+ if (!stateRef.current.isRunning) {
+ frameRef.current = requestAnimationFrame(loop);
+ return;
+ }
+
+ const { G, M_EARTH, R_EARTH, RHO_0, H, g0 } = CONSTANTS;
+ const dtSystem = (time - lastTimeRef.current) / 1000;
+ lastTimeRef.current = time;
+
+ // Physics Sub-stepping to prevent Euler integration explosion
+ const PHYSICS_STEPS = 5;
+ const dt = Math.min(dtSystem, 0.1) / PHYSICS_STEPS;
+
+ let {
+ time: t,
+ altitude: h,
+ downrange: x,
+ vx,
+ vy,
+ pitch,
+ currentStage,
+ fuel,
+ throttle,
+ history,
+ } = stateRef.current;
+ const { area, cd, stages, payloadMass } = rocketRef.current;
+ const stage = stages[currentStage];
+ const { dryMass, maxThrust, isp } = stage;
+
+ let upperStagesMass = 0;
+ for (let i = currentStage + 1; i < stages.length; i++) {
+ upperStagesMass += stages[i].dryMass + stages[i].fuelMass;
+ }
+
+ const mDotMax = maxThrust / (isp * g0);
+ let actualThrottle = throttle;
+ let currentEvents = [...stateRef.current.events];
+ let currentQ = stateRef.current.q;
+ let a = stateRef.current.acceleration;
+ let finalThrust = 0;
+
+ for (let step = 0; step < PHYSICS_STEPS; step++) {
+ const m = dryMass + fuel + upperStagesMass + (payloadMass || 0);
+
+ if (fuel <= 0 && currentStage < stages.length - 1) {
+ currentStage++;
+ fuel = stages[currentStage].fuelMass;
+ if (!currentEvents.find((e) => e.name === `stageSep${currentStage}`)) {
+ currentEvents.push({ name: `stageSep${currentStage}`, time: t });
+ }
+ } else if (fuel <= 0) {
+ actualThrottle = 0;
+ fuel = 0;
+ }
+
+ const currentThrust = maxThrust * actualThrottle;
+ finalThrust = currentThrust;
+ const mDot = mDotMax * actualThrottle;
+
+ if (actualThrottle > 0) {
+ fuel -= mDot * dt;
+ if (fuel < 0) fuel = 0;
+ }
+
+ let v = Math.sqrt(vx * vx + vy * vy);
+
+ // Improved pitch program to prevent deep atmospheric dips and orbit bounce
+ const pitching = h > 500 && v > 50;
+ if (pitching) {
+ // Smooth horizontal transition aiming for 0 pitch at ~120km
+ pitch = Math.max(0, 90 * (1 - Math.min(1, (h - 500) / 120000)));
+ }
+ if (h <= 500) {
+ pitch = 90;
+ }
+
+ const pitchRad = (pitch * Math.PI) / 180;
+
+ const fg = (G * (M_EARTH * m)) / Math.pow(R_EARTH + h, 2);
+ const centrifugalY = (m * vx * vx) / (R_EARTH + h);
+ const rho = h > 100000 ? 0 : RHO_0 * Math.exp(-h / H);
+
+ // Dynamic pressure (q) in Pa
+ currentQ = 0.5 * rho * v * v;
+ let fd = currentQ * cd * area;
+
+ // Prevent drag from reversing velocity in a single step (numerical stability)
+ const maxDragForce = (m * v) / dt;
+ if (fd > maxDragForce * 0.9) fd = maxDragForce * 0.9;
+
+ const thrustX = currentThrust * Math.cos(pitchRad);
+ const thrustY = currentThrust * Math.sin(pitchRad);
+
+ const vDirX = v > 0 ? vx / v : 0;
+ const vDirY = v > 0 ? vy / v : 1;
+
+ const dragX = fd * vDirX;
+ const dragY = fd * vDirY;
+
+ const netForceX = thrustX - dragX;
+ const netForceY = thrustY - dragY - fg + centrifugalY;
+
+ let ax = netForceX / m;
+ let ay = netForceY / m;
+
+ // Ground constraint
+ if (h <= 0 && vy <= 0 && netForceY <= 0) {
+ ay = 0;
+ ax = 0;
+ vx = 0;
+ vy = 0;
+ h = 0;
+ pitch = 90;
+ }
+
+ vx += ax * dt;
+ vy += ay * dt;
+ x += vx * dt;
+ h += vy * dt;
+ if (h < 0) h = 0;
+ t += dt;
+ a = Math.sqrt(ax * ax + ay * ay);
+ }
+
+ let v = Math.sqrt(vx * vx + vy * vy);
+
+ // Event Tracking
+ if (h > 10 && v > 1 && !currentEvents.find((e) => e.name === "liftoff")) {
+ currentEvents.push({ name: "liftoff", time: t });
+ }
+ if (currentQ > maxQRef.current) {
+ maxQRef.current = currentQ;
+ }
+ // Trigger Max-Q if it drops to 95% of peak after reaching at least 20kPa
+ if (
+ maxQRef.current > 20000 &&
+ currentQ < maxQRef.current * 0.95 &&
+ !currentEvents.find((e) => e.name === "maxq")
+ ) {
+ currentEvents.push({ name: "maxq", time: t });
+ }
+
+ if (
+ currentStage === stages.length - 1 &&
+ fuel <= 0 &&
+ t > 10 &&
+ !currentEvents.find((e) => e.name === "meco")
+ ) {
+ currentEvents.push({ name: "meco", time: t });
+ }
+ if (h >= 100000 && !currentEvents.find((e) => e.name === "karman")) {
+ currentEvents.push({ name: "karman", time: t });
+ }
+ if (vx >= 7800 && !currentEvents.find((e) => e.name === "orbit")) {
+ currentEvents.push({ name: "orbit", time: t });
+ }
+
+ let shouldEnd = false;
+ const mecoEvent = currentEvents.find((e) => e.name === "meco");
+ if (
+ mecoEvent &&
+ t > mecoEvent.time + 3 &&
+ !currentEvents.find((e) => e.name === "simEnded")
+ ) {
+ currentEvents.push({ name: "simEnded", time: t });
+ shouldEnd = true;
+ }
+
+ const lastHistoryTime =
+ history.length > 0 ? history[history.length - 1].time : -1;
+ let newHistory = history;
+ if (t - lastHistoryTime >= 0.5) {
+ newHistory = [
+ ...history,
+ {
+ time: t,
+ altitude: h,
+ downrange: x,
+ velocity: v,
+ acceleration: a,
+ thrust: finalThrust,
+ q: currentQ,
+ },
+ ];
+ }
+
+ const newState = {
+ ...stateRef.current,
+ time: t,
+ altitude: h,
+ downrange: x,
+ velocity: v,
+ vx,
+ vy,
+ pitch,
+ acceleration: a,
+ q: currentQ,
+ currentStage,
+ fuel,
+ throttle:
+ fuel <= 0 && currentStage === stages.length - 1 ? 0 : throttle,
+ isRunning: shouldEnd ? false : stateRef.current.isRunning,
+ history: newHistory,
+ events: currentEvents,
+ };
+
+ stateRef.current = newState;
+ setState(newState);
+
+ frameRef.current = requestAnimationFrame(loop);
+ };
+
+ frameRef.current = requestAnimationFrame(loop);
+
+ return () => cancelAnimationFrame(frameRef.current);
+ }, []);
+
+ return {
+ rocket,
+ state,
+ setThrottle,
+ changePreset,
+ toggleSimulation,
+ reset,
+ };
+}
diff --git a/docs/火箭发送模拟/tsconfig.json b/docs/火箭发送模拟/tsconfig.json
new file mode 100644
index 0000000..d88f175
--- /dev/null
+++ b/docs/火箭发送模拟/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ "module": "ESNext",
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ },
+ "allowImportingTsExtensions": true,
+ "noEmit": true
+ }
+}
diff --git a/docs/火箭发送模拟/vite.config.ts b/docs/火箭发送模拟/vite.config.ts
new file mode 100644
index 0000000..fc23e76
--- /dev/null
+++ b/docs/火箭发送模拟/vite.config.ts
@@ -0,0 +1,22 @@
+import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+import {defineConfig} from 'vite';
+
+export default defineConfig(() => {
+ return {
+ plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, '.'),
+ },
+ },
+ server: {
+ // HMR is disabled in AI Studio via DISABLE_HMR env var.
+ // Do not modifyâfile watching is disabled to prevent flickering during agent edits.
+ hmr: process.env.DISABLE_HMR !== 'true',
+ // Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
+ watch: process.env.DISABLE_HMR === 'true' ? null : {},
+ },
+ };
+});
diff --git a/frontend/package.json b/frontend/package.json
index 08bcc4f..e14801f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -34,6 +34,7 @@
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.0",
+ "baseline-browser-mapping": "^2.10.43",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b297dfb..47e32fb 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -14,6 +14,7 @@ import { TimelineController } from './components/TimelineController';
import { Loading } from './components/Loading';
import { ControlPanel } from './components/ControlPanel';
import { AuthModal } from './components/AuthModal';
+import { BodyDetailOverlay } from './components/BodyDetailOverlay';
import { auth } from './utils/auth';
import type { CelestialBody } from './types';
import { useToast } from './contexts/ToastContext';
@@ -21,7 +22,6 @@ import { useToast } from './contexts/ToastContext';
// Lazy load non-critical components for better performance
const InterstellarTicker = lazy(() => import('./components/InterstellarTicker').then(m => ({ default: m.InterstellarTicker })));
const MessageBoard = lazy(() => import('./components/MessageBoard').then(m => ({ default: m.MessageBoard })));
-const BodyDetailOverlay = lazy(() => import('./components/BodyDetailOverlay').then(m => ({ default: m.BodyDetailOverlay })));
// Timeline configuration - will be fetched from backend later
const TIMELINE_DAYS = 30; // Total days in timeline range
@@ -223,6 +223,7 @@ function App() {
setViewMode(prev => prev === 'solar' ? 'galaxy' : 'solar');
setSelectedBody(null); // Reset selection when switching
}}
+ onOpenRocketSimulator={() => navigate('/rocket-simulator')}
/>
{/* Auth Modal */}
diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx
index 1a5322a..3243ab5 100644
--- a/frontend/src/Router.tsx
+++ b/frontend/src/Router.tsx
@@ -1,25 +1,37 @@
/**
* Router configuration
*/
+import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
-import { Login } from './pages/Login';
-import { AdminLayout } from './pages/admin/AdminLayout';
-import { Dashboard } from './pages/admin/Dashboard';
-import { CelestialBodies } from './pages/admin/CelestialBodies';
-import { CelestialEvents } from './pages/admin/CelestialEvents';
-import { StarSystems } from './pages/admin/StarSystems';
-import { StaticData } from './pages/admin/StaticData';
-import { Users } from './pages/admin/Users';
-import { NASADownload } from './pages/admin/NASADownload';
-import { SystemSettings } from './pages/admin/SystemSettings';
-import { Tasks } from './pages/admin/Tasks';
-import { ScheduledJobs } from './pages/admin/ScheduledJobs';
-import { UserProfile } from './pages/admin/UserProfile';
-import { ChangePassword } from './pages/admin/ChangePassword';
-import { MyCelestialBodies } from './pages/admin/MyCelestialBodies';
import { auth } from './utils/auth';
import { ToastProvider } from './contexts/ToastContext';
-import App from './App';
+
+const App = lazy(() => import('./App'));
+const Login = lazy(() => import('./pages/Login').then((module) => ({ default: module.Login })));
+const RocketSimulator = lazy(() => import('./pages/RocketSimulator').then((module) => ({ default: module.RocketSimulator })));
+const AdminLayout = lazy(() => import('./pages/admin/AdminLayout').then((module) => ({ default: module.AdminLayout })));
+const Dashboard = lazy(() => import('./pages/admin/Dashboard').then((module) => ({ default: module.Dashboard })));
+const CelestialBodies = lazy(() => import('./pages/admin/CelestialBodies').then((module) => ({ default: module.CelestialBodies })));
+const CelestialEvents = lazy(() => import('./pages/admin/CelestialEvents').then((module) => ({ default: module.CelestialEvents })));
+const StarSystems = lazy(() => import('./pages/admin/StarSystems').then((module) => ({ default: module.StarSystems })));
+const StaticData = lazy(() => import('./pages/admin/StaticData').then((module) => ({ default: module.StaticData })));
+const Users = lazy(() => import('./pages/admin/Users').then((module) => ({ default: module.Users })));
+const NASADownload = lazy(() => import('./pages/admin/NASADownload').then((module) => ({ default: module.NASADownload })));
+const SystemSettings = lazy(() => import('./pages/admin/SystemSettings').then((module) => ({ default: module.SystemSettings })));
+const Tasks = lazy(() => import('./pages/admin/Tasks').then((module) => ({ default: module.Tasks })));
+const ScheduledJobs = lazy(() => import('./pages/admin/ScheduledJobs').then((module) => ({ default: module.ScheduledJobs })));
+const UserProfile = lazy(() => import('./pages/admin/UserProfile').then((module) => ({ default: module.UserProfile })));
+const ChangePassword = lazy(() => import('./pages/admin/ChangePassword').then((module) => ({ default: module.ChangePassword })));
+const MyCelestialBodies = lazy(() => import('./pages/admin/MyCelestialBodies').then((module) => ({ default: module.MyCelestialBodies })));
+const Rockets = lazy(() => import('./pages/admin/Rockets').then((module) => ({ default: module.Rockets })));
+
+function RouteFallback() {
+ return (
+
+ );
+}
// Protected Route wrapper
function ProtectedRoute({ children }: { children: React.ReactNode }) {
@@ -33,12 +45,14 @@ export function Router() {
return (
-
+ }>
+
{/* Public routes */}
} />
{/* Main app (3D visualization) */}
} />
+ } />
{/* User routes (protected) */}
} />
} />
} />
+ } />
{/* Fallback */}
} />
-
+
+
);
diff --git a/frontend/src/components/AsteroidBelts.tsx b/frontend/src/components/AsteroidBelts.tsx
index 06ccd03..4474b4b 100644
--- a/frontend/src/components/AsteroidBelts.tsx
+++ b/frontend/src/components/AsteroidBelts.tsx
@@ -15,13 +15,6 @@ interface BeltProps {
rotationSpeed: number;
}
-interface BeltData {
- category: string;
- name: string;
- name_zh: string;
- data: BeltProps;
-}
-
function Belt({
innerRadiusAU,
outerRadiusAU,
diff --git a/frontend/src/components/BodyDetailOverlay.tsx b/frontend/src/components/BodyDetailOverlay.tsx
index 7652fcc..a60fb56 100644
--- a/frontend/src/components/BodyDetailOverlay.tsx
+++ b/frontend/src/components/BodyDetailOverlay.tsx
@@ -39,20 +39,30 @@ export function BodyDetailOverlay({ bodyId, preloadedData, onClose }: BodyDetail
return;
}
+ // Guard against out-of-order responses and setState-after-unmount.
+ let active = true;
setLoading(true);
request.get(`/celestial/info/${bodyId}`)
.then(response => {
- setBodyData(response.data);
+ if (active) setBodyData(response.data);
})
.catch(error => {
+ if (!active) return;
console.error("Failed to fetch body details:", error);
toast.error("加载天体详情失败");
onClose(); // Close overlay on error
})
.finally(() => {
- setLoading(false);
+ if (active) setLoading(false);
});
- }, [bodyId, preloadedData, onClose, toast]);
+
+ return () => {
+ active = false;
+ };
+ // Only refetch when the target body changes. `onClose`/`toast` are stable in
+ // behavior but not identity, so excluding them avoids refetch on every parent render.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [bodyId, preloadedData]);
if ((!bodyId && !preloadedData) || !bodyData) {
return null;
diff --git a/frontend/src/components/BodyViewer.tsx b/frontend/src/components/BodyViewer.tsx
index 03fe8c5..1b789eb 100644
--- a/frontend/src/components/BodyViewer.tsx
+++ b/frontend/src/components/BodyViewer.tsx
@@ -1,12 +1,13 @@
import { useRef, useMemo, useState, useEffect, Suspense } from 'react';
import { Mesh, Group, DoubleSide } from 'three';
import * as THREE from 'three';
-import { useGLTF, useTexture } from '@react-three/drei';
+import { useGLTF } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import type { CelestialBody as CelestialBodyType } from '../types';
import { fetchBodyResources } from '../utils/api';
import { getCelestialSize } from '../config/celestialSizes';
import { useSystemSetting } from '../hooks/useSystemSetting';
+import { useOptionalTexture } from '../hooks/useOptionalTexture';
interface BodyViewerProps {
body: CelestialBodyType;
@@ -15,7 +16,7 @@ interface BodyViewerProps {
// Planetary Rings component - handles texture-based rings
function PlanetaryRings({ texturePath, planetRadius }: { texturePath?: string | null, planetRadius: number }) {
- const texture = texturePath ? useTexture(texturePath) : null;
+ const texture = useOptionalTexture(texturePath);
const meshRef = useRef(null);
// Dynamic ring dimensions based on planet size
@@ -273,6 +274,142 @@ function ProbeModelViewer({ modelPath, modelScale }: { modelPath: string; modelS
);
}
+// Irregular Comet Nucleus - potato-like shape (module scope to avoid remount每次渲染)
+function IrregularNucleus({ size, texture }: { size: number; texture: THREE.Texture | null }) {
+ const nucleusRef = useRef(null);
+
+ // Create irregular geometry by deforming a sphere
+ const geometry = useMemo(() => {
+ const geo = new THREE.SphereGeometry(size, 64, 64); // Higher resolution for detail view
+ const positions = geo.attributes.position;
+
+ // Deform vertices to create irregular shape
+ for (let i = 0; i < positions.count; i++) {
+ const x = positions.getX(i);
+ const y = positions.getY(i);
+ const z = positions.getZ(i);
+
+ // Create multiple noise frequencies for complex shape
+ const noise1 = Math.sin(x * 3.5) * Math.cos(y * 2.7) * Math.sin(z * 4.2);
+ const noise2 = Math.cos(x * 5.1) * Math.sin(y * 4.3) * Math.cos(z * 3.8);
+ const noise3 = Math.sin(x * 7.2) * Math.sin(y * 6.1) * Math.cos(z * 5.5);
+
+ const deformation = 1 + (noise1 * 0.25 + noise2 * 0.15 + noise3 * 0.1);
+
+ positions.setXYZ(i, x * deformation, y * deformation, z * deformation);
+ }
+
+ geo.computeVertexNormals();
+ return geo;
+ }, [size]);
+
+ // Slow rotation
+ useFrame((_, delta) => {
+ if (nucleusRef.current) {
+ nucleusRef.current.rotation.y += delta * 0.05;
+ nucleusRef.current.rotation.z += delta * 0.02;
+ }
+ });
+
+ return (
+
+ {texture ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+// Enhanced Coma (gas/dust cloud) around comet nucleus (module scope)
+function CometComa({ radius }: { radius: number }) {
+ const positions = useMemo(() => {
+ const count = 300; // More particles for detail view
+ const p = new Float32Array(count * 3);
+ for (let i = 0; i < count; i++) {
+ // Denser near center, spreading outward
+ const r = radius * (0.8 + Math.pow(Math.random(), 1.5) * 4); // 0.8x to 4.8x radius
+ const theta = Math.random() * Math.PI * 2;
+ const phi = Math.acos(2 * Math.random() - 1);
+
+ p[i * 3] = r * Math.sin(phi) * Math.cos(theta);
+ p[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
+ p[i * 3 + 2] = r * Math.cos(phi);
+ }
+ return p;
+ }, [radius]);
+
+ const pointsRef = useRef(null);
+
+ // Animate coma
+ useFrame((state, delta) => {
+ if (pointsRef.current) {
+ pointsRef.current.rotation.y += delta * 0.05;
+ // Pulsing effect
+ const scale = 1 + Math.sin(state.clock.elapsedTime * 0.5) * 0.1;
+ pointsRef.current.scale.setScalar(scale);
+ }
+ });
+
+ return (
+ <>
+ {/* Inner bright coma */}
+
+
+
+
+
+
+
+ {/* Middle glow layer */}
+
+
+
+
+
+ {/* Outer diffuse glow */}
+
+
+
+
+ >
+ );
+}
+
// Sub-component for Planet models
function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePath, ringTexturePath, meshRef, disableGlow = false }: {
body: CelestialBodyType;
@@ -281,10 +418,10 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat
emissiveIntensity: number;
texturePath: string | null;
ringTexturePath?: string | null;
- meshRef: React.RefObject;
+ meshRef: React.RefObject;
disableGlow?: boolean;
}) {
- const texture = texturePath ? useTexture(texturePath) : null;
+ const texture = useOptionalTexture(texturePath);
// Rotation animation
useFrame((_, delta) => {
@@ -293,140 +430,6 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat
}
});
- // Irregular Comet Nucleus - potato-like shape
- function IrregularNucleus({ size, texture }: { size: number; texture: THREE.Texture | null }) {
- const nucleusRef = useRef(null);
-
- // Create irregular geometry by deforming a sphere
- const geometry = useMemo(() => {
- const geo = new THREE.SphereGeometry(size, 64, 64); // Higher resolution for detail view
- const positions = geo.attributes.position;
-
- // Deform vertices to create irregular shape
- for (let i = 0; i < positions.count; i++) {
- const x = positions.getX(i);
- const y = positions.getY(i);
- const z = positions.getZ(i);
-
- // Create multiple noise frequencies for complex shape
- const noise1 = Math.sin(x * 3.5) * Math.cos(y * 2.7) * Math.sin(z * 4.2);
- const noise2 = Math.cos(x * 5.1) * Math.sin(y * 4.3) * Math.cos(z * 3.8);
- const noise3 = Math.sin(x * 7.2) * Math.sin(y * 6.1) * Math.cos(z * 5.5);
-
- const deformation = 1 + (noise1 * 0.25 + noise2 * 0.15 + noise3 * 0.1);
-
- positions.setXYZ(i, x * deformation, y * deformation, z * deformation);
- }
-
- geo.computeVertexNormals();
- return geo;
- }, [size]);
-
- // Slow rotation
- useFrame((_, delta) => {
- if (nucleusRef.current) {
- nucleusRef.current.rotation.y += delta * 0.05;
- nucleusRef.current.rotation.z += delta * 0.02;
- }
- });
-
- return (
-
- {texture ? (
-
- ) : (
-
- )}
-
- );
- }
-
- // Enhanced Coma (gas/dust cloud) around comet nucleus
- function CometComa({ radius }: { radius: number }) {
- const positions = useMemo(() => {
- const count = 300; // More particles for detail view
- const p = new Float32Array(count * 3);
- for (let i = 0; i < count; i++) {
- // Denser near center, spreading outward
- const r = radius * (0.8 + Math.pow(Math.random(), 1.5) * 4); // 0.8x to 4.8x radius
- const theta = Math.random() * Math.PI * 2;
- const phi = Math.acos(2 * Math.random() - 1);
-
- p[i * 3] = r * Math.sin(phi) * Math.cos(theta);
- p[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
- p[i * 3 + 2] = r * Math.cos(phi);
- }
- return p;
- }, [radius]);
-
- const pointsRef = useRef(null);
-
- // Animate coma
- useFrame((state, delta) => {
- if (pointsRef.current) {
- pointsRef.current.rotation.y += delta * 0.05;
- // Pulsing effect
- const scale = 1 + Math.sin(state.clock.elapsedTime * 0.5) * 0.1;
- pointsRef.current.scale.setScalar(scale);
- }
- });
-
- return (
- <>
- {/* Inner bright coma */}
-
-
-
-
-
-
-
- {/* Middle glow layer */}
-
-
-
-
-
- {/* Outer diffuse glow */}
-
-
-
-
- >
- );
- }
-
return (
{/* Use irregular nucleus for comets, regular sphere for others */}
@@ -440,6 +443,7 @@ function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePat
{/* Higher resolution for detail view */}
{texture ? (
) : (
void;
+}
+
+type CelestialTypeConfigs = Record | null;
interface CameraControllerProps {
focusTarget: CelestialBody | null;
@@ -13,6 +21,8 @@ interface CameraControllerProps {
onAnimationComplete?: () => void;
resetTrigger?: number;
defaultPosition?: [number, number, number];
+ typeConfigs?: CelestialTypeConfigs;
+ controlsRef: RefObject;
}
export function CameraController({
@@ -20,83 +30,78 @@ export function CameraController({
allBodies,
onAnimationComplete,
resetTrigger = 0,
- defaultPosition = [10, 8, 10] // Default closer to inner solar system
+ defaultPosition = [10, 8, 10],
+ typeConfigs,
+ controlsRef,
}: CameraControllerProps) {
const { camera } = useThree();
const targetPosition = useRef(new Vector3());
+ const targetLookPosition = useRef(new Vector3());
const isAnimating = useRef(false);
const isResetting = useRef(false);
const animationProgress = useRef(0);
const startPosition = useRef(new Vector3());
+ const startLookPosition = useRef(new Vector3());
+ const trackedLookPosition = useRef(new Vector3());
const lastResetTrigger = useRef(0);
+ // Keep the latest bodies list without making it a trigger for the focus effect.
+ // `allBodies` gets a new array reference on every timeline/historical frame, and we
+ // don't want that to re-fire the fly-to animation while the user is watching playback.
+ const allBodiesRef = useRef(allBodies);
+ allBodiesRef.current = allBodies;
+
// Handle manual reset trigger
useEffect(() => {
if (resetTrigger !== lastResetTrigger.current) {
lastResetTrigger.current = resetTrigger;
// Force reset
targetPosition.current.set(...defaultPosition);
+ targetLookPosition.current.set(0, 0, 0);
startPosition.current.copy(camera.position);
+ startLookPosition.current.copy(controlsRef.current?.target ?? new Vector3());
isAnimating.current = true;
isResetting.current = true;
animationProgress.current = 0;
}
- }, [resetTrigger, camera, defaultPosition]); // Only run when resetTrigger changes
+ }, [resetTrigger, camera, defaultPosition, controlsRef]); // Only run when resetTrigger changes
// Handle focus target changes
+ // Trigger only on the SELECTED body changing (by id), not on every new bodies array.
useEffect(() => {
+ const bodies = allBodiesRef.current;
if (focusTarget) {
isResetting.current = false; // Cancel reset if target selected
-
+
// Focus on target - use smart rendered position
- const renderPos = calculateRenderPosition(focusTarget, allBodies);
+ const latestBody = bodies.find((body) => body.id === focusTarget.id) ?? focusTarget;
+ const renderPos = calculateRenderPosition(latestBody, bodies, typeConfigs);
const currentTargetPos = new Vector3(renderPos.x, renderPos.z, renderPos.y);
+ const bodySize = getCelestialSize(latestBody, undefined, typeConfigs);
+ const parentInfo = findParentPlanet(latestBody, bodies);
+ const focusDistance = latestBody.type === 'star'
+ ? Math.max(5, bodySize * 9)
+ : latestBody.type === 'planet'
+ ? Math.max(4, bodySize * 7)
+ : latestBody.type === 'dwarf_planet'
+ ? Math.max(2.4, bodySize * 8)
+ : parentInfo
+ ? Math.max(1.4, bodySize * 8)
+ : Math.max(1.6, bodySize * 9);
+ const previousLook = controlsRef.current?.target ?? new Vector3();
+ const viewDirection = camera.position.clone().sub(previousLook);
+ if (viewDirection.lengthSq() < 0.0001) viewDirection.set(1, 0.65, 1);
+ viewDirection.normalize();
- // Calculate ideal camera position based on target
- const pos = focusTarget.positions[0];
- const distance = Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2);
- const parentInfo = findParentPlanet(focusTarget, allBodies);
-
- let offset: number;
- let heightMultiplier = 1;
- let sideMultiplier = 1;
-
- if (focusTarget.type === 'planet') {
- offset = 4;
- heightMultiplier = 1.5;
- sideMultiplier = 1;
- } else if (focusTarget.type === 'probe') {
- if (parentInfo) {
- offset = 3;
- heightMultiplier = 0.8;
- sideMultiplier = 1.2;
- } else if (distance < 10) {
- offset = 5;
- heightMultiplier = 0.6;
- sideMultiplier = 1.5;
- } else if (distance > 50) {
- offset = 4;
- heightMultiplier = 0.8;
- sideMultiplier = 1;
- } else {
- offset = 6;
- heightMultiplier = 0.8;
- sideMultiplier = 1.2;
- }
- } else {
- offset = 10;
- heightMultiplier = 1;
- sideMultiplier = 1;
- }
-
- targetPosition.current.set(
- currentTargetPos.x + (offset * sideMultiplier),
- currentTargetPos.y + (offset * heightMultiplier),
- currentTargetPos.z + offset
- );
+ targetLookPosition.current.copy(currentTargetPos);
+ targetPosition.current.copy(currentTargetPos)
+ .addScaledVector(viewDirection, focusDistance)
+ .add(new Vector3(0, focusDistance * 0.12, 0));
// Start animation to target
startPosition.current.copy(camera.position);
+ startLookPosition.current.copy(previousLook);
+ trackedLookPosition.current.copy(currentTargetPos);
isAnimating.current = true;
animationProgress.current = 0;
@@ -108,9 +113,11 @@ export function CameraController({
animationProgress.current = 0;
}
}
- }, [focusTarget, allBodies, camera]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [focusTarget?.id, camera, controlsRef, typeConfigs]);
useFrame((_, delta) => {
+ const orbitControls = controlsRef.current;
if (isAnimating.current) {
// Smooth animation using easing
animationProgress.current += delta * 0.8; // Animation speed
@@ -129,15 +136,24 @@ export function CameraController({
// Interpolate camera position
camera.position.lerpVectors(startPosition.current, targetPosition.current, eased);
- // Look at target - use smart rendered position (only during animation)
- if (focusTarget) {
- const renderPos = calculateRenderPosition(focusTarget, allBodies);
- camera.lookAt(renderPos.x, renderPos.z, renderPos.y);
- } else {
- camera.lookAt(0, 0, 0);
+ if (orbitControls) {
+ orbitControls.target.lerpVectors(startLookPosition.current, targetLookPosition.current, eased);
+ orbitControls.update();
+ }
+ camera.lookAt(targetLookPosition.current);
+ } else if (focusTarget && orbitControls) {
+ const bodies = allBodiesRef.current;
+ const latestBody = bodies.find((body) => body.id === focusTarget.id) ?? focusTarget;
+ const renderPos = calculateRenderPosition(latestBody, bodies, typeConfigs);
+ const nextLook = new Vector3(renderPos.x, renderPos.z, renderPos.y);
+ const movement = nextLook.sub(trackedLookPosition.current);
+ if (movement.lengthSq() > 0.00000001) {
+ camera.position.add(movement);
+ orbitControls.target.add(movement);
+ trackedLookPosition.current.add(movement);
+ orbitControls.update();
}
}
- // After animation completes, OrbitControls will take over
});
return null;
diff --git a/frontend/src/components/CelestialBody.tsx b/frontend/src/components/CelestialBody.tsx
index f2707a9..4df588a 100644
--- a/frontend/src/components/CelestialBody.tsx
+++ b/frontend/src/components/CelestialBody.tsx
@@ -5,24 +5,26 @@ import { useRef, useMemo, useState, useEffect } from 'react';
import { Mesh, DoubleSide } from 'three'; // Removed AdditiveBlending here
import * as THREE from 'three'; // Imported as * to access AdditiveBlending, SpriteMaterial, CanvasTexture
import { useFrame } from '@react-three/fiber';
-import { useTexture, Billboard } from '@react-three/drei';
+import { Billboard } from '@react-three/drei';
import type { CelestialBody as CelestialBodyType } from '../types';
import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition';
import { fetchBodyResources } from '../utils/api';
import { getCelestialSize } from '../config/celestialSizes';
import { createLabelTexture } from '../utils/labelTexture';
+import { useOptionalTexture } from '../hooks/useOptionalTexture';
interface CelestialBodyProps {
body: CelestialBodyType;
allBodies: CelestialBodyType[];
isSelected?: boolean;
+ showLabel?: boolean;
onBodySelect?: (body: CelestialBodyType) => void;
typeConfigs?: any;
}
// Planetary Rings component - handles texture-based rings
function PlanetaryRings({ texturePath, planetRadius }: { texturePath?: string | null, planetRadius: number }) {
- const texture = texturePath ? useTexture(texturePath) : null;
+ const texture = useOptionalTexture(texturePath);
const meshRef = useRef(null);
// Dynamic ring dimensions based on planet size
@@ -87,13 +89,14 @@ function PlanetaryRings({ texturePath, planetRadius }: { texturePath?: string |
}
// Planet component with texture
-function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected = false, onBodySelect, typeConfigs }: {
+function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected = false, showLabel = true, onBodySelect, typeConfigs }: {
body: CelestialBodyType;
size: number;
emissive: string;
emissiveIntensity: number;
allBodies: CelestialBodyType[];
isSelected?: boolean;
+ showLabel?: boolean;
onBodySelect?: (body: CelestialBodyType) => void;
typeConfigs?: any;
}) {
@@ -164,6 +167,7 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
hasOffset={renderPosition.hasOffset}
allBodies={allBodies}
isSelected={isSelected}
+ showLabel={showLabel}
onBodySelect={onBodySelect}
/>;
}
@@ -215,6 +219,7 @@ function IrregularNucleus({ size, texture, onClick }: { size: number; texture: T
>
{texture ? (
) : (
;
+ meshRef: React.RefObject;
hasOffset: boolean;
allBodies: CelestialBodyType[];
isSelected?: boolean;
+ showLabel?: boolean;
onBodySelect?: (body: CelestialBodyType) => void;
}) {
- // Load texture if path is provided
- const texture = texturePath ? useTexture(texturePath) : null;
+ const texture = useOptionalTexture(texturePath);
// Slow rotation for visual effect
useFrame((_, delta) => {
@@ -380,6 +386,7 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur
{texture ? (
) : (
{
if (body.type === 'star') {
return {
@@ -534,6 +537,13 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec
};
}, [body, typeConfigs]);
+ if (!position) return null;
+
+ // Skip probes - they will use 3D models
+ if (body.type === 'probe') {
+ return null;
+ }
+
return (
diff --git a/frontend/src/components/ControlPanel.tsx b/frontend/src/components/ControlPanel.tsx
index 6c3b533..d42adaa 100644
--- a/frontend/src/components/ControlPanel.tsx
+++ b/frontend/src/components/ControlPanel.tsx
@@ -1,6 +1,5 @@
import {
Calendar,
- Orbit,
Volume2,
VolumeX,
MessageSquare,
@@ -9,6 +8,7 @@ import {
Camera,
Globe,
Sparkles
+ ,Rocket
} from 'lucide-react';
interface ControlPanelProps {
@@ -23,6 +23,7 @@ interface ControlPanelProps {
onScreenshot: () => void;
viewMode: 'solar' | 'galaxy';
onToggleViewMode: () => void;
+ onOpenRocketSimulator: () => void;
}
export function ControlPanel({
@@ -37,6 +38,7 @@ export function ControlPanel({
onScreenshot,
viewMode,
onToggleViewMode,
+ onOpenRocketSimulator,
}: ControlPanelProps) {
const buttonClass = (isActive: boolean) => `
p-2 rounded-lg transition-all duration-200 relative group
@@ -50,6 +52,15 @@ export function ControlPanel({
return (
+
+
{/* View Mode Toggle */}
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/Probe.tsx b/frontend/src/components/Probe.tsx
index ab812d4..c5b953c 100644
--- a/frontend/src/components/Probe.tsx
+++ b/frontend/src/components/Probe.tsx
@@ -15,17 +15,18 @@ interface ProbeProps {
body: CelestialBody;
allBodies: CelestialBody[];
isSelected?: boolean;
+ showLabel?: boolean;
onBodySelect?: (body: CelestialBody) => void;
typeConfigs?: any;
}
// Separate component for each probe type to properly use hooks
-function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, resourceScale = 1.0, onBodySelect, typeConfigs }: {
+function ProbeModel({ body, modelPath, allBodies, isSelected = false, showLabel = true, resourceScale = 1.0, onBodySelect, typeConfigs }: {
body: CelestialBody;
modelPath: string;
allBodies: CelestialBody[];
isSelected?: boolean;
- onError: () => void;
+ showLabel?: boolean;
resourceScale?: number;
onBodySelect?: (body: CelestialBody) => void;
typeConfigs?: any;
@@ -111,25 +112,13 @@ function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, r
}
});
- // Render Logic
- if (!scene || !configuredScene) return null;
-
// Calculate ACTUAL distance from Sun (not scaled)
const distance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2);
// Get offset description if this probe has one
const offsetDesc = renderPosition.hasOffset ? getOffsetDescription(body, allBodies) : null;
- // Handle click event
- const handleClick = (e: any) => {
- e.stopPropagation();
- if (onBodySelect) {
- onBodySelect(body);
- }
- };
-
- // Generate label texture
- // eslint-disable-next-line react-hooks/rules-of-hooks
+ // 6. Hook: Label texture (must be called before any early return to keep hook order stable)
const labelTexture = useMemo(() => {
return createLabelTexture(
body.name_zh || body.name,
@@ -139,6 +128,17 @@ function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, r
);
}, [body.name, body.name_zh, offsetDesc, distance]);
+ // Render Logic
+ if (!scene || !configuredScene) return null;
+
+ // Handle click event
+ const handleClick = (e: any) => {
+ e.stopPropagation();
+ if (onBodySelect) {
+ onBodySelect(body);
+ }
+ };
+
return (
{/* Name label with CanvasTexture */}
- {labelTexture && (
+ {showLabel && labelTexture && (
void;
typeConfigs?: any;
}) {
@@ -231,7 +232,7 @@ function ProbeFallback({ body, allBodies, isSelected = false, onBodySelect, type
{/* Name label with CanvasTexture */}
- {labelTexture && (
+ {showLabel && labelTexture && (
(undefined);
const [loadError, setLoadError] = useState(false);
@@ -312,11 +313,9 @@ export function Probe({ body, allBodies, isSelected = false, onBodySelect, typeC
modelPath={modelPath}
allBodies={allBodies}
isSelected={isSelected}
+ showLabel={showLabel}
resourceScale={resourceScale}
onBodySelect={onBodySelect}
- onError={() => {
- setLoadError(true);
- }}
typeConfigs={typeConfigs}
/>;
}
@@ -325,6 +324,7 @@ export function Probe({ body, allBodies, isSelected = false, onBodySelect, typeC
body={body}
allBodies={allBodies}
isSelected={isSelected}
+ showLabel={showLabel}
onBodySelect={onBodySelect}
typeConfigs={typeConfigs}
/>;
diff --git a/frontend/src/components/ProbeList.tsx b/frontend/src/components/ProbeList.tsx
index 29c18c8..9b01047 100644
--- a/frontend/src/components/ProbeList.tsx
+++ b/frontend/src/components/ProbeList.tsx
@@ -18,10 +18,6 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
// Auto-expand the group when a body is selected from the 3D scene
useEffect(() => {
if (selectedBody) {
- // Don't auto-collapse panel if it's already open
- // Only auto-collapse if panel was already collapsed
- // This allows users to keep the panel open while browsing bodies
-
// Auto-expand the group that contains the selected body
setExpandedGroup(selectedBody.type);
}
@@ -79,6 +75,10 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
setExpandedGroup(prev => prev === groupName ? null : groupName);
};
+ const handleBodySelect = (body: CelestialBody) => {
+ onBodySelect(body);
+ };
+
return (
toggleGroup('star')}
selectedBody={selectedBody}
- onBodySelect={onBodySelect}
+ onBodySelect={handleBodySelect}
/>
)}
@@ -175,7 +175,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
isExpanded={expandedGroup === 'planet'}
onToggle={() => toggleGroup('planet')}
selectedBody={selectedBody}
- onBodySelect={onBodySelect}
+ onBodySelect={handleBodySelect}
/>
)}
@@ -189,7 +189,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
isExpanded={expandedGroup === 'dwarf_planet'}
onToggle={() => toggleGroup('dwarf_planet')}
selectedBody={selectedBody}
- onBodySelect={onBodySelect}
+ onBodySelect={handleBodySelect}
/>
)}
@@ -203,7 +203,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
isExpanded={expandedGroup === 'satellite'}
onToggle={() => toggleGroup('satellite')}
selectedBody={selectedBody}
- onBodySelect={onBodySelect}
+ onBodySelect={handleBodySelect}
/>
)}
@@ -217,7 +217,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
isExpanded={expandedGroup === 'comet'}
onToggle={() => toggleGroup('comet')}
selectedBody={selectedBody}
- onBodySelect={onBodySelect}
+ onBodySelect={handleBodySelect}
/>
)}
@@ -231,7 +231,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
isExpanded={expandedGroup === 'probe'}
onToggle={() => toggleGroup('probe')}
selectedBody={selectedBody}
- onBodySelect={onBodySelect}
+ onBodySelect={handleBodySelect}
/>
)}
diff --git a/frontend/src/components/Scene.tsx b/frontend/src/components/Scene.tsx
index d3afe8b..1e61b2a 100644
--- a/frontend/src/components/Scene.tsx
+++ b/frontend/src/components/Scene.tsx
@@ -3,7 +3,9 @@
*/
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Stars as BackgroundStars, Html } from '@react-three/drei';
-import { useMemo, useState, useEffect } from 'react';
+import * as THREE from 'three';
+import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
+import type { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
import { CelestialBody } from './CelestialBody';
import { Probe } from './Probe';
import { CameraController } from './CameraController';
@@ -15,7 +17,6 @@ import { Galaxies } from './Galaxies';
import { Nebulae } from './Nebulae';
import { FocusInfo } from './FocusInfo';
import { AsteroidBelts } from './AsteroidBelts';
-import { scalePosition } from '../utils/scaleDistance';
import { calculateRenderPosition } from '../utils/renderPosition';
import type { CelestialBody as CelestialBodyType, Position } from '../types';
import type { ToastContextValue } from '../contexts/ToastContext'; // Import ToastContextValue
@@ -35,6 +36,29 @@ interface SceneProps {
export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbits = true, onBodySelect, resetTrigger = 0, toast, onViewDetails }: SceneProps) {
const [typeConfigs] = useSystemSetting('celestial_type_configs', null);
const [rawCameraPos] = useSystemSetting('default_camera_position', [10, 8, 10]);
+ const [rendererGeneration, setRendererGeneration] = useState(0);
+ const recoveryAttemptedRef = useRef(false);
+ const contextListenerCleanupRef = useRef<(() => void) | null>(null);
+ const orbitControlsRef = useRef
(null);
+
+ useEffect(() => () => contextListenerCleanupRef.current?.(), []);
+
+ const handleCanvasCreated = useCallback(({ gl, camera }: { gl: THREE.WebGLRenderer; camera: THREE.Camera }) => {
+ gl.sortObjects = true;
+ gl.setClearColor(0x000000, 1);
+ camera.lookAt(0, 0, 0);
+
+ contextListenerCleanupRef.current?.();
+ const canvas = gl.domElement;
+ const handleContextLost = (event: Event) => {
+ event.preventDefault();
+ if (recoveryAttemptedRef.current) return;
+ recoveryAttemptedRef.current = true;
+ setRendererGeneration((generation) => generation + 1);
+ };
+ canvas.addEventListener('webglcontextlost', handleContextLost, { once: true });
+ contextListenerCleanupRef.current = () => canvas.removeEventListener('webglcontextlost', handleContextLost);
+ }, []);
// Parse camera position if it's a string (from system settings)
const defaultCameraPos = useMemo(() => {
@@ -85,31 +109,21 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi
// Always show all probes (changed from previous behavior)
const visibleProbes = probes;
- // Calculate target position for OrbitControls
- const controlsTarget = useMemo(() => {
- if (selectedBody) {
- const pos = selectedBody.positions[0];
- const scaledPos = scalePosition(pos.x, pos.y, pos.z);
- return [scaledPos.x, scaledPos.z, scaledPos.y] as [number, number, number];
- }
- return [0, 0, 0] as [number, number, number];
- }, [selectedBody]);
-
- // Calculate position for FocusInfo (needs to match rendered position of body)
+ const currentSelectedBody = useMemo(
+ () => selectedBody ? bodies.find((body) => body.id === selectedBody.id) ?? selectedBody : null,
+ [bodies, selectedBody],
+ );
const focusInfoPosition = useMemo(() => {
- if (!selectedBody) return [0, 0, 0] as [number, number, number];
-
- // We need to use the EXACT same logic as CelestialBody/Probe components
- // to ensure the label sticks to the object
- const renderPos = calculateRenderPosition(selectedBody, bodies, typeConfigs);
-
- // Convert to Three.js coordinates (x, z, y)
- return [renderPos.x, renderPos.z, renderPos.y] as [number, number, number];
- }, [selectedBody, bodies, typeConfigs]);
+ if (!currentSelectedBody) return [0, 0, 0] as [number, number, number];
+ const position = calculateRenderPosition(currentSelectedBody, bodies, typeConfigs);
+ return [position.x, position.z, position.y] as [number, number, number];
+ }, [bodies, currentSelectedBody, typeConfigs]);
return (
- }
- type="info"
- showIcon
- style={{ marginBottom: 16 }}
- />
-
-
-
- setSearchQuery(e.target.value)}
- onPressEnter={handleNASASearch}
- />
- }
- onClick={handleNASASearch}
- loading={searching}
- >
- 搜索
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Hidden system_id field - auto-set from selector */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
-
+ searchQuery={searchQuery}
+ onSearchQueryChange={setSearchQuery}
+ onNasaSearch={handleNasaSearch}
+ searching={searching}
+ uploading={uploading}
+ refreshResources={refreshResources}
+ onResourceUpload={handleResourceUpload}
+ onResourceDelete={handleResourceDelete}
+ toast={toast}
+ />
>
);
}
-
-// Resource Manager Component
-function ResourceManager({
- bodyId,
- bodyType,
- resources,
- hasResources,
- onUpload,
- onDelete,
- uploading,
- refreshTrigger,
- toast,
-}: {
- bodyId: string;
- bodyType: string;
- resources?: any;
- hasResources?: boolean;
- onUpload: (file: File, resourceType: string) => Promise;
- onDelete: (resourceId: number) => Promise;
- uploading: boolean;
- refreshTrigger: number;
- toast: any;
-}) {
- const [currentResources, setCurrentResources] = useState(resources);
-
- // Load resources when modal opens
- useEffect(() => {
- request.get(`/celestial/resources/${bodyId}`)
- .then(({ data }) => {
- const grouped: any = {};
- data.resources.forEach((res: any) => {
- if (!grouped[res.resource_type]) {
- grouped[res.resource_type] = [];
- }
- grouped[res.resource_type].push(res);
- });
- setCurrentResources(grouped);
- })
- .catch(() => {
- toast.error('加载资源列表失败');
- });
- }, [refreshTrigger, bodyId]);
-
- const resourceTypes = [
- { key: 'icon', label: '图标 (上传到 icon 目录)', type: 'image' },
- { key: 'texture', label: bodyType === 'probe' ? '纹理 (上传到 model 目录)' : '纹理 (上传到 texture 目录)', type: 'file' },
- { key: 'model', label: bodyType === 'probe' ? '模型 (上传到 model 目录)' : '模型 (上传到 texture 目录)', type: 'file' },
- ];
-
- return (
-
-
-
- {resourceTypes.map(({ key, label, type }) => (
-
-
{label}
-
- {type === 'image' && currentResources?.[key] && currentResources[key].length > 0 ? (
- // Image preview for icon
-
-

-
-
onUpload(file, key)}
- showUploadList={false}
- disabled={uploading}
- accept="image/*"
- >
- } loading={uploading} size="small">
- 更换图标
-
-
-
-
onDelete(currentResources[key][0].id)}
- okText="删除"
- cancelText="取消"
- >
- }
- >
- 删除图标
-
-
-
-
- ({(currentResources[key][0].file_size / 1024).toFixed(2)} KB)
-
-
-
- ) : type === 'image' ? (
- // No icon uploaded yet
-
onUpload(file, key)}
- showUploadList={false}
- disabled={uploading}
- accept="image/*"
- >
- } loading={uploading} size="small">
- 上传图标
-
-
- ) : (
- // File upload for texture/model
- <>
-
onUpload(file, key)}
- showUploadList={false}
- disabled={uploading}
- >
- } loading={uploading} size="small">
- 上传 {label.split(' ')[0]}
-
-
-
- {currentResources?.[key] && currentResources[key].length > 0 && (
-
- {currentResources[key].map((res: any) => (
-
-
-
{res.file_path}
-
- ({(res.file_size / 1024).toFixed(2)} KB)
-
-
onDelete(res.id)}
- okText="删除"
- cancelText="取消"
- >
- }
- >
- 删除
-
-
-
- {key === 'model' && (
-
-
- 显示缩放:
- {
- // Update scale in resource
- const newScale = value || 1.0;
- request.put(`/celestial/resources/${res.id}`, {
- extra_data: { ...res.extra_data, scale: newScale }
- }).then(() => {
- toast.success('缩放参数已更新');
- }).catch(() => {
- toast.error('更新失败');
- });
- }}
- />
-
- (推荐: Webb=0.3, 旅行者=1.5)
-
-
-
- )}
-
- ))}
-
- )}
- >
- )}
-
- ))}
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/src/pages/admin/NASADownload.tsx b/frontend/src/pages/admin/NASADownload.tsx
index c392dd1..04ff114 100644
--- a/frontend/src/pages/admin/NASADownload.tsx
+++ b/frontend/src/pages/admin/NASADownload.tsx
@@ -1,189 +1,104 @@
-/**
- * NASA Data Download Page
- * Downloads position data for celestial bodies from NASA Horizons API
- */
-import { useState, useEffect } from 'react';
-import {
- Row,
- Col,
- Card,
- Checkbox,
- DatePicker,
- Button,
- Badge,
- Spin,
- Typography,
- Collapse,
- Space,
- Progress,
- Calendar,
- Alert,
- Tag,
- Modal,
- Table
-} from 'antd';
-import {
- DownloadOutlined,
- CheckCircleOutlined,
- CloseCircleOutlined,
- LoadingOutlined,
- DeleteOutlined
-} from '@ant-design/icons';
-import type { CheckboxChangeEvent } from 'antd/es/checkbox';
+import { useCallback, useEffect, useState } from 'react';
+import { Badge, Modal } from 'antd';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
-import { request } from '../../utils/request';
+
import { useToast } from '../../contexts/ToastContext';
import { useDataCutoffDate } from '../../hooks/useDataCutoffDate';
+import { request } from '../../utils/request';
+import { NasaDownloadView } from './nasa-download/NasaDownloadView';
+import type { CelestialBody, DateRange, GroupedBodies, PositionRow, ViewingDateData } from './nasa-download/types';
-// Extend dayjs with isBetween plugin
dayjs.extend(isBetween);
-const { Title, Text } = Typography;
-const { RangePicker } = DatePicker;
-
-interface CelestialBody {
- id: string;
- name: string;
- name_zh: string;
- type: string;
- is_active: boolean;
- description: string;
-}
-
-interface GroupedBodies {
- [type: string]: CelestialBody[];
-}
-
export function NASADownload() {
const [loading, setLoading] = useState(false);
const [bodies, setBodies] = useState({});
const [selectedBodies, setSelectedBodies] = useState([]);
- const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
- dayjs().startOf('month'),
- dayjs().endOf('month')
- ]);
+ const [dateRange, setDateRange] = useState([dayjs().startOf('month'), dayjs().endOf('month')]);
const [availableDates, setAvailableDates] = useState