Ruperth commited on
Commit
96bba34
·
0 Parent(s):

feat: polish mobile responsive layout across all dashboard screens

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.dockerignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+
4
+ .venv
5
+ __pycache__
6
+ *.py[cod]
7
+ *.egg-info
8
+ .ruff_cache
9
+ .ipynb_checkpoints
10
+ .pytest_cache
11
+ .mypy_cache
12
+
13
+ frontend/node_modules
14
+ frontend/dist
15
+ frontend/.vite
16
+
17
+ data/raw
18
+ mlruns
19
+ notebooks
20
+
21
+ .vscode
22
+ .idea
23
+ .DS_Store
24
+
25
+ Dockerfile*
26
+ docker-compose*.yml
27
+ .dockerignore
.gitattributes ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ data/processed/*.npy filter=lfs diff=lfs merge=lfs -text
2
+ data/processed/*.npz filter=lfs diff=lfs merge=lfs -text
3
+ models/*.pt filter=lfs diff=lfs merge=lfs -text
4
+ *.png filter=lfs diff=lfs merge=lfs -text
5
+ *.jpg filter=lfs diff=lfs merge=lfs -text
6
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
7
+ *.webp filter=lfs diff=lfs merge=lfs -text
8
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
9
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
10
+ *.wav filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .ipynb_checkpoints/
5
+ .ruff_cache/
6
+ *.egg-info/
7
+ /data/raw
8
+ /mlruns/
9
+ .DS_Store
10
+ .vscode/
11
+ .idea/
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
Dockerfile ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS frontend
2
+
3
+ RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
4
+
5
+ WORKDIR /web
6
+
7
+ COPY frontend/package.json frontend/pnpm-lock.yaml ./
8
+ RUN pnpm install --frozen-lockfile
9
+
10
+ COPY frontend/ ./
11
+ RUN pnpm build
12
+
13
+
14
+ FROM python:3.11-slim AS backend-builder
15
+
16
+ ENV PYTHONDONTWRITEBYTECODE=1 \
17
+ PYTHONUNBUFFERED=1 \
18
+ PIP_NO_CACHE_DIR=1 \
19
+ UV_LINK_MODE=copy \
20
+ UV_PROJECT_ENVIRONMENT=/opt/venv
21
+
22
+ RUN apt-get update && apt-get install -y --no-install-recommends \
23
+ build-essential \
24
+ curl \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /usr/local/bin/uv
28
+
29
+ WORKDIR /app
30
+
31
+ COPY pyproject.toml uv.lock ./
32
+ COPY src ./src
33
+
34
+ RUN uv sync --frozen --no-dev
35
+
36
+
37
+ FROM python:3.11-slim AS runtime
38
+
39
+ ENV PYTHONDONTWRITEBYTECODE=1 \
40
+ PYTHONUNBUFFERED=1 \
41
+ PATH="/opt/venv/bin:$PATH" \
42
+ HOME=/home/user \
43
+ SADAR_PROCESSED_DIR=/app/data/processed \
44
+ SADAR_MODELS_DIR=/app/models \
45
+ SADAR_REPORTS_DIR=/app/reports \
46
+ SADAR_SERVE_CONFIG=/app/configs/serve.yaml \
47
+ SADAR_FRONTEND_DIR=/app/frontend/dist \
48
+ PORT=7860
49
+
50
+ RUN apt-get update && apt-get install -y --no-install-recommends \
51
+ libgomp1 \
52
+ curl \
53
+ && rm -rf /var/lib/apt/lists/* \
54
+ && useradd --create-home --uid 1000 --shell /bin/bash user
55
+
56
+ WORKDIR /app
57
+
58
+ COPY --from=backend-builder /opt/venv /opt/venv
59
+ COPY --from=backend-builder /app/src /app/src
60
+ COPY --from=frontend /web/dist /app/frontend/dist
61
+
62
+ COPY configs ./configs
63
+ COPY pyproject.toml ./
64
+ COPY models ./models
65
+ COPY reports ./reports
66
+ COPY data/processed/scaler.npz data/processed/test.npy data/processed/val.npy ./data/processed/
67
+
68
+ RUN chown -R user:user /app
69
+
70
+ USER user
71
+
72
+ EXPOSE 7860
73
+
74
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
75
+ CMD curl -fsS http://localhost:${PORT}/api/health || exit 1
76
+
77
+ CMD ["sh", "-c", "uvicorn sadar.serve.app:app --host 0.0.0.0 --port ${PORT}"]
Dockerfile.backend ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim AS builder
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ UV_LINK_MODE=copy \
7
+ UV_PROJECT_ENVIRONMENT=/opt/venv
8
+
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ build-essential \
11
+ curl \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /usr/local/bin/uv
15
+
16
+ WORKDIR /app
17
+
18
+ COPY pyproject.toml uv.lock ./
19
+ COPY src ./src
20
+
21
+ RUN uv sync --frozen --no-dev
22
+
23
+
24
+ FROM python:3.11-slim AS runtime
25
+
26
+ ENV PYTHONDONTWRITEBYTECODE=1 \
27
+ PYTHONUNBUFFERED=1 \
28
+ PATH="/opt/venv/bin:$PATH" \
29
+ SADAR_PROCESSED_DIR=/app/data/processed \
30
+ SADAR_MODELS_DIR=/app/models \
31
+ SADAR_REPORTS_DIR=/app/reports \
32
+ SADAR_SERVE_CONFIG=/app/configs/serve.yaml
33
+
34
+ RUN apt-get update && apt-get install -y --no-install-recommends \
35
+ libgomp1 \
36
+ curl \
37
+ && rm -rf /var/lib/apt/lists/* \
38
+ && groupadd --system --gid 1000 sadar \
39
+ && useradd --system --uid 1000 --gid sadar --create-home sadar
40
+
41
+ WORKDIR /app
42
+
43
+ COPY --from=builder /opt/venv /opt/venv
44
+ COPY --from=builder /app/src /app/src
45
+
46
+ COPY configs ./configs
47
+ COPY pyproject.toml ./
48
+
49
+ RUN mkdir -p /app/data/processed /app/models /app/reports \
50
+ && chown -R sadar:sadar /app
51
+
52
+ USER sadar
53
+
54
+ EXPOSE 8000
55
+
56
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
57
+ CMD curl -fsS http://localhost:8000/api/health || exit 1
58
+
59
+ CMD ["uvicorn", "sadar.serve.app:app", "--host", "0.0.0.0", "--port", "8000"]
Makefile ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DEFAULT_GOAL := help
2
+ .PHONY: help install notebook preprocess baseline train-lstm train-transformer train-vae tune-vae evaluate compare serve web dev artifacts mlflow-ui lint clean docker-build docker-up docker-down docker-logs
3
+
4
+ PROCESSED_DIR ?= data/processed
5
+ MODELS_DIR ?= models
6
+ VAE_CHECKPOINT ?= $(MODELS_DIR)/vae_lstm.pt
7
+ SCALER ?= $(PROCESSED_DIR)/scaler.npz
8
+
9
+ help: ## Show this help
10
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
11
+ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
12
+
13
+ install: ## Create the Python environment with uv
14
+ uv sync
15
+
16
+ notebook: ## Open Jupyter for the notebooks (EDA, etc.)
17
+ uv run jupyter lab
18
+
19
+ preprocess: ## Run the preprocessing pipeline (raw -> tensors)
20
+ uv run sadar-preprocess
21
+
22
+ baseline: ## Fit and evaluate the Isolation Forest baseline
23
+ uv run sadar-baseline
24
+
25
+ train-lstm: ## Train the LSTM autoencoder on the normal flights
26
+ uv run sadar-train-lstm
27
+
28
+ train-transformer: ## Train the Transformer autoencoder on the normal flights
29
+ uv run sadar-train-transformer
30
+
31
+ train-vae: ## Train the VAE-LSTM on the normal flights
32
+ uv run sadar-train-vae
33
+
34
+ tune-vae: ## Search VAE-LSTM hyperparameters with Optuna
35
+ uv run sadar-tune-vae
36
+
37
+ evaluate: ## Evaluate a trained autoencoder against synthetic anomalies
38
+ uv run sadar-evaluate
39
+
40
+ compare: ## Compare every detector and select the final model
41
+ uv run sadar-compare
42
+
43
+ artifacts: ## Ensure the preprocessed tensors and the VAE checkpoint exist
44
+ @if [ ! -f "$(SCALER)" ]; then \
45
+ echo "[artifacts] $(SCALER) missing, running preprocess..."; \
46
+ $(MAKE) preprocess; \
47
+ fi
48
+ @if [ ! -f "$(VAE_CHECKPOINT)" ]; then \
49
+ echo "[artifacts] $(VAE_CHECKPOINT) missing, trying to restore from MLflow..."; \
50
+ uv run python -m sadar.models.restore || true; \
51
+ fi
52
+ @if [ ! -f "$(VAE_CHECKPOINT)" ]; then \
53
+ echo "[artifacts] still missing, training VAE-LSTM..."; \
54
+ $(MAKE) train-vae; \
55
+ fi
56
+
57
+ serve: artifacts ## Run the inference API for the dashboard
58
+ uv run uvicorn sadar.serve.app:app --port 8000
59
+
60
+ web: ## Run the frontend dev server (Vite)
61
+ pnpm -C frontend dev
62
+
63
+ dev: artifacts ## Run the backend API and the frontend dev server together
64
+ @trap 'kill 0' EXIT; uv run uvicorn sadar.serve.app:app --port 8000 & pnpm -C frontend dev
65
+
66
+ mlflow-ui: ## Open the MLflow experiment tracking UI
67
+ uv run mlflow ui --backend-store-uri file:mlruns
68
+
69
+ lint: ## Python linter
70
+ uv run ruff check src
71
+
72
+ clean: ## Clean caches and artifacts
73
+ rm -rf .ruff_cache **/__pycache__
74
+
75
+ docker-build: ## Build the backend and frontend container images
76
+ docker compose build
77
+
78
+ docker-up: ## Start the stack in the background (backend on :8000, frontend on :5180)
79
+ docker compose up -d
80
+
81
+ docker-down: ## Stop the stack and remove the containers
82
+ docker compose down
83
+
84
+ docker-logs: ## Tail the logs of the running stack
85
+ docker compose logs -f
README.es.md ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ <img src="docs/assets/logo.png" alt="SADAR" width="220" />
4
+
5
+ <h1>SADAR</h1>
6
+
7
+ <p><b>Smart Anomaly Detection for Aviation Routes</b></p>
8
+
9
+ <p>Detección temprana de comportamiento de vuelo no conforme sobre Madrid-Barajas (LEMD).</p>
10
+
11
+ <p>
12
+ <img src="docs/assets/qr-placeholder.png" alt="QR" width="220" onerror="this.style.display='none'" />
13
+ </p>
14
+
15
+ <p>
16
+ <a href="README.md"><img src="https://img.shields.io/badge/English-0d1a1f?style=flat&logoColor=7fd1c6&labelColor=0a1014" alt="English" /></a>
17
+ ·
18
+ <a href="README.es.md"><img src="https://img.shields.io/badge/Espa%C3%B1ol-0d1a1f?style=flat&logoColor=7fd1c6&labelColor=0a1014" alt="Español" /></a>
19
+ </p>
20
+
21
+ <p>
22
+ <img src="https://img.shields.io/badge/Python-3.11-0a1014?logo=python&logoColor=7fd1c6&labelColor=0d1a1f" />
23
+ <img src="https://img.shields.io/badge/PyTorch-2.x-0a1014?logo=pytorch&logoColor=7fd1c6&labelColor=0d1a1f" />
24
+ <img src="https://img.shields.io/badge/FastAPI-0.136-0a1014?logo=fastapi&logoColor=7fd1c6&labelColor=0d1a1f" />
25
+ <img src="https://img.shields.io/badge/React-18-0a1014?logo=react&logoColor=7fd1c6&labelColor=0d1a1f" />
26
+ <img src="https://img.shields.io/badge/Vite-5-0a1014?logo=vite&logoColor=7fd1c6&labelColor=0d1a1f" />
27
+ <img src="https://img.shields.io/badge/MLflow-3.x-0a1014?logo=mlflow&logoColor=7fd1c6&labelColor=0d1a1f" />
28
+ <img src="https://img.shields.io/badge/Optuna-4.x-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
29
+ <img src="https://img.shields.io/badge/uv-package%20mgr-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
30
+ <img src="https://img.shields.io/badge/Hugging%20Face-Spaces-0a1014?logo=huggingface&logoColor=7fd1c6&labelColor=0d1a1f" />
31
+ <img src="https://img.shields.io/badge/Docker-compose-0a1014?logo=docker&logoColor=7fd1c6&labelColor=0d1a1f" />
32
+ </p>
33
+
34
+ <p>
35
+ <a href="https://medium.com/@devrup404/cuando-el-modelo-m%C3%A1s-complejo-no-es-el-que-detecta-mejor-db64f1195ae9"><img src="https://img.shields.io/badge/Medium-Leer%20el%20art%C3%ADculo-0a1014?logo=medium&logoColor=7fd1c6&labelColor=0d1a1f" alt="Leer en Medium" /></a>
36
+ </p>
37
+
38
+ </div>
39
+
40
+ ---
41
+
42
+ ### Descripción del proyecto
43
+
44
+ **SADAR** (Smart Anomaly Detection for Aviation Routes) es un sistema de deep learning que aprende cómo es una aproximación o despegue **normal** en Madrid-Barajas (LEMD) y marca cualquier trayectoria que se desvíe del patrón aprendido: motores y al aire (go-arounds), rutas anómalas, pérdidas de transpondedor o maniobras compatibles con una emergencia.
45
+
46
+ Está construido sobre tres autoencoders profundos entrenados con unos 20.000 vuelos reales de ADS-B de la red OpenSky: un **autoencoder LSTM**, un **autoencoder Transformer** y un **VAE-LSTM**. Los tres se entrenan en igualdad de condiciones y se comparan cabeza a cabeza; el **VAE-LSTM** es el que se sirve en producción porque combina la mejor PR-AUC con la menor latencia de detección.
47
+
48
+ > **Alcance.** SADAR se entrena exclusivamente con operaciones normales de LEMD y funciona como monitor de conformidad de trayectorias: aprende el patrón normal y señala cualquier desviación para revisión humana. No es un predictor de eventos catastróficos.
49
+
50
+ ### Lo destacado
51
+
52
+ - **Deep learning de una sola clase** sobre 7 variables dinámicas (posición, altitud, velocidad, seno/coseno del rumbo, régimen vertical).
53
+ - **Selección comparativa**: baseline Isolation Forest vs LSTM vs Transformer vs VAE-LSTM con el mismo preprocesado y las mismas métricas.
54
+ - **Banco de anomalías sintéticas** con cinco tipos de inyección (desviación de ruta, salto de altitud, anomalía de velocidad, holding, congelado de transpondedor) en varias intensidades, mide **PR-AUC** y **latencia de detección**.
55
+ - **Dashboard estilo torre de control** con React + Vite: radar en vivo, ranking de anomalías, simulador con sliders y página de métricas.
56
+ - **Totalmente reproducible**: `uv` para Python, `pnpm` para el front, configs YAML, semillas fijas.
57
+
58
+ ### Stack
59
+
60
+ | Capa | Herramientas |
61
+ |---|---|
62
+ | Modelos | PyTorch · scikit-learn (baseline) · Optuna · MLflow |
63
+ | Datos | pandas · pyarrow · pyproj · NumPy |
64
+ | Backend | FastAPI · uvicorn · uv |
65
+ | Frontend | React 18 · Vite 5 · TypeScript · framer-motion |
66
+ | Despliegue | Docker · Hugging Face Spaces |
67
+
68
+ ### Capturas de la demo
69
+
70
+ <details>
71
+ <summary><b>1. Consola torre - radar en directo sobre LEMD</b></summary>
72
+
73
+ ![Radar en vivo sobre Madrid-Barajas](docs/assets/img1.png)
74
+
75
+ Scope en tiempo real centrado en Barajas. Cada aeronave se colorea según su estado (en seguimiento / en alerta) y el panel lateral muestra todos los vuelos del espacio aéreo con su indicativo y su estado.
76
+
77
+ </details>
78
+
79
+ <details>
80
+ <summary><b>2. Simulador - inyecta una anomalía y observa la alerta</b></summary>
81
+
82
+ ![Simulador de anomalías](docs/assets/img2.png)
83
+
84
+ La estrella de la demo. Eliges un vuelo normal, seleccionas el tipo de anomalía (desviación de ruta, salto de altitud, velocidad, holding, corte de transpondedor), ajustas intensidad y momento, y la inyectas en vivo. El score de la derecha cruza el umbral y se reporta la latencia en segundos.
85
+
86
+ </details>
87
+
88
+ <details>
89
+ <summary><b>3. Métricas - comparación de modelos y detector elegido</b></summary>
90
+
91
+ ![Página de comparación de modelos](docs/assets/img3.png)
92
+
93
+ Reporta la PR-AUC y el desempeño por anomalía de baseline, LSTM, Transformer y VAE-LSTM, además del motivo por el que se selecciona el **VAE-LSTM** como modelo de producción.
94
+
95
+ </details>
96
+
97
+ <details>
98
+ <summary><b>4. Métricas - desglose por tipo de anomalía</b></summary>
99
+
100
+ ![Desglose de métricas por anomalía](docs/assets/img4.png)
101
+
102
+ PR-AUC detallada por tipo de anomalía e intensidad. Sirve para justificar qué detector funciona mejor en cada modo de fallo.
103
+
104
+ </details>
105
+
106
+ <details>
107
+ <summary><b>5. Consola torre - instantánea completa del tráfico de LEMD</b></summary>
108
+
109
+ ![Tráfico completo en LEMD](docs/assets/img5.png)
110
+
111
+ Vista más amplia del radar con toda la flota visible, bloque de datos del vuelo y, abajo a la derecha, el histograma en vivo del anomaly score.
112
+
113
+ </details>
114
+
115
+ <details>
116
+ <summary><b>6. Consola torre - alerta levantada sobre un vuelo</b></summary>
117
+
118
+ ![Alerta sobre un vuelo](docs/assets/img6.png)
119
+
120
+ Una aeronave marcada se pone en rojo y el controlador recibe el score, el umbral y la clasificación sugerida en el panel derecho.
121
+
122
+ </details>
123
+
124
+ ### Arquitectura
125
+
126
+ ```
127
+ ┌──────────────────┐ ┌──────────────────┐
128
+ │ Parquet ADS-B │ │ Banco anomalías │
129
+ │ (OpenSky / LEMD)│ │ sintéticas │
130
+ └────────┬─────────┘ └────────┬─────────┘
131
+ │ │
132
+ ┌────────▼────────┐ │
133
+ │ Preprocesado │ │
134
+ │ · limpieza │ │
135
+ │ · resample 10s │ │
136
+ │ · XY relativos │ │
137
+ │ · sin/cos rumbo │ │
138
+ │ · estandarizado │ │
139
+ └────────┬────────┘ │
140
+ │ │
141
+ ┌────────▼────────────────────────────▼────────┐
142
+ │ Entrenamiento (solo vuelos normales) │
143
+ │ ┌──────────┐ ┌─────────────┐ ┌────────────┐ │
144
+ │ │ Isolation│ │ LSTM-AE │ │ Transformer│ │
145
+ │ │ Forest │ │ │ │ AE │ │
146
+ │ └──────────┘ └─────────────┘ └────────────┘ │
147
+ │ ┌─────────────────┐ │
148
+ │ │ VAE-LSTM ★ │ ← elegido │
149
+ │ └─────────────────┘ │
150
+ └────────┬─────────────────────────────────────┘
151
+ │ checkpoint + scaler + umbral
152
+ ┌────────▼─────────┐
153
+ │ Servicio FastAPI│ /api/health · /api/scene
154
+ │ (uvicorn) │ /api/flights · /api/metrics
155
+ └────────┬─────────┘ /api/simulate
156
+ │
157
+ ┌────────▼─────────┐
158
+ │ React + Vite │ Radar · Simulador · Métricas
159
+ │ dashboard │
160
+ └──────────────────┘
161
+ ```
162
+
163
+ ### Ejecutarlo en local
164
+
165
+ Requisitos: **Python 3.11**, [`uv`](https://docs.astral.sh/uv/) para las deps de Python, **Node 20+** y [`pnpm`](https://pnpm.io/) para el frontend.
166
+
167
+ Dashboard en `http://localhost:5180`, API en `http://localhost:8000`.
168
+
169
+ <details>
170
+ <summary><b>macOS / Linux - con Makefile (recomendado)</b></summary>
171
+
172
+ ```bash
173
+ uv sync
174
+ pnpm -C frontend install
175
+ make dev
176
+ ```
177
+
178
+ `make dev` arranca el backend FastAPI y el frontend Vite juntos con hot reload.
179
+
180
+ </details>
181
+
182
+ <details>
183
+ <summary><b>macOS / Linux - sin Makefile</b></summary>
184
+
185
+ Dos terminales:
186
+
187
+ ```bash
188
+ # terminal 1: backend
189
+ uv sync
190
+ uv run uvicorn sadar.serve.app:app --port 8000
191
+
192
+ # terminal 2: frontend
193
+ pnpm -C frontend install
194
+ pnpm -C frontend dev
195
+ ```
196
+
197
+ </details>
198
+
199
+ <details>
200
+ <summary><b>Windows (PowerShell)</b></summary>
201
+
202
+ PowerShell no trae `make`; arranca los dos servicios en dos terminales.
203
+
204
+ ```powershell
205
+ # instalación una sola vez
206
+ uv sync
207
+ pnpm -C frontend install
208
+
209
+ # terminal 1: backend
210
+ uv run uvicorn sadar.serve.app:app --port 8000
211
+
212
+ # terminal 2: frontend
213
+ pnpm -C frontend dev
214
+ ```
215
+
216
+ Si no tienes `pnpm`: `npm install -g pnpm`. Si no tienes `uv`: `winget install astral-sh.uv`.
217
+
218
+ </details>
219
+
220
+ <details>
221
+ <summary><b>Windows con Make (Chocolatey / scoop / WSL)</b></summary>
222
+
223
+ ```powershell
224
+ choco install make # o: scoop install make
225
+ make dev
226
+ ```
227
+
228
+ O ejecuta todo dentro de WSL Ubuntu y sigue las instrucciones de macOS / Linux.
229
+
230
+ </details>
231
+
232
+ ### Ejecutarlo con Docker
233
+
234
+ Igual en cualquier sistema operativo, solo necesitas Docker Desktop / Docker Engine.
235
+
236
+ <details>
237
+ <summary><b>Un contenedor con Docker Compose</b></summary>
238
+
239
+ ```bash
240
+ docker compose up --build
241
+ ```
242
+
243
+ Abre `http://localhost:5180`. Modelos, tensores preprocesados e informes se montan en sólo lectura desde `./models`, `./data/processed` y `./reports`.
244
+
245
+ </details>
246
+
247
+ <details>
248
+ <summary><b>Imagen única (la misma que en producción / Hugging Face Spaces)</b></summary>
249
+
250
+ ```bash
251
+ docker build -t sadar .
252
+ docker run --rm -p 7860:7860 sadar
253
+ ```
254
+
255
+ Abre `http://localhost:7860`. Los modelos, tensores e informes van dentro de la imagen; no necesitas volúmenes.
256
+
257
+ </details>
258
+
259
+ ### Desplegar en Hugging Face Spaces
260
+
261
+ El repositorio trae un [Dockerfile](Dockerfile) listo para HF Spaces (una sola imagen, el frontend servido por FastAPI en el puerto 7860).
262
+
263
+ ```bash
264
+ git lfs install
265
+ git clone https://huggingface.co/spaces/<tu-usuario>/sadar
266
+ cd sadar
267
+ # copia los archivos del repositorio SADAR dentro
268
+ git lfs track "*.npy" "*.npz" "*.pt"
269
+ git add . && git commit -m "feat: initial deploy" && git push
270
+ ```
271
+
272
+ ### Estructura del repositorio
273
+
274
+ ```
275
+ SADAR/
276
+ ├── src/sadar/ Paquete Python (importable como `sadar`)
277
+ │ ├── data/ Pipeline de datos (parquet crudo → tensores)
278
+ │ │ ├── pipeline.py Punto de entrada: `sadar-preprocess`
279
+ │ │ ├── io.py Carga de parquet, dedupe, manifests
280
+ │ │ ├── cleaning.py Nulos, huecos, eliminación de go-arounds
281
+ │ │ ├── features.py XY relativo a pista, sin/cos del rumbo
282
+ │ │ ├── scaling.py Estandarización por feature (fit solo en train)
283
+ │ │ ├── splitting.py Split sin leak por flight_id y fecha
284
+ │ │ └── windowing.py Ventanas de longitud fija
285
+ │ ├── models/ Modelos y entrenadores
286
+ │ │ ├── baseline.py Baseline Isolation Forest
287
+ │ │ ├── lstm_autoencoder.py Arquitectura LSTM-AE
288
+ │ │ ├── transformer_autoencoder.py Arquitectura Transformer-AE
289
+ │ │ ├── vae_lstm.py Arquitectura VAE-LSTM (elegida)
290
+ │ │ ├── training.py Loop común, device, checkpoint I/O
291
+ │ │ ├── train_lstm.py Entrada: `sadar-train-lstm`
292
+ │ │ ├── train_transformer.py Entrada: `sadar-train-transformer`
293
+ │ │ ├── train_vae.py Entrada: `sadar-train-vae`
294
+ │ │ └── tune_vae.py Búsqueda Optuna: `sadar-tune-vae`
295
+ │ ├── eval/ Evaluación y banco sintético
296
+ │ │ ├── synthetic.py Inyección de anomalías (5 tipos, varias intensidades)
297
+ │ │ ├── evaluate.py Evaluación por modelo: `sadar-evaluate`
298
+ │ │ └── compare.py Informe comparativo: `sadar-compare`
299
+ │ └── serve/ Inferencia en producción
300
+ │ ├── app.py App FastAPI (también sirve la SPA en prod)
301
+ │ └── inference.py ConformanceService (carga modelo + scaler)
302
+ │
303
+ ├── frontend/ Dashboard React + Vite (pnpm)
304
+ │ ├── src/
305
+ │ │ ├── pages/ Dashboard, Simulador, Métricas, Presentación
306
+ │ │ ├── components/ RadarScope, paneles, gráficas
307
+ │ │ ├── api.ts Cliente tipado para /api/*
308
+ │ │ └── i18n.tsx Traducciones EN / ES
309
+ │ ├── public/ Logo y estáticos
310
+ │ ├── vite.config.ts Proxy de dev /api → :8000
311
+ │ └── package.json
312
+ │
313
+ ├── configs/ Configuración YAML
314
+ │ ├── preprocessing.yaml
315
+ │ ├── baseline.yaml
316
+ │ ├── lstm.yaml · transformer.yaml · vae.yaml
317
+ │ ├── tune.yaml · eval.yaml · compare.yaml
318
+ │ └── serve.yaml Selecciona el checkpoint de producción
319
+ │
320
+ ├── models/ Checkpoints entrenados (Git LFS)
321
+ │ ├── lstm_autoencoder.pt
322
+ │ ├── transformer_autoencoder.pt
323
+ │ └── vae_lstm.pt ★ servido en producción
324
+ │
325
+ ├── data/
326
+ │ ├── raw/ Parquet ADS-B (fuera del repositorio, $SADAR_DATA_DIR)
327
+ │ └── processed/ Tensores estandarizados (Git LFS)
328
+ │ ├── scaler.npz
329
+ │ ├── train.npy · val.npy · test.npy
330
+ │ └── anomalies.npy
331
+ │
332
+ ├── reports/ Métricas generadas
333
+ │ ├── model_comparison.json
334
+ │ └── vae_tuning.json
335
+ │
336
+ ├── notebooks/ EDA
337
+ ├── docs/assets/ Capturas y logo del README
338
+ │
339
+ ├── Dockerfile Imagen de producción (HF Spaces, un contenedor)
340
+ ├── docker-compose.yml Stack local de dos contenedores
341
+ ├── pyproject.toml · uv.lock Deps Python gestionadas con uv
342
+ ├── Makefile `make dev`, `make preprocess`, ...
343
+ └── README.md · README.es.md Este archivo (EN / ES)
344
+ ```
345
+
346
+ ### Limitaciones
347
+
348
+ - El dataset no contiene incidentes reales catastróficos, SADAR valida una metodología, no eventos históricos.
349
+ - No se dispone de planes de vuelo, la "ruta prevista" se aproxima como "patrón normal aprendido".
350
+ - Los huecos de transpondedor son mayoritariamente cobertura del receptor, se usan como característica, no como etiqueta.
351
+ - Solo hay cuatro vuelos reales con squawk de emergencia, se usan para validar, jamás para entrenar.
352
+
353
+ ---
354
+
355
+ <div align="center">
356
+ <sub><b>SADAR</b> · Smart Anomaly Detection for Aviation Routes</sub>
357
+ <br />
358
+ <sub>Monitorización de conformidad de vuelos con deep learning sobre Madrid-Barajas (LEMD) · 2026</sub>
359
+ </div>
README.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SADAR Flight Conformance Monitor
3
+ emoji: 🛬
4
+ colorFrom: gray
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ short_description: Anomaly detection on ADS-B trajectories at Madrid LEMD.
11
+ ---
12
+
13
+ # SADAR — Hugging Face Space card
14
+
15
+ This file is the README used **only** when the project is deployed to a Hugging Face
16
+ Space. The YAML front-matter at the top is the Space card that Hugging Face needs to
17
+ configure the container (SDK, port, title, colors, license).
18
+
19
+ When deploying to Hugging Face Spaces, copy this file to the repository root as
20
+ `README.md` (overwriting the GitHub one) before pushing to the Space remote. The GitHub
21
+ `README.md` does not include this front-matter on purpose, so it renders cleanly on
22
+ github.com.
23
+
24
+ For the project description, screenshots, architecture and run instructions, see the
25
+ main `README.md` at the repository root.
configs/baseline.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+
3
+ data:
4
+ processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
5
+
6
+ isolation_forest:
7
+ n_estimators: 200
8
+ max_samples: auto
9
+ contamination: auto
10
+
11
+ threshold:
12
+ val_percentile: 99.0
configs/compare.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ eval_config: configs/eval.yaml
2
+
3
+ models:
4
+ - name: LSTM
5
+ checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/lstm_autoencoder.pt
6
+ - name: Transformer
7
+ checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/transformer_autoencoder.pt
8
+ - name: VAE-LSTM
9
+ checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/vae_lstm.pt
10
+
11
+ ensemble:
12
+ enabled: true
13
+ reducers: [mean, max]
14
+
15
+ output:
16
+ path: ${oc.env:SADAR_REPORTS_DIR,reports}/model_comparison.json
17
+ curves_dir: ${oc.env:SADAR_REPORTS_DIR,reports}/figures
configs/eval.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+
3
+ data:
4
+ processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
5
+
6
+ preprocessing_config: configs/preprocessing.yaml
7
+ baseline_config: configs/baseline.yaml
8
+
9
+ model:
10
+ checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/lstm_autoencoder.pt
11
+ device: auto
12
+
13
+ synthetic:
14
+ sample_size: 2000
15
+ onset_fraction: 0.5
16
+ route_deviation:
17
+ magnitudes_m: [20000, 40000, 80000]
18
+ altitude:
19
+ magnitudes_m: [300, 800, 1500]
20
+ speed:
21
+ factors: [1.6, 2.2, 0.4]
22
+ holding:
23
+ turn_periods_s: [240, 120]
24
+ freeze:
25
+ enabled: true
26
+
27
+ threshold:
28
+ val_percentile: 99.0
29
+
30
+ latency:
31
+ normal_step_percentile: 99.0
configs/lstm.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+
3
+ data:
4
+ processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
5
+
6
+ model:
7
+ hidden_size: 64
8
+ latent_size: 16
9
+ num_layers: 1
10
+ dropout: 0.1
11
+
12
+ training:
13
+ epochs: 50
14
+ batch_size: 256
15
+ learning_rate: 0.001
16
+ weight_decay: 0.0001
17
+ patience: 7
18
+ lr_factor: 0.5
19
+ lr_patience: 3
20
+ device: auto
21
+
22
+ output:
23
+ dir: ${oc.env:SADAR_MODELS_DIR,models}
24
+ checkpoint: lstm_autoencoder.pt
configs/preprocessing.yaml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+
3
+ data:
4
+ raw_dir: ${oc.env:SADAR_DATA_DIR,data/raw}
5
+ file_glob: lemd_*.parquet
6
+ airport: LEMD
7
+
8
+ runway:
9
+ ref_lat: 40.4936
10
+ ref_lon: -3.5668
11
+ projection_epsg: 32630
12
+
13
+ resample:
14
+ seconds: 10
15
+
16
+ cleaning:
17
+ min_points_per_flight: 40
18
+ max_gap_seconds: 120
19
+ max_missing_fraction: 0.5
20
+ remove_go_arounds: true
21
+ remove_emergency_squawks: true
22
+ emergency_squawks: [7500, 7600, 7700]
23
+ interpolate_columns: [lat, lon, baroaltitude, velocity, heading, vertrate]
24
+ go_around:
25
+ near_runway_m: 5000
26
+ descend_vertrate: -1.0
27
+ climb_vertrate: 2.0
28
+ min_climb_points: 3
29
+
30
+ features:
31
+ columns: [x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]
32
+ altitude_source: baroaltitude
33
+
34
+ windowing:
35
+ length: 60
36
+ stride: 30
37
+
38
+ split:
39
+ train_years: [2017, 2018, 2019]
40
+ eval_years: [2020]
41
+ val_fraction: 0.5
42
+
43
+ scaling:
44
+ method: standard
45
+
46
+ output:
47
+ dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
48
+ format: npy
configs/serve.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ eval_config: configs/eval.yaml
2
+
3
+ checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/vae_lstm.pt
4
+
5
+ metrics: ${oc.env:SADAR_REPORTS_DIR,reports}/model_comparison.json
configs/transformer.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+
3
+ data:
4
+ processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
5
+
6
+ model:
7
+ d_model: 64
8
+ nhead: 4
9
+ num_layers: 2
10
+ dim_feedforward: 128
11
+ latent_size: 16
12
+ dropout: 0.1
13
+ max_len: 256
14
+
15
+ training:
16
+ epochs: 50
17
+ batch_size: 256
18
+ learning_rate: 0.001
19
+ weight_decay: 0.0001
20
+ patience: 7
21
+ lr_factor: 0.5
22
+ lr_patience: 3
23
+ device: auto
24
+
25
+ output:
26
+ dir: ${oc.env:SADAR_MODELS_DIR,models}
27
+ checkpoint: transformer_autoencoder.pt
configs/tune.yaml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+
3
+ data:
4
+ processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
5
+
6
+ preprocessing_config: configs/preprocessing.yaml
7
+
8
+ n_trials: 20
9
+
10
+ search_space:
11
+ hidden_size: [32, 64, 128]
12
+ latent_size: [8, 16, 32]
13
+ num_layers: [1, 2]
14
+ dropout: [0.0, 0.3]
15
+ beta: [0.0001, 0.1]
16
+ learning_rate: [0.0003, 0.003]
17
+ weight_decay: [0.000001, 0.001]
18
+
19
+ synthetic:
20
+ sample_size: 1500
21
+ onset_fraction: 0.5
22
+ route_deviation:
23
+ magnitudes_m: [20000, 40000, 80000]
24
+ altitude:
25
+ magnitudes_m: [300, 800, 1500]
26
+ speed:
27
+ factors: [1.6, 2.2, 0.4]
28
+ holding:
29
+ turn_periods_s: [240, 120]
30
+ freeze:
31
+ enabled: true
32
+
33
+ training:
34
+ epochs: 30
35
+ batch_size: 256
36
+ learning_rate: 0.001
37
+ weight_decay: 0.0001
38
+ patience: 6
39
+ lr_factor: 0.5
40
+ lr_patience: 3
41
+ device: auto
42
+
43
+ output:
44
+ path: ${oc.env:SADAR_REPORTS_DIR,reports}/vae_tuning.json
configs/vae.yaml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+
3
+ data:
4
+ processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
5
+
6
+ model:
7
+ hidden_size: 64
8
+ latent_size: 8
9
+ num_layers: 1
10
+ dropout: 0.1488
11
+ beta: 0.0001193
12
+
13
+ training:
14
+ epochs: 50
15
+ batch_size: 256
16
+ learning_rate: 0.001436
17
+ weight_decay: 0.0000054741
18
+ patience: 7
19
+ lr_factor: 0.5
20
+ lr_patience: 3
21
+ device: auto
22
+
23
+ output:
24
+ dir: ${oc.env:SADAR_MODELS_DIR,models}
25
+ checkpoint: vae_lstm.pt
data/.gitkeep ADDED
File without changes
data/processed/anomalies.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc9b10f15b6b20da2d6c3ae51336d15f68e92d60802a0539d4ae2b27a5ea5171
3
+ size 1807808
data/processed/scaler.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:50bb65a11f9b41d2d6673b47fd83803cd847f19a5bde5860c05ba271e2891db4
3
+ size 556
data/processed/test.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1b66afdc544b8a34762131901244c30f7702983be692c321959638d6db9ffb66
3
+ size 13083968
data/processed/train.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6ac25b59731b49c1dc19180684a35198074f7e65fdc6cadd9149f42ab2902429
3
+ size 102493568
data/processed/val.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8fc07743435ec2facb7c32649180b1d30d4d30892dbd933b75ce598b63f822a7
3
+ size 12900848
docker-compose.yml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: sadar
2
+
3
+ services:
4
+ backend:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile.backend
8
+ image: sadar-backend:latest
9
+ container_name: sadar-backend
10
+ environment:
11
+ SADAR_PROCESSED_DIR: /app/data/processed
12
+ SADAR_MODELS_DIR: /app/models
13
+ SADAR_REPORTS_DIR: /app/reports
14
+ SADAR_SERVE_CONFIG: /app/configs/serve.yaml
15
+ volumes:
16
+ - ./data/processed:/app/data/processed:ro
17
+ - ./models:/app/models:ro
18
+ - ./reports:/app/reports:ro
19
+ ports:
20
+ - "8000:8000"
21
+ restart: unless-stopped
22
+
23
+ frontend:
24
+ build:
25
+ context: ./frontend
26
+ dockerfile: Dockerfile
27
+ image: sadar-frontend:latest
28
+ container_name: sadar-frontend
29
+ depends_on:
30
+ - backend
31
+ ports:
32
+ - "5180:80"
33
+ restart: unless-stopped
docs/REPORT.es.md ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SADAR
2
+
3
+ **Smart Anomaly Detection for Aviation Routes**
4
+
5
+ Memoria técnica
6
+
7
+ ---
8
+
9
+ **Proyecto:** SADAR · Flight Conformance Monitor
10
+ **Dominio:** Vigilancia de tráfico aéreo · datos ADS-B
11
+ **Aeropuerto de estudio:** Madrid-Barajas (LEMD)
12
+ **Enfoque:** Aprendizaje profundo de una sola clase para detección de anomalías en trayectorias
13
+ **Fecha:** 2026
14
+
15
+ ---
16
+
17
+ ## Resumen ejecutivo
18
+
19
+ SADAR es un sistema de aprendizaje profundo que aprende el patrón normal de las
20
+ operaciones de aproximación y despegue en Madrid-Barajas (LEMD) a partir de datos de
21
+ vigilancia ADS-B y señala en tiempo real cualquier trayectoria que se aparte de ese
22
+ patrón. El sistema está concebido como **monitor de conformidad de vuelos**: no clasifica
23
+ eventos concretos, asigna a cada vuelo del espacio aéreo una puntuación interpretable de
24
+ anomalía para que un operador pueda revisar los casos que quedan fuera de la
25
+ distribución aprendida.
26
+
27
+ Se entrenan tres autoencoders secuenciales en igualdad de condiciones sobre unos 20.000
28
+ vuelos reales obtenidos de la red OpenSky (un autoencoder LSTM, un autoencoder
29
+ Transformer y un VAE-LSTM), junto con un baseline no secuencial basado en Isolation
30
+ Forest. Los cuatro detectores se evalúan sobre el mismo conjunto de test y sobre un banco
31
+ sintético con cinco tipos de inyección y varias intensidades. Se selecciona el
32
+ **VAE-LSTM** como modelo de producción: obtiene la mejor PR-AUC sobre datos reales
33
+ (0,299) y la mejor ROC-AUC media en el banco sintético (0,792). También se evalúan dos
34
+ estrategias de ensemble (consenso y más sensible) y se descartan, porque ninguna mejora
35
+ al mejor modelo individual.
36
+
37
+ El modelo seleccionado se sirve a través de una aplicación FastAPI y de un dashboard
38
+ React + Vite que combina un radar en vivo estilo torre de control, un ranking de
39
+ anomalías, una página de métricas y un simulador interactivo. Todo el sistema es
40
+ reproducible desde una única imagen Docker y está listo para desplegarse en Hugging Face
41
+ Spaces.
42
+
43
+ ---
44
+
45
+ ## 1. Motivación y alcance
46
+
47
+ La monitorización de trayectorias es una función central de la gestión del tráfico
48
+ aéreo. Hoy se apoya en reglas deterministas y en supervisión humana: separación,
49
+ perfiles de descenso, esperas o códigos de transpondedor de emergencia se vigilan
50
+ contra umbrales fijos. Esas reglas funcionan bien para las situaciones para las que
51
+ fueron diseñadas, pero no pueden anticipar comportamientos que sean inusuales sin ser
52
+ una violación de manual.
53
+
54
+ SADAR explora un enfoque complementario, basado en datos. En lugar de codificar reglas,
55
+ el sistema **aprende** cómo es una operación normal en LEMD a partir de una muestra
56
+ amplia de vuelos reales, y trata cada nueva trayectoria como más o menos compatible con
57
+ esa distribución. La salida es una puntuación continua de conformidad que el operador
58
+ puede ordenar, filtrar y umbralizar.
59
+
60
+ El alcance del proyecto es deliberadamente acotado:
61
+
62
+ - **Qué hace.** Detecta desviaciones respecto al patrón normal de tráfico de un único
63
+ aeropuerto (LEMD) a partir de un conjunto fijo de variables dinámicas extraídas de las
64
+ observaciones ADS-B.
65
+ - **Qué no hace.** Predecir eventos operativos concretos, sustituir al controlador,
66
+ emitir recomendaciones de seguridad certificadas, ni generalizar sin reentrenamiento a
67
+ aeropuertos con procedimientos distintos.
68
+
69
+ Esta formulación como **monitor de conformidad** es la que hace tratable el problema con
70
+ los datos disponibles: solo requiere ejemplos de vuelo normal, que son abundantes, y
71
+ deja clara la interpretación de la salida ("esta trayectoria no se parece a las que el
72
+ modelo ha visto"), sin sobrevender.
73
+
74
+ ---
75
+
76
+ ## 2. Datos
77
+
78
+ ### 2.1 Fuente
79
+
80
+ El dataset se construye a partir de observaciones ADS-B captadas por la red OpenSky en
81
+ la zona terminal de Madrid-Barajas. Cubre aproximadamente 18 días de operaciones
82
+ muestreados entre junio de 2017 y marzo de 2020. Cada aeronave se reporta una vez cada
83
+ 10 segundos, la cadencia natural de ADS-B.
84
+
85
+ | Propiedad | Valor |
86
+ |---|---|
87
+ | Trayectorias totales | ≈ 20.000 (≈ 950 a 1.200 por día) |
88
+ | Filas por día | 140.000 a 240.000 |
89
+ | Columnas | 21 |
90
+ | Puntos por vuelo (mín · mediana · máx) | 40 · 188 · 1.246 |
91
+
92
+ ### 2.2 Variables
93
+
94
+ El esquema en bruto contiene información posicional (latitud, longitud, tiempo),
95
+ dinámica (altitud barométrica y geométrica, velocidad respecto al suelo, rumbo, régimen
96
+ vertical), campos derivados (distancia a pista, fase de vuelo), identidad (ICAO24,
97
+ indicativo, identificador de vuelo) y estado del transpondedor (squawk, indicador de
98
+ "en tierra", indicador de alerta, SPI y último contacto). Un campo, `operation`, está
99
+ vacío en todo el dataset y se descarta.
100
+
101
+ ### 2.3 Calidad y limpieza
102
+
103
+ Un análisis de perfilado sobre el dataset completo revela varios problemas que se
104
+ gestionan en el pipeline de preprocesado:
105
+
106
+ - **Valores ausentes.** `geoaltitude` falta en el 34,9 % de las filas, `baroaltitude` en
107
+ el 19,6 %, los campos cinemáticos (`velocity`, `heading`, `vertrate`) en torno al
108
+ 19 %. El resto se queda por debajo del 4 %.
109
+ - **Archivo duplicado.** Un día llega dos veces con nombres distintos y se deduplica por
110
+ hash de contenido.
111
+ - **Días vacíos o erróneos.** Algunos días llegan sin filas o con errores en el
112
+ manifest y se descartan.
113
+ - **Huecos de cobertura.** Cerca del 8 % de los vuelos en aire muestran un salto
114
+ superior a 120 s entre observaciones consecutivas. Casi siempre son artefactos de
115
+ cobertura del receptor, no eventos operativos, por lo que se utilizan como
116
+ característica, jamás como etiqueta.
117
+
118
+ Tras la limpieza, el dataset contiene un conjunto estable de operaciones normales en
119
+ LEMD que abarca varias estaciones e incluye el inicio de 2020, lo que sirve además como
120
+ cambio de distribución natural para poner a prueba el split temporal.
121
+
122
+ ### 2.4 Observaciones destacadas
123
+
124
+ El análisis exploratorio confirma un pequeño conjunto de casos raros que se reservan
125
+ únicamente para validación:
126
+
127
+ - Cuatro vuelos llevan un código de transpondedor de emergencia en algún punto.
128
+ - Aproximadamente un centenar de vuelos muestran un patrón claro de motor y al aire
129
+ (aproximación seguida de ascenso sostenido).
130
+
131
+ Ninguno de estos vuelos entra en el conjunto de entrenamiento. Se utilizan como prueba
132
+ de cordura del modelo desplegado.
133
+
134
+ ---
135
+
136
+ ## 3. Formulación del problema
137
+
138
+ El dataset contiene muchos vuelos normales y casi ninguna anomalía etiquetada. Eso
139
+ descarta la clasificación supervisada y apunta al **aprendizaje de una sola clase**:
140
+ entrenar un modelo que capture la distribución de trayectorias normales y puntuar los
141
+ nuevos vuelos según su compatibilidad con esa distribución.
142
+
143
+ En la práctica, los tres modelos profundos son **autoencoders** entrenados solo con
144
+ vuelos normales. El objetivo de entrenamiento es reconstruir la ventana de entrada a
145
+ partir de una representación latente comprimida; en inferencia se utiliza el **error de
146
+ reconstrucción** como puntuación de conformidad. Una trayectoria parecida a los patrones
147
+ normales se reconstruye con precisión y recibe un score bajo; una trayectoria que se
148
+ aparta de esos patrones es más difícil de reconstruir y recibe un score alto.
149
+
150
+ Esta formulación encaja con el contexto operativo por tres motivos. Sólo necesita datos
151
+ normales, que son los únicos disponibles a escala. Produce una puntuación continua e
152
+ interpretable en lugar de una etiqueta dura, lo que es la salida adecuada para una
153
+ herramienta de apoyo a un operador humano. Y se comporta bien ante modos de fallo
154
+ desconocidos: cualquier trayectoria que el modelo no haya aprendido a reconstruir
155
+ emerge como anómala con independencia de qué es lo que la hace inusual.
156
+
157
+ ---
158
+
159
+ ## 4. Pipeline de preprocesado
160
+
161
+ El pipeline transforma los parquet en bruto en tensores de longitud fija ya
162
+ estandarizados y listos para entrenar. Cada paso está implementado como una función
163
+ pequeña y testeable bajo `src/sadar/data/`.
164
+
165
+ 1. **Carga y fusión.** Se leen todos los parquet bajo `$SADAR_DATA_DIR` con pyarrow, se
166
+ concatenan y se deduplican. Los días vacíos o erróneos se descartan.
167
+ 2. **Transformación de coordenadas.** La latitud y la longitud se proyectan a un sistema
168
+ métrico relativo a pista con `pyproj`. Así el modelo es robusto a la curvatura de las
169
+ coordenadas y recibe metros como unidad.
170
+ 3. **Codificación del rumbo.** El rumbo se codifica como `(sin(rumbo), cos(rumbo))` para
171
+ evitar la discontinuidad entre 0 y 360 grados.
172
+ 4. **Tratamiento de ausentes.** Los huecos cortos dentro de una trayectoria por lo demás
173
+ completa se rellenan con interpolación lineal. Las trayectorias con demasiados
174
+ ausentes se descartan.
175
+ 5. **Resampleado.** Cada vuelo se resamplea a una rejilla uniforme de 10 s, que coincide
176
+ con la cadencia nominal de ADS-B.
177
+ 6. **Ventanado.** Cada trayectoria resampleada se trocea en ventanas de longitud fija.
178
+ La longitud de ventana es un hiperparámetro definido en
179
+ `configs/preprocessing.yaml`.
180
+ 7. **Estandarización.** Cada variable se normaliza a media cero y varianza unidad. El
181
+ escalador se ajusta **solo con el conjunto de entrenamiento** para evitar fugas, y se
182
+ guarda como `data/processed/scaler.npz`, de manera que la misma transformación se
183
+ aplica en inferencia.
184
+ 8. **Split sin fugas.** La división se hace conjuntamente por `flight_id` y por fecha:
185
+ ningún vuelo aparece en más de un conjunto, y el test es estrictamente posterior en
186
+ el tiempo al entrenamiento. Los años 2017 a 2019 se usan para entrenamiento y
187
+ validación; 2020 se reserva para test. Este split realista es lo que permite tomar
188
+ las cifras de latencia con confianza.
189
+ 9. **Limpieza del set de entrenamiento.** Los pocos vuelos con squawks de emergencia y
190
+ los go-around sospechosos se eliminan del entrenamiento. Sólo trayectorias
191
+ "normales" entran al modelo.
192
+
193
+ La entrada de todos los modelos profundos es una ventana de siete variables:
194
+ `[x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]`.
195
+
196
+ ---
197
+
198
+ ## 5. Modelos
199
+
200
+ ### 5.1 Baseline Isolation Forest
201
+
202
+ Detector de referencia, no secuencial. Cada ventana se resume por estadísticos por
203
+ variable (media, desviación, mínimo, máximo) y se alimenta a un Isolation Forest. El
204
+ baseline sirve para cuantificar cuánto añade realmente el modelado secuencial.
205
+
206
+ ### 5.2 Autoencoder LSTM
207
+
208
+ Un encoder LSTM de dos capas comprime la ventana de entrada en un vector latente, y un
209
+ decoder LSTM espejado la reconstruye. Se entrena de extremo a extremo con MSE y el loop
210
+ de entrenamiento compartido descrito más abajo. Es la arquitectura caballo de batalla:
211
+ estable, rápida en inferencia y alineada con la bibliografía sobre modelado de
212
+ trayectorias ADS-B.
213
+
214
+ ### 5.3 Autoencoder Transformer
215
+
216
+ El mismo esqueleto encoder-decoder, pero con atención multi-cabeza en lugar de
217
+ recurrencia. El Transformer captura dependencias a largo plazo dentro de la ventana sin
218
+ el cuello de botella secuencial de las LSTM, a cambio de más parámetros y una superficie
219
+ de hiperparámetros mayor.
220
+
221
+ ### 5.4 VAE-LSTM (modelo de producción seleccionado)
222
+
223
+ Un autoencoder variacional construido sobre encoder y decoder LSTM. En lugar de producir
224
+ un único vector latente, el encoder produce una distribución posterior; el decoder
225
+ reconstruye a partir de una muestra. La función de pérdida es la ELBO habitual: MSE de
226
+ reconstrucción más un término KL que regulariza la posterior hacia una gaussiana
227
+ estándar, escalado por un factor beta. Esto aporta una interpretación probabilística al
228
+ score y se comporta mejor en el paso de selección de umbral.
229
+
230
+ ### 5.5 Ensembles (evaluados y descartados)
231
+
232
+ Como parte de la comparación se evalúan dos estrategias de ensemble. Tras calibrar cada
233
+ modelo con z-score sobre las normales de validación, los tres modelos profundos se
234
+ combinan por la **media** de sus scores (consenso) o por el **máximo** (más sensible).
235
+ Ninguna combinación supera al mejor modelo individual en el test real, así que el
236
+ ensemble queda documentado y descartado. El detector final es el VAE-LSTM en solitario.
237
+
238
+ ### 5.6 Infraestructura de entrenamiento compartida
239
+
240
+ Los tres modelos profundos comparten el mismo loop de entrenamiento, implementado una
241
+ sola vez en `src/sadar/models/training.py`. El loop usa:
242
+
243
+ - **Adam** con weight decay,
244
+ - planificador **ReduceLROnPlateau**,
245
+ - **early stopping** sobre la pérdida de validación,
246
+ - **MLflow** para tracking (losses, hiperparámetros, mejor checkpoint),
247
+ - **Optuna** para búsqueda de hiperparámetros, usado sobre todo en el VAE-LSTM.
248
+
249
+ Cada arquitectura tiene su propio fichero YAML bajo `configs/`. La reproducibilidad se
250
+ garantiza con semillas fijas y guardando junto al checkpoint la configuración exacta de
251
+ preprocesado utilizada.
252
+
253
+ ---
254
+
255
+ ## 6. Banco de anomalías sintéticas
256
+
257
+ Como las anomalías reales son escasas y carecen de etiqueta, se construye un banco
258
+ controlado sobre los vuelos normales del test. Cada escenario parte de una ventana
259
+ normal real e inyecta una perturbación conocida y etiquetada que se aplica de forma
260
+ gradual a partir de la mitad de la ventana. Se implementan cinco tipos de inyección,
261
+ cada uno en varias intensidades:
262
+
263
+ | Tipo | Descripción |
264
+ |---|---|
265
+ | Desviación de ruta | Salida lateral del corredor, parametrizada en metros. |
266
+ | Anomalía de altitud | Desplazamiento de altitud inconsistente con la fase. |
267
+ | Anomalía de velocidad | Aceleración o deceleración multiplicativa. |
268
+ | Holding | Giro de 360 ° repetido con periodo configurable. |
269
+ | Congelado de transpondedor | La señal se mantiene fija desde un punto dado. |
270
+
271
+ El banco produce etiquetas de verdad (cada inyección sabe exactamente cuándo empieza) y
272
+ por tanto permite dos métricas que los datos reales no pueden dar: **PR-AUC por tipo de
273
+ anomalía** y **latencia de detección** (mediana de segundos entre el inicio de la
274
+ inyección y el primer instante en que el score por paso cruza el umbral de alerta).
275
+
276
+ El banco sintético se utiliza únicamente para evaluación. Ninguna ventana sintética
277
+ entra jamás en el entrenamiento.
278
+
279
+ ---
280
+
281
+ ## 7. Evaluación
282
+
283
+ ### 7.1 Conjunto de test real
284
+
285
+ Los cuatro detectores se puntúan sobre el mismo split de test. La métrica principal es
286
+ **PR-AUC**, más informativa que la ROC-AUC dado el fuerte desbalance de clases en datos
287
+ reales.
288
+
289
+ | Modelo | ROC-AUC | PR-AUC | ROC-AUC media sintética | Latencia mediana |
290
+ |---|---|---|---|---|
291
+ | Baseline Isolation Forest | 0,515 | 0,133 | 0,593 | n/d |
292
+ | Autoencoder LSTM | 0,648 | 0,260 | 0,779 | 115 s |
293
+ | Autoencoder Transformer | 0,614 | 0,227 | 0,743 | 115 s |
294
+ | **VAE-LSTM (seleccionado)** | **0,659** | **0,299** | **0,792** | 120 s |
295
+ | Ensemble (media) | 0,652 | 0,273 | 0,778 | n/d |
296
+ | Ensemble (máximo) | 0,658 | 0,278 | 0,774 | n/d |
297
+
298
+ Se desprenden tres conclusiones:
299
+
300
+ - Los modelos profundos aportan valor sustancial sobre el baseline (la PR-AUC
301
+ aproximadamente se duplica).
302
+ - El VAE-LSTM es el mejor en conjunto: mayor PR-AUC sobre datos reales, mayor ROC-AUC
303
+ media sintética y latencia competitiva.
304
+ - Las dos estrategias de ensemble no superan al mejor modelo individual y no se
305
+ retienen.
306
+
307
+ ### 7.2 Banco sintético (ROC-AUC por anomalía)
308
+
309
+ El VAE-LSTM es el mejor en todos los tipos de anomalía. El patrón es consistente: las
310
+ perturbaciones grandes y sostenidas son más fáciles que las sutiles, y las maniobras
311
+ estructuradas (holding, cambios fuertes de velocidad) son más fáciles que los
312
+ desplazamientos pequeños.
313
+
314
+ | Anomalía | Intensidad | Baseline | LSTM | Transformer | VAE-LSTM |
315
+ |---|---|---|---|---|---|
316
+ | Desviación de ruta | 20.000 m | 0,513 | 0,556 | 0,543 | 0,558 |
317
+ | Desviación de ruta | 40.000 m | 0,537 | 0,684 | 0,657 | 0,700 |
318
+ | Desviación de ruta | 80.000 m | 0,618 | 0,883 | 0,865 | 0,899 |
319
+ | Desviación de altitud | 300 m | 0,502 | 0,512 | 0,505 | 0,514 |
320
+ | Desviación de altitud | 800 m | 0,518 | 0,581 | 0,531 | 0,593 |
321
+ | Desviación de altitud | 1.500 m | 0,561 | 0,726 | 0,606 | 0,752 |
322
+ | Factor de velocidad | x1,6 | 0,620 | 0,901 | 0,835 | 0,927 |
323
+ | Factor de velocidad | x2,2 | 0,743 | 0,985 | 0,965 | 0,989 |
324
+ | Factor de velocidad | x0,4 | 0,673 | 0,901 | 0,884 | 0,925 |
325
+ | Holding | 240 s/giro | 0,614 | 0,972 | 0,979 | 0,978 |
326
+ | Holding | 120 s/giro | 0,634 | 0,975 | 0,985 | 0,984 |
327
+ | Congelado transpondedor | fijo | 0,582 | 0,668 | 0,558 | 0,686 |
328
+
329
+ ### 7.3 Curvas y matrices de confusión
330
+
331
+ Para cada detector se generan curvas precision-recall y ROC, junto con una matriz de
332
+ confusión tomada al umbral del percentil 99 de validación. Las figuras se guardan como
333
+ PNG en `reports/figures/` y los arrays subyacentes se almacenan dentro de
334
+ `reports/model_comparison.json`, de modo que el dashboard puede renderizarlas sin
335
+ recalcular.
336
+
337
+ ---
338
+
339
+ ## 8. Inferencia y servicio
340
+
341
+ El VAE-LSTM seleccionado se sirve mediante una aplicación FastAPI compacta
342
+ (`src/sadar/serve/app.py`). En el arranque el servicio carga:
343
+
344
+ - el checkpoint del modelo de producción,
345
+ - el escalador ajustado con los datos de entrenamiento,
346
+ - el informe de métricas precomputado,
347
+ - y una muestra curada de trayectorias para que el dashboard pueda funcionar sin los
348
+ datos brutos.
349
+
350
+ La superficie HTTP es deliberadamente reducida. El dashboard se comunica con el backend
351
+ a través de cinco endpoints:
352
+
353
+ | Endpoint | Función |
354
+ |---|---|
355
+ | `GET /api/health` | Sonda de vida (también devuelve un identificador de build). |
356
+ | `GET /api/scene` | Escena de radar actual: aeronaves, scores, umbral. |
357
+ | `GET /api/flights` | Lista ordenada de vuelos monitorizados con metadatos. |
358
+ | `GET /api/metrics` | Métricas comparativas y curvas precomputadas. |
359
+ | `POST /api/simulate` | Inyecta una anomalía en un vuelo y devuelve la respuesta. |
360
+
361
+ En producción, el mismo proceso FastAPI también sirve la SPA de React ya compilada (la
362
+ SPA se monta en `/`, la API queda bajo `/api`). Todo el sistema corre en un único
363
+ contenedor.
364
+
365
+ ---
366
+
367
+ ## 9. Frontend
368
+
369
+ El dashboard es una aplicación React 18 + Vite + TypeScript diseñada para que parezca y
370
+ se sienta como una consola de torre de control: fondo oscuro, bloques de datos en
371
+ monoespaciada, criticidad codificada por color (verde y cian para normal, ámbar y rojo
372
+ para alerta). Expone cuatro pantallas:
373
+
374
+ 1. **Consola torre.** Un radar en directo centrado en LEMD con un panel lateral que
375
+ lista todos los vuelos monitorizados y su estado actual. Las aeronaves en seguimiento
376
+ o en alerta se diferencian por color.
377
+ 2. **Simulador.** El corazón interactivo de la demo. El operador elige un vuelo, un tipo
378
+ de anomalía, una intensidad y un instante de inicio, e inyecta la perturbación en
379
+ tiempo real. El score de la derecha reacciona a la inyección y la latencia de
380
+ detección se reporta en segundos.
381
+ 3. **Métricas.** El informe comparativo completo: curvas ROC y PR, desglose de PR-AUC
382
+ por anomalía, justificación de la selección del modelo y resumen del detector final.
383
+ 4. **Presentación.** Un recorrido guiado utilizado para presentaciones en vivo del
384
+ proyecto.
385
+
386
+ La aplicación está internacionalizada en inglés y español mediante un pequeño contexto
387
+ de traducción.
388
+
389
+ ---
390
+
391
+ ## 10. Reproducibilidad y despliegue
392
+
393
+ El proyecto está diseñado para que el pipeline completo (preprocesado, entrenamiento,
394
+ evaluación, servicio) pueda reproducirse desde cero.
395
+
396
+ - **Entorno Python.** Fijado con `uv` y `pyproject.toml`; un único comando
397
+ (`uv sync`) reconstruye el entorno.
398
+ - **Entorno de frontend.** Gestionado con `pnpm` y `pnpm-lock.yaml`.
399
+ - **Configuración.** Todos los parámetros (rutas, longitud de ventana, dimensiones del
400
+ modelo, plan de entrenamiento, umbrales de evaluación) viven en ficheros YAML bajo
401
+ `configs/`. El código no lee rutas literales.
402
+ - **Semillas.** Una única semilla se fija al inicio de cada entry point y se propaga a
403
+ NumPy, scikit-learn y PyTorch.
404
+ - **Tracking.** MLflow registra todas las ejecuciones en local; Optuna guarda su
405
+ historial de búsqueda junto a los hiperparámetros resultantes.
406
+ - **Containerización.** Un único Dockerfile construye una imagen autocontenida que
407
+ incluye el modelo entrenado, el escalador, los informes precomputados y la SPA. La
408
+ misma imagen se utiliza en local (`docker compose up`) y en producción (Hugging Face
409
+ Spaces).
410
+
411
+ Un `Makefile` expone el flujo como targets con nombre: `make install`,
412
+ `make preprocess`, `make train-lstm`, `make train-transformer`, `make train-vae`,
413
+ `make tune-vae`, `make compare`, `make serve`, `make dev`, `make docker-up`.
414
+
415
+ ---
416
+
417
+ ## 11. Limitaciones y trabajo futuro
418
+
419
+ ### 11.1 Limitaciones
420
+
421
+ - **Alcance.** El dataset cubre un solo aeropuerto (LEMD) y un periodo acotado. No se
422
+ espera que el modelo transfiera a otras terminales sin reentrenamiento.
423
+ - **Sin planes de vuelo.** La "ruta prevista" se aproxima como "patrón normal
424
+ aprendido". Un vuelo que se aparte de su plan de vuelo pero permanezca dentro del
425
+ patrón normal del tráfico podría no ser marcado.
426
+ - **Huecos de cobertura.** Los huecos del transpondedor en ADS-B están dominados por la
427
+ cobertura del receptor. Se usan como característica de entrada, no como etiqueta.
428
+ - **Desbalance de clases.** Los eventos anómalos reales son extremadamente raros. Las
429
+ cifras de PR-AUC sobre el test real se calculan con un conjunto positivo pequeño; el
430
+ banco sintético es el referente cuantitativo principal.
431
+
432
+ ### 11.2 Trabajo futuro
433
+
434
+ - Ampliar el dataset a más aeropuertos y validar la generalización entre terminales.
435
+ - Incorporar los planes de vuelo, cuando estén disponibles, como señal auxiliar.
436
+ - Combinar el score de conformidad con un clasificador posterior entrenado sobre
437
+ eventos operativos etiquetados (motor y al aire, aproximaciones frustradas, holding)
438
+ para producir alertas más interpretables.
439
+ - Explorar ensembles probabilísticos que ponderen cada modelo por su verosimilitud
440
+ calibrada en lugar de hacerlo con un reductor fijo.
441
+
442
+ ---
443
+
444
+ ## 12. Conclusiones
445
+
446
+ SADAR muestra que un enfoque de aprendizaje profundo de una sola clase es una base
447
+ viable para la monitorización de conformidad de trayectorias en un aeropuerto
448
+ importante. Tres autoencoders secuenciales entrenados con los mismos datos y evaluados
449
+ sobre el mismo banco arrojan una elección de modelo clara y defendible: el
450
+ **VAE-LSTM** combina la mejor PR-AUC sobre datos reales, el mejor desempeño medio en el
451
+ banco sintético y una latencia mediana de detección competitiva, mejorando con holgura
452
+ a un baseline no secuencial. El sistema es completamente reproducible, se empaqueta en
453
+ un único contenedor y se sirve a través de un dashboard construido en torno a un caso
454
+ de uso real.
455
+
456
+ El resultado no es un producto cerrado, pero sí un prototipo sólido de cómo la
457
+ monitorización de conformidad basada en datos puede complementar las herramientas
458
+ basadas en reglas que usan hoy los operadores del tráfico aéreo.
459
+
460
+ ---
461
+
462
+ **Repositorio.** Smart Anomaly Detection for Aviation Routes (SADAR).
463
+ Madrid-Barajas (LEMD) · 2026.
docs/REPORT.md ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SADAR
2
+
3
+ **Smart Anomaly Detection for Aviation Routes**
4
+
5
+ Technical Report
6
+
7
+ ---
8
+
9
+ **Project:** SADAR · Flight Conformance Monitor
10
+ **Domain:** Air traffic surveillance · ADS-B data
11
+ **Airport of study:** Madrid-Barajas (LEMD)
12
+ **Approach:** One-class deep learning for trajectory anomaly detection
13
+ **Date:** 2026
14
+
15
+ ---
16
+
17
+ ## Executive summary
18
+
19
+ SADAR is a deep-learning system that learns the normal pattern of approach and departure
20
+ operations at Madrid-Barajas (LEMD) from ADS-B surveillance data and surfaces, in real time,
21
+ any trajectory that deviates from that pattern. The system is built as a **flight
22
+ conformance monitor**: it does not classify specific events, it raises an interpretable
23
+ anomaly score for every flight in the airspace so that an operator can review the cases
24
+ that fall outside the learned distribution.
25
+
26
+ Three sequence autoencoders are trained under identical conditions on roughly 20.000 real
27
+ flights from the OpenSky Network (an LSTM autoencoder, a Transformer autoencoder and a
28
+ VAE-LSTM), together with a non-sequential Isolation Forest baseline. The four detectors
29
+ are evaluated on the same held-out test set and on a synthetic anomaly bench with five
30
+ injection types and several intensities. The **VAE-LSTM** is selected as the production
31
+ model: it achieves the highest PR-AUC on real held-out data (0.299) and the highest
32
+ synthetic mean ROC-AUC (0.792). Two ensemble strategies (consensus and most-sensitive) are
33
+ also evaluated and discarded because neither beats the best individual model.
34
+
35
+ The selected model is wrapped in a FastAPI inference service and served through a
36
+ React + Vite dashboard that combines a tower-style live radar, a ranked anomaly list, a
37
+ metrics page and a fully interactive simulator. The whole system is reproducible from a
38
+ single Docker image and ships ready for deployment on Hugging Face Spaces.
39
+
40
+ ---
41
+
42
+ ## 1. Motivation and scope
43
+
44
+ Trajectory monitoring is a core function of air traffic management. Today it relies on
45
+ deterministic rules and human supervision: separation, descent profiles, holding patterns
46
+ and emergency squawk codes are all monitored against fixed thresholds. These rules work
47
+ well for the situations they were designed for, but they cannot anticipate behavior that
48
+ is unusual without being a textbook violation.
49
+
50
+ SADAR explores a complementary, data-driven approach. Instead of encoding rules, the
51
+ system **learns** what a normal operation at LEMD looks like from a large sample of real
52
+ flights, and treats every new trajectory as more or less compatible with that learned
53
+ distribution. The output is a continuous conformance score that an operator can rank,
54
+ filter and threshold.
55
+
56
+ The scope of the project is deliberately narrow and well defined:
57
+
58
+ - **What it does.** Detects deviations from the normal traffic pattern at one airport
59
+ (LEMD) based on a fixed set of dynamic features extracted from ADS-B reports.
60
+ - **What it does not do.** Predict specific operational events, replace controllers,
61
+ produce certified safety recommendations or generalize without retraining to airports
62
+ with different procedures.
63
+
64
+ This framing as a **conformance monitor** is what makes the problem tractable with the
65
+ data available: it requires only examples of normal flight, which are abundant, and it
66
+ keeps the interpretation of the output clear ("this trajectory is unlike the ones the
67
+ model has seen") without overclaiming.
68
+
69
+ ---
70
+
71
+ ## 2. Data
72
+
73
+ ### 2.1 Source
74
+
75
+ The dataset is built from ADS-B reports collected by the OpenSky Network around the
76
+ Madrid-Barajas terminal area. It covers approximately 18 days of operations sampled
77
+ between June 2017 and March 2020. Each aircraft is reported once every 10 seconds, which
78
+ is the natural cadence of ADS-B.
79
+
80
+ | Property | Value |
81
+ |---|---|
82
+ | Total trajectories | ≈ 20.000 (≈ 950 to 1.200 per day) |
83
+ | Rows per day | 140.000 to 240.000 |
84
+ | Columns | 21 |
85
+ | Points per flight (min · median · max) | 40 · 188 · 1.246 |
86
+
87
+ ### 2.2 Variables
88
+
89
+ The raw schema includes positional information (latitude, longitude, time), dynamics
90
+ (barometric and geometric altitude, ground velocity, heading, vertical rate), derived
91
+ fields (distance to runway, flight phase), identity (ICAO24, callsign, flight id) and
92
+ transponder state (squawk, on-ground flag, alert flag, special-purpose indicator, last
93
+ contact). One field, `operation`, is entirely missing and is dropped.
94
+
95
+ ### 2.3 Quality and cleaning
96
+
97
+ A profiling pass over the full dataset surfaces several issues that are handled in the
98
+ preprocessing pipeline:
99
+
100
+ - **Missing values.** `geoaltitude` is missing in 34.9 % of rows, `baroaltitude` in
101
+ 19.6 %, kinematic fields (`velocity`, `heading`, `vertrate`) around 19 %. The rest are
102
+ below 4 %.
103
+ - **Duplicate file.** One day is shipped twice with different filenames and is
104
+ deduplicated by content hash.
105
+ - **Empty or errored days.** A handful of days arrive with zero rows or with manifest
106
+ errors and are dropped.
107
+ - **Coverage gaps.** About 8 % of airborne flights show a gap greater than 120 s between
108
+ consecutive reports. These gaps are almost always receiver-coverage artifacts, not
109
+ operational events, and are used as a feature, never as a label.
110
+
111
+ After cleaning, the dataset contains a stable set of normal LEMD operations spanning
112
+ multiple seasons and including the start of 2020, which doubles as a natural distribution
113
+ shift to stress-test the temporal split.
114
+
115
+ ### 2.4 Notable observations
116
+
117
+ The exploratory analysis confirms a small set of rare cases that are kept aside for
118
+ validation only:
119
+
120
+ - Four flights carry an emergency squawk code at some point of their trajectory.
121
+ - Roughly one hundred flights show a clear go-around pattern (an approach followed by a
122
+ sustained climb).
123
+
124
+ None of these flights enter the training set. They serve as a sanity check for the
125
+ deployed model.
126
+
127
+ ---
128
+
129
+ ## 3. Problem formulation
130
+
131
+ The dataset contains many normal flights and almost no labeled anomalies. This rules out
132
+ supervised classification and points to **one-class learning**: train a model that
133
+ captures the distribution of normal trajectories and score new flights by how compatible
134
+ they are with that distribution.
135
+
136
+ Concretely, all three deep models are **autoencoders** trained on normal flights only.
137
+ The training objective is to reconstruct the input window from a compressed latent
138
+ representation; at inference time the **reconstruction error** is used as the conformance
139
+ score. A trajectory that resembles the normal patterns is reconstructed accurately and
140
+ gets a low score; a trajectory that departs from those patterns is harder to reconstruct
141
+ and gets a high score.
142
+
143
+ This formulation has three properties that match the operational context. It needs only
144
+ normal data, which is the only data available at scale. It produces a continuous,
145
+ interpretable score instead of a hard label, which is the right output for a tool that
146
+ supports a human operator. And it gracefully handles unknown failure modes: any
147
+ trajectory the model has not learned to reconstruct will surface as anomalous regardless
148
+ of what makes it unusual.
149
+
150
+ ---
151
+
152
+ ## 4. Preprocessing pipeline
153
+
154
+ The preprocessing pipeline turns raw parquet files into fixed-length, standardized
155
+ tensors ready for training. Every step is implemented as a small, testable function under
156
+ `src/sadar/data/`.
157
+
158
+ 1. **Load and merge.** All parquet files under `$SADAR_DATA_DIR` are read with pyarrow,
159
+ concatenated and deduplicated. Empty or errored days are dropped.
160
+ 2. **Coordinate transform.** Latitude and longitude are projected to a runway-relative
161
+ metric system using `pyproj`. This makes the model robust to the curvature of
162
+ coordinates and gives the network meters as input units.
163
+ 3. **Heading encoding.** Heading is encoded as `(sin(heading), cos(heading))` to avoid
164
+ the discontinuity at 0 / 360 degrees.
165
+ 4. **Missing-value handling.** Short gaps inside an otherwise complete trajectory are
166
+ filled with linear interpolation. Trajectories with too many missing samples are
167
+ discarded.
168
+ 5. **Resampling.** Every flight is resampled to a uniform 10 s grid, which matches the
169
+ nominal cadence of ADS-B.
170
+ 6. **Windowing.** Each resampled trajectory is sliced into fixed-length windows. The
171
+ window length is a hyperparameter, set in `configs/preprocessing.yaml`.
172
+ 7. **Standardization.** Each feature is normalized to zero mean and unit variance. The
173
+ scaler is fit **only on the training split** to avoid leakage and saved as
174
+ `data/processed/scaler.npz`, so the exact same transform is applied at inference time.
175
+ 8. **Leak-free split.** The split is done jointly by `flight_id` and by date: no flight
176
+ ever appears in more than one split, and the test split is strictly later in time than
177
+ the training split. Years 2017 to 2019 are used for training and validation; 2020 is
178
+ reserved for testing. This realistic split is what allows latency numbers to be taken
179
+ at face value.
180
+ 9. **Training set cleaning.** The few flights with emergency squawks and the suspected
181
+ go-arounds are removed from the training split. Only "normal" trajectories enter the
182
+ training set.
183
+
184
+ The input to all deep models is a window of seven features:
185
+ `[x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]`.
186
+
187
+ ---
188
+
189
+ ## 5. Models
190
+
191
+ ### 5.1 Isolation Forest baseline
192
+
193
+ A non-sequential reference detector. Each window is summarized by per-feature statistics
194
+ (mean, standard deviation, min, max) and fed to an Isolation Forest. The baseline is
195
+ useful to quantify how much sequential modeling actually adds.
196
+
197
+ ### 5.2 LSTM autoencoder
198
+
199
+ A two-layer LSTM encoder compresses the input window into a latent vector, and a mirror
200
+ LSTM decoder reconstructs it. Trained end-to-end with MSE loss and the shared training
201
+ loop described below. This is the workhorse architecture: stable to train, fast to score
202
+ and well aligned with the literature on ADS-B trajectory modeling.
203
+
204
+ ### 5.3 Transformer autoencoder
205
+
206
+ The same encoder-decoder skeleton, but with multi-head self-attention instead of
207
+ recurrence. The Transformer captures long-range dependencies inside the window without
208
+ the sequential bottleneck of LSTMs, at the cost of more parameters and a larger
209
+ hyperparameter surface.
210
+
211
+ ### 5.4 VAE-LSTM (selected production model)
212
+
213
+ A variational autoencoder built on LSTM encoder and decoder. Instead of producing a single
214
+ latent vector, the encoder produces a posterior distribution; the decoder reconstructs
215
+ from a sample drawn from it. The training loss is the standard ELBO: reconstruction MSE
216
+ plus a KL term that regularizes the posterior toward a unit Gaussian, scaled by a
217
+ beta factor. This adds a principled, probabilistic interpretation to the score and
218
+ behaves better at the threshold-selection step.
219
+
220
+ ### 5.5 Ensembling (evaluated and discarded)
221
+
222
+ Two ensemble strategies are evaluated as part of the comparison. After per-model
223
+ z-score calibration on validation normals, the three deep models are combined either by
224
+ their **mean** score (consensus) or by their **max** score (most-sensitive). Neither
225
+ combination beats the best individual model on the real test set, so the ensemble is
226
+ documented and discarded. The final detector is the VAE-LSTM alone.
227
+
228
+ ### 5.6 Shared training infrastructure
229
+
230
+ All three deep models share the same training loop, implemented once in
231
+ `src/sadar/models/training.py`. The loop uses:
232
+
233
+ - **Adam** with weight decay,
234
+ - a **ReduceLROnPlateau** scheduler,
235
+ - **early stopping** on the validation loss,
236
+ - **MLflow** for tracking (losses, hyperparameters, best checkpoint),
237
+ - **Optuna** for hyperparameter search, used here mainly for the VAE-LSTM.
238
+
239
+ Each architecture has its own YAML configuration file under `configs/`. Reproducibility
240
+ is enforced by fixed seeds and by storing the exact preprocessing config alongside the
241
+ checkpoint.
242
+
243
+ ---
244
+
245
+ ## 6. Synthetic anomaly bench
246
+
247
+ Because real anomalies are rare and unlabeled, a controlled benchmark is built on top of
248
+ normal test flights. Each scenario starts from a real normal window and injects a known,
249
+ labeled perturbation that ramps in halfway through the window. Five types of injection
250
+ are implemented, each at multiple intensities:
251
+
252
+ | Type | Description |
253
+ |---|---|
254
+ | Route deviation | Lateral departure from the corridor, parameterized in meters. |
255
+ | Altitude anomaly | Altitude offset inconsistent with the flight phase. |
256
+ | Speed anomaly | Multiplicative acceleration or deceleration. |
257
+ | Holding | A repeated 360 ° turn at a configurable period. |
258
+ | Transponder freeze | The signal is held constant from a chosen point. |
259
+
260
+ The bench produces ground-truth labels (each injection knows exactly when it starts) and
261
+ therefore enables two metrics that real data cannot give us: **PR-AUC per anomaly type**
262
+ and **detection latency** (median seconds between the injection onset and the first
263
+ moment the per-step score crosses the alert threshold).
264
+
265
+ The synthetic bench is used only for evaluation. No synthetic window ever enters the
266
+ training set.
267
+
268
+ ---
269
+
270
+ ## 7. Evaluation
271
+
272
+ ### 7.1 Real test set
273
+
274
+ The four detectors are scored on the same held-out test split. The headline metric is
275
+ **PR-AUC**, which is more informative than ROC-AUC under the strong class imbalance
276
+ present in real data.
277
+
278
+ | Model | ROC-AUC | PR-AUC | Synthetic mean ROC-AUC | Median latency |
279
+ |---|---|---|---|---|
280
+ | Isolation Forest baseline | 0.515 | 0.133 | 0.593 | n/a |
281
+ | LSTM autoencoder | 0.648 | 0.260 | 0.779 | 115 s |
282
+ | Transformer autoencoder | 0.614 | 0.227 | 0.743 | 115 s |
283
+ | **VAE-LSTM (selected)** | **0.659** | **0.299** | **0.792** | 120 s |
284
+ | Ensemble (mean) | 0.652 | 0.273 | 0.778 | n/a |
285
+ | Ensemble (max) | 0.658 | 0.278 | 0.774 | n/a |
286
+
287
+ Three conclusions stand out:
288
+
289
+ - The deep models add substantial value over the baseline (PR-AUC roughly doubles).
290
+ - The VAE-LSTM is the best overall: highest PR-AUC on real data, highest synthetic mean
291
+ ROC-AUC, competitive latency.
292
+ - The two ensemble strategies do not beat the best individual model and are not retained.
293
+
294
+ ### 7.2 Synthetic bench (per-anomaly ROC-AUC)
295
+
296
+ The VAE-LSTM performs best across all anomaly types. The pattern is consistent: large,
297
+ sustained perturbations are easier than subtle ones, and structured maneuvers (holding,
298
+ high-magnitude speed change) are easier than small drifts.
299
+
300
+ | Anomaly | Intensity | Baseline | LSTM | Transformer | VAE-LSTM |
301
+ |---|---|---|---|---|---|
302
+ | Route deviation | 20.000 m | 0.513 | 0.556 | 0.543 | 0.558 |
303
+ | Route deviation | 40.000 m | 0.537 | 0.684 | 0.657 | 0.700 |
304
+ | Route deviation | 80.000 m | 0.618 | 0.883 | 0.865 | 0.899 |
305
+ | Altitude offset | 300 m | 0.502 | 0.512 | 0.505 | 0.514 |
306
+ | Altitude offset | 800 m | 0.518 | 0.581 | 0.531 | 0.593 |
307
+ | Altitude offset | 1.500 m | 0.561 | 0.726 | 0.606 | 0.752 |
308
+ | Speed factor | x1.6 | 0.620 | 0.901 | 0.835 | 0.927 |
309
+ | Speed factor | x2.2 | 0.743 | 0.985 | 0.965 | 0.989 |
310
+ | Speed factor | x0.4 | 0.673 | 0.901 | 0.884 | 0.925 |
311
+ | Holding | 240 s/turn | 0.614 | 0.972 | 0.979 | 0.978 |
312
+ | Holding | 120 s/turn | 0.634 | 0.975 | 0.985 | 0.984 |
313
+ | Transponder freeze | stuck | 0.582 | 0.668 | 0.558 | 0.686 |
314
+
315
+ ### 7.3 Curves and confusion matrices
316
+
317
+ Precision-recall and ROC curves are produced for every detector, together with a
318
+ confusion matrix taken at the validation-99-percentile threshold. The figures are
319
+ saved as PNGs under `reports/figures/` and the underlying arrays are stored inside
320
+ `reports/model_comparison.json`, so the dashboard can render them without recomputing.
321
+
322
+ ---
323
+
324
+ ## 8. Serving
325
+
326
+ The selected VAE-LSTM is served by a small FastAPI application
327
+ (`src/sadar/serve/app.py`). At startup, the service loads:
328
+
329
+ - the checkpoint of the production model,
330
+ - the scaler fit on the training data,
331
+ - the precomputed metrics report,
332
+ - and a curated sample of trajectories so the dashboard can run without raw data.
333
+
334
+ The HTTP surface is intentionally small. The dashboard talks to the backend through five
335
+ endpoints:
336
+
337
+ | Endpoint | Purpose |
338
+ |---|---|
339
+ | `GET /api/health` | Liveness probe (also serves a build identifier). |
340
+ | `GET /api/scene` | Current radar scene: aircraft, scores, threshold. |
341
+ | `GET /api/flights` | Ranked list of monitored flights with metadata. |
342
+ | `GET /api/metrics` | Pre-computed comparative metrics and curves. |
343
+ | `POST /api/simulate` | Inject an anomaly on a chosen flight and return the response. |
344
+
345
+ In production, the same FastAPI process also serves the built React app (the SPA is
346
+ mounted on `/`, the API stays under `/api`). The whole system runs in a single
347
+ container.
348
+
349
+ ---
350
+
351
+ ## 9. Frontend
352
+
353
+ The dashboard is a React 18 + Vite + TypeScript application designed to look and feel
354
+ like a tower control console: dark background, monospace data blocks, criticality
355
+ encoded by color (green/cyan for normal, amber/red for alert). It exposes four screens:
356
+
357
+ 1. **Tower console.** A live radar scope centered on LEMD with a side panel listing every
358
+ tracked flight and its current state. Tracked vs alerted aircraft are color-coded.
359
+ 2. **Simulator.** The interactive heart of the demo. The operator picks a flight, an
360
+ anomaly type, an intensity and an onset, and injects the perturbation in real time.
361
+ The score on the right reacts to the injection and the detection latency is reported
362
+ in seconds.
363
+ 3. **Metrics.** The full comparative report: ROC and PR curves, per-anomaly PR-AUC
364
+ breakdown, model selection rationale and final-detector summary.
365
+ 4. **Presentation.** A guided walkthrough used for live presentations of the project.
366
+
367
+ The app is internationalized in English and Spanish through a small translation context.
368
+
369
+ ---
370
+
371
+ ## 10. Reproducibility and deployment
372
+
373
+ The project is engineered so that the entire pipeline (preprocessing, training,
374
+ evaluation, serving) can be reproduced from scratch.
375
+
376
+ - **Python environment.** Locked with `uv` and `pyproject.toml`; one command (`uv sync`)
377
+ rebuilds the environment.
378
+ - **Frontend environment.** Managed with `pnpm` and `pnpm-lock.yaml`.
379
+ - **Configuration.** All knobs (paths, window length, model dimensions, training
380
+ schedule, evaluation thresholds) live in YAML files under `configs/`. The code reads no
381
+ literal paths.
382
+ - **Seeds.** A single seed is set at the top of every entry point; it propagates to
383
+ NumPy, scikit-learn and PyTorch.
384
+ - **Tracking.** MLflow logs all training runs locally; Optuna stores its search history
385
+ alongside the resulting hyperparameters.
386
+ - **Containerization.** A single Dockerfile builds a self-contained image that bundles
387
+ the trained model, the scaler, the precomputed reports and the SPA. The same image is
388
+ used locally (`docker compose up`) and in production (Hugging Face Spaces).
389
+
390
+ A `Makefile` exposes the workflow as named targets: `make install`, `make preprocess`,
391
+ `make train-lstm`, `make train-transformer`, `make train-vae`, `make tune-vae`,
392
+ `make compare`, `make serve`, `make dev`, `make docker-up`.
393
+
394
+ ---
395
+
396
+ ## 11. Limitations and future work
397
+
398
+ ### 11.1 Limitations
399
+
400
+ - **Scope.** The dataset covers one airport (LEMD) and a limited time span. The model is
401
+ not expected to transfer to other terminals without retraining.
402
+ - **No flight plans.** "Intended route" is approximated as "learned normal pattern". A
403
+ flight that deviates from its filed route but stays inside the normal traffic pattern
404
+ may not be flagged.
405
+ - **Coverage gaps.** Transponder gaps in ADS-B are dominated by receiver coverage. They
406
+ are used as an input feature, not as a label.
407
+ - **Class imbalance.** Real anomalous events are extremely rare. The PR-AUC numbers on
408
+ the real test set are computed on a small positive set; the synthetic bench is the
409
+ primary quantitative benchmark.
410
+
411
+ ### 11.2 Future work
412
+
413
+ - Extend the dataset to additional airports and validate cross-airport generalization.
414
+ - Incorporate filed flight plans, when available, as an auxiliary signal.
415
+ - Combine the conformance score with a downstream classifier trained on labeled
416
+ operational events (go-arounds, missed approaches, holding) for more interpretable
417
+ alerts.
418
+ - Explore probabilistic ensembles that weight each model by its calibrated likelihood
419
+ rather than by a fixed reducer.
420
+
421
+ ---
422
+
423
+ ## 12. Conclusions
424
+
425
+ SADAR shows that a one-class deep-learning approach is a viable foundation for trajectory
426
+ conformance monitoring at a major airport. Three sequence autoencoders trained on the
427
+ same data and evaluated on the same bench produce a clear, defensible model choice: the
428
+ **VAE-LSTM** combines the best PR-AUC on real data, the best mean performance on the
429
+ synthetic bench and a competitive median detection latency, while improving substantially
430
+ over a non-sequential baseline. The system is fully reproducible, packaged as a single
431
+ container and served through a dashboard built around a real operational use case.
432
+
433
+ The result is not a finished product, but it is a solid prototype of how data-driven
434
+ conformance monitoring could complement the rule-based tools that air traffic operators
435
+ use today.
436
+
437
+ ---
438
+
439
+ **Repository.** Smart Anomaly Detection for Aviation Routes (SADAR).
440
+ Madrid-Barajas (LEMD) · 2026.
docs/assets/eda/flight_phases.png ADDED

Git LFS Details

  • SHA256: d0bb0ae258d5cef4bd2157d66f491d832bc02de669179e26507cefadba3f5e84
  • Pointer size: 130 Bytes
  • Size of remote file: 29.7 kB
docs/assets/eda/heading_polar.png ADDED

Git LFS Details

  • SHA256: c7afbba8ca92e0a2a504beda68f55ee7be377287162437b6a296ddfb9f165263
  • Pointer size: 131 Bytes
  • Size of remote file: 150 kB
docs/assets/eda/nulls_per_column.png ADDED

Git LFS Details

  • SHA256: c8dd9b82c16358f6f0bfdb309449baf2af868b227e08430c5459d10ce8df0a22
  • Pointer size: 130 Bytes
  • Size of remote file: 29.7 kB
docs/assets/eda/trajectories_map.png ADDED

Git LFS Details

  • SHA256: 63dff8fde7995ec5ee6728a02561d98a82430c829f673e5452b9a9f222fda6f4
  • Pointer size: 131 Bytes
  • Size of remote file: 154 kB
docs/assets/img1.png ADDED

Git LFS Details

  • SHA256: ab9cba574557b4f894aa9be2255abd9a11c1011df2935aa2bd19d0c938af6670
  • Pointer size: 131 Bytes
  • Size of remote file: 304 kB
docs/assets/img2.png ADDED

Git LFS Details

  • SHA256: 5335ec5d101cab23e4475471a4b9392b4be30e22a88b71fd4509f51bfc0073c3
  • Pointer size: 131 Bytes
  • Size of remote file: 167 kB
docs/assets/img3.png ADDED

Git LFS Details

  • SHA256: f583a61049105c04c29e164e9b01b286e9c843bcc5b4a3a5f3d6980aa1a1a881
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
docs/assets/img4.png ADDED

Git LFS Details

  • SHA256: 9035b0de26441f87ef0e876ed111ab2f00c83167c333a6ac35efce272420d769
  • Pointer size: 131 Bytes
  • Size of remote file: 342 kB
docs/assets/img5.png ADDED

Git LFS Details

  • SHA256: 912ddfe4b8f3c7631287a7a4780a5b6ffe3322b1814b63c0f2fc06d661fa429d
  • Pointer size: 131 Bytes
  • Size of remote file: 313 kB
docs/assets/img6.png ADDED

Git LFS Details

  • SHA256: 85d1ff4195d7b3f42e519cfe3ae6dbd6fef14fcb740864a0202a29683f8d00c0
  • Pointer size: 131 Bytes
  • Size of remote file: 325 kB
docs/assets/logo.png ADDED

Git LFS Details

  • SHA256: 421e0dc32c06a165952d5f836349de14207a0524ab75a8871a267e52612986e0
  • Pointer size: 131 Bytes
  • Size of remote file: 646 kB
docs/assets/qr-placeholder.png ADDED

Git LFS Details

  • SHA256: 7d2e6193f46477961c8a761dc129991aba19218e4fb359a6f24e21edd227232b
  • Pointer size: 131 Bytes
  • Size of remote file: 611 kB
docs/assets/tower-day.png ADDED

Git LFS Details

  • SHA256: 84bd7db7967e5f80e05bb5495d3e40637d3c62f80006f35dbc017ac9fc35df18
  • Pointer size: 132 Bytes
  • Size of remote file: 2.19 MB
docs/assets/tower-night.png ADDED

Git LFS Details

  • SHA256: e12f63e5dcdf426c107a0c39b4e51d0e9edc26ec7cd4054168fd8278e76a8324
  • Pointer size: 132 Bytes
  • Size of remote file: 1.98 MB
docs/medium_post.en.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # When the most complex model is not the one that detects best
2
+
3
+ *A comparison of LSTM, Transformer and VAE-LSTM autoencoders over 19,000 real flights at Madrid-Barajas. And why validation loss is a poor advisor.*
4
+
5
+ ![Control tower with air traffic over the runway at night.](../docs/assets/tower-night.png)
6
+
7
+ > **Code:** [github.com/iRuperth/SADAR](https://github.com/iRuperth/SADAR)
8
+ > **Live demo:** [huggingface.co/spaces/devrup404/sadar](https://huggingface.co/spaces/devrup404/sadar)
9
+ >
10
+ > The repository is public and open to collaborations, suggestions and forks. Any technical discussion, *issue* or *pull request* is welcome.
11
+
12
+ ---
13
+
14
+ The most interesting data point in the dataset is not a hijacking. It is an aircraft that pretended to be one for thirty seconds, by mistake.
15
+
16
+ On July 31, 2017, an Iberia Airbus flew over Madrid with the hijack code active on its transponder during three points of its trajectory. Then it went back to normal. The flight was IBE2845, the transmitted code was 7500, and in the secondary surveillance system that code means unlawful interference.
17
+
18
+ It was not a hijacking. It was almost certainly a transponder fault: three points out of the 171 that make up the complete flight record, 800 meters from the runway, with no other operational anomaly. That flight, logged in a parquet file of 3.4 million rows, marks the beginning of this article. And it also marks the technical question I took into the project: can a model learn what a normal flight looks like and raise a flag when one stops looking like one?
19
+
20
+ This article presents SADAR, an anomaly detection system for flight trajectories trained on eighteen days of real traffic from Adolfo Suárez Madrid-Barajas airport. It compares three deep autoencoders against a classical baseline, evaluates the system on a synthetic anomaly bench and discusses with candor where it works and where it does not.
21
+
22
+ Let us start with the problem.
23
+
24
+ ## Problem statement
25
+
26
+ Incident detection in civil aviation can rarely be framed as a supervised problem: the data is dominated by normal operation and the few labeled events are ambiguous. The standard alternative is **conformance monitoring**: learn the distribution of normal behavior and flag trajectories that deviate from it. This is the framing SADAR adopts.
27
+
28
+ The distinction matters. Saying "this flight deviates from the learned pattern" is defensible with data. Saying "this flight is an incident" requires operational context that ADS-B does not provide. SADAR operates strictly under the first statement.
29
+
30
+ ## Data
31
+
32
+ The data comes from OpenSky Network, a collaborative network of ADS-B receivers. The set covers 18 non-consecutive days between June 2017 and March 2020, centered on the airspace of Barajas. Each aircraft reports a position every 10 seconds, with 21 columns per point: position and dynamics (latitude, longitude, barometric and geometric altitude, velocity, heading, vertical rate), identity (callsign, icao24, flight_id) and transponder state (squawk, alert, onground).
33
+
34
+ The raw figures after merging and deduplication:
35
+
36
+ - 3,429,638 rows.
37
+ - 19,057 distinct flights.
38
+ - Median of 175 points per flight, maximum 1,606.
39
+ - Median duration of 30.2 minutes.
40
+
41
+ The first finding of the exploratory analysis was the gaps. GPS altitude (`geoaltitude`) shows 24.79 % missing values. Velocity and heading, around 13 %. These gaps are not errors: they correspond to areas of lower ground receiver coverage. I discarded GPS altitude and kept barometric altitude, more complete, treating the gaps as a property of the problem rather than a fault to correct.
42
+
43
+ ![Sampled trajectories over the airspace of LEMD. The approach and departure corridors are visually identifiable before any projection. In red, the points of the four flights with an emergency squawk.](../docs/assets/eda/trajectories_map.png)
44
+
45
+ The second finding came from the heading histogram. Plotted on a polar chart, the two parallel runways of Barajas appear as two opposite lobes.
46
+
47
+ ![Polar histogram of heading over the full dataset. The two opposite lobes correspond to the parallel runways of Barajas. This circular property motivated the encoding of heading as the (sin, cos) pair.](../docs/assets/eda/heading_polar.png)
48
+
49
+ The airport is identifiable in the data before projecting it onto a map.
50
+
51
+ That circular property of heading motivated its encoding as a sine and cosine pair: for an autoencoder, 359° and 1° must remain adjacent, and a linear encoding prevents that.
52
+
53
+ ![Distribution of rows by flight phase in the dataset. The descent phase dominates, followed by climb and ground operations, which conditions which patterns the model learns best.](../docs/assets/eda/flight_phases.png)
54
+
55
+ ## Real events in the set
56
+
57
+ The dataset contains only four flights with an emergency code on the transponder. They deserve detailed description because their scarcity determines the framing of the problem:
58
+
59
+ - **IBE2845** (Iberia, July 31, 2017). Squawk 7500 for three points out of 171, transmitted 800 meters from the runway. Consistent with a transponder fault.
60
+ - **ELY395** (El Al, October 2, 2017). Squawk 7700 for five points, declared 800 meters from the runway. The aircraft landed. The most plausible case of a real emergency.
61
+ - **RYR61AD** (Ryanair, January 28, 2019). Squawk 7600 (communications failure). One point. Landed.
62
+ - **SWR202Y** (Swiss, February 3, 2020). Squawk 7700. One point. Landed.
63
+
64
+ Four labels in a universe of non-labels. This proportion rules out supervised learning: no classifier is trained on four positives. The four flights were set aside as a qualitative validation set, grouped in 1,076 labeled windows in `data/processed/anomalies.npy`, and do not appear at any point of training.
65
+
66
+ ## Methodology
67
+
68
+ ### One-class learning
69
+
70
+ Without enough labels, the framing is **one-class learning** through autoencoders. The network learns to reconstruct normal trajectories. The reconstruction error operates as an anomaly score: an input similar to those in training produces a low error; an unusual input raises it.
71
+
72
+ I trained four detectors under identical conditions (same preprocessing, same split, same threshold) to allow a rigorous comparison:
73
+
74
+ | # | Model | Idea | Weights |
75
+ |---|---|---|---|
76
+ | 0 | Isolation Forest | Per-window summary statistics, not sequential | baseline |
77
+ | 1 | LSTM autoencoder | Sequential reconstruction | 224 KB |
78
+ | 2 | Transformer autoencoder | Attention over the full window | 635 KB |
79
+ | 3 | VAE-LSTM | Probabilistic encoding of the latent space | 222 KB |
80
+
81
+ ### Preprocessing
82
+
83
+ Four preprocessing decisions deserve specific mention because they determine the final quality of a trajectory detector.
84
+
85
+ **Metric coordinates relative to the runway.** Latitude and longitude in degrees are inadequate for modeling movement over an airport: distances depend on latitude, and the model does not incorporate geodesy. The pipeline projects coordinates to UTM zone 30 (EPSG:32630), centered on the runway of Barajas, and operates in meters.
86
+
87
+ **Heading as the (sin, cos) pair.** A circular variable that linear encoding does not represent faithfully. Trigonometric encoding ensures that values close on the circle are also close in feature space.
88
+
89
+ **Split by date and by flight_id.** Training on 2017-2019, validation and test on 2020. Each flight appears in a single split. This blocks both temporal leakage and instance leakage simultaneously.
90
+
91
+ **Cleaning of the training set.** I excluded from training the 100 flights with a go-around pattern and the four with an emergency squawk. The training set must represent the most conservative version of "normal flight". Leaving go-arounds in makes the model learn to reconstruct them without difficulty, nullifying their value as anomalies.
92
+
93
+ The final windows have 60 steps of 10 seconds (10 minutes per window), with 50 % overlap. Seven features per step:
94
+
95
+ ```
96
+ [x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]
97
+ ```
98
+
99
+ The resulting set: 61,008 training windows, 7,679 validation, 7,788 test and 1,076 labeled as anomalous. The scaler standardizes features by fitting only on the training set.
100
+
101
+ ### Architectures
102
+
103
+ **Isolation Forest** over summary statistics (mean, standard deviation, minimum and maximum per feature). A non-sequential baseline whose function is to establish the reference line that deep learning must surpass.
104
+
105
+ **LSTM autoencoder**: encoder with a single LSTM layer (hidden 64), linear bottleneck to a latent of dimension 16, symmetric decoder that unrolls from the repeated latent.
106
+
107
+ **Transformer autoencoder**: two encoder layers with four attention heads (d_model 64), sinusoidal positional encoding, same bottleneck to latent 16.
108
+
109
+ **VAE-LSTM**: LSTM encoder that produces the mean and log-variance, reparameterization so that gradients flow through the sampling, LSTM decoder from z. The loss combines reconstruction error (MSE) with the Kullback-Leibler divergence weighted by a beta coefficient of 0.0001193, selected by Optuna.
110
+
111
+ ```python
112
+ std = torch.exp(0.5 * logvar)
113
+ eps = torch.randn_like(std)
114
+ z = mu + std * eps
115
+ ```
116
+
117
+ All models share the same training loop: Adam optimizer with weight decay, ReduceLROnPlateau scheduler, early stopping with patience 7, fixed seed 42, complete tracking with MLflow.
118
+
119
+ ### Synthetic anomaly bench
120
+
121
+ Four real events do not allow stable quantitative evaluation. To address this limitation, controlled anomalies are injected over normal flights from the test set. Five families, all with a linear ramp at onset to allow the measurement of **detection latency** (seconds from the start of the perturbation to the first threshold crossing):
122
+
123
+ - **Route deviation**: lateral deformation, magnitudes of 20, 40 and 80 km.
124
+ - **Altitude anomaly**: offset of 300, 800 or 1,500 m.
125
+ - **Speed anomaly**: multiplicative factors of 0.4x, 1.6x and 2.2x.
126
+ - **Holding**: continuous turns with a period of 120 or 240 seconds.
127
+ - **Transponder freeze**: the ADS-B retains the last value for the rest of the window.
128
+
129
+ Validation against an in-house simulator might seem circular. It is standard practice in *safety engineering*: the simulator was designed before the models and describes well-defined failure modes, not patterns learned from the model. The set produces 12 variants (combinations of type and intensity) and 24,000 synthetic windows per evaluated model.
130
+
131
+ So much for the setup. Next, whether it works.
132
+
133
+ ## Results
134
+
135
+ Results on the 2020 test set:
136
+
137
+ | Model | Real ROC-AUC | Real PR-AUC | Synthetic ROC | Median latency |
138
+ |---|---|---|---|---|
139
+ | Isolation Forest | 0.515 | 0.133 | 0.593 | n/a |
140
+ | LSTM-AE | 0.648 | 0.260 | 0.779 | 115 s |
141
+ | Transformer-AE | 0.614 | 0.227 | 0.743 | 115 s |
142
+ | **VAE-LSTM** | **0.659** | **0.299** | **0.792** | 120 s |
143
+
144
+ The deep models nearly double the PR-AUC of the baseline. This difference justifies the investment in deep learning: if the LSTM had not beaten the Isolation Forest, the rest of the work would not have made sense.
145
+
146
+ ![Precision-recall curve of the VAE-LSTM on the real test set. The PR-AUC of 0.299 doubles the baseline under heavy imbalance.](../reports/figures/pr_vae-lstm.png)
147
+
148
+ ![ROC curve of the VAE-LSTM on the same set. ROC-AUC of 0.659.](../reports/figures/roc_vae-lstm.png)
149
+
150
+ One observation deserves attention.
151
+
152
+ The Transformer won the validation loss (0.038 against 0.053 for the VAE and 0.072 for the LSTM). And lost the PR-AUC. Something did not add up.
153
+
154
+ The explanation is direct: an architecture with enough capacity also reconstructs anomalies with some fidelity, which reduces the separability between normal and anomalous as measured by reconstruction error. In reconstruction-based detection, validation loss is a poor proxy for performance. We know it and keep looking at it anyway.
155
+
156
+ I also evaluated two ensembles, by mean and by maximum of the z-normalized scores. Neither beat the individual VAE (PR-AUC 0.273 and 0.278 against 0.299), and I discarded them. The ensemble was an attempt to close with a push. It did not deliver. It is honest to report it.
157
+
158
+ The numbers tell one part. The other lies in what does not show up in the table.
159
+
160
+ ## Discussion
161
+
162
+ The results show a detection profile that is **heterogeneous by anomaly type**. The final model easily detects loud perturbations: ROC-AUC 0.984 in holding with a 120 s period, 0.989 in acceleration with factor 2.2, 0.899 in route deviation of 80 km. Subtle perturbations (300 m of altitude, small drifts) require close to two minutes and fall into the 0.51-0.55 range.
163
+
164
+ The most relevant case of the discussion is the **transponder freeze**.
165
+
166
+ The VAE detects it in 1.2 % of cases. One out of every eighty attempts.
167
+
168
+ The explanation is structural: an aircraft in stable cruise whose values freeze produces a pattern very close to that of an aircraft in stable cruise that keeps transmitting. The reconstruction error remains low and the score does not cross the threshold. It is a limitation of the reconstruction approach, not of training.
169
+
170
+ ![Confusion matrix of the VAE-LSTM with the threshold set at the 99th percentile of validation. The balance favors precision over recall, consistent with a target false alarm rate of 1 %.](../reports/figures/cm_vae-lstm.png)
171
+
172
+ This heterogeneity defines the scope of the system. SADAR is a detector of **anomalous maneuvers**, not a universal incident detector. Its operational value lies in those deviations that the traditional rule-based network (systems such as STCA or MSAW) does not cover, not in replacing those systems.
173
+
174
+ Before closing, it is worth being explicit about what the system does not do.
175
+
176
+ ## Limitations
177
+
178
+ The system assumes certain limitations documented explicitly in the technical report.
179
+
180
+ - **Absence of flight plans**. SADAR does not have the planned route for each flight. The "deviation from the expected route" is approximated by "deviation from the mean pattern". A flight that departs from its assigned plan but stays within the mean pattern will not be flagged.
181
+ - **Temporal coverage**. The set consists of 18 non-consecutive days between 2017 and 2020. Modeling demand, seasonality or continuous temporal prediction falls outside the scope.
182
+ - **Transponder freeze**. Within-window detection rate of 1.2 %. Any serious extension of the system must address this case specifically (derivative signals, change-point detection, dedicated model).
183
+ - **Coverage gaps**. 24.2 % of in-air flights present gaps longer than 120 seconds in reception. These gaps are receiver artifacts, not operational incidents. Treating them as positive labels would generate systematic noise.
184
+
185
+ ## Deployed system
186
+
187
+ The final model is served through an API built on FastAPI that mounts a React + Vite + TypeScript frontend on the same port, allowing a *single-image* deployment on Hugging Face Spaces. The dashboard consists of three screens:
188
+
189
+ 1. **Tower console**: live radar over Barajas with the day's flights and their scores, visual alert when one crosses the threshold.
190
+ 2. **Simulator**: controlled injection of anomalies over a reference flight, with visualization of the deformed trajectory, the score and the latency.
191
+ 3. **Metrics**: model comparison table, precision-recall and ROC curves, confusion matrices, breakdown by anomaly type.
192
+
193
+ ## Stack and reproducibility
194
+
195
+ Python 3.11, PyTorch, FastAPI, MLflow and Optuna on the backend. Vite, React 18 and TypeScript on the frontend. Packaged in a *single-image* Docker container. Reproducibility guaranteed by fixed seeds, YAML configurations that resolve environment variables, and checkpoints stored also as MLflow artifacts.
196
+
197
+ ## Conclusions and future work
198
+
199
+ SADAR shows that conformance monitoring of flight trajectories is tractable with deep autoencoders trained exclusively on normal operation, with defensible metrics and a reproducible evaluation protocol. The comparison between three model families under identical conditions allows the choice of VAE-LSTM to be justified not only by its final PR-AUC, but by its overall detection and latency profile.
200
+
201
+ The Transformer was not the right choice for this problem. I leave it documented because discarded options also count.
202
+
203
+ Immediate future work points to three directions. First, replace the Transformer with a TCN or a convolutional autoencoder to bound capacity without losing temporal modeling. Second, incorporate derivatives and change-point detection to address the transponder freeze. Third, evaluate the incorporation of flight plans when available, which would reframe the problem as plan-conditioned prediction rather than approximation to the mean pattern.
204
+
205
+ IBE2845 landed without incident on July 31, 2017 and its three red points received no further attention. Glitch, anomaly or incident, reality rarely arrives labeled, and building systems that operate in that terrain begins by accepting it.
docs/medium_post.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cuando el modelo más complejo no es el que detecta mejor
2
+
3
+ *Comparativa entre LSTM, Transformer y VAE-LSTM sobre 19.000 vuelos reales de Barajas. Y por qué la pérdida de validación no es buena consejera.*
4
+
5
+ ![Torre de control con tráfico aéreo sobre la pista al atardecer.](../docs/assets/tower-day.png)
6
+
7
+ > **Código:** [github.com/iRuperth/SADAR](https://github.com/iRuperth/SADAR)
8
+ > **Demo en vivo:** [huggingface.co/spaces/devrup404/sadar](https://huggingface.co/spaces/devrup404/sadar)
9
+ >
10
+ > El repositorio es público y está abierto a colaboraciones, sugerencias y forks. Cualquier discusión técnica, *issue* o *pull request* es bienvenida.
11
+
12
+ ---
13
+
14
+ El dato más interesante del dataset no es un secuestro. Es un avión que fingió serlo durante treinta segundos por error.
15
+
16
+ El 31 de julio de 2017, un Airbus de Iberia voló sobre Madrid con el código de secuestro activado en el transpondedor durante tres puntos de su trayectoria. Después volvió a la normalidad. El vuelo era el IBE2845, el código transmitido era el 7500, y en el sistema secundario de vigilancia ese código significa interferencia ilícita.
17
+
18
+ No era un secuestro. Era casi con total seguridad un fallo del transpondedor: tres puntos sobre los 171 que componen el registro completo del vuelo, a 800 metros de la pista, sin más anomalía operativa. Aquel vuelo, anotado en un fichero parquet de 3,4 millones de filas, marca el inicio de este artículo. Y marca también la pregunta técnica que me llevé al proyecto: ¿puede un modelo aprender qué aspecto tiene un vuelo normal y avisar cuando uno deja de parecerlo?
19
+
20
+ Este artículo presenta SADAR, un sistema de detección de anomalías en trayectorias aéreas entrenado sobre dieciocho días de tráfico real procedente del aeropuerto Adolfo Suárez Madrid-Barajas. Compara tres autoencoders profundos contra un baseline clásico, evalúa el sistema sobre un banco de anomalías sintéticas y discute con franqueza dónde funciona y dónde no.
21
+
22
+ Empecemos por el problema.
23
+
24
+ ## Planteamiento
25
+
26
+ La detección de incidentes en aviación civil rara vez puede formularse como un problema supervisado: los datos están dominados por operación normal y los pocos eventos etiquetados son ambiguos. La alternativa estándar es la **monitorización de conformidad**: aprender la distribución del comportamiento normal y señalar las trayectorias que se apartan de ella. Esta es la formulación que adopta SADAR.
27
+
28
+ La distinción importa. Decir "este vuelo se aparta del patrón aprendido" es defendible con datos. Decir "este vuelo es un incidente" requiere contexto operacional que el ADS-B no proporciona. SADAR opera estrictamente en el primer enunciado.
29
+
30
+ ## Datos
31
+
32
+ Los datos provienen de OpenSky Network, una red colaborativa de receptores ADS-B. El conjunto cubre 18 días no consecutivos entre junio de 2017 y marzo de 2020, centrados en el espacio aéreo de Barajas. Cada aeronave reporta una posición cada 10 segundos, con 21 columnas por punto: posición y dinámica (latitud, longitud, altitud barométrica y geométrica, velocidad, rumbo, régimen vertical), identidad (callsign, icao24, flight_id) y estado del transpondedor (squawk, alert, onground).
33
+
34
+ Las cifras crudas, tras la unión y deduplicación:
35
+
36
+ - 3.429.638 filas.
37
+ - 19.057 vuelos distintos.
38
+ - Mediana de 175 puntos por vuelo, máximo 1.606.
39
+ - Mediana de duración de 30,2 minutos.
40
+
41
+ El primer hallazgo del análisis exploratorio fueron los huecos. La altitud GPS (`geoaltitude`) presenta un 24,79 % de valores ausentes. La velocidad y el rumbo, alrededor del 13 %. Estos huecos no son errores: corresponden a zonas de menor cobertura del receptor terrestre. Descarté la altitud GPS y me quedé con la barométrica, más completa, tratando los huecos como una propiedad del problema y no como un fallo a corregir.
42
+
43
+ ![Trayectorias muestreadas sobre el espacio aéreo de LEMD. Los corredores de aproximación y salida se identifican visualmente antes de cualquier proyección. En rojo, los puntos de los cuatro vuelos con squawk de emergencia.](../docs/assets/eda/trajectories_map.png)
44
+
45
+ El segundo hallazgo provino del histograma del rumbo. Representado en un gráfico polar, las dos pistas paralelas de Barajas aparecen como dos lóbulos opuestos.
46
+
47
+ ![Histograma polar del rumbo sobre el dataset completo. Los dos lóbulos opuestos corresponden a las pistas paralelas de Barajas. Esta propiedad circular motivó la codificación del rumbo como par (sin, cos).](../docs/assets/eda/heading_polar.png)
48
+
49
+ El aeropuerto es identificable en los datos antes de proyectarlos sobre un mapa.
50
+
51
+ Esa propiedad circular del rumbo motivó su codificación como par seno y coseno: para un autoencoder, 359° y 1° deben quedar adyacentes, y una codificación lineal lo impide.
52
+
53
+ ![Distribución de filas por fase de vuelo en el dataset. La fase de descenso domina, seguida de ascenso y operaciones en superficie, lo que condiciona qué patrones aprende mejor el modelo.](../docs/assets/eda/flight_phases.png)
54
+
55
+ ## Eventos reales en el conjunto
56
+
57
+ El dataset contiene únicamente cuatro vuelos con código de emergencia en el transpondedor. Conviene describirlos con detalle, porque su escasez determina el planteamiento del problema:
58
+
59
+ - **IBE2845** (Iberia, 31 de julio de 2017). Squawk 7500 durante tres puntos sobre 171, transmitido a 800 metros de pista. Compatible con un fallo del transpondedor.
60
+ - **ELY395** (El Al, 2 de octubre de 2017). Squawk 7700 durante cinco puntos, declarado a 800 metros de pista. El avión aterrizó. Es el caso más plausible de emergencia real.
61
+ - **RYR61AD** (Ryanair, 28 de enero de 2019). Squawk 7600 (fallo de comunicaciones). Un punto. Aterrizó.
62
+ - **SWR202Y** (Swiss, 3 de febrero de 2020). Squawk 7700. Un punto. Aterrizó.
63
+
64
+ Cuatro etiquetas en un universo de no etiquetas. Esta proporción descarta el aprendizaje supervisado: ningún clasificador se entrena con cuatro positivos. Los cuatro vuelos se reservaron como conjunto de validación cualitativa, agrupados en 1.076 ventanas etiquetadas en `data/processed/anomalies.npy`, y no aparecen en ningún momento del entrenamiento.
65
+
66
+ ## Metodología
67
+
68
+ ### Aprendizaje de una sola clase
69
+
70
+ Sin etiquetas suficientes, el planteamiento es **aprendizaje de una sola clase** mediante autoencoders. La red aprende a reconstruir trayectorias normales. El error de reconstrucción opera como score de anomalía: una entrada parecida a las del entrenamiento produce un error bajo; una entrada inusual lo eleva.
71
+
72
+ Entrené cuatro detectores bajo idénticas condiciones (mismo preprocesado, mismo split, mismo umbral) para permitir una comparación rigurosa:
73
+
74
+ | # | Modelo | Idea | Pesos |
75
+ |---|---|---|---|
76
+ | 0 | Isolation Forest | Estadísticas resumen por ventana, no secuencial | baseline |
77
+ | 1 | LSTM autoencoder | Reconstrucción secuencial | 224 KB |
78
+ | 2 | Transformer autoencoder | Atención sobre la ventana completa | 635 KB |
79
+ | 3 | VAE-LSTM | Codificación probabilística del espacio latente | 222 KB |
80
+
81
+ ### Preprocesado
82
+
83
+ Cuatro decisiones de preprocesado merecen mención específica porque determinan la calidad final de un detector de trayectorias.
84
+
85
+ **Coordenadas métricas relativas a la pista.** Latitud y longitud en grados resultan inadecuadas para modelar movimiento sobre un aeropuerto: las distancias dependen de la latitud, y el modelo no incorpora geodesia. El pipeline proyecta las coordenadas a UTM zona 30 (EPSG:32630), centradas en la pista de Barajas, y opera en metros.
86
+
87
+ **Rumbo como par (sin, cos).** Variable circular que la codificación lineal no representa con fidelidad. La codificación trigonométrica asegura que valores próximos en el círculo lo sean también en el espacio de features.
88
+
89
+ **Split por fecha y por flight_id.** Entrenamiento sobre 2017-2019, validación y test sobre 2020. Cada vuelo aparece en un único split. Esto bloquea simultáneamente la fuga temporal y la de instancia.
90
+
91
+ **Limpieza del conjunto de entrenamiento.** Excluí del entrenamiento los 100 vuelos con patrón de go-around y los cuatro con squawk de emergencia. El conjunto de entrenamiento debe representar la versión más conservadora de "vuelo normal". Mantener go-arounds dentro hace que el modelo aprenda a reconstruirlos sin dificultad, anulando su valor como anomalías.
92
+
93
+ Las ventanas finales tienen 60 pasos de 10 segundos (10 minutos por ventana), con solapamiento del 50 %. Siete features por paso:
94
+
95
+ ```
96
+ [x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]
97
+ ```
98
+
99
+ El conjunto resultante: 61.008 ventanas de entrenamiento, 7.679 de validación, 7.788 de test y 1.076 etiquetadas como anómalas. El escalador estandariza las features ajustándose únicamente sobre el conjunto de entrenamiento.
100
+
101
+ ### Arquitecturas
102
+
103
+ **Isolation Forest** sobre estadísticas resumen (media, desviación, mínimo y máximo por feature). Baseline no secuencial cuya función es establecer la línea de referencia que el deep learning debe superar.
104
+
105
+ **LSTM autoencoder**: encoder con una capa LSTM (hidden 64), cuello de botella lineal a un latente de dimensión 16, decoder simétrico que desenrolla desde el latente repetido.
106
+
107
+ **Transformer autoencoder**: dos capas de encoder con cuatro cabezas de atención (d_model 64), codificación posicional sinusoidal, mismo cuello de botella a latente 16.
108
+
109
+ **VAE-LSTM**: encoder LSTM que produce la media y el logaritmo de la varianza, reparametrización para que los gradientes fluyan a través del muestreo, decoder LSTM desde z. La pérdida combina el error de reconstrucción (MSE) con la divergencia de Kullback-Leibler ponderada por un coeficiente beta de 0,0001193, seleccionado por Optuna.
110
+
111
+ ```python
112
+ std = torch.exp(0.5 * logvar)
113
+ eps = torch.randn_like(std)
114
+ z = mu + std * eps
115
+ ```
116
+
117
+ Todos los modelos comparten el mismo loop de entrenamiento: optimizador Adam con weight decay, scheduler ReduceLROnPlateau, early stopping con paciencia 7, semilla fija a 42, tracking completo con MLflow.
118
+
119
+ ### Banco de anomalías sintéticas
120
+
121
+ Cuatro eventos reales no permiten una evaluación cuantitativa estable. Para suplir esta limitación, se inyectan anomalías controladas sobre vuelos normales del conjunto de test. Cinco familias, todas con rampa lineal de inicio para permitir la medición de la **latencia de detección** (segundos desde el inicio de la perturbación hasta el primer cruce del umbral):
122
+
123
+ - **Desviación de ruta**: deformación lateral, magnitudes de 20, 40 y 80 km.
124
+ - **Anomalía de altitud**: offset de 300, 800 o 1.500 m.
125
+ - **Anomalía de velocidad**: factores multiplicativos de 0,4x, 1,6x y 2,2x.
126
+ - **Holding**: giros continuos con período de 120 o 240 segundos.
127
+ - **Congelación del transpondedor**: el ADS-B retiene el último valor durante el resto de la ventana.
128
+
129
+ La validación contra un simulador propio podría parecer circular. Es una práctica estándar en *safety engineering*: el simulador se diseñó antes que los modelos y describe modos de fallo bien definidos, no patrones aprendidos del modelo. El conjunto produce 12 variantes (combinaciones de tipo e intensidad) y 24.000 ventanas sintéticas por modelo evaluado.
130
+
131
+ Hasta aquí, el planteamiento. Lo siguiente es ver si funciona.
132
+
133
+ ## Resultados
134
+
135
+ Resultados sobre el conjunto de test de 2020:
136
+
137
+ | Modelo | ROC-AUC real | PR-AUC real | ROC sintético | Latencia mediana |
138
+ |---|---|---|---|---|
139
+ | Isolation Forest | 0,515 | 0,133 | 0,593 | n/d |
140
+ | LSTM-AE | 0,648 | 0,260 | 0,779 | 115 s |
141
+ | Transformer-AE | 0,614 | 0,227 | 0,743 | 115 s |
142
+ | **VAE-LSTM** | **0,659** | **0,299** | **0,792** | 120 s |
143
+
144
+ Los modelos profundos prácticamente duplican la PR-AUC del baseline. Esta diferencia justifica la inversión en deep learning: si el LSTM no hubiera batido al Isolation Forest, el resto del trabajo no habría tenido sentido.
145
+
146
+ ![Curva precision-recall del VAE-LSTM sobre el conjunto de test real. La PR-AUC de 0,299 duplica al baseline en condiciones de fuerte desbalance.](../reports/figures/pr_vae-lstm.png)
147
+
148
+ ![Curva ROC del VAE-LSTM sobre el mismo conjunto. ROC-AUC de 0,659.](../reports/figures/roc_vae-lstm.png)
149
+
150
+ Una observación merece atención.
151
+
152
+ El Transformer ganó la pérdida de validación (0,038 frente a 0,053 del VAE y 0,072 del LSTM). Y perdió la PR-AUC. Algo no cuadraba.
153
+
154
+ La explicación es directa: una arquitectura con suficiente capacidad reconstruye también con cierta fidelidad las anomalías, lo que reduce la separabilidad entre normal y anómalo medida por el error de reconstrucción. En detección por reconstrucción, la pérdida de validación es un mal proxy del rendimiento. Lo sabemos y aun así la seguimos mirando.
155
+
156
+ Evalué además dos ensembles, por promedio y por máximo de los scores z-normalizados. Ninguno superó al VAE individual (PR-AUC 0,273 y 0,278 frente a 0,299), y los descarté. El ensemble fue un intento de cerrar con un empujón. No lo dio. Es honesto reportarlo.
157
+
158
+ Los números cuentan una parte. La otra está en lo que no sale en la tabla.
159
+
160
+ ## Discusión
161
+
162
+ Los resultados muestran un perfil de detección **heterogéneo por tipo de anomalía**. El modelo final detecta con facilidad las perturbaciones ruidosas: ROC-AUC 0,984 en holding con periodo de 120 s, 0,989 en aceleración con factor 2,2, 0,899 en desviación de ruta de 80 km. Las perturbaciones sutiles (300 m de altitud, derivas pequeñas) requieren tiempos cercanos a los dos minutos y bajan al rango 0,51-0,55.
163
+
164
+ El caso más relevante de la discusión es la **congelación del transpondedor**.
165
+
166
+ El VAE lo detecta en el 1,2 % de los casos. Uno de cada ochenta intentos.
167
+
168
+ La explicación es estructural: un avión en crucero estable cuyos valores se congelan produce un patrón muy próximo al de un avión en crucero estable que continúa transmitiendo. El error de reconstrucción permanece bajo y el score no llega a cruzar el umbral. Es una limitación del enfoque de reconstrucción, no del entrenamiento.
169
+
170
+ ![Matriz de confusión del VAE-LSTM con umbral en el percentil 99 de la validación. El balance favorece la precisión sobre la sensibilidad, consistente con una tasa de falsa alarma objetivo del 1 %.](../reports/figures/cm_vae-lstm.png)
171
+
172
+ Esta heterogeneidad delimita el alcance del sistema. SADAR es un detector de **maniobras anómalas**, no un detector universal de incidentes. Su valor operativo está en aquellas desviaciones que la red de reglas tradicional (sistemas tipo STCA o MSAW) no cubre, no en sustituir esos sistemas.
173
+
174
+ Antes de cerrar, conviene ser explícito en lo que el sistema no hace.
175
+
176
+ ## Limitaciones
177
+
178
+ El sistema asume ciertas limitaciones documentadas explícitamente en el informe técnico.
179
+
180
+ - **Ausencia de planes de vuelo**. SADAR no dispone de la ruta prevista de cada vuelo. La "desviación respecto a la ruta esperada" se aproxima por "desviación respecto al patrón medio". Un vuelo que se aparte de su plan asignado pero permanezca dentro del patrón medio no será marcado.
181
+ - **Cobertura temporal**. El conjunto consta de 18 días no consecutivos entre 2017 y 2020. Modelar demanda, estacionalidad o predicción temporal continua queda fuera del alcance.
182
+ - **Congelación del transpondedor**. Tasa de detección dentro de ventana del 1,2 %. Cualquier extensión seria del sistema debe atacar este caso de forma específica (señales derivadas, detección de cambio de régimen, modelo dedicado).
183
+ - **Huecos de cobertura**. El 24,2 % de los vuelos en aire presentan gaps superiores a 120 segundos en la recepción. Estos huecos son artefactos del receptor, no incidentes operativos. Tratarlos como etiquetas positivas generaría ruido sistemático.
184
+
185
+ ## Sistema desplegado
186
+
187
+ El modelo final se sirve a través de una API construida sobre FastAPI que monta un frontend React + Vite + TypeScript en el mismo puerto, lo que permite un despliegue *single-image* en Hugging Face Spaces. El dashboard consta de tres pantallas:
188
+
189
+ 1. **Tower console**: radar en vivo sobre Barajas con los vuelos del día y sus scores, alerta visual al cruzar el umbral.
190
+ 2. **Simulator**: inyección controlada de anomalías sobre un vuelo de referencia, con visualización de la trayectoria deformada, el score y la latencia.
191
+ 3. **Metrics**: tabla comparativa de modelos, curvas precision-recall y ROC, matrices de confusión, desglose por tipo de anomalía.
192
+
193
+ ## Stack y reproducibilidad
194
+
195
+ Python 3.11, PyTorch, FastAPI, MLflow y Optuna en el backend. Vite, React 18 y TypeScript en el frontend. Empaquetado en Docker *single-image*. Reproducibilidad garantizada por semillas fijas, configuraciones YAML que resuelven variables de entorno, y los checkpoints almacenados también como artifacts de MLflow.
196
+
197
+ ## Conclusiones y trabajo futuro
198
+
199
+ SADAR demuestra que la monitorización de conformidad de trayectorias aéreas es abordable mediante autoencoders profundos entrenados exclusivamente sobre operación normal, con métricas defendibles y un protocolo de evaluación reproducible. La comparación entre tres familias de modelos bajo idénticas condiciones permite justificar la elección del VAE-LSTM no solo por su PR-AUC final, sino por su perfil global de detección y latencia.
200
+
201
+ El Transformer no fue la elección correcta para este problema. Lo dejo documentado porque los descartes también cuentan.
202
+
203
+ El trabajo futuro inmediato apunta a tres líneas. Primero, sustituir el Transformer por un TCN o un autoencoder convolucional para acotar la capacidad sin perder modelado temporal. Segundo, incorporar derivadas y detección de cambio de régimen para atacar la congelación del transpondedor. Tercero, evaluar la incorporación de planes de vuelo cuando estén disponibles, lo que replantearía el problema como predicción condicionada por el plan en lugar de aproximación al patrón medio.
204
+
205
+ El IBE2845 aterrizó sin incidentes el 31 de julio de 2017 y sus tres puntos en rojo no recibieron mayor atención. Glitch, anomalía o incidente, la realidad rara vez llega etiquetada, y construir sistemas que operen en ese terreno empieza por aceptarlo.
frontend/.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .vite
4
+ .DS_Store
5
+ .env
6
+ .env.*
7
+ Dockerfile
8
+ .dockerignore
frontend/.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ dist-ssr
4
+ *.local
5
+ .vite
frontend/Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS builder
2
+
3
+ RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
4
+
5
+ WORKDIR /app
6
+
7
+ COPY package.json pnpm-lock.yaml ./
8
+
9
+ RUN pnpm install --frozen-lockfile
10
+
11
+ COPY . .
12
+
13
+ ARG VITE_API_BASE=/api
14
+ ENV VITE_API_BASE=$VITE_API_BASE
15
+
16
+ RUN pnpm build
17
+
18
+
19
+ FROM nginx:1.27-alpine AS runtime
20
+
21
+ COPY --from=builder /app/dist /usr/share/nginx/html
22
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
23
+
24
+ EXPOSE 80
25
+
26
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
27
+ CMD wget -q -O - http://localhost/ >/dev/null || exit 1
frontend/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/png" href="/sadar-mark.png" />
7
+ <link rel="apple-touch-icon" href="/sadar-mark.png" />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=B612+Mono:wght@400;700&display=swap"
12
+ rel="stylesheet"
13
+ />
14
+ <title>SADAR Flight Conformance Monitor</title>
15
+ </head>
16
+ <body>
17
+ <div id="root"></div>
18
+ <script type="module" src="/src/main.tsx"></script>
19
+ </body>
20
+ </html>
frontend/nginx.conf ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 80;
3
+ server_name _;
4
+
5
+ root /usr/share/nginx/html;
6
+ index index.html;
7
+
8
+ gzip on;
9
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
10
+ gzip_min_length 1024;
11
+
12
+ location /api/ {
13
+ proxy_pass http://backend:8000;
14
+ proxy_http_version 1.1;
15
+ proxy_set_header Host $host;
16
+ proxy_set_header X-Real-IP $remote_addr;
17
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
18
+ proxy_set_header X-Forwarded-Proto $scheme;
19
+ proxy_read_timeout 120s;
20
+ }
21
+
22
+ location / {
23
+ try_files $uri $uri/ /index.html;
24
+ }
25
+
26
+ location ~* \.(?:js|css|woff2?|ttf|otf|eot|svg|png|jpg|jpeg|gif|webp|ico|map)$ {
27
+ expires 7d;
28
+ access_log off;
29
+ add_header Cache-Control "public, max-age=604800, immutable";
30
+ }
31
+ }
frontend/package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sadar-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "framer-motion": "^11.5.4",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1",
16
+ "react-router-dom": "^6.26.2"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.3.10",
20
+ "@types/react-dom": "^18.3.0",
21
+ "@vitejs/plugin-react": "^4.3.2",
22
+ "typescript": "^5.6.2",
23
+ "vite": "^5.4.8"
24
+ },
25
+ "pnpm": {
26
+ "onlyBuiltDependencies": ["esbuild"]
27
+ }
28
+ }
frontend/pnpm-lock.yaml ADDED
@@ -0,0 +1,1160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ dependencies:
11
+ framer-motion:
12
+ specifier: ^11.5.4
13
+ version: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
14
+ react:
15
+ specifier: ^18.3.1
16
+ version: 18.3.1
17
+ react-dom:
18
+ specifier: ^18.3.1
19
+ version: 18.3.1(react@18.3.1)
20
+ react-router-dom:
21
+ specifier: ^6.26.2
22
+ version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
23
+ devDependencies:
24
+ '@types/react':
25
+ specifier: ^18.3.10
26
+ version: 18.3.29
27
+ '@types/react-dom':
28
+ specifier: ^18.3.0
29
+ version: 18.3.7(@types/react@18.3.29)
30
+ '@vitejs/plugin-react':
31
+ specifier: ^4.3.2
32
+ version: 4.7.0(vite@5.4.21)
33
+ typescript:
34
+ specifier: ^5.6.2
35
+ version: 5.9.3
36
+ vite:
37
+ specifier: ^5.4.8
38
+ version: 5.4.21
39
+
40
+ packages:
41
+
42
+ '@babel/code-frame@7.29.7':
43
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
44
+ engines: {node: '>=6.9.0'}
45
+
46
+ '@babel/compat-data@7.29.7':
47
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
48
+ engines: {node: '>=6.9.0'}
49
+
50
+ '@babel/core@7.29.7':
51
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
52
+ engines: {node: '>=6.9.0'}
53
+
54
+ '@babel/generator@7.29.7':
55
+ resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
56
+ engines: {node: '>=6.9.0'}
57
+
58
+ '@babel/helper-compilation-targets@7.29.7':
59
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
60
+ engines: {node: '>=6.9.0'}
61
+
62
+ '@babel/helper-globals@7.29.7':
63
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
64
+ engines: {node: '>=6.9.0'}
65
+
66
+ '@babel/helper-module-imports@7.29.7':
67
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
68
+ engines: {node: '>=6.9.0'}
69
+
70
+ '@babel/helper-module-transforms@7.29.7':
71
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
72
+ engines: {node: '>=6.9.0'}
73
+ peerDependencies:
74
+ '@babel/core': ^7.0.0
75
+
76
+ '@babel/helper-plugin-utils@7.29.7':
77
+ resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
78
+ engines: {node: '>=6.9.0'}
79
+
80
+ '@babel/helper-string-parser@7.29.7':
81
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
82
+ engines: {node: '>=6.9.0'}
83
+
84
+ '@babel/helper-validator-identifier@7.29.7':
85
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
86
+ engines: {node: '>=6.9.0'}
87
+
88
+ '@babel/helper-validator-option@7.29.7':
89
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
90
+ engines: {node: '>=6.9.0'}
91
+
92
+ '@babel/helpers@7.29.7':
93
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
94
+ engines: {node: '>=6.9.0'}
95
+
96
+ '@babel/parser@7.29.7':
97
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
98
+ engines: {node: '>=6.0.0'}
99
+ hasBin: true
100
+
101
+ '@babel/plugin-transform-react-jsx-self@7.29.7':
102
+ resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==}
103
+ engines: {node: '>=6.9.0'}
104
+ peerDependencies:
105
+ '@babel/core': ^7.0.0-0
106
+
107
+ '@babel/plugin-transform-react-jsx-source@7.29.7':
108
+ resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==}
109
+ engines: {node: '>=6.9.0'}
110
+ peerDependencies:
111
+ '@babel/core': ^7.0.0-0
112
+
113
+ '@babel/template@7.29.7':
114
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
115
+ engines: {node: '>=6.9.0'}
116
+
117
+ '@babel/traverse@7.29.7':
118
+ resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
119
+ engines: {node: '>=6.9.0'}
120
+
121
+ '@babel/types@7.29.7':
122
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
123
+ engines: {node: '>=6.9.0'}
124
+
125
+ '@esbuild/aix-ppc64@0.21.5':
126
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
127
+ engines: {node: '>=12'}
128
+ cpu: [ppc64]
129
+ os: [aix]
130
+
131
+ '@esbuild/android-arm64@0.21.5':
132
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
133
+ engines: {node: '>=12'}
134
+ cpu: [arm64]
135
+ os: [android]
136
+
137
+ '@esbuild/android-arm@0.21.5':
138
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
139
+ engines: {node: '>=12'}
140
+ cpu: [arm]
141
+ os: [android]
142
+
143
+ '@esbuild/android-x64@0.21.5':
144
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
145
+ engines: {node: '>=12'}
146
+ cpu: [x64]
147
+ os: [android]
148
+
149
+ '@esbuild/darwin-arm64@0.21.5':
150
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
151
+ engines: {node: '>=12'}
152
+ cpu: [arm64]
153
+ os: [darwin]
154
+
155
+ '@esbuild/darwin-x64@0.21.5':
156
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
157
+ engines: {node: '>=12'}
158
+ cpu: [x64]
159
+ os: [darwin]
160
+
161
+ '@esbuild/freebsd-arm64@0.21.5':
162
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
163
+ engines: {node: '>=12'}
164
+ cpu: [arm64]
165
+ os: [freebsd]
166
+
167
+ '@esbuild/freebsd-x64@0.21.5':
168
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
169
+ engines: {node: '>=12'}
170
+ cpu: [x64]
171
+ os: [freebsd]
172
+
173
+ '@esbuild/linux-arm64@0.21.5':
174
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
175
+ engines: {node: '>=12'}
176
+ cpu: [arm64]
177
+ os: [linux]
178
+
179
+ '@esbuild/linux-arm@0.21.5':
180
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
181
+ engines: {node: '>=12'}
182
+ cpu: [arm]
183
+ os: [linux]
184
+
185
+ '@esbuild/linux-ia32@0.21.5':
186
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
187
+ engines: {node: '>=12'}
188
+ cpu: [ia32]
189
+ os: [linux]
190
+
191
+ '@esbuild/linux-loong64@0.21.5':
192
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
193
+ engines: {node: '>=12'}
194
+ cpu: [loong64]
195
+ os: [linux]
196
+
197
+ '@esbuild/linux-mips64el@0.21.5':
198
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
199
+ engines: {node: '>=12'}
200
+ cpu: [mips64el]
201
+ os: [linux]
202
+
203
+ '@esbuild/linux-ppc64@0.21.5':
204
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
205
+ engines: {node: '>=12'}
206
+ cpu: [ppc64]
207
+ os: [linux]
208
+
209
+ '@esbuild/linux-riscv64@0.21.5':
210
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
211
+ engines: {node: '>=12'}
212
+ cpu: [riscv64]
213
+ os: [linux]
214
+
215
+ '@esbuild/linux-s390x@0.21.5':
216
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
217
+ engines: {node: '>=12'}
218
+ cpu: [s390x]
219
+ os: [linux]
220
+
221
+ '@esbuild/linux-x64@0.21.5':
222
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
223
+ engines: {node: '>=12'}
224
+ cpu: [x64]
225
+ os: [linux]
226
+
227
+ '@esbuild/netbsd-x64@0.21.5':
228
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
229
+ engines: {node: '>=12'}
230
+ cpu: [x64]
231
+ os: [netbsd]
232
+
233
+ '@esbuild/openbsd-x64@0.21.5':
234
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
235
+ engines: {node: '>=12'}
236
+ cpu: [x64]
237
+ os: [openbsd]
238
+
239
+ '@esbuild/sunos-x64@0.21.5':
240
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
241
+ engines: {node: '>=12'}
242
+ cpu: [x64]
243
+ os: [sunos]
244
+
245
+ '@esbuild/win32-arm64@0.21.5':
246
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
247
+ engines: {node: '>=12'}
248
+ cpu: [arm64]
249
+ os: [win32]
250
+
251
+ '@esbuild/win32-ia32@0.21.5':
252
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
253
+ engines: {node: '>=12'}
254
+ cpu: [ia32]
255
+ os: [win32]
256
+
257
+ '@esbuild/win32-x64@0.21.5':
258
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
259
+ engines: {node: '>=12'}
260
+ cpu: [x64]
261
+ os: [win32]
262
+
263
+ '@jridgewell/gen-mapping@0.3.13':
264
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
265
+
266
+ '@jridgewell/remapping@2.3.5':
267
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
268
+
269
+ '@jridgewell/resolve-uri@3.1.2':
270
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
271
+ engines: {node: '>=6.0.0'}
272
+
273
+ '@jridgewell/sourcemap-codec@1.5.5':
274
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
275
+
276
+ '@jridgewell/trace-mapping@0.3.31':
277
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
278
+
279
+ '@remix-run/router@1.23.3':
280
+ resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==}
281
+ engines: {node: '>=14.0.0'}
282
+
283
+ '@rolldown/pluginutils@1.0.0-beta.27':
284
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
285
+
286
+ '@rollup/rollup-android-arm-eabi@4.60.4':
287
+ resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==}
288
+ cpu: [arm]
289
+ os: [android]
290
+
291
+ '@rollup/rollup-android-arm64@4.60.4':
292
+ resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==}
293
+ cpu: [arm64]
294
+ os: [android]
295
+
296
+ '@rollup/rollup-darwin-arm64@4.60.4':
297
+ resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==}
298
+ cpu: [arm64]
299
+ os: [darwin]
300
+
301
+ '@rollup/rollup-darwin-x64@4.60.4':
302
+ resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==}
303
+ cpu: [x64]
304
+ os: [darwin]
305
+
306
+ '@rollup/rollup-freebsd-arm64@4.60.4':
307
+ resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==}
308
+ cpu: [arm64]
309
+ os: [freebsd]
310
+
311
+ '@rollup/rollup-freebsd-x64@4.60.4':
312
+ resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==}
313
+ cpu: [x64]
314
+ os: [freebsd]
315
+
316
+ '@rollup/rollup-linux-arm-gnueabihf@4.60.4':
317
+ resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==}
318
+ cpu: [arm]
319
+ os: [linux]
320
+ libc: [glibc]
321
+
322
+ '@rollup/rollup-linux-arm-musleabihf@4.60.4':
323
+ resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==}
324
+ cpu: [arm]
325
+ os: [linux]
326
+ libc: [musl]
327
+
328
+ '@rollup/rollup-linux-arm64-gnu@4.60.4':
329
+ resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==}
330
+ cpu: [arm64]
331
+ os: [linux]
332
+ libc: [glibc]
333
+
334
+ '@rollup/rollup-linux-arm64-musl@4.60.4':
335
+ resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==}
336
+ cpu: [arm64]
337
+ os: [linux]
338
+ libc: [musl]
339
+
340
+ '@rollup/rollup-linux-loong64-gnu@4.60.4':
341
+ resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==}
342
+ cpu: [loong64]
343
+ os: [linux]
344
+ libc: [glibc]
345
+
346
+ '@rollup/rollup-linux-loong64-musl@4.60.4':
347
+ resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==}
348
+ cpu: [loong64]
349
+ os: [linux]
350
+ libc: [musl]
351
+
352
+ '@rollup/rollup-linux-ppc64-gnu@4.60.4':
353
+ resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==}
354
+ cpu: [ppc64]
355
+ os: [linux]
356
+ libc: [glibc]
357
+
358
+ '@rollup/rollup-linux-ppc64-musl@4.60.4':
359
+ resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==}
360
+ cpu: [ppc64]
361
+ os: [linux]
362
+ libc: [musl]
363
+
364
+ '@rollup/rollup-linux-riscv64-gnu@4.60.4':
365
+ resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==}
366
+ cpu: [riscv64]
367
+ os: [linux]
368
+ libc: [glibc]
369
+
370
+ '@rollup/rollup-linux-riscv64-musl@4.60.4':
371
+ resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==}
372
+ cpu: [riscv64]
373
+ os: [linux]
374
+ libc: [musl]
375
+
376
+ '@rollup/rollup-linux-s390x-gnu@4.60.4':
377
+ resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==}
378
+ cpu: [s390x]
379
+ os: [linux]
380
+ libc: [glibc]
381
+
382
+ '@rollup/rollup-linux-x64-gnu@4.60.4':
383
+ resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==}
384
+ cpu: [x64]
385
+ os: [linux]
386
+ libc: [glibc]
387
+
388
+ '@rollup/rollup-linux-x64-musl@4.60.4':
389
+ resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==}
390
+ cpu: [x64]
391
+ os: [linux]
392
+ libc: [musl]
393
+
394
+ '@rollup/rollup-openbsd-x64@4.60.4':
395
+ resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==}
396
+ cpu: [x64]
397
+ os: [openbsd]
398
+
399
+ '@rollup/rollup-openharmony-arm64@4.60.4':
400
+ resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==}
401
+ cpu: [arm64]
402
+ os: [openharmony]
403
+
404
+ '@rollup/rollup-win32-arm64-msvc@4.60.4':
405
+ resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==}
406
+ cpu: [arm64]
407
+ os: [win32]
408
+
409
+ '@rollup/rollup-win32-ia32-msvc@4.60.4':
410
+ resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==}
411
+ cpu: [ia32]
412
+ os: [win32]
413
+
414
+ '@rollup/rollup-win32-x64-gnu@4.60.4':
415
+ resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==}
416
+ cpu: [x64]
417
+ os: [win32]
418
+
419
+ '@rollup/rollup-win32-x64-msvc@4.60.4':
420
+ resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==}
421
+ cpu: [x64]
422
+ os: [win32]
423
+
424
+ '@types/babel__core@7.20.5':
425
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
426
+
427
+ '@types/babel__generator@7.27.0':
428
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
429
+
430
+ '@types/babel__template@7.4.4':
431
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
432
+
433
+ '@types/babel__traverse@7.28.0':
434
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
435
+
436
+ '@types/estree@1.0.8':
437
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
438
+
439
+ '@types/prop-types@15.7.15':
440
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
441
+
442
+ '@types/react-dom@18.3.7':
443
+ resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
444
+ peerDependencies:
445
+ '@types/react': ^18.0.0
446
+
447
+ '@types/react@18.3.29':
448
+ resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==}
449
+
450
+ '@vitejs/plugin-react@4.7.0':
451
+ resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
452
+ engines: {node: ^14.18.0 || >=16.0.0}
453
+ peerDependencies:
454
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
455
+
456
+ baseline-browser-mapping@2.10.32:
457
+ resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==}
458
+ engines: {node: '>=6.0.0'}
459
+ hasBin: true
460
+
461
+ browserslist@4.28.2:
462
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
463
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
464
+ hasBin: true
465
+
466
+ caniuse-lite@1.0.30001793:
467
+ resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
468
+
469
+ convert-source-map@2.0.0:
470
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
471
+
472
+ csstype@3.2.3:
473
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
474
+
475
+ debug@4.4.3:
476
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
477
+ engines: {node: '>=6.0'}
478
+ peerDependencies:
479
+ supports-color: '*'
480
+ peerDependenciesMeta:
481
+ supports-color:
482
+ optional: true
483
+
484
+ electron-to-chromium@1.5.364:
485
+ resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==}
486
+
487
+ esbuild@0.21.5:
488
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
489
+ engines: {node: '>=12'}
490
+ hasBin: true
491
+
492
+ escalade@3.2.0:
493
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
494
+ engines: {node: '>=6'}
495
+
496
+ framer-motion@11.18.2:
497
+ resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
498
+ peerDependencies:
499
+ '@emotion/is-prop-valid': '*'
500
+ react: ^18.0.0 || ^19.0.0
501
+ react-dom: ^18.0.0 || ^19.0.0
502
+ peerDependenciesMeta:
503
+ '@emotion/is-prop-valid':
504
+ optional: true
505
+ react:
506
+ optional: true
507
+ react-dom:
508
+ optional: true
509
+
510
+ fsevents@2.3.3:
511
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
512
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
513
+ os: [darwin]
514
+
515
+ gensync@1.0.0-beta.2:
516
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
517
+ engines: {node: '>=6.9.0'}
518
+
519
+ js-tokens@4.0.0:
520
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
521
+
522
+ jsesc@3.1.0:
523
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
524
+ engines: {node: '>=6'}
525
+ hasBin: true
526
+
527
+ json5@2.2.3:
528
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
529
+ engines: {node: '>=6'}
530
+ hasBin: true
531
+
532
+ loose-envify@1.4.0:
533
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
534
+ hasBin: true
535
+
536
+ lru-cache@5.1.1:
537
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
538
+
539
+ motion-dom@11.18.1:
540
+ resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
541
+
542
+ motion-utils@11.18.1:
543
+ resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
544
+
545
+ ms@2.1.3:
546
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
547
+
548
+ nanoid@3.3.12:
549
+ resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
550
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
551
+ hasBin: true
552
+
553
+ node-releases@2.0.46:
554
+ resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
555
+ engines: {node: '>=18'}
556
+
557
+ picocolors@1.1.1:
558
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
559
+
560
+ postcss@8.5.15:
561
+ resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
562
+ engines: {node: ^10 || ^12 || >=14}
563
+
564
+ react-dom@18.3.1:
565
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
566
+ peerDependencies:
567
+ react: ^18.3.1
568
+
569
+ react-refresh@0.17.0:
570
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
571
+ engines: {node: '>=0.10.0'}
572
+
573
+ react-router-dom@6.30.4:
574
+ resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==}
575
+ engines: {node: '>=14.0.0'}
576
+ peerDependencies:
577
+ react: '>=16.8'
578
+ react-dom: '>=16.8'
579
+
580
+ react-router@6.30.4:
581
+ resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==}
582
+ engines: {node: '>=14.0.0'}
583
+ peerDependencies:
584
+ react: '>=16.8'
585
+
586
+ react@18.3.1:
587
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
588
+ engines: {node: '>=0.10.0'}
589
+
590
+ rollup@4.60.4:
591
+ resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==}
592
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
593
+ hasBin: true
594
+
595
+ scheduler@0.23.2:
596
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
597
+
598
+ semver@6.3.1:
599
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
600
+ hasBin: true
601
+
602
+ source-map-js@1.2.1:
603
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
604
+ engines: {node: '>=0.10.0'}
605
+
606
+ tslib@2.8.1:
607
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
608
+
609
+ typescript@5.9.3:
610
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
611
+ engines: {node: '>=14.17'}
612
+ hasBin: true
613
+
614
+ update-browserslist-db@1.2.3:
615
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
616
+ hasBin: true
617
+ peerDependencies:
618
+ browserslist: '>= 4.21.0'
619
+
620
+ vite@5.4.21:
621
+ resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
622
+ engines: {node: ^18.0.0 || >=20.0.0}
623
+ hasBin: true
624
+ peerDependencies:
625
+ '@types/node': ^18.0.0 || >=20.0.0
626
+ less: '*'
627
+ lightningcss: ^1.21.0
628
+ sass: '*'
629
+ sass-embedded: '*'
630
+ stylus: '*'
631
+ sugarss: '*'
632
+ terser: ^5.4.0
633
+ peerDependenciesMeta:
634
+ '@types/node':
635
+ optional: true
636
+ less:
637
+ optional: true
638
+ lightningcss:
639
+ optional: true
640
+ sass:
641
+ optional: true
642
+ sass-embedded:
643
+ optional: true
644
+ stylus:
645
+ optional: true
646
+ sugarss:
647
+ optional: true
648
+ terser:
649
+ optional: true
650
+
651
+ yallist@3.1.1:
652
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
653
+
654
+ snapshots:
655
+
656
+ '@babel/code-frame@7.29.7':
657
+ dependencies:
658
+ '@babel/helper-validator-identifier': 7.29.7
659
+ js-tokens: 4.0.0
660
+ picocolors: 1.1.1
661
+
662
+ '@babel/compat-data@7.29.7': {}
663
+
664
+ '@babel/core@7.29.7':
665
+ dependencies:
666
+ '@babel/code-frame': 7.29.7
667
+ '@babel/generator': 7.29.7
668
+ '@babel/helper-compilation-targets': 7.29.7
669
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
670
+ '@babel/helpers': 7.29.7
671
+ '@babel/parser': 7.29.7
672
+ '@babel/template': 7.29.7
673
+ '@babel/traverse': 7.29.7
674
+ '@babel/types': 7.29.7
675
+ '@jridgewell/remapping': 2.3.5
676
+ convert-source-map: 2.0.0
677
+ debug: 4.4.3
678
+ gensync: 1.0.0-beta.2
679
+ json5: 2.2.3
680
+ semver: 6.3.1
681
+ transitivePeerDependencies:
682
+ - supports-color
683
+
684
+ '@babel/generator@7.29.7':
685
+ dependencies:
686
+ '@babel/parser': 7.29.7
687
+ '@babel/types': 7.29.7
688
+ '@jridgewell/gen-mapping': 0.3.13
689
+ '@jridgewell/trace-mapping': 0.3.31
690
+ jsesc: 3.1.0
691
+
692
+ '@babel/helper-compilation-targets@7.29.7':
693
+ dependencies:
694
+ '@babel/compat-data': 7.29.7
695
+ '@babel/helper-validator-option': 7.29.7
696
+ browserslist: 4.28.2
697
+ lru-cache: 5.1.1
698
+ semver: 6.3.1
699
+
700
+ '@babel/helper-globals@7.29.7': {}
701
+
702
+ '@babel/helper-module-imports@7.29.7':
703
+ dependencies:
704
+ '@babel/traverse': 7.29.7
705
+ '@babel/types': 7.29.7
706
+ transitivePeerDependencies:
707
+ - supports-color
708
+
709
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
710
+ dependencies:
711
+ '@babel/core': 7.29.7
712
+ '@babel/helper-module-imports': 7.29.7
713
+ '@babel/helper-validator-identifier': 7.29.7
714
+ '@babel/traverse': 7.29.7
715
+ transitivePeerDependencies:
716
+ - supports-color
717
+
718
+ '@babel/helper-plugin-utils@7.29.7': {}
719
+
720
+ '@babel/helper-string-parser@7.29.7': {}
721
+
722
+ '@babel/helper-validator-identifier@7.29.7': {}
723
+
724
+ '@babel/helper-validator-option@7.29.7': {}
725
+
726
+ '@babel/helpers@7.29.7':
727
+ dependencies:
728
+ '@babel/template': 7.29.7
729
+ '@babel/types': 7.29.7
730
+
731
+ '@babel/parser@7.29.7':
732
+ dependencies:
733
+ '@babel/types': 7.29.7
734
+
735
+ '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
736
+ dependencies:
737
+ '@babel/core': 7.29.7
738
+ '@babel/helper-plugin-utils': 7.29.7
739
+
740
+ '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
741
+ dependencies:
742
+ '@babel/core': 7.29.7
743
+ '@babel/helper-plugin-utils': 7.29.7
744
+
745
+ '@babel/template@7.29.7':
746
+ dependencies:
747
+ '@babel/code-frame': 7.29.7
748
+ '@babel/parser': 7.29.7
749
+ '@babel/types': 7.29.7
750
+
751
+ '@babel/traverse@7.29.7':
752
+ dependencies:
753
+ '@babel/code-frame': 7.29.7
754
+ '@babel/generator': 7.29.7
755
+ '@babel/helper-globals': 7.29.7
756
+ '@babel/parser': 7.29.7
757
+ '@babel/template': 7.29.7
758
+ '@babel/types': 7.29.7
759
+ debug: 4.4.3
760
+ transitivePeerDependencies:
761
+ - supports-color
762
+
763
+ '@babel/types@7.29.7':
764
+ dependencies:
765
+ '@babel/helper-string-parser': 7.29.7
766
+ '@babel/helper-validator-identifier': 7.29.7
767
+
768
+ '@esbuild/aix-ppc64@0.21.5':
769
+ optional: true
770
+
771
+ '@esbuild/android-arm64@0.21.5':
772
+ optional: true
773
+
774
+ '@esbuild/android-arm@0.21.5':
775
+ optional: true
776
+
777
+ '@esbuild/android-x64@0.21.5':
778
+ optional: true
779
+
780
+ '@esbuild/darwin-arm64@0.21.5':
781
+ optional: true
782
+
783
+ '@esbuild/darwin-x64@0.21.5':
784
+ optional: true
785
+
786
+ '@esbuild/freebsd-arm64@0.21.5':
787
+ optional: true
788
+
789
+ '@esbuild/freebsd-x64@0.21.5':
790
+ optional: true
791
+
792
+ '@esbuild/linux-arm64@0.21.5':
793
+ optional: true
794
+
795
+ '@esbuild/linux-arm@0.21.5':
796
+ optional: true
797
+
798
+ '@esbuild/linux-ia32@0.21.5':
799
+ optional: true
800
+
801
+ '@esbuild/linux-loong64@0.21.5':
802
+ optional: true
803
+
804
+ '@esbuild/linux-mips64el@0.21.5':
805
+ optional: true
806
+
807
+ '@esbuild/linux-ppc64@0.21.5':
808
+ optional: true
809
+
810
+ '@esbuild/linux-riscv64@0.21.5':
811
+ optional: true
812
+
813
+ '@esbuild/linux-s390x@0.21.5':
814
+ optional: true
815
+
816
+ '@esbuild/linux-x64@0.21.5':
817
+ optional: true
818
+
819
+ '@esbuild/netbsd-x64@0.21.5':
820
+ optional: true
821
+
822
+ '@esbuild/openbsd-x64@0.21.5':
823
+ optional: true
824
+
825
+ '@esbuild/sunos-x64@0.21.5':
826
+ optional: true
827
+
828
+ '@esbuild/win32-arm64@0.21.5':
829
+ optional: true
830
+
831
+ '@esbuild/win32-ia32@0.21.5':
832
+ optional: true
833
+
834
+ '@esbuild/win32-x64@0.21.5':
835
+ optional: true
836
+
837
+ '@jridgewell/gen-mapping@0.3.13':
838
+ dependencies:
839
+ '@jridgewell/sourcemap-codec': 1.5.5
840
+ '@jridgewell/trace-mapping': 0.3.31
841
+
842
+ '@jridgewell/remapping@2.3.5':
843
+ dependencies:
844
+ '@jridgewell/gen-mapping': 0.3.13
845
+ '@jridgewell/trace-mapping': 0.3.31
846
+
847
+ '@jridgewell/resolve-uri@3.1.2': {}
848
+
849
+ '@jridgewell/sourcemap-codec@1.5.5': {}
850
+
851
+ '@jridgewell/trace-mapping@0.3.31':
852
+ dependencies:
853
+ '@jridgewell/resolve-uri': 3.1.2
854
+ '@jridgewell/sourcemap-codec': 1.5.5
855
+
856
+ '@remix-run/router@1.23.3': {}
857
+
858
+ '@rolldown/pluginutils@1.0.0-beta.27': {}
859
+
860
+ '@rollup/rollup-android-arm-eabi@4.60.4':
861
+ optional: true
862
+
863
+ '@rollup/rollup-android-arm64@4.60.4':
864
+ optional: true
865
+
866
+ '@rollup/rollup-darwin-arm64@4.60.4':
867
+ optional: true
868
+
869
+ '@rollup/rollup-darwin-x64@4.60.4':
870
+ optional: true
871
+
872
+ '@rollup/rollup-freebsd-arm64@4.60.4':
873
+ optional: true
874
+
875
+ '@rollup/rollup-freebsd-x64@4.60.4':
876
+ optional: true
877
+
878
+ '@rollup/rollup-linux-arm-gnueabihf@4.60.4':
879
+ optional: true
880
+
881
+ '@rollup/rollup-linux-arm-musleabihf@4.60.4':
882
+ optional: true
883
+
884
+ '@rollup/rollup-linux-arm64-gnu@4.60.4':
885
+ optional: true
886
+
887
+ '@rollup/rollup-linux-arm64-musl@4.60.4':
888
+ optional: true
889
+
890
+ '@rollup/rollup-linux-loong64-gnu@4.60.4':
891
+ optional: true
892
+
893
+ '@rollup/rollup-linux-loong64-musl@4.60.4':
894
+ optional: true
895
+
896
+ '@rollup/rollup-linux-ppc64-gnu@4.60.4':
897
+ optional: true
898
+
899
+ '@rollup/rollup-linux-ppc64-musl@4.60.4':
900
+ optional: true
901
+
902
+ '@rollup/rollup-linux-riscv64-gnu@4.60.4':
903
+ optional: true
904
+
905
+ '@rollup/rollup-linux-riscv64-musl@4.60.4':
906
+ optional: true
907
+
908
+ '@rollup/rollup-linux-s390x-gnu@4.60.4':
909
+ optional: true
910
+
911
+ '@rollup/rollup-linux-x64-gnu@4.60.4':
912
+ optional: true
913
+
914
+ '@rollup/rollup-linux-x64-musl@4.60.4':
915
+ optional: true
916
+
917
+ '@rollup/rollup-openbsd-x64@4.60.4':
918
+ optional: true
919
+
920
+ '@rollup/rollup-openharmony-arm64@4.60.4':
921
+ optional: true
922
+
923
+ '@rollup/rollup-win32-arm64-msvc@4.60.4':
924
+ optional: true
925
+
926
+ '@rollup/rollup-win32-ia32-msvc@4.60.4':
927
+ optional: true
928
+
929
+ '@rollup/rollup-win32-x64-gnu@4.60.4':
930
+ optional: true
931
+
932
+ '@rollup/rollup-win32-x64-msvc@4.60.4':
933
+ optional: true
934
+
935
+ '@types/babel__core@7.20.5':
936
+ dependencies:
937
+ '@babel/parser': 7.29.7
938
+ '@babel/types': 7.29.7
939
+ '@types/babel__generator': 7.27.0
940
+ '@types/babel__template': 7.4.4
941
+ '@types/babel__traverse': 7.28.0
942
+
943
+ '@types/babel__generator@7.27.0':
944
+ dependencies:
945
+ '@babel/types': 7.29.7
946
+
947
+ '@types/babel__template@7.4.4':
948
+ dependencies:
949
+ '@babel/parser': 7.29.7
950
+ '@babel/types': 7.29.7
951
+
952
+ '@types/babel__traverse@7.28.0':
953
+ dependencies:
954
+ '@babel/types': 7.29.7
955
+
956
+ '@types/estree@1.0.8': {}
957
+
958
+ '@types/prop-types@15.7.15': {}
959
+
960
+ '@types/react-dom@18.3.7(@types/react@18.3.29)':
961
+ dependencies:
962
+ '@types/react': 18.3.29
963
+
964
+ '@types/react@18.3.29':
965
+ dependencies:
966
+ '@types/prop-types': 15.7.15
967
+ csstype: 3.2.3
968
+
969
+ '@vitejs/plugin-react@4.7.0(vite@5.4.21)':
970
+ dependencies:
971
+ '@babel/core': 7.29.7
972
+ '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
973
+ '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
974
+ '@rolldown/pluginutils': 1.0.0-beta.27
975
+ '@types/babel__core': 7.20.5
976
+ react-refresh: 0.17.0
977
+ vite: 5.4.21
978
+ transitivePeerDependencies:
979
+ - supports-color
980
+
981
+ baseline-browser-mapping@2.10.32: {}
982
+
983
+ browserslist@4.28.2:
984
+ dependencies:
985
+ baseline-browser-mapping: 2.10.32
986
+ caniuse-lite: 1.0.30001793
987
+ electron-to-chromium: 1.5.364
988
+ node-releases: 2.0.46
989
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
990
+
991
+ caniuse-lite@1.0.30001793: {}
992
+
993
+ convert-source-map@2.0.0: {}
994
+
995
+ csstype@3.2.3: {}
996
+
997
+ debug@4.4.3:
998
+ dependencies:
999
+ ms: 2.1.3
1000
+
1001
+ electron-to-chromium@1.5.364: {}
1002
+
1003
+ esbuild@0.21.5:
1004
+ optionalDependencies:
1005
+ '@esbuild/aix-ppc64': 0.21.5
1006
+ '@esbuild/android-arm': 0.21.5
1007
+ '@esbuild/android-arm64': 0.21.5
1008
+ '@esbuild/android-x64': 0.21.5
1009
+ '@esbuild/darwin-arm64': 0.21.5
1010
+ '@esbuild/darwin-x64': 0.21.5
1011
+ '@esbuild/freebsd-arm64': 0.21.5
1012
+ '@esbuild/freebsd-x64': 0.21.5
1013
+ '@esbuild/linux-arm': 0.21.5
1014
+ '@esbuild/linux-arm64': 0.21.5
1015
+ '@esbuild/linux-ia32': 0.21.5
1016
+ '@esbuild/linux-loong64': 0.21.5
1017
+ '@esbuild/linux-mips64el': 0.21.5
1018
+ '@esbuild/linux-ppc64': 0.21.5
1019
+ '@esbuild/linux-riscv64': 0.21.5
1020
+ '@esbuild/linux-s390x': 0.21.5
1021
+ '@esbuild/linux-x64': 0.21.5
1022
+ '@esbuild/netbsd-x64': 0.21.5
1023
+ '@esbuild/openbsd-x64': 0.21.5
1024
+ '@esbuild/sunos-x64': 0.21.5
1025
+ '@esbuild/win32-arm64': 0.21.5
1026
+ '@esbuild/win32-ia32': 0.21.5
1027
+ '@esbuild/win32-x64': 0.21.5
1028
+
1029
+ escalade@3.2.0: {}
1030
+
1031
+ framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
1032
+ dependencies:
1033
+ motion-dom: 11.18.1
1034
+ motion-utils: 11.18.1
1035
+ tslib: 2.8.1
1036
+ optionalDependencies:
1037
+ react: 18.3.1
1038
+ react-dom: 18.3.1(react@18.3.1)
1039
+
1040
+ fsevents@2.3.3:
1041
+ optional: true
1042
+
1043
+ gensync@1.0.0-beta.2: {}
1044
+
1045
+ js-tokens@4.0.0: {}
1046
+
1047
+ jsesc@3.1.0: {}
1048
+
1049
+ json5@2.2.3: {}
1050
+
1051
+ loose-envify@1.4.0:
1052
+ dependencies:
1053
+ js-tokens: 4.0.0
1054
+
1055
+ lru-cache@5.1.1:
1056
+ dependencies:
1057
+ yallist: 3.1.1
1058
+
1059
+ motion-dom@11.18.1:
1060
+ dependencies:
1061
+ motion-utils: 11.18.1
1062
+
1063
+ motion-utils@11.18.1: {}
1064
+
1065
+ ms@2.1.3: {}
1066
+
1067
+ nanoid@3.3.12: {}
1068
+
1069
+ node-releases@2.0.46: {}
1070
+
1071
+ picocolors@1.1.1: {}
1072
+
1073
+ postcss@8.5.15:
1074
+ dependencies:
1075
+ nanoid: 3.3.12
1076
+ picocolors: 1.1.1
1077
+ source-map-js: 1.2.1
1078
+
1079
+ react-dom@18.3.1(react@18.3.1):
1080
+ dependencies:
1081
+ loose-envify: 1.4.0
1082
+ react: 18.3.1
1083
+ scheduler: 0.23.2
1084
+
1085
+ react-refresh@0.17.0: {}
1086
+
1087
+ react-router-dom@6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
1088
+ dependencies:
1089
+ '@remix-run/router': 1.23.3
1090
+ react: 18.3.1
1091
+ react-dom: 18.3.1(react@18.3.1)
1092
+ react-router: 6.30.4(react@18.3.1)
1093
+
1094
+ react-router@6.30.4(react@18.3.1):
1095
+ dependencies:
1096
+ '@remix-run/router': 1.23.3
1097
+ react: 18.3.1
1098
+
1099
+ react@18.3.1:
1100
+ dependencies:
1101
+ loose-envify: 1.4.0
1102
+
1103
+ rollup@4.60.4:
1104
+ dependencies:
1105
+ '@types/estree': 1.0.8
1106
+ optionalDependencies:
1107
+ '@rollup/rollup-android-arm-eabi': 4.60.4
1108
+ '@rollup/rollup-android-arm64': 4.60.4
1109
+ '@rollup/rollup-darwin-arm64': 4.60.4
1110
+ '@rollup/rollup-darwin-x64': 4.60.4
1111
+ '@rollup/rollup-freebsd-arm64': 4.60.4
1112
+ '@rollup/rollup-freebsd-x64': 4.60.4
1113
+ '@rollup/rollup-linux-arm-gnueabihf': 4.60.4
1114
+ '@rollup/rollup-linux-arm-musleabihf': 4.60.4
1115
+ '@rollup/rollup-linux-arm64-gnu': 4.60.4
1116
+ '@rollup/rollup-linux-arm64-musl': 4.60.4
1117
+ '@rollup/rollup-linux-loong64-gnu': 4.60.4
1118
+ '@rollup/rollup-linux-loong64-musl': 4.60.4
1119
+ '@rollup/rollup-linux-ppc64-gnu': 4.60.4
1120
+ '@rollup/rollup-linux-ppc64-musl': 4.60.4
1121
+ '@rollup/rollup-linux-riscv64-gnu': 4.60.4
1122
+ '@rollup/rollup-linux-riscv64-musl': 4.60.4
1123
+ '@rollup/rollup-linux-s390x-gnu': 4.60.4
1124
+ '@rollup/rollup-linux-x64-gnu': 4.60.4
1125
+ '@rollup/rollup-linux-x64-musl': 4.60.4
1126
+ '@rollup/rollup-openbsd-x64': 4.60.4
1127
+ '@rollup/rollup-openharmony-arm64': 4.60.4
1128
+ '@rollup/rollup-win32-arm64-msvc': 4.60.4
1129
+ '@rollup/rollup-win32-ia32-msvc': 4.60.4
1130
+ '@rollup/rollup-win32-x64-gnu': 4.60.4
1131
+ '@rollup/rollup-win32-x64-msvc': 4.60.4
1132
+ fsevents: 2.3.3
1133
+
1134
+ scheduler@0.23.2:
1135
+ dependencies:
1136
+ loose-envify: 1.4.0
1137
+
1138
+ semver@6.3.1: {}
1139
+
1140
+ source-map-js@1.2.1: {}
1141
+
1142
+ tslib@2.8.1: {}
1143
+
1144
+ typescript@5.9.3: {}
1145
+
1146
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
1147
+ dependencies:
1148
+ browserslist: 4.28.2
1149
+ escalade: 3.2.0
1150
+ picocolors: 1.1.1
1151
+
1152
+ vite@5.4.21:
1153
+ dependencies:
1154
+ esbuild: 0.21.5
1155
+ postcss: 8.5.15
1156
+ rollup: 4.60.4
1157
+ optionalDependencies:
1158
+ fsevents: 2.3.3
1159
+
1160
+ yallist@3.1.1: {}