diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index e7ae28b..9c5b24e 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -90,6 +90,42 @@ jobs: export PYTHONPATH=$PYTHONPATH:. python -m pytest tests/integration/ -v --tb=short -x || echo "No integration tests found" + - name: Run Geo Spatial Integration Tests (real PostGIS + Redis) + run: | + cd backend + docker run -d --name geo-test-pg \ + -e POSTGRES_DB=quakeguard_test \ + -e POSTGRES_USER=quakeguard \ + -e POSTGRES_PASSWORD=ci-postgres-password \ + -p 55432:5432 postgis/postgis:15-3.4-alpine + docker run -d --name geo-test-redis -p 56379:6379 redis:7-alpine + echo "Waiting for spatial test infrastructure readiness..." + timeout 60s bash -c "until nc -z localhost 55432 && nc -z localhost 56379; do sleep 2; done" + DATABASE_URL="postgresql://quakeguard:ci-postgres-password@localhost:55432/quakeguard_test" \ + REDIS_URL="redis://localhost:56379/0" \ + python -m pytest tests/integration/test_geo_integration.py -v --tb=short + docker rm -f geo-test-pg geo-test-redis + + - name: Run TimescaleDB Integration Tests (real hypertable) + run: | + cd backend + DATABASE_URL="postgresql://quakeguard:ci-postgres-password@localhost:5432/quakeguard_test" \ + python -m pytest tests/integration/test_timescale_integration.py -v --tb=short + + - name: Run Zone Readings Integration Tests (real PostGIS) + run: | + cd backend + docker run -d --name zone-test-pg \ + -e POSTGRES_DB=quakeguard_test \ + -e POSTGRES_USER=quakeguard \ + -e POSTGRES_PASSWORD=ci-postgres-password \ + -p 55433:5432 postgis/postgis:15-3.4-alpine + echo "Waiting for zone test infrastructure readiness..." + timeout 60s bash -c "until nc -z localhost 55433; do sleep 2; done" + DATABASE_URL="postgresql://quakeguard:ci-postgres-password@localhost:55433/quakeguard_test" \ + python -m pytest tests/integration/test_zone_readings.py -v --tb=short + docker rm -f zone-test-pg + - name: Execute Critical Stress Test env: API_URL: "http://localhost:8000" diff --git a/CHANGELOG.md b/CHANGELOG.md index c711503..3fcb9ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - (Target: v1.2.0) +## [1.2.1] - 2026-08-14 +### Added +- **Geo-Zoning:** PostGIS zones as the source of truth (`Zone` model, `GET /zones`, `POST /zones/`), with a geohash-based Redis fast path for coordinate→zone lookup. +- **Zone Detection:** `GET /zones/locate` resolves a device's GPS position into a monitored polygon; Settings now ships "Detect my zone via GPS". +- **Per-Zone Seismograph:** `GET /zones/{zone_id}/readings` + `DELETE /zones/{zone_id}/readings`; the mobile dashboard renders a live seismograph per zone (horizontal zone strip) instead of mixing network-wide telemetry. +- **Per-Area Cooldown Fragmentation:** alert cooldown keys are now area-based (geohash region or zone), not global. +- **GNSS-Ready Data Model:** `Sensor.last_fix_at`, `Reading.lat/lon` captured at ingestion for area-fragmented cooldowns and future spatial correlation. +- **Per-Zone Alerts Feed:** `GET /zones/{zone_id}/alerts` retrieves the confirmed seismic alerts raised for a single area. +- **Live Chart Overhaul (mobile):** sliding window anchored to the wall clock (stale readings leave the window), linear MAG scale (MIN 3.5 / MED 4.0 / ALTO 4.5) with the axis pinned left, positive X seconds and a centered `TIME` label; the trace always renders inside the plot. +- **Settings Explore Section:** links to the GitHub repository (`GiZano/QuakeGuard`) and the QuakeGuard site; `QuakeGuard v1.2.1` footer. + +### Changed +- Worker alert pipeline uses per-area cooldown keys and rounds-trip normalization (magnitude estimation shared with the mobile client). +- Peripheral MQTT subscriber wiring and `scripts/simulate_zone.py` updated for the per-zone stream flow. +- Version artifacts bumped to v1.2.1 (CITATION.cff, mobile footer, README roadmap). + +## [1.2.0] - 2026-08-06 ### Added - **On-Premise AI Emergency Reports:** New AI layer generates human-readable emergency reports from confirmed seismic alerts via a local Ollama LLM (`llama3.2:1b` default), keeping telemetry on the host. - **`ollama_client.py`:** Deterministic report generation (`temperature 0.0`, `top_k 1`, streaming disabled) with a strict system prompt ("Only use the provided JSON telemetry. Do not invent data.") and explicit `"AI report unavailable."` fallback on failure. diff --git a/CITATION.cff b/CITATION.cff index 5004223..579009e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -10,12 +10,12 @@ authors: given-names: "Riccardo" alias: "riccardo0731" title: "QuakeGuard" -version: 1.2.0 +version: 1.2.1 identifiers: - type: doi value: "10.5281/zenodo.21814890" - description: "Software archive (v1.2.0)" -date-released: 2026-08-06 + description: "Software archive (v1.2.1)" +date-released: 2026-08-14 url: "https://giovanni-zanotti.is-a.dev/Pages/Works/quakeguard/" repository-code: "https://github.com/GiZano/QuakeGuard" license: "AGPL-3.0" diff --git a/README.md b/README.md index e232033..7bebba6 100644 --- a/README.md +++ b/README.md @@ -416,8 +416,9 @@ QuakeGuard/ |---------|-------| | **v1.0** | ✅ Released — edge seismic detection on ESP32, local alerts | | **v1.1** | ✅ Released — HiveMQ Cloud MQTT (TLS), ngrok HTTPS tunnel, security hardening | -| **v1.2** | ✅ Current — On-Premise AI Worker (Local Ollama / Llama 3.2) for privacy-preserving emergency reports | -| **v1.2.1** | Zero-Trust Serial Fallback — signed telemetry over USB CDC (serial) when MQTT is unreachable | +| **v1.2** | ✅ Released — On-Premise AI Worker (Local Ollama / Llama 3.2) for privacy-preserving emergency reports | +| **v1.2.1** | ✅ Current — Geo-Zoning & Cooldown Fragmentation — geohash Redis zone index (FastAPI/PostGIS source of truth), per-area cooldown, GNSS-ready data model (`Sensor.last_fix_at`, `Reading.lat/lon`), per-zone live seismograph | +| **v1.2.2** | Zero-Trust Serial Fallback — signed telemetry over USB CDC (serial) when MQTT is unreachable | | **v1.3** | GNSS sync — accurate node timestamps, GPS coordinate resolution, ADXL345 calibration | | **v2.0** | Triangulation — multi-node spatial correlation + AI reports for epicenter calculation | | **v2.1** | Data Dashboards — Grafana dashboards for real-time visualization of seismic telemetry | diff --git a/ROADMAP.md b/ROADMAP.md index ae1fdb4..64c49b0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -27,7 +27,7 @@ Data Plane migration to MQTT Cloud (HiveMQ), REST Control Plane (HTTPS) and TLS --- -## v1.2.0 — On-Premise AI Reports (Current) +## v1.2.0 — On-Premise AI Reports (Released) On-premise AI integration (LLM in the backend) to generate textual emergency reports from MQTT data. @@ -39,7 +39,23 @@ On-premise AI integration (LLM in the backend) to generate textual emergency rep --- -## v1.2.1 — Zero-Trust Serial Fallback +## v1.2.1 — Geo-Zoning & Cooldown Fragmentation (GNSS-ready) (Current) + +Geographic zone division designed so the system is ready for the GNSS upgrade (v1.3). The alert cooldown is fragmented from the coarse macro-region level down to a per-area geohash granularity, and the zone-assignment hot path is offloaded from PostGIS queries to a Redis geohash index — with PostGIS kept as the single source of truth. + +> **Blocking prerequisite for v1.3 (GNSS):** real GNSS coordinates replace the hardcoded Rome fix in the firmware (`main.cpp`), so the geo layer must already resolve zones and fragment cooldowns from raw coordinates — not from a fixed registration-time zone. + +- ✅ **Geohash zone index (Redis fast path)** — `backend/src/geo.py`: at seed time every zone polygon is decomposed into the set of geohash cells (prec 3, ~156 km) it intersects; `resolve_zone()` (in `main.py`) resolves a coordinate from the Redis SET without a DB round-trip. Redis miss or ambiguous multi-zone match falls back to the authoritative PostGIS `ST_Contains` + `ST_Area ASC` query, so the cache can never assign a wrong zone +- ✅ **Pure-Python geohash encoder** matching PostGIS `ST_GeoHash` — zero new dependencies, deterministic keys shared by seed-time index and runtime lookup +- ✅ **Fragmented cooldown lock** — `alert_cooldown:` (prec 4, ~50 km) instead of `alert_cooldown:`: two independent events inside the same overlapping macro-polygon no longer silence each other. Legacy `alert_cooldown:zone:` retained for coordinates-less sensors +- ✅ **GNSS-ready data model** — `Sensor.last_fix_at`, `Reading.latitude`/`longitude`; the ingestion payload carries the sensor's fix + geohash; `/devices/register` re-resolves the zone if a relocated node reports a changed fix +- ✅ **Real spatial tests** — integration tests assert resolution against the seeded PostGIS polygons (Milan, Madrid, Tokyo, unknown point), and `point_to_geohash` is cross-validated against PostGIS `ST_GeoHash` + +> **Future hardening (for administrative polygons):** `ST_SimplifyPreserveTopology` + GiST tuning, sizing zones so an event cannot physically reach the adjacent zone (~50–100 km for destructive surface-wave propagation). + +--- + +## v1.2.2 — Zero-Trust Serial Fallback Signed telemetry over a serial link (USB CDC) when MQTT/WiFi connectivity is lost, so the host still receives data during offline simulations. @@ -68,6 +84,7 @@ Triangulation algorithm. Multi-node spatial correlation combined with AI reports - Multi-node spatial and temporal correlation - Internal epicenter computation - AI + triangulation data fusion for precise alerts +- KiCad schematics and Gerber files of the node PCB (hardware blueprints for the triangulation node) — already designed; fabrication order + bring-up pending --- @@ -81,14 +98,14 @@ Grafana dashboards for real-time visualization of seismic telemetry. --- > **Geo-zoning & cooldown-lock design (relevant for the paper's System-Engineering claim):** -> - The per-zone Redis cooldown lock (`lock:cooldown:`) can *silence independent -> earthquakes* inside a single macro-region (e.g. two events in the same "North America" -> polygon trigger one lock and the second alert is dropped as a duplicate). -> - Plan fragmentation to Geohash/H3 keys (`lock:cooldown:`) — this offloads zone -> assignment from PostGIS `ST_Contains` to fast Redis lookups. -> - For real administrative polygons: apply `ST_SimplifyPreserveTopology` + a GiST index; -> size zones so an event cannot physically reach the adjacent zone (~50–100 km for -> destructive surface-wave propagation). +> - **Implemented in v1.2.1:** fragmentation to Geohash keys (`alert_cooldown:`), +> which offloads zone assignment from PostGIS `ST_Contains` to fast Redis lookups +> and stops overlapping macro-regions from silencing independent earthquakes. +> - **Future:** H3 hex-grid reindexing is re-evaluated when the v2.0 triangulation +> clustering is designed; daily H3 resolution can replace the coarsen geohash grid +> with no zone-model change. For real administrative polygons: apply +> `ST_SimplifyPreserveTopology` + a GiST index; size zones so an event cannot +> physically reach the adjacent zone (~50–100 km for destructive surface-wave propagation). --- @@ -109,6 +126,22 @@ Crowning of the engineering phase. Two-tier edge cluster where TinyML is **not** --- +## v2.3.0 — Right-Sized Ingestion at Scale (Redis Streams + TimescaleDB) + +Backend ingestion redesigned so the control plane sustains tens of thousands of sensors on a small footprint instead of degrading into a single-queue toy. + +- ✅ **Redis Streams replaces the single-consumer list queue** — producers XADD to `readings:stream` (O(1) append); N worker processes drain via consumer groups (`docker compose scale worker=N`); `XAUTOCLAIM` recovers pending entries across worker restarts (at-least-once delivery); poisoned heartbeats park on the `readings:dlq` stream so they never stall the group — `backend/src/ingest.py` +- ✅ **Batched DB commits** — a stream batch (default 64) is written in one transaction instead of one commit per heartbeat +- ✅ **TimescaleDB hypertable on `readings`** — chunked on `recorded_at`; continuous aggregate `readings_minute` serves the dashboard rollups; compression + retention policies. Migration (`backend/src/timescale.py`) is idempotent and **fails closed** on plain PostGIS (dev/CI) +- ✅ **Statistics fast-path** — `/sensors/{id}/statistics` reads the continuous aggregate when present, falls back to a COUNT otherwise +- ✅ **Single TimescaleDB+PostGIS image** — `backend/docker/postgres-timescale.Dockerfile`, wired into `docker-compose.yml` +- ✅ **Real migration coverage** — dedicated CI job runs the hypertable + aggregate tests against the actual deployment image +- ✅ **Load generator** `backend/scripts/load_test.py` — N sensors at H Hz (default matches the 150-sensor CI requirement), stream or HTTP transport + +> **Scale math (design target):** 150 sensors @ 1/5s ≈ 30 msg/s (trivial today); 10k sensors @ 1 Hz ≈ 2k msg/s (bounded by worker count + hypertable inserts, still 1 Postgres node). The MQTT transport already exists (firmware → broker → bridge → API); the bridge stays HTTP-proxying by design — direct MQTT→stream is the documented next step only if the broker becomes the bottleneck. + +--- + ## #Research — Scientific Validation (SIL) Parallel node (non-semantic). **R1 is the Foundation**: it starts immediately, in parallel with v1.3 (GNSS), and its calibration is urgent because it is the blocking prerequisite for v2.2.0 and the paper. @@ -190,9 +223,12 @@ Production-grade cloud platform behind the alert pipeline: the MQTT/REST/AI stac ### Performance & scaling engineering (post-paper, not needed at current scale) +- **Kafka / Redpanda as the central ingestion buffer (millions-class)** — replaces Redis Streams as the durable, replayable backbone once sustained ingestion exceeds what a single Redis node can buffer. The v2.3.0 consumer interface (`src/ingest.py`) is deliberately transport-agnostic: `enqueue_reading` / `read_batch` / `ack` / `recover_pending` are re-pointable so a Kafka-backed implementation can slot in without touching the worker. Also unlocks partitions-per-sensor ordering and backfill reprocessing for the triangulation engine (v2.0). +- **ClickHouse for cold-path analytics** — move long-range dashboards / multi-node correlation queries (epicenter triangulation, swarm clustering) off the operational Postgres node onto a columnar store with a Kafka connector. Cold reads never contend with the ingestion hot path; TimescaleDB continuous aggregates keep serving the real-time dashboard. - **Non-blocking MQTT-Bridge refactor** — `aiomqtt` + async push to Redis (or `httpx`/`aiohttp`) - to make the bridge relay fully non-blocking. *Parked: the current synchronous bridge is not - the bottleneck at sandbox scale.* + to make the bridge relay fully non-blocking. *Partially superseded by v2.3.0: the ingestion + endpoint is now an O(1) stream append, so the HTTP-proxying bridge is no longer the DB + bottleneck; direct MQTT→stream still removes the HTTP hop and is the documented next step.* - **Rust ingestion microservice (Axum) + ECDSA verification via PyO3** — the hybrid path: keep FastAPI/PostGIS/Ollama, move only the CPU-bound signature verification (P-256/SHA-256) to native speed; a dedicated Axum ingestion endpoint can later absorb `POST /readings/`. diff --git a/backend/README.md b/backend/README.md index f121d1c..e168109 100644 --- a/backend/README.md +++ b/backend/README.md @@ -17,12 +17,19 @@ backend/ │ └── ollama-entrypoint.sh # Auto-pulls the Ollama model on startup ├── src/ # Source Code │ ├── main.py # FastAPI Gateway & REST Endpoints - │ ├── worker.py # Async Background Event Processor + │ ├── worker.py # Stream consumer (Redis Streams, batched persistence) + │ ├── ingest.py # Redis Streams ingestion substrate (group/DLQ/recovery) + │ ├── timescale.py # Idempotent TimescaleDB hypertable + aggregates migration + │ ├── geo.py # Geohash zone-index (Redis) + candidate pruning │ ├── ai_report_worker.py # Dedicated AI report consumer (Ollama + state machine) │ ├── ollama_client.py # Deterministic LLM client (anti-hallucination) │ ├── database.py # SQLAlchemy Connection & Pool config │ ├── models.py # ORM Models (GeoAlchemy2 + EmergencyReport) │ └── schemas.py # Pydantic DTOs + ├── scripts/ + │ └── load_test.py # Load generator (N sensors at H Hz, stream or HTTP) + ├── docker/ + │ └── postgres-timescale.Dockerfile # TimescaleDB + PostGIS DB image ├── tests/ # Testing Suite │ ├── __init__.py │ ├── stress_test.py # Load testing & ECDSA simulation tool @@ -46,18 +53,19 @@ The system operates on three decoupled layers: * Asynchronous request handling (`async/await`). * **Zero-Trust Security:** Enforces ECDSA (NIST256p) signature verification with SHA-256 hashing on every payload. * **Polyglot Crypto Support:** Handles both DER (MbedTLS/C++) and RAW (Python/JS) signature formats. - * **Non-Blocking:** Offloads valid payloads immediately to a Redis queue (`seismic_events`). + * **O(1) enqueue:** Valid payloads are appended to a **Redis Streams** bus (`readings:stream`) instead of touching the database — the API stays fast no matter how many sensors emit. 2. **Processing Layer (Worker):** - * **Role:** Consumes the message queue and analyzes data streams. + * **Role:** Drains the stream via consumer groups and persists + analyzes the data. * **Features:** - * Persists raw telemetry to PostgreSQL. - * Implements a sliding window counter in Redis to detect seismic swarms in real-time. - * Triggers persistent `Alerts` when predefined thresholds are breached. + * **Horizontally scalable:** `docker compose scale worker=N` — Redis balances deliveries across consumers; `XAUTOCLAIM` reclaims entries left pending by crashed workers (at-least-once delivery). + * **Batched persistence:** a stream batch (default 64 heartbeats) is committed to PostgreSQL in one transaction. + * **Poison-message isolation:** unparseable/failing heartbeats are parked on the `readings:dlq` stream and ACKed, so they can never stall the group. + * Triggers persistent `Alerts` when predefined thresholds are breached (Redis `SET NX` cooldown deduplication). 3. **Persistence Layer:** - * **PostgreSQL + PostGIS:** Primary storage for time-series data and geospatial entities (Zones, Sensors). - * **Redis:** In-memory data structure store used for message queuing and high-speed counters. + * **TimescaleDB + PostGIS (single image):** `readings` is a TimescaleDB *hypertable* chunked on `recorded_at` (continuous aggregate `readings_minute` serves the dashboard rollups; compression + retention). PostGIS remains the authoritative source for zone geometry. + * **Redis:** Streams for ingestion buffering, Pub/Sub for real-time alert distribution, geohash zone-index + cooldown keys for the alerting fast path. --- @@ -118,6 +126,11 @@ Endpoints for provisioning the infrastructure. * **GET** `/zones/` - Retrieve available zones. * **GET** `/sensors/` - Retrieve registered sensors. +### 📍 Geo-Location (v1.2.1) +* **GET** `/zones/locate?latitude=..&longitude=..` - Resolve a GPS fix into the smallest containing monitored zone (powers "Detect my zone"). +* **GET** `/zones/{zone_id}/readings` - Fetch the most recent readings emitted by sensors of a single zone (per-zone seismograph feed). +* **DELETE** `/zones/{zone_id}/readings` - Clear the telemetry emitted by a single zone (returns `{ "deleted": n }`). + ### 📥 Data Ingestion (IoT) * **POST** `/readings/` - High-frequency ingestion endpoint. * **Payload:** Telemetry data including `value`, `device_timestamp`, and `signature_hex`. @@ -150,18 +163,36 @@ To handle bursts of traffic during seismic events, the database engine is optimi * **Max Overflow:** 60 additional temporary connections (Total capacity: 100 concurrent threads). * **Pre-Ping:** Enabled to prevent stale connection errors. +### Scaling (right-sized) +| Tier | Configuration | Sustained write rate | +| :--- | :--- | :--- | +| 150 sensors (test) | 1 worker, default settings | ~30 msg/s (1 reading / 5 s) | +| 10k sensors | `scale worker=N`, TimescaleDB hypertable | ~2k msg/s (1 Hz each) on 1 Postgres node | +| 1M sensors | + EMQX federation, Kafka/Redpanda buffer, ClickHouse analytics | beyond single-node; ports are designed, not deployed | + +The TimescaleDB migration (`src/timescale.py`) is idempotent and **fails closed**: on a stock PostGIS container every step is skipped with a warning and the service keeps running on the plain relational table. Provisioning happens automatically at startup and via `python -m src.timescale`. + +Load generator for capacity planning: + +```bash +# 150 sensors @ 1 Hz into the stream bus (matches the CI requirement) +python scripts/load_test.py --sensors 150 --hz 1 --duration 60 +# End-to-end through the FastAPI ingress +python scripts/load_test.py --sensors 150 --hz 1 --mode http --api http://localhost:8000 +``` + --- ## 🧪 Stress Testing -A specialized load testing script is located in `tests/stress_test.py`. It simulates a fleet of 100 concurrent sensors generating cryptographically valid payloads. +A specialized end-to-end stress suite lives in `tests/stress_test.py`. It simulates a fleet of **150 sensors** (configurable via `NUM_SENSORS`) firehosing cryptographically valid MQTT telemetry, verifies end-to-end persistence, and runs active security attacks (invalid signature + replay) against the API. **To run the test:** 1. Ensure the Docker stack is running. 2. Install test dependencies: ```bash - pip install aiohttp ecdsa + pip install aiohttp ecdsa aiomqtt ``` 3. Execute the script: ```bash diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index f928a12..12aa190 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,9 +1,14 @@ services: - # --- DATABASE --- + # --- DATABASE (TimescaleDB + PostGIS) --- postgres: - image: postgis/postgis:15-3.4-alpine + build: + context: . + dockerfile: docker/postgres-timescale.Dockerfile + image: quakeguard-postgres-timescale:pg15 restart: unless-stopped - command: postgres -c 'max_connections=200' -c 'shared_buffers=128MB' + command: postgres -c 'shared_preload_libraries=timescaledb' -c 'max_connections=200' -c 'shared_buffers=128MB' + ports: + - "${POSTGRES_PORT:-5432}:5432" environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} diff --git a/backend/docker/postgres-timescale.Dockerfile b/backend/docker/postgres-timescale.Dockerfile new file mode 100644 index 0000000..ff54085 --- /dev/null +++ b/backend/docker/postgres-timescale.Dockerfile @@ -0,0 +1,12 @@ +# TimescaleDB + PostGIS in a single Postgres 15 image. +# The Timescale HA image bundles BOTH the timescaledb extension (hypertable / +# continuous-aggregate time-series substrate) and postgis (zone geometry), so +# geospatial and time-series data share one database, one image, one backup. +FROM timescale/timescaledb-ha:pg15 + +# shared_preload_libraries=timescaledb is already injected by this image's +# entrypoint; docker-compose additionally passes it via `command` to be explicit. + +# The base image already runs as the non-root `postgres` user (uid 1000); +# declare it explicitly so the image never falls back to root. +USER postgres diff --git a/backend/scripts/load_test.py b/backend/scripts/load_test.py new file mode 100644 index 0000000..c40f6b2 --- /dev/null +++ b/backend/scripts/load_test.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Load generator for the QuakeGuard ingestion path. + +Simulates ``--sensors`` IoT devices emitting heartbeats at ``--hz`` each and +measures sustained throughput. Two transport modes: + + --mode stream XADD directly to the Redis Streams bus (bypasses the API; this + is the steady-state ingestion bottleneck we must keep O(1)). + --mode http POST to the FastAPI /readings/ endpoint (exercises the full + ingress, but without ECDSA signature validation). + +Usage: + python scripts/load_test.py --sensors 150 --hz 1 --duration 60 + python scripts/load_test.py --sensors 1000 --hz 1 --duration 30 --mode http --api http://localhost:8000 +""" + +import argparse +import json +import time + +import redis + + +def stream_worker(args, stop_event): + client = redis.from_url(args.redis, decode_responses=True) + + def emit(sensor_id): + payload = { + "value": 150, + "sensor_id": sensor_id, + "latitude": 45.0 + (sensor_id % 100) / 1000.0, + "longitude": 9.0 + (sensor_id % 100) / 1000.0, + } + client.xadd( + args.stream, {"payload": json.dumps(payload)}, maxlen=200000, approximate=True + ) + + next_emit = time.monotonic() + interval = 1.0 / args.hz + while not stop_event.is_set(): + now = time.monotonic() + if now < next_emit: + time.sleep(0.001) + continue + next_emit += interval + for sid in range(1, args.sensors + 1): + emit(sid) + + +def http_worker(args, stop_event): + import requests + + def emit(sensor_id): + payload = { + "value": 150, + "sensor_id": sensor_id, + "latitude": 45.0, + "longitude": 9.0, + } + requests.post( + args.api + "/readings/", + json=payload, + headers={"X-API-Key": args.api_key}, + timeout=5, + ) + + emitted = 0 + next_emit = time.monotonic() + interval = 1.0 / args.hz + while not stop_event.is_set(): + now = time.monotonic() + if now < next_emit: + time.sleep(0.001) + continue + next_emit += interval + emit((emitted % args.sensors) + 1) + emitted += 1 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--sensors", type=int, default=150, help="number of simulated devices") + parser.add_argument("--hz", type=float, default=1.0, help="heartbeats per second per device") + parser.add_argument("--duration", type=int, default=60, help="test duration in seconds") + parser.add_argument("--mode", choices=["stream", "http"], default="stream") + parser.add_argument("--redis", default="redis://localhost:6379/0") + parser.add_argument("--stream", default="readings:stream") + parser.add_argument("--api", default="http://localhost:8000") + parser.add_argument("--api-key", default="ci-test-key-123") + args = parser.parse_args() + + import threading + + stop_event = threading.Event() + worker = stream_worker if args.mode == "stream" else http_worker + + print( + f"🚀 Load test: {args.sensors} sensors @ {args.hz} Hz each, " + f"target {args.sensors * args.hz:.0f} msg/s, {args.duration}s, mode={args.mode}", + flush=True, + ) + + worker_thread = threading.Thread(target=worker, args=(args, stop_event), daemon=True) + worker_thread.start() + + start = time.monotonic() + time.sleep(args.duration) + stop_event.set() + elapsed = time.monotonic() - start + worker_thread.join(timeout=2) + + total_msgs = args.sensors * args.hz * elapsed + print(f"📊 Emitted ~{total_msgs:.0f} messages in {elapsed:.1f}s", flush=True) + print(f"📊 Sustained rate: {total_msgs / elapsed:.0f} msg/s", flush=True) + + +if __name__ == "__main__": + main() diff --git a/backend/scripts/simulate_zone.py b/backend/scripts/simulate_zone.py new file mode 100644 index 0000000..83fcc1f --- /dev/null +++ b/backend/scripts/simulate_zone.py @@ -0,0 +1,144 @@ +""" +QuakeGuard Zone Demo Simulator +------------------------------ +Injects a 20-second stream of (signed) telemetry into the live pipeline for a +given zone, ending with an earthquake ramp that crosses the CRITICAL threshold +(~M4.7+) so the dashboard seismograph and the alert websocket come to life. + +How it works: +1. Ensures the target zone exists (POST /zones/). +2. Registers a throwaway ECDSA (NIST P-256) sensor pinned to that zone. +3. Streams 20 samples (1/sec): random noise, then a seismic ramp spike. +Each reading is signed exactly like the firmware does +(signature over "value:timestamp") and POSTed to the /readings/ ingestion API. + +Run inside the backend container (has requests + cryptography + IOT_API_KEY): + docker cp scripts/simulate_zone.py backend-fastapi-app-1:/tmp/ + docker exec backend-fastapi-app-1 python /tmp/simulate_zone.py +""" + +import os +import time +import math +import random +import json + +import requests +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +API_URL = os.getenv("API_URL", "http://localhost:8000") +IOT_API_KEY = os.getenv("IOT_API_KEY", "") +ZONE_NAME = os.getenv("ZONE_NAME", "TEST ZONE") +SENSOR_LAT = float(os.getenv("SENSOR_LAT", "41.9028")) +SENSOR_LON = float(os.getenv("SENSOR_LON", "12.4964")) +DURATION_SECONDS = int(os.getenv("DURATION_SECONDS", "20")) + + +def normalize(name: str) -> str: + """'ZONE A - TEST' / 'A-TEST' / 'a test' all collapse to the same key.""" + return name.replace("-", "").replace("_", "").replace(" ", "").lower() + +B_OFFSET = 3.0 +K_CALIBRATION = 1.6 +SENSOR_SCALE = 100.0 + + +def estimate_magnitude(value: int) -> float: + """Same M_IoT formula as the backend worker (for the printout only).""" + pga = value / SENSOR_SCALE / K_CALIBRATION + if pga <= 0: + return 0.0 + return max(0.0, min(math.log10(pga) + B_OFFSET, 9.9)) + + +def headers() -> dict: + if not IOT_API_KEY: + raise RuntimeError("IOT_API_KEY not set") + return {"X-API-Key": IOT_API_KEY, "Content-Type": "application/json"} + + +def ensure_zone() -> int: + """Reuse an existing zone whenever possible — never duplicate a test zone. + + Matching rules: + 1. Exact city name. + 2. Normalized fuzzy match ('ZONE A - TEST' == 'A-TEST' == 'a test'). + Only when nothing matches do we create a new zone. + """ + existing = requests.get(f"{API_URL}/zones/", params={"limit": 1000}, headers=headers(), timeout=10) + existing.raise_for_status() + for z in existing.json(): + if z["city"] == ZONE_NAME or normalize(z["city"]) == normalize(ZONE_NAME): + print(f"🗺️ Reusing existing zone '{z['city']}' (id={z['id']})", flush=True) + return z["id"] + + resp = requests.post(f"{API_URL}/zones/", json={"city": ZONE_NAME}, headers=headers(), timeout=10) + resp.raise_for_status() + print(f"🗺️ Created zone '{ZONE_NAME}' (id={resp.json()['id']})", flush=True) + return resp.json()["id"] + + +def register_sensor(zone_id: int) -> tuple: + sk = ec.generate_private_key(ec.SECP256R1()) + public_key = sk.public_key() + public_key_hex = public_key.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo).hex() + payload = { + "active": True, + "zone_id": zone_id, + "latitude": SENSOR_LAT, + "longitude": SENSOR_LON, + "public_key_hex": public_key_hex, + } + resp = requests.post(f"{API_URL}/sensors/", json=payload, headers=headers(), timeout=10) + resp.raise_for_status() + sensor_id = resp.json()["id"] + print(f"📡 Sensor registered (id={sensor_id})", flush=True) + return sensor_id, sk + + +def sign(sk, value: int, ts: int) -> str: + sig = sk.sign(f"{value}:{ts}".encode(), ec.ECDSA(hashes.SHA256())) + return sig.hex() + + +def sample_value(step: int) -> int: + """Random noise for most of the window, then an escalating seismic ramp.""" + if step >= 15: + ramp = [1800, 2600, 3400, 5600, 7600] + return ramp[min(step - 15, len(ramp) - 1)] + if step == 6: + return random.SystemRandom().randint(900, 1600) + return random.SystemRandom().randint(120, 750) + + +def main() -> None: + zone_id = ensure_zone() + sensor_id, sk = register_sensor(zone_id) + + print(f"🌊 Streaming {DURATION_SECONDS}s of telemetry into zone '{ZONE_NAME}'...", flush=True) + for step in range(DURATION_SECONDS): + value = sample_value(step) + ts = int(time.time()) + payload = { + "value": value, + "sensor_id": sensor_id, + "device_timestamp": ts, + "signature_hex": sign(sk, value, ts), + } + resp = requests.post(f"{API_URL}/readings/", json=payload, headers=headers(), timeout=10) + status = resp.status_code + mag = estimate_magnitude(value) + flag = " 🚨 CRITICAL!" if mag >= 4.5 else (" ⚠️ caution" if mag >= 4.0 else "") + print(f" t+{step:>2}s value={value:>5} M≈{mag:.2f}{flag} http {status}", flush=True) + if status != 202: + print(f" API: {resp.text}", flush=True) + time.sleep(1.0) + + print("\n✅ Stream complete. The dashboard should now show the wave; " + "the CRITICAL at the end triggers the siren in the app.", flush=True) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/src/geo.py b/backend/src/geo.py new file mode 100644 index 0000000..006403b --- /dev/null +++ b/backend/src/geo.py @@ -0,0 +1,249 @@ +""" +Geographic zoning helpers (geohash-based Redis fast path) +--------------------------------------------------------- +PostGIS remains the single source of truth for zone geometry. This module +pre-computes, at seed time, the set of geohash cells that intersect each zone +polygon and exposes a fast-path resolver backed by Redis SET lookups. + +On a Redis miss (or an ambiguous multi-zone match) the caller falls back to +the authoritative PostGIS ``ST_Contains`` query, so correctness never depends +on the cache being warm. + +The geohash encoder below implements the standard algorithm (as used by +PostGIS ``ST_GeoHash``) in pure Python, so the Redis keys written at seed time +match the keys computed at runtime with zero new dependencies. +""" + +import math +import os + +import redis +import src.models as models +from sqlalchemy import text +from sqlalchemy.orm import Session + +# --- Configuration ----------------------------------------------------------- +# Geohash precision used for the Redis zone-index lookup sets. +# prec 3 -> cell ~1.40625deg (~156 km at the equator): small cardinality, so +# the zone index set stays cheap to bulk-cover at seed time. +ZONE_INDEX_PRECISION = int(os.getenv("ZONE_INDEX_PRECISION", "3")) + +# Geohash precision used for per-area event cooldown keys. +# prec 4 -> cell ~0.7deg (~39 x 19 km at the equator), aligned with the +# 50-100 km destructive surface-wave reach documented in the roadmap. +COOLDOWN_PRECISION = int(os.getenv("COOLDOWN_PRECISION", "4")) + +ZONE_INDEX_KEY_PREFIX = "zoneindex" +COOLDOWN_KEY_PREFIX = "alert_cooldown" + +REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0") +redis_sync = redis.from_url(REDIS_URL, decode_responses=True) + +# --- Pure-Python geohash encoder (matches PostGIS ST_GeoHash) --------------- +_BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz" + + +def point_to_geohash(latitude: float, longitude: float, precision: int) -> str: + """Encode a WGS84 coordinate into a geohash string of ``precision`` chars.""" + lat_range = [-90.0, 90.0] + lon_range = [-180.0, 180.0] + bit = 0 + ch = 0 + even = True + out = [] + while len(out) < precision: + if even: + mid = (lon_range[0] + lon_range[1]) / 2.0 + if longitude >= mid: + ch |= 1 << 4 - bit + lon_range[0] = mid + else: + lon_range[1] = mid + else: + mid = (lat_range[0] + lat_range[1]) / 2.0 + if latitude >= mid: + ch |= 1 << 4 - bit + lat_range[0] = mid + else: + lat_range[1] = mid + even = not even + if bit < 4: + bit += 1 + else: + out.append(_BASE32[ch]) + bit = 0 + ch = 0 + return "".join(out) + + +def geohash_bounds(geohash: str) -> tuple[float, float, float, float]: + """Decode a geohash into ``(lon_min, lat_min, lon_max, lat_max)``.""" + lat_range = [-90.0, 90.0] + lon_range = [-180.0, 180.0] + even = True + for char in geohash: + cd = _BASE32.index(char) + for mask in (16, 8, 4, 2, 1): + if even: + mid = (lon_range[0] + lon_range[1]) / 2.0 + if cd & mask: + lon_range[0] = mid + else: + lon_range[1] = mid + else: + mid = (lat_range[0] + lat_range[1]) / 2.0 + if cd & mask: + lat_range[0] = mid + else: + lat_range[1] = mid + even = not even + return lon_range[0], lat_range[0], lon_range[1], lat_range[1] + + +def _cell_indices_to_geohash(lon_index: int, lat_index: int, precision: int) -> str: + """Reconstruct the geohash of a cell from its (lon, lat) grid indices.""" + bits = precision * 5 + lon_bits = (bits + 1) // 2 + lat_bits = bits // 2 + chars = [] + for c in range(precision): + chunk = 0 + for k in range(5): + bit_pos = c * 5 + k + if bit_pos % 2 == 0: + axis_bit = (lon_index >> (lon_bits - 1 - bit_pos // 2)) & 1 + else: + axis_bit = (lat_index >> (lat_bits - 1 - bit_pos // 2)) & 1 + chunk = (chunk << 1) | axis_bit + chars.append(_BASE32[chunk]) + return "".join(chars) + + +def _cells_in_bbox(lat_min: float, lon_min: float, lat_max: float, lon_max: float, precision: int) -> list[str]: + """List the geohash cells (at ``precision``) whose rectangle overlaps the bbox.""" + bits = precision * 5 + lon_bits = (bits + 1) // 2 + lat_bits = bits // 2 + lon_step = 360.0 / (1 << lon_bits) + lat_step = 180.0 / (1 << lat_bits) + + lon_start = max(0, math.floor((lon_min + 180.0) / lon_step)) + lon_end = min((1 << lon_bits) - 1, math.floor((lon_max + 180.0) / lon_step)) + lat_start = max(0, math.floor((lat_min + 90.0) / lat_step)) + lat_end = min((1 << lat_bits) - 1, math.floor((lat_max + 90.0) / lat_step)) + + cells = [] + for lat_i in range(lat_start, lat_end + 1): + for lon_i in range(lon_start, lon_end + 1): + cells.append(_cell_indices_to_geohash(lon_i, lat_i, precision)) + return cells + + +def _zone_bbox( + db: Session, zone_id: int +) -> tuple[float, float, float, float] | None: + """Fetch ``(lon_min, lat_min, lon_max, lat_max)`` for a zone polygon.""" + row = db.execute( + text("SELECT ST_XMin(geom), ST_YMin(geom), ST_XMax(geom), ST_YMax(geom) FROM zones WHERE id = :zid"), + {"zid": zone_id}, + ).first() + if not row or any(v is None for v in row): + return None + return tuple(float(v) for v in row) + + +def zone_covering_geohashes(db: Session, zone: models.Zone, precision: int = ZONE_INDEX_PRECISION) -> list[str]: + """Return the geohash cells (at ``precision``) intersecting a zone polygon.""" + bbox = _zone_bbox(db, zone.id) + if bbox is None: + return [] + lon_min, lat_min, lon_max, lat_max = bbox + candidates = _cells_in_bbox(lat_min, lon_min, lat_max, lon_max, precision) + if not candidates: + return [] + + # Stroke each candidate cell's rectangle into a temp table, then keep only + # the cells actually intersecting the zone polygon. Static SQL + bound + # params throughout (no f-string interpolation), and no bind-param limits. + # IF NOT EXISTS + DELETE keep the temp table reusable across zones within + # the same transaction (ON COMMIT DROP fires only at commit). + db.execute(text("CREATE TEMP TABLE IF NOT EXISTS _zone_cells(cell_key text, geom geometry) ON COMMIT DROP")) + db.execute(text("DELETE FROM _zone_cells")) + db.execute( + text( + "INSERT INTO _zone_cells (cell_key, geom) " + "VALUES (:key, ST_GeomFromText(:wkt, 4326))" + ), + [ + {"key": cell, "wkt": _cell_wkt(cell)} + for cell in candidates + ], + ) + + rows = db.execute( + text( + "SELECT c.cell_key FROM _zone_cells c " + "WHERE ST_Intersects(c.geom, (SELECT geom FROM zones WHERE id = :zid))" + ), + {"zid": zone.id}, + ).fetchall() + return [row[0] for row in rows] + + +def _cell_wkt(cell: str) -> str: + """WKT rectangle for a geohash cell (used only to feed ST_GeomFromText).""" + lon_c, lat_c, lon_c2, lat_c2 = geohash_bounds(cell) + return ( + f"POLYGON(({lon_c} {lat_c},{lon_c2} {lat_c}," + f"{lon_c2} {lat_c2},{lon_c} {lat_c2},{lon_c} {lat_c}))" + ) + + +def clear_zone_index(redis_client=redis_sync) -> None: + """Delete every zone-index key (idempotent rebuild helper).""" + for key in redis_client.scan_iter(match=f"{ZONE_INDEX_KEY_PREFIX}:*"): + redis_client.delete(key) + + +def build_zone_index( + db: Session, + redis_client=redis_sync, + precision: int = ZONE_INDEX_PRECISION, +) -> dict[str, list[int]]: + """ + (Re)build the Redis zone-index from the authoritative PostGIS polygons. + + Maps ``zoneindex:`` -> SET of zone ids whose polygon intersects + that cell. Cells with zero matches are simply absent from Redis. + """ + clear_zone_index(redis_client) + zones = db.query(models.Zone).filter(models.Zone.geom.isnot(None)).all() + + index: dict[str, list[int]] = {} + for zone in zones: + cells = zone_covering_geohashes(db, zone, precision) + for cell in cells: + index.setdefault(cell, []).append(zone.id) + print(f" 🌐 Indexed Zone '{zone.city}': {len(cells)} cells", flush=True) + + pipe = redis_client.pipeline(transaction=False) + for cell, zone_ids in index.items(): + pipe.sadd(f"{ZONE_INDEX_KEY_PREFIX}:{cell}", *zone_ids) + pipe.execute() + print(f"✅ Zone geo-index rebuilt: {len(index)} cells across {len(zones)} zones", flush=True) + return index + + +def candidate_zone_ids( + latitude: float, + longitude: float, + redis_client=redis_sync, + precision: int = ZONE_INDEX_PRECISION, +) -> set[int]: + """Best-effort Redis fast path: zone ids whose cells contain the coordinate.""" + try: + cell = point_to_geohash(latitude, longitude, precision) + members = redis_client.smembers(f"{ZONE_INDEX_KEY_PREFIX}:{cell}") + return {int(m) for m in members} + except Exception: + return set() diff --git a/backend/src/ingest.py b/backend/src/ingest.py new file mode 100644 index 0000000..941655c --- /dev/null +++ b/backend/src/ingest.py @@ -0,0 +1,123 @@ +""" +Redis Streams Ingestion Substrate +--------------------------------- +Right-sized backpressure + horizontal scale for the IoT ingestion path. + +The old design enqueued heartbeats into a Redis LIST ('seismic_events') consumed +by a single worker via BRPOP. Lists cannot support consumer goups, so scale was +bounded to one process and a crash mid-read meant a lost message. + +This module replaces that with Redis Streams + consumer groups: + - Any number of producers XADD to the stream (HTTP API, MQTT bridge, ...). + - Any number of worker processes consume with XREADGROUP; Redis balances + deliveries across the group. Adding replicas = `scale worker=N`. + - XAUTOCLAIM recovers stale pending entries from crashed consumers, so + at-least-once delivery holds across worker restarts. + - Unparseable / fatally-failing messages are parked on a DLQ stream and ACKed, + so a poisoned heartbeat can never stall the group. + +Every key can be re-pointed with environment variables so a single backend can +serve multiple logical regions simply by renaming the stream. +""" + +import os +from redis.exceptions import ResponseError + +# --- Stream / group topology ------------------------------------------------- +READINGS_STREAM = os.getenv("READINGS_STREAM", "readings:stream") +READINGS_GROUP = os.getenv("READINGS_GROUP", "quakeguard-ingest") +READINGS_DLQ = os.getenv("READINGS_DLQ", "readings:dlq") +CONSUMER_PREFIX = os.getenv("READINGS_CONSUMER_PREFIX", "worker") +MAXLEN = int(os.getenv("READINGS_STREAM_MAXLEN", "200000")) +MIN_IDLE_MS = int(os.getenv("READINGS_MIN_IDLE_MS", "30000")) +BATCH_SIZE = int(os.getenv("READINGS_BATCH_SIZE", "64")) +BLOCK_MS = int(os.getenv("READINGS_BLOCK_MS", "500")) + +# Field name carrying the serialized payload inside each stream entry. +PAYLOAD_FIELD = "payload" + + +def _is_group_exists(exc: Exception) -> bool: + """BUSYGROUP (group exists) is the expected steady state -> ignore only it.""" + return isinstance(exc, ResponseError) or "BUSYGROUP" in str(exc) + + +def ensure_group(client) -> None: + """Create the consumer group (and stream) if missing. Best-effort: producers + may legitimately run before any worker, so XADD will create the stream.""" + try: + client.xgroup_create( + READINGS_STREAM, READINGS_GROUP, id="0", mkstream=True + ) + except Exception as exc: # noqa: BLE001 - re-raise anything unexpected + if not _is_group_exists(exc): + raise + + +async def ensure_group_async(client) -> None: + """Async counterpart of :func:`ensure_group` for the FastAPI event loop.""" + try: + await client.xgroup_create( + READINGS_STREAM, READINGS_GROUP, id="0", mkstream=True + ) + except Exception as exc: # noqa: BLE001 - re-raise anything unexpected + if not _is_group_exists(exc): + raise + + +def enqueue_reading(client, payload_json: str) -> str: + """Append one serialized heartbeat to the stream. Returns its message id.""" + return client.xadd( + READINGS_STREAM, + {PAYLOAD_FIELD: payload_json}, + maxlen=MAXLEN, + approximate=True, + ) + + +def read_batch(client, consumer: str, count: int = BATCH_SIZE, + block_ms: int = BLOCK_MS) -> list: + """Block for new messages assigned to this consumer. Returns a list of + ``(message_id, payload_json)`` tuples ([] on timeout).""" + raw = client.xreadgroup( + READINGS_GROUP, consumer, + {READINGS_STREAM: ">"}, + count=count, + block=block_ms, + ) + if not raw: + return [] + # raw == [ [stream_name, [[msg_id, {field: value}], ...]] ] + _, entries = raw[0] + return [(entry_id, values[PAYLOAD_FIELD]) for entry_id, values in entries] + + +def ack(client, message_ids) -> None: + if not message_ids: + return + client.xack(READINGS_STREAM, READINGS_GROUP, *message_ids) + + +def move_to_dlq(client, message_id: str, payload_json: str, reason: str) -> None: + """Park a poisoned message on the DLQ stream and acknowledge the original.""" + client.xadd( + READINGS_DLQ, + {"reason": reason, "original_id": message_id, PAYLOAD_FIELD: payload_json}, + maxlen=MAXLEN, + approximate=True, + ) + ack(client, [message_id]) + + +def recover_pending(client, consumer: str, min_idle_ms: int = MIN_IDLE_MS) -> int: + """Reclaim entries left pending by crashed consumers (at-least-once across + restarts). Returns the number of reclaimed entries.""" + reclaimed = client.xautoclaim( + READINGS_STREAM, + READINGS_GROUP, + consumer, + min_idle_time=min_idle_ms, + start_id="0", + ) + # XAUTOCLAIM => [next_cursor, [ [id, {field: value}], ... ], deleted_ids] + return len(reclaimed[1]) diff --git a/backend/src/main.py b/backend/src/main.py index 59731bb..cb1ceae 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -28,10 +28,19 @@ # --- LOCAL MODULES --- from src.database import get_db, engine, SessionLocal, DATABASE_URL +from src.geo import ( + COOLDOWN_PRECISION, + ZONE_INDEX_PRECISION, + build_zone_index, + candidate_zone_ids, + point_to_geohash, +) +from src import ingest import src.models as models import src.schemas as schemas from src.security import verify_api_key, validate_iot_payload from src.seed import seed_zones +from src.timescale import apply_timescale PING_QUERY = "SELECT 1" @@ -116,17 +125,34 @@ async def redis_alert_listener() -> None: async def lifespan(app: FastAPI): loop = asyncio.get_running_loop() await loop.run_in_executor(None, wait_for_db) + # PostGIS must exist before create_all: the models use `geometry` columns. + # Some DB images (e.g. timescale-ha) do not run the postgis init hook, so the + # app ensures the extension itself — idempotent and safe on stock PostGIS. + with engine.begin() as conn: + conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) await loop.run_in_executor(None, lambda: models.Base.metadata.create_all(bind=engine)) with SessionLocal() as db: seed_zones(db) + build_zone_index(db) + # TimescaleDB hypertable + rollups (best-effort; no-op on plain PostGIS). + try: + apply_timescale(db) + except Exception as e: + print(f"⚠️ TimescaleDB migration skipped: {e}", flush=True) + + # Ensure the ingestion consumer group exists so stream reads never error. + try: + await ingest.ensure_group_async(redis_client) + except Exception as e: + print(f"⚠️ Ingest group ensure skipped: {e}", flush=True) listener_task = asyncio.create_task(redis_alert_listener()) yield listener_task.cancel() # Initialize FastAPI -app = FastAPI(title="QuakeGuard Backend", version="2.2.0", lifespan=lifespan) +app = FastAPI(title="QuakeGuard Backend", version="1.2.1", lifespan=lifespan) # ========================================== # MIDDLEWARE @@ -158,13 +184,23 @@ def resolve_zone(db: Session, latitude: float | None, longitude: float | None) - Spatial auto-assignment helper. Finds the smallest containing polygon for given GPS coordinates. Falls back to 'Unknown Region' if no match is found or coordinates are null. + + Fast path: the Redis zone-index (geohash -> zone ids) resolves the zone for + a coordinate without a DB round-trip. On a miss or an ambiguous multi-zone + match we fall back to the authoritative PostGIS ST_Contains query, so the + Redis cache is purely an optimization and can never assign a wrong zone. """ if latitude is None or longitude is None: fallback = db.query(models.Zone).filter(models.Zone.city == "Unknown Region").first() return fallback.id if fallback else 1 + candidates = candidate_zone_ids(latitude, longitude, precision=ZONE_INDEX_PRECISION) + if len(candidates) == 1: + return candidates.pop() + + # Redis miss, empty or ambiguous match -> authoritative PostGIS query point = WKTElement(f"POINT({longitude} {latitude})", srid=4326) - + # Query PostGIS to find the containing polygon, ordered by smallest area first matched_zone = db.query(models.Zone).filter( func.ST_Contains(models.Zone.geom, point) @@ -172,10 +208,10 @@ def resolve_zone(db: Session, latitude: float | None, longitude: float | None) - if matched_zone: return matched_zone.id - + # Fallback to Unknown Region fallback = db.query(models.Zone).filter(models.Zone.city == "Unknown Region").first() - return fallback.id if fallback else 1 # Final failsafe + return fallback.id if fallback else 1 # Final failsafe # ========================================== # WEBSOCKET ENDPOINT @@ -303,6 +339,25 @@ def get_zones(skip: int = 0, limit: int = 20, db: Session = Depends(get_db)): limit = min(limit, 1000) return db.query(models.Zone).offset(skip).limit(limit).all() +@app.get("/zones/locate", response_model=schemas.Zone, tags=["Data Retrieval"], dependencies=[Depends(verify_api_key)]) +def locate_zone(latitude: float, longitude: float, db: Session = Depends(get_db)): + """ + Resolve GPS coordinates to the smallest containing monitored zone. + + Powers the "Detect my zone" flow: the app pings this with a GPS fix and gets + the zone back so alert alarms can be scoped to the operator's own area. + Returns 404 when the coordinate is not covered by any monitored polygon. + """ + matched = ( + db.query(models.Zone) + .filter(func.ST_Contains(models.Zone.geom, WKTElement(f"POINT({longitude} {latitude})", srid=4326))) + .order_by(func.ST_Area(models.Zone.geom).asc()) + .first() + ) + if matched is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Coordinates not inside any monitored zone") + return matched + def _create_sensor(db: Session, active: bool, zone_id: int | None, latitude: float | None, longitude: float | None, public_key_hex: str, mac_address: str | None = None) -> models.Sensor: """Create and persist a Sensor with spatial zone auto-assignment.""" assigned_zone_id = zone_id or resolve_zone(db, latitude, longitude) @@ -340,6 +395,26 @@ def register_device(payload: schemas.DeviceRegisterRequest, db: Session = Depend ).first() if existing: + # GNSS-ready: a relocated node (or a spare re-deployed after a cold + # boot) re-reports its real fix at handshake time. Keep the last known + # coordinates fresh and re-assign the zone if it actually moved. + if payload.latitude is not None and payload.longitude is not None: + moved = ( + existing.latitude != payload.latitude + or existing.longitude != payload.longitude + ) + if moved: + existing.latitude = payload.latitude + existing.longitude = payload.longitude + existing.location = WKTElement( + f"POINT({payload.longitude} {payload.latitude})", srid=4326 + ) + existing.zone_id = resolve_zone(db, payload.latitude, payload.longitude) + db.commit() + print( + f"[PROV] Sensor {existing.id} moved -> new zone_id {existing.zone_id}", + flush=True, + ) return {"sensor_id": existing.id} new_device = _create_sensor(db, True, None, payload.latitude, payload.longitude, payload.public_key_hex, payload.mac_address) @@ -364,13 +439,38 @@ async def create_reading_async( # Enqueue for Worker payload = reading.model_dump() payload['zone_id'] = sensor.zone_id + # GNSS-ready: propagate the sensor's current fix so the worker can persist + # coordinates and fragment the cooldown lock per-area instead of per-zone. + payload['latitude'] = sensor.latitude + payload['longitude'] = sensor.longitude + if sensor.latitude is not None and sensor.longitude is not None: + payload['sensor_geohash'] = point_to_geohash(sensor.latitude, sensor.longitude, COOLDOWN_PRECISION) - # Offload to the Redis queue - await redis_client.lpush("seismic_events", json.dumps(payload)) + # Append to the Redis Streams ingestion bus (O(1)); the worker group drains it. + await ingest.enqueue_reading(redis_client, json.dumps(payload)) return {"status": "accepted"} @app.get("/sensors/{id}/statistics", tags=["Data Retrieval"], dependencies=[Depends(verify_api_key)]) def get_sensor_statistics(id: int, db: Session = Depends(get_db)): + # Fast path: when TimescaleDB provisioned the continuous aggregate, the + # dashboard rollups are served from pre-computed buckets instead of a COUNT + # scan over the hypertable. + has_aggregate = db.execute( + text("SELECT to_regclass('public.readings_minute') IS NOT NULL") + ).scalar() + if has_aggregate: + row = db.execute( + text( + "SELECT COALESCE(SUM(n), 0) AS total, COALESCE(MAX(peak), 0) AS peak " + "FROM readings_minute WHERE sensor_id = :sensor_id" + ), + {"sensor_id": id}, + ).one() + return { + "sensor_id": id, + "total_readings": row.total, + "peak_value": row.peak, + } count = db.query(models.Reading).filter(models.Reading.sensor_id == id).count() return { "sensor_id": id, @@ -388,6 +488,73 @@ def get_readings(skip: int = 0, limit: int = 20, db: Session = Depends(get_db)): limit = min(limit, 1000) return db.query(models.Reading).order_by(models.Reading.recorded_at.desc()).offset(skip).limit(limit).all() +@app.get("/zones/{zone_id}/readings", response_model=List[schemas.Reading], tags=["Data Retrieval"], dependencies=[Depends(verify_api_key)]) +def get_zone_readings(zone_id: int, limit: int = 60, db: Session = Depends(get_db)): + """ + Fetch the most recent readings emitted by sensors belonging to a single + PostGIS zone. Powers the per-zone seismograph on the dashboard: each zone + renders its own sliding window instead of mixing the whole network. + """ + zone = db.query(models.Zone).filter(models.Zone.id == zone_id).first() + if not zone: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Zone not found") + limit = max(1, min(limit, 200)) + return ( + db.query(models.Reading) + .join(models.Sensor, models.Reading.sensor_id == models.Sensor.id) + .filter(models.Sensor.zone_id == zone_id) + .order_by(models.Reading.recorded_at.desc()) + .limit(limit) + .all() + ) + +@app.delete("/zones/{zone_id}/readings", tags=["Data Management"], dependencies=[Depends(verify_api_key)]) +def delete_zone_readings(zone_id: int, db: Session = Depends(get_db)): + """ + Clear the telemetry emitted by sensors belonging to a single PostGIS zone. + + Removes every reading whose sensor is assigned to the given zone so the + per-zone seismograph can be reset without touching data from other areas. + Returns the number of deleted readings. + """ + zone = db.query(models.Zone).filter(models.Zone.id == zone_id).first() + if not zone: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Zone not found") + + sensor_ids = [ + row[0] + for row in db.query(models.Sensor.id).filter(models.Sensor.zone_id == zone_id).all() + ] + deleted = 0 + if sensor_ids: + deleted = ( + db.query(models.Reading) + .filter(models.Reading.sensor_id.in_(sensor_ids)) + .delete(synchronize_session=False) + ) + db.commit() + return {"deleted": deleted} + +@app.get("/zones/{zone_id}/alerts", response_model=List[schemas.Alert], tags=["Data Retrieval"], dependencies=[Depends(verify_api_key)]) +def get_zone_alerts(zone_id: int, limit: int = 20, db: Session = Depends(get_db)): + """ + Retrieve the confirmed seismic alerts raised for a specific PostGIS zone. + + Orders by most recent first so the dashboard can render an area-scoped + alert history. Returns 404 when the zone does not exist. + """ + zone = db.query(models.Zone).filter(models.Zone.id == zone_id).first() + if not zone: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Zone not found") + limit = max(1, min(limit, 100)) + return ( + db.query(models.Alert) + .filter(models.Alert.zone_id == zone_id) + .order_by(models.Alert.created_at.desc()) + .limit(limit) + .all() + ) + @app.get("/reports/{alert_id}", response_model=schemas.EmergencyReport, tags=["Data Retrieval"], dependencies=[Depends(verify_api_key)]) def get_emergency_report(alert_id: int, db: Session = Depends(get_db)): """ diff --git a/backend/src/models.py b/backend/src/models.py index 1871b31..6e5f03e 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -5,7 +5,9 @@ Updated to support Device Provisioning (MAC Address & Firmware Version). """ -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Float, Text +from sqlalchemy import ( + Column, Integer, String, Boolean, DateTime, ForeignKey, Float, Text, +) from sqlalchemy.sql import func from sqlalchemy.orm import relationship from geoalchemy2 import Geometry @@ -46,6 +48,8 @@ class Sensor(Base): latitude = Column(Float, nullable=True) longitude = Column(Float, nullable=True) location = Column(Geometry('POINT', srid=4326), nullable=True) + # GNSS-ready: last reliable fix timestamp (v1.2.1). Null until the first fix. + last_fix_at = Column(DateTime(timezone=True), nullable=True) # --- SECURITY & IDENTITY --- # The public key is the primary cryptographic identity @@ -61,10 +65,17 @@ class Sensor(Base): class Reading(Base): __tablename__ = "readings" - id = Column(Integer, primary_key=True, index=True) - recorded_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True) + # Composite PK (id, recorded_at): TimescaleDB requires the partitioning + # column to be part of every primary/unique key of the hypertable. + id = Column(Integer, primary_key=True, autoincrement=True, index=True) + recorded_at = Column(DateTime(timezone=True), primary_key=True, server_default=func.now(), nullable=False, index=True) value = Column(Integer, nullable=False) + # GNSS-ready: event coordinates captured at ingestion (v1.2.1) so the worker + # can fragment cooldowns per-area and v2.0 can correlate nodes spatially. + latitude = Column(Float, nullable=True) + longitude = Column(Float, nullable=True) + sensor_id = Column(Integer, ForeignKey("sensors.id"), nullable=False) sensor = relationship("Sensor", back_populates="readings") diff --git a/backend/src/mqtt_subscriber.py b/backend/src/mqtt_subscriber.py index 9e738e5..6bcfbc9 100644 --- a/backend/src/mqtt_subscriber.py +++ b/backend/src/mqtt_subscriber.py @@ -56,8 +56,11 @@ def on_message(client, userdata, msg): if MQTT_USERNAME and MQTT_PASSWORD: client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) - - client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLS) + + # TLS is required on the WAN port (8883) but must be skipped for the local + # plaintext mosquitto (1883), mirroring the firmware's dual-mode transport. + if MQTT_PORT == 8883: + client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLS) print(f"🔌 Connecting to MQTT Broker at {MQTT_BROKER}:{MQTT_PORT}...") client.connect(MQTT_BROKER, MQTT_PORT, 60) diff --git a/backend/src/schemas.py b/backend/src/schemas.py index d8dcf1a..7c37b2d 100644 --- a/backend/src/schemas.py +++ b/backend/src/schemas.py @@ -51,6 +51,7 @@ class Sensor(SensorBase): id: int latitude: Optional[float] = None longitude: Optional[float] = None + last_fix_at: Optional[datetime] = None # We do not return the public key by default to keep responses clean, # but it can be added if needed. model_config = ConfigDict(from_attributes=True) @@ -78,6 +79,8 @@ class Reading(BaseModel): value: int sensor_id: int recorded_at: datetime + latitude: Optional[float] = None + longitude: Optional[float] = None model_config = ConfigDict(from_attributes=True) @@ -92,6 +95,18 @@ class DeviceRegisterRequest(BaseModel): latitude: float = Field(default=None, ge=-90, le=90) longitude: float = Field(default=None, ge=-180, le=180) +# ========================================== +# ALERT SCHEMAS +# ========================================== +class Alert(BaseModel): + id: int + zone_id: int + magnitude: float + message: Optional[str] = None + created_at: datetime + + model_config = ConfigDict(from_attributes=True) + # ========================================== # EMERGENCY REPORT SCHEMAS (AI / Ollama) # ========================================== diff --git a/backend/src/timescale.py b/backend/src/timescale.py new file mode 100644 index 0000000..3670d33 --- /dev/null +++ b/backend/src/timescale.py @@ -0,0 +1,147 @@ +""" +TimescaleDB Provisioning (best-effort, idempotent) +-------------------------------------------------- +Upgrades the ``readings`` table to a TimescaleDB *hypertable* partitioned on +``recorded_at`` so the time-series ingestion path scales with chunking, +compression and continuous aggregates — without touching the ORM model. + +The migration is deliberately **best-effort**: on a stock PostGIS container +(no TimescaleDB), every step fails closed with a warning and the application +keeps running on the plain relational table. This lets CI and local dev use the +standard PostGIS image while production uses the TimescaleDB+PostGIS image. + +Run manually: python -m src.timescale +Auto-applied: on FastAPI startup (see src/main.py lifespan) +""" + +import os + +from sqlalchemy import text +from sqlalchemy.orm import Session + +TSDB_RETENTION_DAYS = os.getenv("TSDB_RETENTION_DAYS", "180") + + +def apply_timescale(db: Session) -> dict: + """Apply TimescaleDB DDL where possible. Returns a per-step report dict. + + Steps are isolated: a failure in any step never blocks the others nor the + application startup. + """ + report = { + "timescaledb": False, + "hypertable": False, + "aggregate": False, + "retention": False, + } + + # 1. Extension --------------------------------------------------------- + try: + db.execute(text("CREATE EXTENSION IF NOT EXISTS timescaledb")) + db.commit() + report["timescaledb"] = True + except Exception as e: + db.rollback() + print(f"⚠️ TimescaleDB extension unavailable: {e}", flush=True) + return report + + # 2. Hypertable -------------------------------------------------------- + try: + already_hypertable = db.execute( + text( + "SELECT (EXISTS (SELECT 1 FROM timescaledb_information.hypertables " + "WHERE hypertable_name = 'readings'))" + ) + ).scalar() + if not already_hypertable: + db.execute( + text( + "SELECT create_hypertable('readings', 'recorded_at', " + "if_not_exists => TRUE, migrate_data => TRUE)" + ) + ) + db.commit() + report["hypertable"] = True + print("✅ readings is a TimescaleDB hypertable (chunked on recorded_at).", flush=True) + except Exception as e: + db.rollback() + print(f"⚠️ Hypertable creation skipped: {e}", flush=True) + + # 3. Continuous aggregate (per-sensor minute rollups) ------------------- + try: + db.execute( + text( + "CREATE MATERIALIZED VIEW IF NOT EXISTS readings_minute " + "WITH (timescaledb.continuous) AS " + "SELECT sensor_id, time_bucket('1 minute', recorded_at) AS bucket, " + "count(*) AS n, max(value) AS peak " + "FROM readings GROUP BY sensor_id, bucket WITH NO DATA" + ) + ) + db.commit() + try: + db.execute( + text( + "SELECT add_continuous_aggregate_policy('readings_minute', " + "start_offset => INTERVAL '3 hours', " + "end_offset => INTERVAL '10 seconds', " + "schedule_interval => INTERVAL '1 minute')" + ) + ) + db.commit() + except Exception: + db.rollback() # policy already present or not yet refreshable + report["aggregate"] = True + print("✅ Continuous aggregate readings_minute created.", flush=True) + except Exception as e: + db.rollback() + print(f"⚠️ Continuous aggregate skipped: {e}", flush=True) + + # 4. Compression + retention ------------------------------------------- + try: + # Newer TimescaleDB (2.13+/3.x) requires columnstore on the hypertable + # before a columnstore compression policy can be added. + db.execute(text("ALTER TABLE readings SET (timescaledb.columnstore = true)")) + db.commit() + except Exception as e: + db.rollback() + if "already" not in str(e).lower(): + print(f"⚠️ Columnstore enable skipped: {e}", flush=True) + try: + db.execute(text("SELECT add_compression_policy('readings', INTERVAL '1 day')")) + db.commit() + except Exception as e: + db.rollback() + if "already exists" not in str(e).lower(): + print(f"⚠️ Compression policy skipped: {e}", flush=True) + try: + db.execute( + text( + "SELECT add_retention_policy('readings', " + f"INTERVAL '{TSDB_RETENTION_DAYS} days')" + ) + ) + db.commit() + report["retention"] = True + except Exception as e: + db.rollback() + if "already exists" in str(e).lower(): + report["retention"] = True + else: + print(f"⚠️ Retention policy skipped: {e}", flush=True) + if report["retention"]: + print(f"✅ Retention policy set to {TSDB_RETENTION_DAYS} days.", flush=True) + + return report + + +def run_migration() -> dict: + from src.database import SessionLocal + with SessionLocal() as db: + report = apply_timescale(db) + print(f"📋 TimescaleDB migration report: {report}", flush=True) + return report + + +if __name__ == "__main__": + run_migration() diff --git a/backend/src/worker.py b/backend/src/worker.py index 7552ef1..6223c3b 100644 --- a/backend/src/worker.py +++ b/backend/src/worker.py @@ -1,11 +1,25 @@ -import os import json -import time import math +import os +import socket +import time from datetime import datetime, timezone import redis from sqlalchemy.orm import Session from src.database import SessionLocal, engine +from src.geo import COOLDOWN_KEY_PREFIX +from src.ingest import ( + READINGS_STREAM, + READINGS_GROUP, + CONSUMER_PREFIX, + BATCH_SIZE, + BLOCK_MS, + ensure_group, + read_batch, + ack, + move_to_dlq, + recover_pending, +) from src.models import Reading, Alert, EmergencyReport, Zone # Redis Config @@ -42,30 +56,31 @@ def estimate_magnitude(sensor_value: int) -> float: # Clamp to physically meaningful range for MEMS sensors return max(0.0, min(magnitude, 9.9)) -def process_event(event: dict, db: Session): - """Inserts a single sensor measurement into PostGIS and triggers alerts with deduplication.""" - - # 1. Save to Database +def _enrich_event(event: dict, db: Session) -> dict: + """Stage one reading (+ optional alert) into the session. Returns a resolution + dict consumed by ``_finish_alerts`` after the shared commit. No commit here so + a whole stream batch can be flushed in a single transaction.""" new_entry = Reading( value=event.get("value"), - sensor_id=event.get("sensor_id") + sensor_id=event.get("sensor_id"), + latitude=event.get("latitude"), + longitude=event.get("longitude"), ) db.add(new_entry) - # 2. 🚨 ALARM LOGIC: Check if threshold is breached sensor_value = event.get("value", 0) magnitude = estimate_magnitude(sensor_value) zone_id = event.get("zone_id", 0) - alert_published = False alert_entry = None - - # Trigger a CRITICAL alert if physical magnitude is 4.5 or higher + if magnitude >= 4.5: - cooldown_key = f"alert_cooldown:{zone_id}" - - # Atomic check-and-set with 60s TTL (Deduplication) + area_key = event.get("sensor_geohash") or event.get("geohash") + if area_key: + cooldown_key = f"{COOLDOWN_KEY_PREFIX}:{area_key}" + else: + cooldown_key = f"{COOLDOWN_KEY_PREFIX}:zone:{zone_id}" + if redis_sync.set(cooldown_key, "active", nx=True, ex=60): - alert_published = True alert_entry = Alert( zone_id=zone_id, magnitude=magnitude, @@ -73,30 +88,57 @@ def process_event(event: dict, db: Session): ) db.add(alert_entry) else: - print(f"🚫 ALERT SUPPRESSED: Zone {zone_id} is in 60s cooldown.", flush=True) + print(f"🚫 ALERT SUPPRESSED: area '{area_key or zone_id}' is in 60s cooldown.", flush=True) - # 3. Commit atomically: Reading (+ Alert) — flush to obtain IDs before enqueue - db.commit() - if alert_published and alert_entry is not None: + return { + "event": event, + "magnitude": magnitude, + "zone_id": zone_id, + "sensor_id": event.get("sensor_id"), + "alert": alert_entry, + } + +def _finish_alerts(db: Session, resolutions: list) -> None: + """After the shared commit: publish any CRITICAL alert to the Redis Pub/Sub + channel and enqueue AI report generation. Runs per-resolution (alerts are rare).""" + for resolution in resolutions: + alert_entry = resolution["alert"] + if alert_entry is None: + continue db.refresh(alert_entry) + zone_id = resolution["zone_id"] + magnitude = resolution["magnitude"] + sensor_id = resolution["sensor_id"] + event = resolution["event"] - # 4. Publish alert to Redis (best-effort after DB commit — outbox pattern) - if alert_published and alert_entry is not None: alert_payload = { "type": "CRITICAL", "alert_id": alert_entry.id, "zone_id": zone_id, "magnitude": round(magnitude, 1), - "message": f"High seismic activity detected (Sensor {event.get('sensor_id')})!", + "message": f"High seismic activity detected (Sensor {sensor_id})!", "timestamp": datetime.now(timezone.utc).isoformat() } redis_sync.publish("quake_alerts", json.dumps(alert_payload)) print(f"🚨 ALERT PUBLISHED: Zone {zone_id} - Mag {round(magnitude, 1)}", flush=True) - # 5. 🤖 AI REPORT: enqueue context for the dedicated worker (non-blocking) if AI_REPORT_ENABLED: enqueue_ai_report(db, event, alert_entry.id, zone_id, magnitude) +def process_event(event: dict, db: Session): + """Single-event processing (compatibility + tests). Batch workloads use + ``process_batch`` so one transaction covers N readings.""" + resolution = _enrich_event(event, db) + db.commit() + _finish_alerts(db, [resolution]) + +def process_batch(events: list, db: Session): + """Process a stream batch in ONE transaction. Cooldown locks are taken + per-event (atomic Redis SET NX), DB writes are batched and committed once.""" + resolutions = [_enrich_event(event, db) for event in events] + db.commit() + _finish_alerts(db, resolutions) + def enqueue_ai_report(db: Session, event: dict, alert_id: int, zone_id: int, magnitude: float) -> None: """Create a PENDING EmergencyReport and enqueue the AI report job. @@ -133,25 +175,60 @@ def enqueue_ai_report(db: Session, event: dict, alert_id: int, zone_id: int, mag print(f"🤖 AI Report enqueued (report_id={report.id})", flush=True) def run_worker(): - print("👷 Worker started. Listening for 'seismic_events'...") + consumer = f"{CONSUMER_PREFIX}-{socket.gethostname()}-{os.getpid()}" + print(f"👷 Worker started. stream='{READINGS_STREAM}' group='{READINGS_GROUP}' consumer='{consumer}'") db = SessionLocal() - + + # Group must exist before any XREADGROUP; idempotent. + ensure_group(redis_sync) + # Reclaim entries left pending by a crashed/restarted sibling (at-least-once). + try: + recovered = recover_pending(redis_sync, consumer) + if recovered: + print(f"🔁 Reclaimed {recovered} stale pending entr{'y' if recovered == 1 else 'ies'}.", flush=True) + except Exception as e: + print(f"⚠️ Pending recovery skipped: {e}", flush=True) + while True: try: - # Block until data is available in the queue - result = redis_sync.brpop("seismic_events", timeout=0) - if result: - _, data = result - event = json.loads(data) - + batch = read_batch(redis_sync, consumer, count=BATCH_SIZE, block_ms=BLOCK_MS) + if not batch: + continue + + pending = [] + for message_id, payload in batch: try: - process_event(event, db) - print(f"✅ Processed sensor {event.get('sensor_id')} -> {event.get('value')} (Mag: {estimate_magnitude(event.get('value', 0))})", flush=True) - except Exception as e: - print(f"❌ DB Error: {e}. Moving to DLQ.", flush=True) - db.rollback() - redis_sync.lpush("seismic_events_dlq", data) - + event = json.loads(payload) + except Exception: + print("❌ Malformed payload -> DLQ.", flush=True) + try: + move_to_dlq(redis_sync, message_id, payload, reason="malformed_json") + except Exception as e: + print(f"❌ DLQ write failed: {e}", flush=True) + continue + pending.append((message_id, event, payload)) + + if not pending: + continue + + try: + process_batch([event for _, event, _ in pending], db) + ack(redis_sync, [message_id for message_id, _, _ in pending]) + for _, event, _ in pending: + print( + f"✅ Processed sensor {event.get('sensor_id')} -> {event.get('value')} " + f"(Mag: {estimate_magnitude(event.get('value', 0))})", + flush=True, + ) + except Exception as e: + print(f"❌ Batch DB Error: {e}. Moving batch to DLQ.", flush=True) + db.rollback() + for message_id, _, payload in pending: + try: + move_to_dlq(redis_sync, message_id, payload, reason=f"process_error: {e}") + except Exception as dlq_err: + print(f"❌ DLQ write failed: {dlq_err}", flush=True) + except Exception as e: print(f"❌ Redis Connection Error: {e}", flush=True) time.sleep(2) diff --git a/backend/tests/integration/test_api.py b/backend/tests/integration/test_api.py index bffad2e..e9eee1e 100644 --- a/backend/tests/integration/test_api.py +++ b/backend/tests/integration/test_api.py @@ -97,6 +97,8 @@ def test_get_readings(self, client, override_db, auth_headers): assert resp.status_code == 200 def test_get_statistics(self, client, override_db, auth_headers): + # No continuous aggregate available -> the COUNT fallback path runs. + override_db.execute.return_value.scalar.return_value = False override_db.query.return_value.filter.return_value.count.return_value = 42 resp = client.get("/sensors/1/statistics", headers=auth_headers) assert resp.status_code == 200 diff --git a/backend/tests/integration/test_geo_integration.py b/backend/tests/integration/test_geo_integration.py new file mode 100644 index 0000000..a6b923b --- /dev/null +++ b/backend/tests/integration/test_geo_integration.py @@ -0,0 +1,131 @@ +""" +Real-spatial integration tests (run when a live PostGIS + Redis are reachable). + +These tests exercise the actual seeded polygons and the Redis zone index, so +they cross-validate the pure-Python geohash encoder against PostGIS +``ST_GeoHash`` and assert ``resolve_zone`` on known coordinates. + +They skip cleanly when no database/redis is reachable (the shared integration +conftest runs against mocked infra in the base CI job); a dedicated CI job +runs them against the seeded Docker stack. +""" + +import os + +import pytest +from sqlalchemy import text + +from src.database import Base, engine, SessionLocal +from src.geo import ( + COOLDOWN_PRECISION, + ZONE_INDEX_PRECISION, + build_zone_index, + candidate_zone_ids, + clear_zone_index, + point_to_geohash, + redis_sync, +) +from src.main import resolve_zone +from src.seed import seed_zones + +# (latitude, longitude, expected zone name) +KNOWN_POINTS = [ + (45.4642, 9.1900, "Italy - North"), # Milan + (40.4168, -3.7038, "Western Europe"), # Madrid + (35.6762, 139.6503, "East Asia"), # Tokyo + (48.8566, 2.3522, "Western Europe"), # Paris + (4.7110, -74.0721, "South America"), # Bogota + (-23.5505, -46.6333, "South America"), # Sao Paulo + (37.7749, -122.4194, "North America"), # San Francisco + (-1.2833, 36.8167, "Unknown Region"), # Nairobi (no polygon) +] + + +@pytest.fixture(scope="module") +def infra_available(): + try: + with engine.connect() as conn: + conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) + redis_sync.ping() + except Exception: + pytest.skip("PostGIS and/or Redis not reachable; skipping spatial integration tests") + return + + # Create only the `zones` table via raw DDL: the shared integration conftest + # replaces Base.metadata.create_all with a mock (it is designed for + # mocked-DB tests), so we bypass it with an explicit schema. + with engine.begin() as conn: + conn.execute(text("DROP TABLE IF EXISTS zones CASCADE")) + conn.execute( + text( + "CREATE TABLE zones (" + "id SERIAL PRIMARY KEY, " + "city VARCHAR(100) NOT NULL UNIQUE, " + "created_at TIMESTAMPTZ NOT NULL DEFAULT now(), " + "geom geometry(Polygon, 4326))" + ) + ) + try: + with SessionLocal() as db: + seed_zones(db) + build_zone_index(db) + yield True + finally: + with engine.begin() as conn: + conn.execute(text("DROP TABLE IF EXISTS zones CASCADE")) + clear_zone_index() + + +def zone_name_of(zone_id: int) -> str: + with SessionLocal() as db: + row = db.execute(text("SELECT city FROM zones WHERE id = :zid"), {"zid": zone_id}).first() + return row[0] if row else "missing" + + +class TestResolveZoneReal: + @pytest.mark.parametrize("lat,lon,expected", KNOWN_POINTS) + def test_resolves_known_points(self, infra_available, lat, lon, expected): + with SessionLocal() as db: + assert zone_name_of(resolve_zone(db, lat, lon)) == expected + + def test_null_coordinates_use_unknown(self, infra_available): + with SessionLocal() as db: + assert zone_name_of(resolve_zone(db, None, None)) == "Unknown Region" + + +class TestGeohashVsPostGIS: + @pytest.mark.parametrize( + "lat,lon", + [(45.4642, 9.19), (40.4168, -3.7038), (35.6762, 139.6503), (0.0, 0.0), (-33.8688, 151.2093)], + ) + def test_encoder_matches_st_geohash(self, infra_available, lat, lon): + with engine.connect() as conn: + row = conn.execute( + text("SELECT ST_GeoHash(ST_SetSRID(ST_MakePoint(:lon, :lat), 4326), :prec)"), + {"lon": lon, "lat": lat, "prec": ZONE_INDEX_PRECISION}, + ).first() + assert row[0] == point_to_geohash(lat, lon, ZONE_INDEX_PRECISION) + + def test_cooldown_geohash_matches_st_geohash(self, infra_available): + with engine.connect() as conn: + row = conn.execute( + text("SELECT ST_GeoHash(ST_SetSRID(ST_MakePoint(:lon, :lat), 4326), :prec)"), + {"lon": -74.006, "lat": 40.7128, "prec": COOLDOWN_PRECISION}, + ).first() + assert row[0] == point_to_geohash(40.7128, -74.006, COOLDOWN_PRECISION) + + +class TestZoneIndexFastPath: + def test_candidates_resolve_to_expected_zone(self, infra_available): + with SessionLocal() as db: + milan_id = resolve_zone(db, 45.4642, 9.1900) + candidates = candidate_zone_ids(45.4642, 9.1900, precision=ZONE_INDEX_PRECISION) + assert milan_id in candidates + + def test_unknown_point_has_no_candidates(self, infra_available): + assert candidate_zone_ids(-1.2833, 36.8167, precision=ZONE_INDEX_PRECISION) == set() + + def test_index_header_env(self, infra_available): + """Sanity: precision env knobs are processable ints in the default range.""" + assert 1 <= ZONE_INDEX_PRECISION <= 12 + assert 1 <= COOLDOWN_PRECISION <= 12 \ No newline at end of file diff --git a/backend/tests/integration/test_timescale_integration.py b/backend/tests/integration/test_timescale_integration.py new file mode 100644 index 0000000..4a4e374 --- /dev/null +++ b/backend/tests/integration/test_timescale_integration.py @@ -0,0 +1,107 @@ +"""Real TimescaleDB migration tests (run when a live TimescaleDB is reachable). + +Skipped automatically when DATABASE_URL points at a plain Postgres/PostGIS +container (e.g. the geo integration job), because the migration is designed to +fail closed there. The dedicated CI job boots timescale/timescaledb so the +hypertable + continuous aggregate paths are exercised against the real engine. +""" + +import os + +import pytest +from sqlalchemy import text + +from src.database import engine, Base +from src.models import Reading, Sensor, Zone +from src.timescale import apply_timescale + +pytestmark = pytest.mark.integration + + +def _seed_zone_and_sensor(session): + from geoalchemy2.elements import WKTElement + + zone = session.query(Zone).filter(Zone.city == "Unknown Region").first() + if zone is None: + zone = Zone(city="Unknown Region") + session.add(zone) + session.flush() + sensor = Sensor( + active=True, + zone_id=zone.id, + latitude=45.46, + longitude=9.19, + location=WKTElement("POINT(9.19 45.46)", srid=4326), + public_key_hex="a" * 64, + mac_address="00:00:00:00:00:01", + ) + session.add(sensor) + session.commit() + + +@pytest.fixture(scope="module") +def tsdb_session(): + from src.database import SessionLocal + try: + with engine.connect() as c: + c.execute(text("SELECT 1")) + except Exception: + pytest.skip("Database not reachable; skipping TimescaleDB integration tests") + + with engine.begin() as c: + c.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) + # The shared integration conftest mocks Base.metadata.create_all (it is + # designed for mocked-DB API tests), so run the real DDL through a clean + # MetaData carrying the ORM tables. + from sqlalchemy import MetaData + + md = MetaData() + for table in Base.metadata.tables.values(): + table.to_metadata(md) + md.create_all(bind=engine) + with SessionLocal() as session: + if session.execute(text("SELECT count(*) FROM sensors")).scalar() == 0: + _seed_zone_and_sensor(session) + yield session + engine.dispose() + + +class TestHypertableMigration: + def test_migration_creates_hypertable(self, tsdb_session): + report = apply_timescale(tsdb_session) + assert report["hypertable"] is True + + def test_readings_is_listed_as_hypertable(self, tsdb_session): + row = tsdb_session.execute( + text( + "SELECT (EXISTS (SELECT 1 FROM timescaledb_information.hypertables " + "WHERE hypertable_name = 'readings'))" + ) + ).scalar() + assert row is True + + def test_continuous_aggregate_view_exists(self, tsdb_session): + assert tsdb_session.execute( + text("SELECT to_regclass('public.readings_minute') IS NOT NULL") + ).scalar() is True + + def test_orm_insert_into_hypertable(self, tsdb_session): + from src.database import SessionLocal + + with SessionLocal() as session: + sensor_id = session.execute(text("SELECT min(id) FROM sensors")).scalar() + if sensor_id is None: + pytest.skip("No sensors seeded; can't insert a reading") + reading = Reading(value=150, sensor_id=sensor_id, latitude=45.46, longitude=9.19) + session.add(reading) + session.commit() + persisted = session.execute( + text("SELECT count(*) FROM readings WHERE sensor_id = :sid"), + {"sid": sensor_id}, + ).scalar() + assert persisted >= 1 + + def test_migration_is_idempotent(self, tsdb_session): + first = apply_timescale(tsdb_session) + second = apply_timescale(tsdb_session) + assert first == second diff --git a/backend/tests/integration/test_zone_readings.py b/backend/tests/integration/test_zone_readings.py new file mode 100644 index 0000000..1cce76a --- /dev/null +++ b/backend/tests/integration/test_zone_readings.py @@ -0,0 +1,211 @@ +"""Integration tests for the per-zone readings endpoint (PostGIS zones). + +These verify the zone-scoped seismograph feed: readings are attributed to a +zone through their sensor's ``zone_id`` and sensors in other zones (or with no +zone) never leak into a zone's window. + +They skip cleanly when no database is reachable. +""" + +import pytest +from sqlalchemy import MetaData, text + +from src.database import Base, engine, SessionLocal +from src.main import delete_zone_readings, get_zone_alerts, get_zone_readings +from src.models import Alert, Reading, Sensor, Zone +from geoalchemy2.elements import WKTElement + + +def _ensure_schema() -> None: + with engine.begin() as c: + c.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) + # The shared integration conftest mocks Base.metadata.create_all (it is + # designed for mocked-DB API tests), so run the real DDL through a clean + # MetaData carrying the ORM tables. + md = MetaData() + for table in Base.metadata.tables.values(): + table.to_metadata(md) + md.create_all(bind=engine) + + +@pytest.fixture(scope="module") +def zone_db(): + try: + with engine.connect() as c: + c.execute(text("SELECT 1")) + except Exception: + pytest.skip("Database not reachable; skipping zone readings integration tests") + + _ensure_schema() + with SessionLocal() as session: + yield session + engine.dispose() + + +@pytest.fixture(scope="module") +def seeded_zones_and_sensors(zone_db): + # Idempotent setup: wipe any leftover test data so repeated runs on a + # shared/live DB never accumulate readings across executions. + from src.models import EmergencyReport + + zone_db.query(EmergencyReport).delete() + zone_db.query(Alert).delete() + zone_db.query(Reading).delete() + zone_db.query(Sensor).delete() + zone_db.query(Zone).filter(Zone.city.in_(["Zone A - Test", "Zone B - Test"])).delete() + zone_db.commit() + + zone_a = Zone(city="Zone A - Test") + zone_b = Zone(city="Zone B - Test") + zone_db.add_all([zone_a, zone_b]) + zone_db.flush() + + sensor_in_a = Sensor( + active=True, + zone_id=zone_a.id, + latitude=45.0, + longitude=9.0, + location=WKTElement("POINT(9 45)", srid=4326), + public_key_hex="a" * 64, + mac_address="00:00:00:00:00:AA", + ) + sensor_in_b = Sensor( + active=True, + zone_id=zone_b.id, + latitude=45.1, + longitude=9.1, + location=WKTElement("POINT(9.1 45.1)", srid=4326), + public_key_hex="b" * 64, + mac_address="00:00:00:00:00:BB", + ) + sensor_unassigned = Sensor( + active=True, + zone_id=zone_b.id, + latitude=46.0, + longitude=8.0, + location=WKTElement("POINT(8 46)", srid=4326), + public_key_hex="c" * 64, + mac_address="00:00:00:00:00:CC", + ) + zone_db.add_all([sensor_in_a, sensor_in_b, sensor_unassigned]) + zone_db.flush() + + from datetime import datetime, timedelta, timezone + + base = datetime.now(timezone.utc) + for i, value in enumerate([100, 200, 300]): + zone_db.add( + Reading( + value=value, + sensor_id=sensor_in_a.id, + recorded_at=base - timedelta(seconds=i), + ) + ) + zone_db.add( + Reading( + value=9999, + sensor_id=sensor_in_b.id, + recorded_at=base - timedelta(seconds=1), + ) + ) + zone_db.add( + Reading( + value=7777, + sensor_id=sensor_unassigned.id, + recorded_at=base - timedelta(seconds=2), + ) + ) + zone_db.commit() + + return {"a": zone_a.id, "b": zone_b.id} + + +class TestGetZoneReadings: + def test_returns_only_zone_sensors(self, zone_db, seeded_zones_and_sensors): + readings = get_zone_readings( + zone_id=seeded_zones_and_sensors["a"], limit=10, db=zone_db + ) + assert [r.value for r in readings] == [100, 200, 300] + assert all(r.sensor_id is not None for r in readings) + + def test_other_zone_data_is_excluded(self, zone_db, seeded_zones_and_sensors): + readings = get_zone_readings( + zone_id=seeded_zones_and_sensors["a"], limit=10, db=zone_db + ) + assert 9999 not in [r.value for r in readings] + assert 7777 not in [r.value for r in readings] + + def test_empty_zone_returns_empty_list(self, zone_db, seeded_zones_and_sensors): + empty = Zone(city="Zone Empty - Test") + zone_db.add(empty) + zone_db.commit() + readings = get_zone_readings(zone_id=empty.id, limit=10, db=zone_db) + assert readings == [] + zone_db.delete(empty) + zone_db.commit() + + def test_missing_zone_raises_404(self, zone_db): + from fastapi import HTTPException + + with pytest.raises(HTTPException) as excinfo: + get_zone_readings(zone_id=999999, limit=10, db=zone_db) + assert excinfo.value.status_code == 404 + + +class TestDeleteZoneReadings: + def test_deletes_only_zone_readings(self, zone_db, seeded_zones_and_sensors): + result = delete_zone_readings(zone_id=seeded_zones_and_sensors["a"], db=zone_db) + assert result["deleted"] == 3 + + remaining = get_zone_readings(zone_id=seeded_zones_and_sensors["a"], limit=10, db=zone_db) + assert remaining == [] + + def test_other_zone_data_survives(self, zone_db, seeded_zones_and_sensors): + delete_zone_readings(zone_id=seeded_zones_and_sensors["a"], db=zone_db) + from src.models import Reading + + other = zone_db.query(Reading).filter(Reading.value.in_([9999, 7777])).count() + assert other == 2 + + def test_missing_zone_raises_404(self, zone_db): + from fastapi import HTTPException + + with pytest.raises(HTTPException) as excinfo: + delete_zone_readings(zone_id=999999, db=zone_db) + assert excinfo.value.status_code == 404 + + +class TestGetZoneAlerts: + def test_returns_zone_alerts_desc(self, zone_db, seeded_zones_and_sensors): + from datetime import datetime, timedelta, timezone + + base = datetime.now(timezone.utc) + zone_db.add_all( + [ + Alert(zone_id=seeded_zones_and_sensors["a"], magnitude=4.0, created_at=base - timedelta(seconds=2)), + Alert(zone_id=seeded_zones_and_sensors["a"], magnitude=5.2, created_at=base), + ] + ) + zone_db.commit() + + alerts = get_zone_alerts(zone_id=seeded_zones_and_sensors["a"], limit=10, db=zone_db) + assert [a.magnitude for a in alerts] == [5.2, 4.0] + + def test_other_zone_alerts_excluded(self, zone_db, seeded_zones_and_sensors): + from datetime import datetime, timedelta, timezone + + base = datetime.now(timezone.utc) + zone_db.add( + Alert(zone_id=seeded_zones_and_sensors["b"], magnitude=6.0, created_at=base) + ) + zone_db.commit() + + alerts = get_zone_alerts(zone_id=seeded_zones_and_sensors["a"], limit=10, db=zone_db) + assert all(a.zone_id == seeded_zones_and_sensors["a"] for a in alerts) + + def test_missing_zone_raises_404(self, zone_db): + from fastapi import HTTPException + + with pytest.raises(HTTPException) as excinfo: + get_zone_alerts(zone_id=999999, limit=10, db=zone_db) + assert excinfo.value.status_code == 404 diff --git a/backend/tests/unit/test_geo.py b/backend/tests/unit/test_geo.py new file mode 100644 index 0000000..fe64d4e --- /dev/null +++ b/backend/tests/unit/test_geo.py @@ -0,0 +1,113 @@ +import math +import pytest +from unittest.mock import MagicMock + +from src.geo import ( + _cells_in_bbox, + _cell_indices_to_geohash, + candidate_zone_ids, + geohash_bounds, + point_to_geohash, + zone_covering_geohashes, +) + + +class TestPointToGeohash: + def test_wikipedia_reference(self): + # Aalborg University, the canonical geohash example from Wikipedia. + assert point_to_geohash(57.64911, 10.40744, 11) == "u4pruydqqvj" + assert point_to_geohash(57.64911, 10.40744, 3) == "u4p" + + def test_known_low_precision(self): + # London (BBC) resolves to the "gcp" cells at precision 3. + assert point_to_geohash(51.5218, -0.1817, 3) == "gcp" + + def test_equator_origin(self): + assert point_to_geohash(0.0, 0.0, 3) == "s00" + + def test_consistent_precision_prefix(self): + full = point_to_geohash(45.4642, 9.1900, 6) + assert point_to_geohash(45.4642, 9.1900, 3) == full[:3] + + +class TestGeohashBounds: + @pytest.mark.parametrize( + "lat,lon,prec", + [(45.4642, 9.19, 4), (-33.8688, 151.2093, 5), (0.0, 0.0, 3), (40.7128, -74.006, 6)], + ) + def test_point_inside_own_cell(self, lat, lon, prec): + cell = point_to_geohash(lat, lon, prec) + lon_min, lat_min, lon_max, lat_max = geohash_bounds(cell) + assert lon_min <= lon <= lon_max + assert lat_min <= lat <= lat_max + + def test_cell_indices_reconstruct(self): + for lat, lon in [(45.4642, 9.19), (35.6762, 139.6503), (-1.2833, 36.8167)]: + for precision in (3, 4, 5): + cell = point_to_geohash(lat, lon, precision) + lon_bits = (precision * 5 + 1) // 2 + lat_bits = precision * 5 // 2 + lon_step = 360.0 / (1 << lon_bits) + lat_step = 180.0 / (1 << lat_bits) + lon_min, lat_min, _, _ = geohash_bounds(cell) + lon_idx = math.floor((lon_min + 180.0) / lon_step) + lat_idx = math.floor((lat_min + 90.0) / lat_step) + assert _cell_indices_to_geohash(lon_idx, lat_idx, precision) == cell + + +class TestCellsInBbox: + def test_small_bbox(self): + # ~0.75 deg around Paris -> a handful of prec-3 cells. + cells = _cells_in_bbox(48.5, 1.9, 49.3, 2.7, 3) + assert cells + for cell in cells: + assert len(cell) == 3 + + def test_known_cell_included(self): + cells = _cells_in_bbox(48.0, 1.0, 50.0, 3.0, 3) + assert point_to_geohash(48.8566, 2.3522, 3) in cells + + +class TestCandidateZoneIds: + def test_resolves_from_redis_set(self): + mock_redis = MagicMock() + mock_redis.smembers.return_value = {"7", "9"} + assert candidate_zone_ids(45.46, 9.19, redis_client=mock_redis) == {7, 9} + mock_redis.smembers.assert_called_once() + + def test_redis_failure_is_non_fatal(self): + mock_redis = MagicMock() + mock_redis.smembers.side_effect = Exception("Redis down") + assert candidate_zone_ids(45.46, 9.19, redis_client=mock_redis) == set() + + def test_uses_configured_precision(self): + mock_redis = MagicMock() + mock_redis.smembers.return_value = set() + candidate_zone_ids(45.46, 9.19, redis_client=mock_redis, precision=4) + cell = point_to_geohash(45.46, 9.19, 4) + mock_redis.smembers.assert_called_once_with(f"zoneindex:{cell}") + + +class TestZoneCoveringGeohashes: + def test_queries_db_and_returns_cell_keys(self): + zone = MagicMock() + zone.id = 1 + db = MagicMock() + + bbox_result = MagicMock() + bbox_result.first.return_value = (1.0, 48.0, 3.0, 49.0) + select_result = MagicMock() + select_result.fetchall.return_value = [("spd",), ("spe",)] + # _zone_bbox -> CREATE TEMP TABLE -> DELETE -> INSERT -> SELECT + db.execute.side_effect = [bbox_result, MagicMock(), MagicMock(), MagicMock(), select_result] + + cells = zone_covering_geohashes(db, zone, precision=3) + assert cells == ["spd", "spe"] + assert db.execute.call_count == 5 + + def test_empty_bbox_returns_empty(self): + zone = MagicMock() + zone.id = 99 + db = MagicMock() + db.execute.return_value.first.return_value = None + assert zone_covering_geohashes(db, zone) == [] \ No newline at end of file diff --git a/backend/tests/unit/test_ingest.py b/backend/tests/unit/test_ingest.py new file mode 100644 index 0000000..1f9bf20 --- /dev/null +++ b/backend/tests/unit/test_ingest.py @@ -0,0 +1,90 @@ +from unittest.mock import MagicMock + +from src import ingest + + +def _client(): + return MagicMock() + + +class TestEnqueueReading: + def test_xadd_with_maxlen_and_payload_field(self): + client = _client() + client.xadd.return_value = "42-0" + mid = ingest.enqueue_reading(client, '{"value": 150}') + assert mid == "42-0" + args, kwargs = client.xadd.call_args + assert args[0] == ingest.READINGS_STREAM + assert args[1] == {ingest.PAYLOAD_FIELD: '{"value": 150}'} + assert kwargs["maxlen"] == ingest.MAXLEN + assert kwargs["approximate"] is True + + +class TestReadBatch: + def test_returns_empty_on_timeout(self): + client = _client() + client.xreadgroup.return_value = None + assert ingest.read_batch(client, "worker-a") == [] + + def test_parses_entries_and_forwards_group(self): + client = _client() + client.xreadgroup.return_value = [ + ["readings:stream", [["1-0", {"payload": "{}"}], ["2-0", {"payload": "{}"}]]] + ] + batch = ingest.read_batch(client, "worker-a", count=2, block_ms=100) + assert batch == [("1-0", "{}"), ("2-0", "{}")] + args, kwargs = client.xreadgroup.call_args + assert args[0] == ingest.READINGS_GROUP + assert args[1] == "worker-a" + assert args[2] == {ingest.READINGS_STREAM: ">"} + assert kwargs["count"] == 2 + assert kwargs["block"] == 100 + + +class TestAck: + def test_ack_skips_empty_list(self): + client = _client() + ingest.ack(client, []) + client.xack.assert_not_called() + + def test_ack_forwards_ids(self): + client = _client() + ingest.ack(client, ["1-0", "2-0"]) + client.xack.assert_called_once_with( + ingest.READINGS_STREAM, ingest.READINGS_GROUP, "1-0", "2-0" + ) + + +class TestMoveToDlq: + def test_parks_message_and_acks_original(self): + client = _client() + ingest.move_to_dlq(client, "5-0", '{"x": 1}', reason="malformed_json") + args, _ = client.xadd.call_args + assert args[0] == ingest.READINGS_DLQ + assert args[1]["reason"] == "malformed_json" + assert args[1]["original_id"] == "5-0" + client.xack.assert_called_once_with( + ingest.READINGS_STREAM, ingest.READINGS_GROUP, "5-0" + ) + + +class TestEnsureGroup: + def test_ignores_busygroup(self): + client = _client() + client.xgroup_create.side_effect = Exception("BUSYGROUP Consumer Group name already exists") + ingest.ensure_group(client) # must not raise + client.xgroup_create.assert_called_once() + + def test_creates_with_mkstream(self): + client = _client() + ingest.ensure_group(client) + client.xgroup_create.assert_called_once_with( + ingest.READINGS_STREAM, ingest.READINGS_GROUP, id="0", mkstream=True + ) + + +class TestRecoverPending: + def test_counts_reclaimed_entries(self): + client = _client() + client.xautoclaim.return_value = ("9-9", [["1-0", {}], ["2-0", {}]], []) + assert ingest.recover_pending(client, "worker-a") == 2 diff --git a/backend/tests/unit/test_timescale.py b/backend/tests/unit/test_timescale.py new file mode 100644 index 0000000..e6e9990 --- /dev/null +++ b/backend/tests/unit/test_timescale.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock + +from src import timescale + + +class FakeResult: + def __init__(self, scalar): + self._scalar = scalar + + def scalar(self): + return self._scalar + + +def _db_with_execute(fn): + db = MagicMock() + db.execute.side_effect = fn + return db + + +def _ok_execute(statement, *args, **kwargs): + """Success for everything except Timescale policy helpers (already-exists).""" + s = str(statement) + if "add_continuous_aggregate_policy" in s or "add_compression_policy" in s \ + or "add_retention_policy" in s: + raise Exception("policy already exists") + if "timescaledb_information.hypertables" in s: + return FakeResult(False) + return MagicMock() + + +class TestApplyTimescale: + def test_noop_when_extension_unavailable(self): + db = _db_with_execute(lambda *a, **k: (_ for _ in ()).throw(Exception("extension not found"))) + report = timescale.apply_timescale(db) + assert report["timescaledb"] is False + assert report["hypertable"] is False + assert report["aggregate"] is False + + def test_creates_hypertable_and_aggregate_when_missing(self): + db = _db_with_execute(_ok_execute) + report = timescale.apply_timescale(db) + assert report["timescaledb"] is True + assert report["hypertable"] is True + assert report["aggregate"] is True + calls = [str(c.args[0]) for c in db.execute.call_args_list] + assert any("create_hypertable" in c for c in calls) + assert any("readings_minute" in c for c in calls) + assert any("create extension if not exists timescaledb" in c.lower() for c in calls) + + def test_skips_hypertable_when_already_present(self): + def fake_execute(statement, *args, **kwargs): + s = str(statement) + if "timescaledb_information.hypertables" in s: + return FakeResult(True) + return MagicMock() + + db = _db_with_execute(fake_execute) + report = timescale.apply_timescale(db) + assert report["hypertable"] is True + calls = [str(c.args[0]) for c in db.execute.call_args_list] + assert not any("create_hypertable" in c for c in calls) + + def test_continues_after_hypertable_failure(self): + def fake_execute(statement, *args, **kwargs): + s = str(statement) + if "create_hypertable" in s: + raise Exception("TimescaleDB extension not loaded (shared_preload_libraries)") + return _ok_execute(statement, *args, **kwargs) + + db = _db_with_execute(fake_execute) + report = timescale.apply_timescale(db) + assert report["timescaledb"] is True + assert report["hypertable"] is False + assert report["aggregate"] is True + + def test_tolerates_already_existing_policies(self): + db = _db_with_execute(_ok_execute) + report = timescale.apply_timescale(db) + assert report["aggregate"] is True + assert report["retention"] is True + db.rollback.assert_called() diff --git a/backend/tests/unit/test_worker.py b/backend/tests/unit/test_worker.py index 85528ca..b92edab 100644 --- a/backend/tests/unit/test_worker.py +++ b/backend/tests/unit/test_worker.py @@ -72,6 +72,34 @@ def test_process_suppresses_duplicate_alert(self, mock_db_session, mock_redis): assert not mock_redis.publish.called assert not mock_redis.lpush.called + def test_process_fragments_cooldown_by_geohash(self, mock_db_session, mock_redis): + """GNSS-ready: a sensor fix fragments the cooldown key per-area.""" + _mock_zone(mock_db_session) + event = { + "value": 5500, + "sensor_id": 1, + "zone_id": 2, + "latitude": 45.4642, + "longitude": 9.19, + "sensor_geohash": "u0nd", + } + process_event(event, mock_db_session) + called_key = mock_redis.set.call_args[0][0] + assert called_key == "alert_cooldown:u0nd" + assert mock_redis.set.call_args[1]["nx"] is True + + def test_process_falls_back_to_zone_cooldown_without_fix(self, mock_db_session, mock_redis): + """Legacy path: coordinates-less sensors keep the per-zone key.""" + _mock_zone(mock_db_session) + event = { + "value": 5500, + "sensor_id": 1, + "zone_id": 7, + } + process_event(event, mock_db_session) + called_key = mock_redis.set.call_args[0][0] + assert called_key == "alert_cooldown:zone:7" + def test_process_propagates_db_error(self, mock_db_session, mock_redis): mock_db_session.commit.side_effect = Exception("DB Error") event = { @@ -116,37 +144,39 @@ def fake_cooldown_set(*args, **kwargs): class TestWorkerLoopResilience: - def test_loop_moves_db_error_to_dlq_and_continues(self, mock_redis): - """Ingestion resilience: an event that raises is pushed to the DLQ, loop keeps running.""" + def test_loop_moves_batch_error_to_dlq_and_continues(self, mock_redis): + """Ingestion resilience: a failed batch and a malformed entry go to the DLQ + stream (and are ACKed), the loop keeps running and later entries still drain.""" + mock_redis.xautoclaim.return_value = ("0-0", [], []) db = MagicMock() batch = [ - (b"seismic_events", json.dumps({"value": 150, "sensor_id": 1})), - (b"seismic_events", "{ not-valid-json"), # malformed -> consumer must survive - (b"seismic_events", json.dumps({"value": 160, "sensor_id": 1})), - None, # brpop timeout -> loop iterates again + [("readings:stream", [ + [b"1-0", {"payload": '{"value": 150, "sensor_id": 1}'}], + [b"2-0", {"payload": "{ not-valid-json"}], + [b"3-0", {"payload": '{"value": 160, "sensor_id": 1}'}], + ])], + [], # xreadgroup timeout -> loop iterates again KeyboardInterrupt, ] - mock_redis.brpop.side_effect = batch + mock_redis.xreadgroup.side_effect = batch processed = [] - def fake_process_event(event, session): - processed.append(event) - if event.get("sensor_id") == 1 and event.get("value") == 150: + def fake_process_batch(events, session): + processed.extend(events) + if any(e.get("sensor_id") == 1 and e.get("value") == 150 for e in events): raise RuntimeError("simulated DB failure") with patch("src.worker.SessionLocal", return_value=db): - with patch("src.worker.process_event", side_effect=fake_process_event): + with patch("src.worker.process_batch", side_effect=fake_process_batch): with patch("src.worker.time.sleep"): with pytest.raises(KeyboardInterrupt): run_worker() assert db.rollback.called - assert mock_redis.lpush.called - dlq_queue, dlq_payload = mock_redis.lpush.call_args[0] - assert dlq_queue == "seismic_events_dlq" - assert json.loads(dlq_payload)["value"] == 150 - assert mock_redis.brpop.call_count >= 4 - # Second + third valid/clean items still reached process_event + dlq_reasons = [c.args[1].get("reason") for c in mock_redis.xadd.call_args_list] + assert "malformed_json" in dlq_reasons + assert any(r.startswith("process_error") for r in dlq_reasons) + assert mock_redis.xack.called assert processed[-1]["value"] == 160 diff --git a/firmware/README.md b/firmware/README.md index 8342ea3..85d3c6e 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -49,6 +49,14 @@ Before compiling, ensure the network and server credentials in `src/main.cpp` ar #endif ``` +### Enabling the Optional GNSS Module (Experimental) +The firmware ships with a **GNSS heartbeats module**, disabled by default. When enabled, the device reads a connected UART GNSS receiver, computes a geohash from the current fix, and includes it in every heartbeat sent to the server (used by the geo-spatial zone-alerting feature on the backend). + +* **Makefile/environment flag:** set `GNSS_ENABLED=1` in the `esp32_config.env` file (see `esp32_config.env.example`) and rebuild. Without the flag (or without the file), the module compiles out entirely — zero RAM/flash overhead. +* **Hardware wiring (UART):** connect the GNSS receiver TX to a free UART-capable pin and set `GNSS_UART_TX_PIN` / `GNSS_UART_RX_PIN` in `src/gnss/GnssModule.h`. +* **Behavior while unlocked:** the module stays **UART-silent** (no `$GxRMC`/`$GxGGA` requests) so legacy GeoGuessr-style debugging on the same UART line is preserved. +* **Fallback:** if the receiver never produces a fix, the heartbeat is sent without location and the server falls back to the device's registered location. + ## 5. Installation & Provisioning ### Step 1: Upload Firmware diff --git a/firmware/esp32_config.env.example b/firmware/esp32_config.env.example index ffaf889..339ec77 100644 --- a/firmware/esp32_config.env.example +++ b/firmware/esp32_config.env.example @@ -43,4 +43,17 @@ MQTT_PASSWORD="your_mqtt_password" # and stored securely in the device's NVS (Non-Volatile Storage) partition. # # Check the Serial Monitor output on boot to retrieve the Public Key. -ENROLLMENT_TOKEN=your_enrollment_token \ No newline at end of file +ENROLLMENT_TOKEN=your_enrollment_token + +# ============================================================================== +# OPTIONAL GNSS (v1.3 readiness) +# ============================================================================== +# Uncomment to compile the GNSS sub-system (GnssModule.h/.cpp): real coordinates +# are reported at registration and the last-known fix is persisted in NVS as a +# fallback for cold boots before the first satellite fix. Leave commented to +# keep the default hermetic build (hardcoded fallback coordinates). +# +# GNSS_ENABLED=1 +# GPS_SERIAL_RX_PIN=0 +# GPS_SERIAL_TX_PIN=1 +# GPS_SERIAL_BAUD=9600 \ No newline at end of file diff --git a/firmware/platformio.ini b/firmware/platformio.ini index bc3d92c..422dced 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -34,4 +34,12 @@ lib_deps = adafruit/Adafruit Unified Sensor@^1.1.14 bblanchon/ArduinoJson@^7.0.3 tzapu/WiFiManager@^2.0.17 - knolleary/PubSubClient @ ^2.8 \ No newline at end of file + knolleary/PubSubClient @ ^2.8 + mikalhart/TinyGPSPlus@^1.1.0 + +; Optional GNSS (v1.3 readiness): add `GNSS_ENABLED=1` to esp32_config.env to +; compile the GnssModule (coordinates + NVS last-known fix). The default build +; does not use it, so the firmware stays hermetic without a GNSS module. You +; can also override the UART pins, e.g.: +; GPS_SERIAL_RX_PIN=0 +; GPS_SERIAL_TX_PIN=1 \ No newline at end of file diff --git a/firmware/src/GnssModule.cpp b/firmware/src/GnssModule.cpp new file mode 100644 index 0000000..a47cefc --- /dev/null +++ b/firmware/src/GnssModule.cpp @@ -0,0 +1,93 @@ +#include "GnssModule.h" + +#ifdef GNSS_ENABLED + +namespace { + +float scaleLat(double lat) { + return static_cast(lat); +} + +float scaleLon(double lon) { + return static_cast(lon); +} + +} // namespace + +GnssModule& gnss() { + static GnssModule instance; + return instance; +} + +void GnssModule::begin() { + loadLastKnownFix(); + serial_.begin(GPS_SERIAL_BAUD, SERIAL_8N1, GPS_SERIAL_RX_PIN, GPS_SERIAL_TX_PIN); + Serial.printf("[GNSS] Module started (UART RX=%d TX=%d, baud=%d)\n", + GPS_SERIAL_RX_PIN, GPS_SERIAL_TX_PIN, GPS_SERIAL_BAUD); + if (last_known_valid_) { + Serial.printf("[GNSS] Last-known fix loaded: %.5f, %.5f\n", last_known_lat_, last_known_lon_); + } else { + Serial.println("[GNSS] No last-known fix in NVS."); + } +} + +void GnssModule::loop() { + while (serial_.available()) { + gps_.encode(serial_.read()); + } + + // Persist a newly observed valid fix (throttled for NVS wear levelling). + if (gps_.location.isValid() && gps_.location.age() < GNSS_FIX_MAX_AGE_MS) { + unsigned long now = millis(); + if (now - last_save_ms_ >= GNSS_SAVE_INTERVAL_MS) { + last_known_valid_ = true; + last_known_lat_ = scaleLat(gps_.location.lat()); + last_known_lon_ = scaleLon(gps_.location.lng()); + last_save_ms_ = now; + saveLastKnownFix(); + Serial.printf("[GNSS] FIX locked: %.5f, %.5f\n", last_known_lat_, last_known_lon_); + } + } +} + +bool GnssModule::getFix(GnssFix& out) const { + out = GnssFix{}; + if (gps_.location.isValid() && gps_.location.age() < GNSS_FIX_MAX_AGE_MS) { + out.latitude = scaleLat(gps_.location.lat()); + out.longitude = scaleLon(gps_.location.lng()); + out.age_ms = gps_.location.age(); + out.from_storage = false; + out.valid = true; + return true; + } + if (last_known_valid_) { + out.latitude = last_known_lat_; + out.longitude = last_known_lon_; + out.age_ms = 0; + out.from_storage = true; + out.valid = true; + return true; + } + return false; +} + +void GnssModule::saveLastKnownFix() { + prefs_.begin(GNSS_NVS_NAMESPACE, false); + prefs_.putFloat(GNSS_NVS_LAT, last_known_lat_); + prefs_.putFloat(GNSS_NVS_LON, last_known_lon_); + prefs_.putBool(GNSS_NVS_VALID, true); + prefs_.end(); +} + +void GnssModule::loadLastKnownFix() { + prefs_.begin(GNSS_NVS_NAMESPACE, true); + bool valid = prefs_.getBool(GNSS_NVS_VALID, false); + if (valid) { + last_known_valid_ = true; + last_known_lat_ = prefs_.getFloat(GNSS_NVS_LAT, 0.0f); + last_known_lon_ = prefs_.getFloat(GNSS_NVS_LON, 0.0f); + } + prefs_.end(); +} + +#endif // GNSS_ENABLED \ No newline at end of file diff --git a/firmware/src/GnssModule.h b/firmware/src/GnssModule.h new file mode 100644 index 0000000..c7c4fbf --- /dev/null +++ b/firmware/src/GnssModule.h @@ -0,0 +1,82 @@ +/** + * Project: QuakeGuard - Professional Seismic Node + * Optional GNSS sub-system (v1.3 readiness). + * + * Compiled ONLY when `GNSS_ENABLED=1` is present in esp32_config.env. The + * default build remains hermetic (no TinyGPSPlus dependency is pulled). + * + * Responsibilities at this stage (v1.2.x / GNSS-ready): + * - Parse NMEA from a u-blox / NEO-6M / NEO-M8N module on UART. + * - Persist the last reliable fix in NVS so provisioning can report real + * coordinates even before the first fix after a cold boot. + * - Expose the most recent fix (live GNSS, or last-known-from-NVS). + * + * NTP + PPS timestamp discipline is v1.3 scope (see ROADMAP): this module + * only guarantees the coordinates pipeline is ready for the GNSS upgrade. + */ + +#ifndef QUAKEGUARD_GNSS_MODULE_H +#define QUAKEGUARD_GNSS_MODULE_H + +#include + +#ifdef GNSS_ENABLED + +#include +#include +#include + +#ifndef GPS_SERIAL_RX_PIN + #define GPS_SERIAL_RX_PIN 0 +#endif +#ifndef GPS_SERIAL_TX_PIN + #define GPS_SERIAL_TX_PIN 1 +#endif +#ifndef GPS_SERIAL_BAUD + #define GPS_SERIAL_BAUD 9600 +#endif + +namespace { +constexpr const char* GNSS_NVS_NAMESPACE = "quake-gnss"; +constexpr const char* GNSS_NVS_LAT = "lat"; +constexpr const char* GNSS_NVS_LON = "lon"; +constexpr const char* GNSS_NVS_VALID = "valid"; + +// Save the last-known fix at most once per this interval (NVS wear levelling). +constexpr unsigned long GNSS_SAVE_INTERVAL_MS = 60000UL; +// A live fix older than this is treated as stale (fall back to last-known). +constexpr unsigned long GNSS_FIX_MAX_AGE_MS = 10000UL; +} + +struct GnssFix { + float latitude; + float longitude; + bool valid; + unsigned long age_ms; // elapsed since this fix was obtained + bool from_storage; // true when served from NVS last-known +}; + +class GnssModule { +public: + void begin(); + void loop(); + bool getFix(GnssFix& out) const; + +private: + void saveLastKnownFix(); + void loadLastKnownFix(); + + HardwareSerial serial_{1}; + TinyGPSPlus gps_; + Preferences prefs_; + + bool last_known_valid_ = false; + float last_known_lat_ = 0.0f; + float last_known_lon_ = 0.0f; + unsigned long last_save_ms_ = 0; +}; + +GnssModule& gnss(); + +#endif // GNSS_ENABLED +#endif // QUAKEGUARD_GNSS_MODULE_H \ No newline at end of file diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index ea5ac87..57da972 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -30,6 +30,7 @@ #include #include #include "DetectionCore.h" +#include "GnssModule.h" // -------------------------------------------------------------------------- // HARDWARE & SERVER CONFIGURATION @@ -211,9 +212,26 @@ bool performProvisioning() { doc["public_key_hex"] = crypto().getPublicKeyHex(); doc["mac_address"] = WiFi.macAddress(); doc["enrollment_token"] = ENROLLMENT_TOKEN; - + +#ifdef GNSS_ENABLED + // GNSS-ready: report the real fix when available, else the last-known fix + // persisted in NVS. If neither exists, omit coordinates -> the backend + // keeps its last-known location / assigns "Unknown Region". + GnssFix fix; + if (gnss().getFix(fix)) { + doc["latitude"] = fix.latitude; + doc["longitude"] = fix.longitude; + Serial.printf("[PROV] Sending fix: %.5f, %.5f (from %s)\n", + fix.latitude, fix.longitude, + fix.from_storage ? "NVS" : "GNSS"); + } else { + Serial.println("[PROV] No GNSS fix available: omitting coordinates"); + } +#else + // Hardcoded fallback until a GNSS module is attached (see ROADMAP v1.3). doc["latitude"] = 41.9028; doc["longitude"] = 12.4964; +#endif String requestBody; serializeJson(doc, requestBody); @@ -340,6 +358,19 @@ void networkTask(void *pvParameters) { // NOSONAR } } +// -------------------------------------------------------------------------- +// TASK 3 (OPTIONAL): GNSS ACQUISITION +// -------------------------------------------------------------------------- +#ifdef GNSS_ENABLED +void gnssTask(void *pvParameters) { // NOSONAR + gnss().begin(); + for(;;) { + gnss().loop(); + vTaskDelay(pdMS_TO_TICKS(50)); + } +} +#endif + // -------------------------------------------------------------------------- // MAIN ENTRY POINTS // -------------------------------------------------------------------------- @@ -391,6 +422,9 @@ void setup() { eventQueue = xQueueCreate(20, sizeof(SeismicEvent)); xTaskCreate(sensorTask, "SensorTask", 8192, NULL, 5, NULL); xTaskCreate(networkTask, "NetworkTask", 8192, NULL, 1, NULL); +#ifdef GNSS_ENABLED + xTaskCreate(gnssTask, "GnssTask", 8192, NULL, 2, NULL); +#endif Serial.println("[SYS] System Running."); } diff --git a/mobile/__tests__/quakeStore.test.ts b/mobile/__tests__/quakeStore.test.ts index c7eb091..fb5dac5 100644 --- a/mobile/__tests__/quakeStore.test.ts +++ b/mobile/__tests__/quakeStore.test.ts @@ -35,7 +35,7 @@ describe("useQuakeStore", () => { }); it("fetchSensors handles network error gracefully", async () => { - global.fetch = jest.fn().mockRejectedValue(new Error("Network error")); + globalThis.fetch = jest.fn().mockRejectedValue(new Error("Network error")); await useQuakeStore.getState().fetchSensors(); expect(useQuakeStore.getState().sensors).toEqual([]); }); @@ -44,7 +44,7 @@ describe("useQuakeStore", () => { const mockSensors = [ { id: 1, lat: 41.9, lon: 12.5, status: "Active" }, ]; - global.fetch = jest.fn().mockResolvedValue({ + globalThis.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(mockSensors), }); @@ -53,7 +53,7 @@ describe("useQuakeStore", () => { }); it("startMonitoring sets up polling interval", () => { - const spy = jest.spyOn(global, "setInterval"); + const spy = jest.spyOn(globalThis, "setInterval"); useQuakeStore.getState().startMonitoring(); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith(expect.any(Function), 2000); @@ -67,7 +67,7 @@ describe("useQuakeStore", () => { }); it("does not start multiple intervals", () => { - const spy = jest.spyOn(global, "setInterval"); + const spy = jest.spyOn(globalThis, "setInterval"); useQuakeStore.getState().startMonitoring(); useQuakeStore.getState().startMonitoring(); expect(spy).toHaveBeenCalledTimes(1); diff --git a/mobile/__tests__/usePreferencesStore.test.ts b/mobile/__tests__/usePreferencesStore.test.ts index 68c84f3..8cac4fa 100644 --- a/mobile/__tests__/usePreferencesStore.test.ts +++ b/mobile/__tests__/usePreferencesStore.test.ts @@ -29,4 +29,16 @@ describe("usePreferencesStore", () => { usePreferencesStore.getState().toggleNotifications(); expect(usePreferencesStore.getState().notificationsEnabled).toBe(true); }); + + it("starts with no home zone (ring for every alert)", () => { + expect(usePreferencesStore.getState().homeZoneId).toBeNull(); + }); + + it("sets and clears the home zone", () => { + usePreferencesStore.getState().setHomeZoneId(3); + expect(usePreferencesStore.getState().homeZoneId).toBe(3); + + usePreferencesStore.getState().setHomeZoneId(null); + expect(usePreferencesStore.getState().homeZoneId).toBeNull(); + }); }); diff --git a/mobile/__tests__/useThemeStore.test.ts b/mobile/__tests__/useThemeStore.test.ts new file mode 100644 index 0000000..bf47fd0 --- /dev/null +++ b/mobile/__tests__/useThemeStore.test.ts @@ -0,0 +1,30 @@ +import { useThemeStore } from "../store/useThemeStore"; +import { THEMES } from "../theme"; + +beforeEach(() => { + useThemeStore.setState({ themeMode: "dark" }); +}); + +describe("useThemeStore", () => { + it("starts in MIC (dark) mode", () => { + expect(useThemeStore.getState().themeMode).toBe("dark"); + }); + + it("toggles between dark and light", () => { + useThemeStore.getState().toggleTheme(); + expect(useThemeStore.getState().themeMode).toBe("light"); + + useThemeStore.getState().toggleTheme(); + expect(useThemeStore.getState().themeMode).toBe("dark"); + }); + + it("sets mode explicitly", () => { + useThemeStore.getState().setThemeMode("light"); + expect(useThemeStore.getState().themeMode).toBe("light"); + }); + + it("both palettes exist and differ", () => { + expect(THEMES.dark.bg).not.toBe(THEMES.light.bg); + expect(THEMES.dark.text).not.toBe(THEMES.light.text); + }); +}); \ No newline at end of file diff --git a/mobile/api/hooks/useDashboard.ts b/mobile/api/hooks/useDashboard.ts index 7ad2179..cba78bd 100644 --- a/mobile/api/hooks/useDashboard.ts +++ b/mobile/api/hooks/useDashboard.ts @@ -11,11 +11,50 @@ export const useSensors = () => { const { data } = await apiClient.get('/sensors/'); return data; }, - refetchInterval: 10000, + refetchInterval: 10000, enabled: !isOfflineMode, }); }; +/** PostGIS zones for the per-zone dashboard selector. */ +export const useZones = () => { + const isOfflineMode = usePreferencesStore((state) => state.isOfflineMode); + + return useQuery({ + queryKey: ['zones'], + queryFn: async () => { + const { data } = await apiClient.get('/zones/'); + return data; + }, + refetchInterval: 15000, + enabled: !isOfflineMode, + }); +}; + +/** + * Live per-zone seismograph feed: the latest N readings emitted by sensors + * belonging to one PostGIS zone. Polls every second so the sliding window + * scrolls smoothly. + */ +export const useZoneReadings = (zoneId: number | undefined, limit = 60) => { + const isOfflineMode = usePreferencesStore((state) => state.isOfflineMode); + + return useQuery({ + queryKey: ['zoneReadings', zoneId, limit], + queryFn: async () => { + const { data } = await apiClient.get(`/zones/${zoneId}/readings`, { + params: { limit }, + }); + // Newest-first from the API; the dashboard maintains its own sliding + // window in chronological order. + return data; + }, + refetchInterval: 1000, + enabled: !isOfflineMode && zoneId != null, + placeholderData: (prev) => prev, + }); +}; + export const useRecentReadings = () => { const isOfflineMode = usePreferencesStore((state) => state.isOfflineMode); @@ -23,9 +62,9 @@ export const useRecentReadings = () => { queryKey: ['recentReadings'], queryFn: async () => { const { data } = await apiClient.get('/readings/?limit=50'); - return data.reverse(); + return data.reverse(); }, - refetchInterval: 2000, + refetchInterval: 2000, enabled: !isOfflineMode, }); -}; \ No newline at end of file +}; diff --git a/mobile/app.json b/mobile/app.json index a386e5f..5cbe1bc 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -6,12 +6,12 @@ "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "frontendmobileapp", - "userInterfaceStyle": "automatic", + "userInterfaceStyle": "dark", "newArchEnabled": true, "splash": { "image": "./assets/images/splash-icon.png", "resizeMode": "contain", - "backgroundColor": "#ffffff" + "backgroundColor": "#09090b" }, "ios": { "supportsTablet": true @@ -35,7 +35,24 @@ "plugins": [ "expo-router", "expo-notifications", - "expo-font" + "expo-font", + "expo-audio", + [ + "expo-location", + { + "locationWhenInUsePermission": "QuakeGuard uses your location to detect which monitored region you are in." + } + ], + [ + "expo-splash-screen", + { + "backgroundColor": "#09090b", + "image": "./assets/images/splash-icon.png", + "imageWidth": 200 + } + ], + "expo-web-browser", + "expo-asset" ], "experiments": { "typedRoutes": true diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx index 46d92c3..6cda05e 100644 --- a/mobile/app/(tabs)/_layout.tsx +++ b/mobile/app/(tabs)/_layout.tsx @@ -1,30 +1,33 @@ import { Tabs } from "expo-router"; import { Map, ShieldCheck, Settings } from "lucide-react-native"; import React from "react"; -// 💡 IMPORT THE INSETS HOOK -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useAppTheme } from "../../theme/useTheme"; +import { MONO } from "../../theme"; export default function TabLayout() { - // 💡 GET SYSTEM DIMENSIONS - const insets = useSafeAreaInsets(); + const insets = useSafeAreaInsets(); + const { colors } = useAppTheme(); return ( diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx index f7007d6..40aec27 100644 --- a/mobile/app/(tabs)/index.tsx +++ b/mobile/app/(tabs)/index.tsx @@ -1,6 +1,6 @@ import { ShieldAlert, ShieldCheck, Wifi, WifiOff, Activity } from "lucide-react-native"; -import React, { useEffect, useRef, useState } from "react"; -import { StyleSheet, Text, View, ScrollView } from "react-native"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { StyleSheet, Text, View, ScrollView, Pressable } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import Animated, { Easing, @@ -10,20 +10,32 @@ import Animated, { withSequence, withTiming, } from "react-native-reanimated"; -import { VictoryChart, VictoryLine, VictoryTheme } from "victory-native"; +import { VictoryChart, VictoryLine, VictoryAxis, VictoryLabel } from "victory-native"; import { useWebSocket } from "../../context/WebSocketContext"; -import { useSensors, useRecentReadings } from "../../api/hooks/useDashboard"; +import { useSensors, useZones, useZoneReadings } from "../../api/hooks/useDashboard"; import { LoadingSkeleton } from "../../components/LoadingSkeleton"; import { ErrorBanner } from "../../components/ErrorBanner"; import { AlertHistoryList } from "../../components/AlertHistoryList"; +import { useAppTheme } from "../../theme/useTheme"; +import { createQuakeGuardTheme } from "../../theme/victory"; +import { MONO } from "../../theme"; +import { estimateMagnitude, thresholdOf } from "../../utils/magnitude"; -function TopBar({ isConnected }: Readonly<{ isConnected: boolean }>) { +const WINDOW_MAX = 60; // samples kept in the sliding window +const WINDOW_SECONDS = 30; // time domain of the seismograph +const RIGHT_PAD = 8; // breathing room right of x=0 so live samples stay in-field + +type ThemeColors = ReturnType["colors"]; + +function TopBar({ isConnected, colors }: Readonly<{ isConnected: boolean; colors: ThemeColors }>) { + const styles = createStyles(colors); return ( - Network Status + NETWORK STATUS - {isConnected ? : } - + {isConnected ? : } + + {isConnected ? "LIVE" : "OFFLINE"} @@ -31,43 +43,46 @@ function TopBar({ isConnected }: Readonly<{ isConnected: boolean }>) { ); } -function HeroSection({ isAlertActive, animatedStyle, textColor }: Readonly<{ +function HeroSection({ isAlertActive, animatedStyle, colors }: Readonly<{ isAlertActive: boolean; animatedStyle: any; - textColor: string; + colors: ThemeColors; }>) { + const styles = createStyles(colors); return ( {isAlertActive ? ( - + ) : ( - + )} - - {isAlertActive ? "⚠️ SEISMIC ALERT ⚠️" : "SYSTEM SECURE"} + + {isAlertActive ? "⚠ SEISMIC ALERT ⚠" : "SYSTEM SECURE"} ); } -function AlertBanner({ lastAlert }: Readonly<{ lastAlert: any }>) { +function AlertBanner({ lastAlert, colors }: Readonly<{ lastAlert: any; colors: ThemeColors }>) { + const styles = createStyles(colors); return ( - Mag: {lastAlert.magnitude.toFixed(1)} + MAG {lastAlert.magnitude.toFixed(1)} {`"${lastAlert.message}"`} ); } -function AiReportCard({ lastReport }: Readonly<{ lastReport: any }>) { +function AiReportCard({ lastReport, colors }: Readonly<{ lastReport: any; colors: ThemeColors }>) { + const styles = createStyles(colors); if (!lastReport) return null; if (lastReport.status === "FAILED") { return ( - 🤖 AI Report Unavailable + AI REPORT // UNAVAILABLE The AI report could not be generated. Verify with local authorities. ); @@ -75,12 +90,12 @@ function AiReportCard({ lastReport }: Readonly<{ lastReport: any }>) { return ( - 🤖 AI Emergency Report + AI EMERGENCY REPORT {lastReport.summary} {lastReport.recommendations && lastReport.recommendations.length > 0 && ( - {[...new Set(lastReport.recommendations)].map((r: string) => ( - {`• ${r}`} + {[...new Set(lastReport.recommendations)].map((r) => ( + {`> ${r}`} ))} )} @@ -88,79 +103,226 @@ function AiReportCard({ lastReport }: Readonly<{ lastReport: any }>) { ); } -function NetworkChart({ readings, isAlertActive }: Readonly<{ readings: any[]; isAlertActive: boolean }>) { - if (!readings || readings.length === 0) { - return Awaiting telemetry...; - } - +/** Horizontal strip of PostGIS zones — one seismograph per zone. */ +function ZoneSelector({ zones, selectedId, onSelect, colors }: Readonly<{ + zones: any[]; + selectedId: number | undefined; + onSelect: (id: number) => void; + colors: ThemeColors; +}>) { + const styles = createStyles(colors); return ( - - - + {zones.map((zone) => { + const active = zone.id === selectedId; + return ( + onSelect(zone.id)} + style={[styles.zoneChip, active && styles.zoneChipActive]} + > + + {zone.city.toUpperCase()} + + + ); + })} + ); } -function DashboardContent({ errorSensors, errorReadings, loadingSensors, loadingReadings, activeSensors, totalSensors, readings, isAlertActive }: Readonly<{ - errorSensors: boolean; - errorReadings: boolean; - loadingSensors: boolean; - loadingReadings: boolean; - activeSensors: number; - totalSensors: number; - readings: any[]; +/** Per-zone telemetry strip: nodes, current magnitude, signal state. */ +function ZoneSummaryStrip({ activeNodes, totalNodes, latestMagnitude, isAlertActive, colors }: Readonly<{ + activeNodes: number; + totalNodes: number; + latestMagnitude: number | null; isAlertActive: boolean; + colors: ThemeColors; }>) { - if (errorSensors || errorReadings) return ; - if (loadingSensors || loadingReadings) return ; + const styles = createStyles(colors); + const mag = latestMagnitude ?? 0; + const magColor = mag >= 4.5 ? colors.alert : mag >= 4.0 ? colors.caution : colors.live; return ( - <> - - - Active Nodes - {activeSensors} / {totalSensors} - - - Signal Status - - - Stable - + + + NODES + {activeNodes} / {totalNodes} + + + MAG + {mag.toFixed(2)} + + + SIGNAL + + + + {isAlertActive ? "ALERT" : "STABLE"} + + + ); +} - - Live Network Seismograph - - +type WindowPoint = { x: number; y: number; t: number }; // x = seconds relative to newest +type WindowEntry = { t: number; y: number }; // t = epoch ms (merge key) + +function NetworkChart({ points, isAlertActive, colors }: Readonly<{ + points: WindowPoint[]; + isAlertActive: boolean; + colors: ThemeColors; +}>) { + const styles = createStyles(colors); + const theme = useMemo(() => createQuakeGuardTheme(colors), [colors]); + const last = points[points.length - 1]; + const lineColor = last + ? thresholdOf(last.y) === "alert" + ? colors.alert + : thresholdOf(last.y) === "caution" + ? colors.caution + : colors.live + : colors.live; + + if (points.length === 0) { + return ( + AWAITING TELEMETRY // ZONE ... + ); + } + + // Right breathing room so the newest live sample (x≈0) never touches or + // spills past the plot edge — that read as "the graph starts out of field". + const magTicks = [3.5, 4.0, 4.5]; // MIN / MED / ALTO thresholds on a linear MAG axis + + // Plot in magnitude units (linear scale) instead of raw sensor values + // (log-compressed near the noise floor — that made the 3.0/3.5 ticks crowd). + const data = points.map(({ x, y, t }) => ({ x, y: estimateMagnitude(y), t })); - - + const CHART_WIDTH = 450; // victory-native default + const PAD_LEFT = 52; + const PAD_RIGHT = 12; + const PAD_BOTTOM = 46; + const chartHeight = isAlertActive ? 150 : 200; + + // TIME under the X axis, centered on the middle of the DATA domain (-15s, + // i.e. between the 20s and 10s ticks) rather than the physical chart center, + // which sits right-of-center because of the RIGHT_PAD breathing room. + const spanX = WINDOW_SECONDS + RIGHT_PAD; + const timeX = PAD_LEFT + ((WINDOW_SECONDS - 15) / spanX) * (CHART_WIDTH - PAD_LEFT - PAD_RIGHT); + const timeY = chartHeight - PAD_BOTTOM + 34; + + return ( + + + `${t.toFixed(1)}`} + label="MAG" + style={{ + axisLabel: { padding: 38, fill: colors.tick, fontFamily: MONO, fontSize: 10 }, + }} + /> + `${Math.abs(Math.round(t))}s`} + /> + + ); } export default function MonitorScreen() { const { isConnected, lastAlert, lastReport } = useWebSocket(); + const { colors } = useAppTheme(); const [isAlertActive, setIsAlertActive] = useState(false); + const [selectedZoneId, setSelectedZoneId] = useState(undefined); + const [window, setWindow] = useState([]); const pulse = useSharedValue(1); const alertTimerRef = useRef | null>(null); + const styles = createStyles(colors); const { data: sensors, isLoading: loadingSensors, isError: errorSensors } = useSensors(); - const { data: readings, isLoading: loadingReadings, isError: errorReadings } = useRecentReadings(); + const { data: zones, isLoading: loadingZones, isError: errorZones } = useZones(); + const { data: readings, isLoading: loadingReadings, isError: errorReadings } = useZoneReadings(selectedZoneId, WINDOW_MAX); const totalSensors = sensors?.length || 0; const activeSensors = sensors?.filter((s: any) => s.active).length || 0; + const selectedZone = zones?.find((z: any) => z.id === selectedZoneId); + + // Default to the first zone once the PostGIS list is available. + useEffect(() => { + if (selectedZoneId === undefined && zones && zones.length > 0) { + setSelectedZoneId(zones[0].id); + } + }, [zones, selectedZoneId]); + + // Reset the sliding window when switching zone. + useEffect(() => { + setWindow([]); + }, [selectedZoneId]); + + // Merge incoming readings into the sliding window (push newest, drop oldest). + // The time axis is anchored to the wall clock (Date.now), NOT to the newest + // sample: stale readings drift to the left and leave the window, while live + // ones enter at the right edge. Anchoring to the newest sample instead pins + // a finished run at the right edge forever ("lines going out of the field"). + useEffect(() => { + if (!readings || readings.length === 0) return; + setWindow((prev) => { + const now = Date.now(); + const merged = new Map(); + for (const p of prev) merged.set(p.t, { t: p.t, y: p.y }); + for (const r of readings) { + const t = Date.parse(r.recorded_at); + if (Number.isNaN(t)) continue; + merged.set(t, { t, y: r.value }); + } + const list = [...merged.values()].filter(({ t }) => (now - t) / 1000 <= WINDOW_SECONDS); + if (list.length === 0) return prev; + return list + .sort((a, b) => a.t - b.t) + .slice(-WINDOW_MAX) + .map(({ t, y }) => ({ + // Clamp to the domain so nothing can ever spill past the plot edges. + x: Math.max(-WINDOW_SECONDS, Math.min(RIGHT_PAD, (t - now) / 1000)), + y, + t, + })); + }); + }, [readings, selectedZoneId]); + + const latestMagnitude = useMemo(() => { + if (!readings || readings.length === 0) return null; + return estimateMagnitude(readings[0].value); + }, [readings]); useEffect(() => { if (lastAlert) { @@ -195,78 +357,137 @@ export default function MonitorScreen() { opacity: isAlertActive ? pulse.value : 1, })); - const backgroundColor = isAlertActive ? "#fef2f2" : "#f9fafb"; - const textColor = isAlertActive ? "#991b1b" : "#1f2937"; + const loading = loadingSensors || loadingZones || loadingReadings; + const errored = errorSensors || errorZones || errorReadings; return ( - + - + - + - {isAlertActive && lastAlert && } + {isAlertActive && lastAlert && } - + - + {errored ? ( + + ) : loading ? ( + + ) : ( + <> + + + + + SEISMOGRAPH // {selectedZone?.city?.toUpperCase() ?? "ZONE"} + + Z-ACCEL // RAW + + + + + + + + + )} + + ); } -const styles = StyleSheet.create({ - safeArea: { flex: 1 }, - scrollContent: { flexGrow: 1, padding: 20 }, - topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, - headerTitle: { fontSize: 24, fontWeight: "bold", color: "#111827" }, - connectionBadge: { flexDirection: "row", alignItems: "center", gap: 6, backgroundColor: "#ffffff", paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 }, - connectionText: { fontSize: 12, fontWeight: "800" }, - heroSection: { alignItems: 'center', marginVertical: 10 }, - iconContainer: { marginBottom: 10 }, - statusText: { fontSize: 26, fontWeight: "900", textAlign: "center" }, - dashboardCard: { - backgroundColor: "white", - borderRadius: 20, - padding: 20, - elevation: 3, - marginTop: 10 - }, - summaryRow: { - flexDirection: 'row', - justifyContent: 'space-between', - borderBottomWidth: 1, - borderBottomColor: "#f3f4f6", - paddingBottom: 15, - marginBottom: 10, - flexShrink: 0 - }, - summaryItem: { alignItems: 'flex-start' }, - summaryLabel: { fontSize: 12, color: "#6b7280", fontWeight: "600", textTransform: "uppercase" }, - summaryValue: { fontSize: 20, fontWeight: "bold", color: "#1f2937" }, - chartContainer: { marginVertical: 10 }, - chartTitle: { fontSize: 16, fontWeight: "700", color: "#374151" }, - alertDetails: { marginBottom: 10, padding: 10, backgroundColor: "#fee2e2", borderRadius: 12, alignItems: 'center' }, - alertValue: { fontSize: 18, fontWeight: "800", color: "#b91c1c" }, - alertMessage: { fontSize: 14, fontStyle: "italic", color: "#991b1b" }, - reportCard: { marginBottom: 10, padding: 12, backgroundColor: "#f5f3ff", borderRadius: 12 }, - reportCardFailed: { marginBottom: 10, padding: 12, backgroundColor: "#fef3c7", borderRadius: 12 }, - reportCardTitle: { fontSize: 14, fontWeight: "800", color: "#7c3aed", marginBottom: 6 }, - reportCardBody: { fontSize: 13, color: "#4c1d95", lineHeight: 18 }, - reportRecommendations: { marginTop: 8 }, - reportRecommendationItem: { fontSize: 12, color: "#6d28d9", lineHeight: 16 } -}); +const createStyles = (c: ThemeColors) => + StyleSheet.create({ + safeArea: { flex: 1 }, + scrollContent: { flexGrow: 1, padding: 20 }, + topBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, + headerTitle: { fontSize: 18, fontWeight: "700", color: c.text, letterSpacing: 1.5, fontFamily: MONO }, + connectionBadge: { + flexDirection: "row", + alignItems: "center", + gap: 6, + backgroundColor: c.surface, + borderColor: c.border, + borderWidth: 1, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 20, + }, + statusDot: { width: 8, height: 8, borderRadius: 4 }, + connectionText: { fontSize: 12, fontWeight: "800", fontFamily: MONO }, + heroSection: { alignItems: 'center', marginVertical: 10 }, + iconContainer: { marginBottom: 10 }, + statusText: { fontSize: 24, fontWeight: "900", textAlign: "center", letterSpacing: 2, fontFamily: MONO }, + dashboardCard: { + backgroundColor: c.surface, + borderColor: c.border, + borderWidth: 1, + borderRadius: 16, + padding: 20, + marginTop: 10, + }, + zoneStrip: { gap: 8, paddingVertical: 4, marginBottom: 16 }, + zoneChip: { + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 20, + backgroundColor: c.surfaceAlt, + borderColor: c.border, + borderWidth: 1, + }, + zoneChipActive: { + borderColor: c.live, + backgroundColor: c.bg, + }, + zoneChipText: { fontSize: 11, fontWeight: "700", color: c.textSecondary, letterSpacing: 1, fontFamily: MONO }, + zoneChipTextActive: { color: c.live }, + chartHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 10, gap: 8 }, + chartTitle: { fontSize: 13, fontWeight: "700", color: c.text, letterSpacing: 1.2, fontFamily: MONO, flexShrink: 1 }, + chartSubtitle: { fontSize: 10, color: c.textMuted, letterSpacing: 1, fontFamily: MONO, flexShrink: 0 }, + summaryRow: { + flexDirection: 'row', + justifyContent: 'space-between', + borderTopWidth: 1, + borderTopColor: c.border, + borderBottomWidth: 1, + borderBottomColor: c.border, + paddingVertical: 10, + marginBottom: 10, + }, + summaryItem: { alignItems: 'flex-start', flex: 1 }, + summaryLabel: { fontSize: 10, color: c.textMuted, fontWeight: "700", letterSpacing: 1.2, fontFamily: MONO }, + summaryValue: { fontSize: 16, fontWeight: "700", color: c.text, fontFamily: MONO, marginTop: 2 }, + summaryValueSmall: { fontSize: 14, fontWeight: "700", fontFamily: MONO }, + signalRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 2 }, + chartContainer: { marginVertical: 4 }, + chartEmpty: { textAlign: 'center', color: c.textMuted, fontFamily: MONO, fontSize: 12, paddingVertical: 60, letterSpacing: 1 }, + alertDetails: { marginBottom: 10, padding: 12, backgroundColor: c.bg, borderColor: c.alert, borderWidth: 1, borderRadius: 12, alignItems: 'center' }, + alertValue: { fontSize: 18, fontWeight: "800", color: c.alert, fontFamily: MONO }, + alertMessage: { fontSize: 14, fontStyle: "italic", color: c.textSecondary }, + reportCard: { marginBottom: 10, padding: 12, backgroundColor: c.surfaceAlt, borderColor: c.border, borderWidth: 1, borderRadius: 12 }, + reportCardFailed: { marginBottom: 10, padding: 12, backgroundColor: c.surfaceAlt, borderColor: c.caution, borderWidth: 1, borderRadius: 12 }, + reportCardTitle: { fontSize: 12, fontWeight: "800", color: c.info, marginBottom: 6, letterSpacing: 1.2, fontFamily: MONO }, + reportCardBody: { fontSize: 13, color: c.textSecondary, lineHeight: 18 }, + reportRecommendations: { marginTop: 8 }, + reportRecommendationItem: { fontSize: 12, color: c.text, lineHeight: 16, fontFamily: MONO }, + }); \ No newline at end of file diff --git a/mobile/app/(tabs)/map.tsx b/mobile/app/(tabs)/map.tsx index a9e51b2..feb05a8 100644 --- a/mobile/app/(tabs)/map.tsx +++ b/mobile/app/(tabs)/map.tsx @@ -1,41 +1,48 @@ import { Radio } from "lucide-react-native"; import React from "react"; -import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; +import { ActivityIndicator, Platform, StyleSheet, Text, View } from "react-native"; import MapView, { Callout, Marker, PROVIDER_DEFAULT } from "react-native-maps"; import { useSensors } from "../../api/hooks/useDashboard"; import { useSensorStatistics } from "../../api/hooks/useSensors"; import { LoadingSkeleton } from "../../components/LoadingSkeleton"; import { ErrorBanner } from "../../components/ErrorBanner"; +import { useAppTheme } from "../../theme/useTheme"; +import { MONO } from "../../theme"; +import { darkMapStyle, lightMapStyle } from "../../theme/mapStyle"; -const CalloutStats = ({ stats }: { stats: any }) => { +type ThemeColors = ReturnType["colors"]; + +const CalloutStats = ({ stats, colors }: { stats: any; colors: ThemeColors }) => { + const styles = createStyles(colors); return ( - Total Readings: + TOTAL READINGS: {stats?.total_readings || 0} ); }; -const SensorCalloutDetails = ({ sensor }: { sensor: any }) => { +const SensorCalloutDetails = ({ sensor, colors }: { sensor: any; colors: ThemeColors }) => { const { data: stats, isLoading, isError } = useSensorStatistics(sensor.id); + const styles = createStyles(colors); let content: React.JSX.Element; if (isLoading) { - content = ; + content = ; } else if (isError) { - content = Data unavailable; + content = DATA UNAVAILABLE; } else { - content = ; + content = ; } return ( - Sensor ID: {sensor.id} - + SENSOR {sensor.id} + - - - {sensor.active ? "Active" : "Offline"} + + + {sensor.active ? "ACTIVE" : "OFFLINE"} @@ -48,6 +55,8 @@ const SensorCalloutDetails = ({ sensor }: { sensor: any }) => { export default function MapScreen() { const { data: sensors, isLoading, isError } = useSensors(); + const { colors, isDark } = useAppTheme(); + const styles = createStyles(colors); if (isLoading) { return ; @@ -55,9 +64,9 @@ export default function MapScreen() { if (isError) { return ( - ); } @@ -67,6 +76,7 @@ export default function MapScreen() { {/* Inject the lazy-loading details component */} - + ))} @@ -91,15 +101,16 @@ export default function MapScreen() { ); } -const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: "#fff" }, - map: { width: "100%", height: "100%" }, - calloutContainer: { padding: 10, width: 160, backgroundColor: "white", borderRadius: 8 }, - calloutTitle: { fontWeight: "800", fontSize: 14, marginBottom: 6, color: "#111827" }, - statusRow: { flexDirection: "row", alignItems: "center", gap: 6, marginBottom: 8 }, - statusText: { fontSize: 12, fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.5 }, - statsDivider: { height: 1, backgroundColor: "#e5e7eb", marginVertical: 6 }, - statsRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginTop: 4 }, - statsLabel: { fontSize: 12, color: "#4b5563", fontWeight: "500" }, - statsValue: { fontSize: 14, fontWeight: "700", color: "#4f46e5" }, -}); \ No newline at end of file +const createStyles = (c: ThemeColors) => + StyleSheet.create({ + container: { flex: 1, backgroundColor: c.bg }, + map: { width: "100%", height: "100%" }, + calloutContainer: { padding: 10, width: 180, backgroundColor: c.surfaceAlt, borderRadius: 8, borderColor: c.borderStrong, borderWidth: 1 }, + calloutTitle: { fontWeight: "800", fontSize: 14, marginBottom: 6, color: c.text, fontFamily: MONO, letterSpacing: 1 }, + statusRow: { flexDirection: "row", alignItems: "center", gap: 6, marginBottom: 8 }, + statusText: { fontSize: 12, fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.5, fontFamily: MONO }, + statsDivider: { height: 1, backgroundColor: c.border, marginVertical: 6 }, + statsRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginTop: 4 }, + statsLabel: { fontSize: 11, color: c.textSecondary, fontWeight: "500", fontFamily: MONO }, + statsValue: { fontSize: 14, fontWeight: "700", color: c.live, fontFamily: MONO }, + }); diff --git a/mobile/app/(tabs)/settings.tsx b/mobile/app/(tabs)/settings.tsx index 0e40eae..c8ca621 100644 --- a/mobile/app/(tabs)/settings.tsx +++ b/mobile/app/(tabs)/settings.tsx @@ -1,103 +1,360 @@ -import React from "react"; -// 💡 IMPORT Alert and TouchableOpacity -import { View, Text, StyleSheet, Switch, Alert, TouchableOpacity } from "react-native"; +import React, { useState } from "react"; +import { View, Text, StyleSheet, Switch, Alert, TouchableOpacity, ActivityIndicator, ScrollView, Linking } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -// 💡 ADD Trash2 icon -import { Settings as SettingsIcon, Bell, WifiOff, Trash2 } from "lucide-react-native"; +import { + Settings as SettingsIcon, + Bell, + WifiOff, + Trash2, + Microscope, + RadioTower, + MapPin, + Crosshair, + Globe, + Github, + ExternalLink, +} from "lucide-react-native"; +import * as Location from "expo-location"; import { usePreferencesStore } from "../../store/usePreferencesStore"; -// 💡 IMPORT the store import { useAlertStore } from "../../store/useAlertStore"; +import { useThemeStore } from "../../store/useThemeStore"; +import { useAppTheme } from "../../theme/useTheme"; +import { useZones } from "../../api/hooks/useDashboard"; +import { apiClient } from "../../api/client"; +import { MONO } from "../../theme"; + +interface Zone { + id: number; + city: string; +} export default function SettingsScreen() { - const { isOfflineMode, notificationsEnabled, setOfflineMode, toggleNotifications } = usePreferencesStore(); - // 💡 Consume the orphaned action and the alerts array (to disable the button if empty) + const { + isOfflineMode, + notificationsEnabled, + homeZoneId, + setOfflineMode, + toggleNotifications, + setHomeZoneId, + } = usePreferencesStore(); const { clearAlerts, alerts } = useAlertStore(); + const { colors, mode } = useAppTheme(); + const { toggleTheme } = useThemeStore(); + const { data: zones } = useZones(); + const [detecting, setDetecting] = useState(false); + const styles = createStyles(colors); const handleClearHistory = () => { if (alerts.length === 0) return; - + Alert.alert( "Clear History", "Are you sure you want to delete all recent alerts?", [ { text: "Cancel", style: "cancel" }, - { - text: "Clear", - style: "destructive", + { + text: "Clear", + style: "destructive", onPress: () => { clearAlerts(); console.log("[Settings] Alert history cleared."); - } - } + }, + }, ] ); }; + const myZoneCity = + homeZoneId == null + ? "ALL REGIONS (ring for every alert)" + : (zones as Zone[] | undefined)?.find((z) => z.id === homeZoneId)?.city; + + const handleDetectZone = async () => { + if (isOfflineMode) { + Alert.alert("Offline", "Connect to the network to detect your zone."); + return; + } + + setDetecting(true); + try { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== Location.PermissionStatus.GRANTED) { + Alert.alert("Permission denied", "Location access is required to detect your zone. Pick it manually below."); + return; + } + + const position = await Location.getCurrentPositionAsync({ + accuracy: Location.Accuracy.Balanced, + }); + + const { data } = await apiClient.get("/zones/locate", { + params: { latitude: position.coords.latitude, longitude: position.coords.longitude }, + }); + + setHomeZoneId(data.id); + Alert.alert("Zone updated", `Your area resolved to "${data.city}". Alerts will now ring only for this region.`); + } catch (err: any) { + if (err?.response?.status === 404) { + Alert.alert( + "Outside monitored area", + "Your position is not covered by a monitored polygon. Pick a zone manually below." + ); + } else { + Alert.alert("Detection failed", "Could not resolve your zone. Pick one manually below."); + } + } finally { + setDetecting(false); + } + }; + + const selectZone = (zoneId: number | null) => { + setHomeZoneId(zoneId); + const city = + zoneId == null + ? "ALL REGIONS" + : (zones as Zone[] | undefined)?.find((z) => z.id === zoneId)?.city ?? `Zone #${zoneId}`; + console.log(`[Settings] Home zone set: ${city}`); + }; + return ( - + - - Settings + + SYSTEM CONFIG + + + {/* Appearance */} + APPEARANCE + + + + {mode === "dark" ? : } + + + {mode === "dark" ? "MIC MODE (Military)" : "RESEARCH MODE"} + + + {mode === "dark" ? "Dark command console" : "Light scientific study"} + + + + + + {/* Alerts */} + GLOBAL - + Enable Notifications - {/* 💡 Removed styles.lastRow from here */} - + Force Offline Mode - {/* 💡 NEW ROW: Clear History */} - - - + + Clear Alert History + + + {/* My Zone */} + ZONE + + + + + + MY ZONE + ONLY RING FOR YOUR REGION + + + + + + + + Detect my zone via GPS + + + {detecting ? ( + + ) : ( + DETECT + )} + + + + + + + + Ring only for + + {myZoneCity} + + + + + + + selectZone(null)} + > + ALL + + {(zones as Zone[] | undefined)?.map((zone) => { + const active = homeZoneId === zone.id; + return ( + selectZone(zone.id)} + > + {zone.city} + + ); + })} + + + + {/* Explore */} + EXPLORE + + Linking.openURL("https://github.com/Gizano/QuakeGuard")} + > + + + + GitHub repo + Gizano/QuakeGuard + + + + + Linking.openURL("https://giovanni-zanotti.is-a.dev/Projects/quakeguard.html")} + > + + + + QuakeGuard website + Discover more about QuakeGuard! + + + + - + + QuakeGuard v1.2.1 + ); } -// ... [styles remain exactly the same] -const styles = StyleSheet.create({ - safeArea: { flex: 1, backgroundColor: "#f9fafb" }, - container: { flex: 1, padding: 20 }, - header: { flexDirection: "row", alignItems: "center", marginBottom: 30, marginTop: 10, gap: 10 }, - headerTitle: { fontSize: 28, fontWeight: "bold", color: "#1f2937" }, - card: { backgroundColor: "white", borderRadius: 16, padding: 16, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8, elevation: 2 }, - settingRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: "#f3f4f6" }, - lastRow: { borderBottomWidth: 0 }, - settingLabelContainer: { flexDirection: "row", alignItems: "center", gap: 12 }, - settingLabel: { fontSize: 16, fontWeight: "500", color: "#374151" }, -}); \ No newline at end of file +const createStyles = (c: ReturnType["colors"]) => + StyleSheet.create({ + safeArea: { flex: 1, backgroundColor: c.bg }, + container: { padding: 20, paddingBottom: 100 }, + header: { flexDirection: "row", alignItems: "center", marginBottom: 30, marginTop: 10, gap: 10 }, + headerTitle: { fontSize: 22, fontWeight: "bold", color: c.text, letterSpacing: 1.5, fontFamily: MONO }, + card: { + backgroundColor: c.surface, + borderColor: c.border, + borderWidth: 1, + borderRadius: 16, + padding: 16, + }, + sectionTitle: { + fontSize: 11, + color: c.textMuted, + fontWeight: "700", + letterSpacing: 1.5, + fontFamily: MONO, + marginBottom: 8, + marginTop: 4, + }, + sectionTitleSpaced: { marginTop: 24 }, + settingRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: c.border, + }, + lastRow: { borderBottomWidth: 0 }, + settingLabelContainer: { flexDirection: "row", alignItems: "center", gap: 12, flex: 1 }, + settingLabel: { fontSize: 15, fontWeight: "500", color: c.text }, + settingHint: { fontSize: 11, color: c.textMuted, fontFamily: MONO, marginTop: 2, letterSpacing: 0.5 }, + detectButton: { + backgroundColor: c.live, + borderRadius: 8, + paddingHorizontal: 14, + paddingVertical: 8, + minWidth: 74, + alignItems: "center", + }, + detectButtonText: { color: c.bg, fontSize: 12, fontWeight: "700", fontFamily: MONO, letterSpacing: 1 }, + chipWrap: { flexDirection: "row", flexWrap: "wrap", gap: 8, paddingTop: 12 }, + chip: { + borderWidth: 1, + borderColor: c.borderStrong, + borderRadius: 20, + paddingHorizontal: 12, + paddingVertical: 6, + }, + chipActive: { backgroundColor: c.live, borderColor: c.live }, + chipText: { color: c.textSecondary, fontSize: 12, fontFamily: MONO }, + chipTextActive: { color: c.bg, fontSize: 12, fontFamily: MONO, fontWeight: "700" }, + versionFooter: { + marginTop: 24, + textAlign: "center", + fontSize: 11, + color: c.textMuted, + fontFamily: MONO, + letterSpacing: 1, + }, + }); \ No newline at end of file diff --git a/mobile/app/+html.tsx b/mobile/app/+html.tsx index e94a5ed..358e21f 100644 --- a/mobile/app/+html.tsx +++ b/mobile/app/+html.tsx @@ -29,10 +29,8 @@ export default function Root({ children }: Readonly<{ children: React.ReactNode const responsiveBackground = ` body { - background-color: #fff; + background-color: #09090b; } -@media (prefers-color-scheme: dark) { - body { - background-color: #000; - } +html { + background-color: #09090b; }`; diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 54b5a58..40606c0 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -5,6 +5,7 @@ import * as SplashScreen from "expo-splash-screen"; import { useEffect } from "react"; import { WebSocketProvider } from "../context/WebSocketContext"; import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { FONTS } from "../theme"; // 1. Import TanStack Query essentials import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -18,6 +19,7 @@ const queryClient = new QueryClient(); export default function RootLayout() { const [loaded, error] = useFonts({ ...FontAwesome.font, + [FONTS.mono]: require("../assets/fonts/SpaceMono-Regular.ttf"), }); useEffect(() => { diff --git a/mobile/assets/sounds/alarm.wav b/mobile/assets/sounds/alarm.wav new file mode 100644 index 0000000..93dac04 Binary files /dev/null and b/mobile/assets/sounds/alarm.wav differ diff --git a/mobile/audio/alarm.ts b/mobile/audio/alarm.ts new file mode 100644 index 0000000..bd4c534 --- /dev/null +++ b/mobile/audio/alarm.ts @@ -0,0 +1,47 @@ +import { createAudioPlayer, setAudioModeAsync, AudioPlayer } from "expo-audio"; + +/** + * Civil-defense siren player. Played on critical alerts matching the + * operator's own zone (see WebSocketContext). Loops for a bounded window and + * auto-stops, so it never rings forever and never overlaps a second alarm. + */ +let player: AudioPlayer | null = null; +let stopTimer: ReturnType | null = null; + +const ALARM_SOURCE = require("../assets/sounds/alarm.wav"); + +function ensurePlayer(): AudioPlayer { + if (!player) { + player = createAudioPlayer(ALARM_SOURCE); + player.loop = true; + player.volume = 1; + } + return player; +} + +export async function playAlarm(durationMs: number = 15000): Promise { + try { + await setAudioModeAsync({ playsInSilentMode: true }); + const p = ensurePlayer(); + stopAlarm(); + await p.seekTo(0); + p.play(); + if (durationMs > 0) { + stopTimer = setTimeout(() => stopAlarm(), durationMs); + } + } catch (err) { + console.warn("⚠️ Alarm playback failed:", err); + } +} + +export function stopAlarm(): void { + if (stopTimer) { + clearTimeout(stopTimer); + stopTimer = null; + } + try { + player?.pause(); + } catch { + // ignore — player may already be released + } +} \ No newline at end of file diff --git a/mobile/components/AlertHistoryList.tsx b/mobile/components/AlertHistoryList.tsx index 4157e41..00e8e3c 100644 --- a/mobile/components/AlertHistoryList.tsx +++ b/mobile/components/AlertHistoryList.tsx @@ -2,15 +2,26 @@ import React from 'react'; import { View, Text, StyleSheet, FlatList } from 'react-native'; import { AlertTriangle, Sparkles } from 'lucide-react-native'; import { useAlertStore } from '../store/useAlertStore'; +import { useZones } from '../api/hooks/useDashboard'; +import { useAppTheme } from '../theme/useTheme'; +import { MONO } from '../theme'; export function AlertHistoryList() { const { alerts, reports } = useAlertStore(); + const { data: zones } = useZones(); + const { colors } = useAppTheme(); + const styles = createStyles(colors); + + const zoneName = (zoneId: number): string => { + const city = (zones ?? []).find((z: any) => z.id === zoneId)?.city; + return city ? city.toUpperCase() : `ZONE ${zoneId}`; + }; if (alerts.length === 0) return null; return ( - Recent Activity + RECENT ACTIVITY `${item.timestamp}-${index}`} @@ -19,9 +30,9 @@ export function AlertHistoryList() { const report = item.alert_id != null ? reports[item.alert_id] : undefined; return ( - + - Zone {item.zone_id} • Mag {item.magnitude.toFixed(1)} + {zoneName(item.zone_id)}{' // MAG '}{item.magnitude.toFixed(1)} {new Date(item.timestamp).toLocaleTimeString()} {report && ( @@ -29,18 +40,18 @@ export function AlertHistoryList() { {report.status === 'COMPLETED' ? ( <> - - AI Report + + AI REPORT {report.summary} {report.recommendations && report.recommendations.length > 0 && ( - {report.recommendations.map((r) => `• ${r}`).join('\n')} + {report.recommendations.map((r) => `> ${r}`).join('\n')} )} ) : ( - AI Report Unavailable + AI REPORT UNAVAILABLE )} )} @@ -52,17 +63,18 @@ export function AlertHistoryList() { ); } -const styles = StyleSheet.create({ - container: { marginTop: 20, paddingTop: 20, borderTopWidth: 1, borderTopColor: '#f3f4f6' }, - title: { fontSize: 14, fontWeight: '700', color: '#374151', marginBottom: 12, textTransform: 'uppercase' }, - row: { flexDirection: 'column', backgroundColor: '#fef2f2', padding: 12, borderRadius: 8, marginBottom: 8 }, - textContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, - message: { fontSize: 14, fontWeight: '600', color: '#991b1b' }, - time: { fontSize: 12, color: '#dc2626' }, - reportCard: { marginTop: 10, backgroundColor: '#f5f3ff', padding: 10, borderRadius: 8 }, - reportHeader: { flexDirection: 'row', alignItems: 'center', gap: 4, marginBottom: 4 }, - reportTitle: { fontSize: 11, fontWeight: '700', color: '#7c3aed', textTransform: 'uppercase', letterSpacing: 0.5 }, - reportSummary: { fontSize: 13, color: '#4c1d95', lineHeight: 18 }, - reportRecommendations: { marginTop: 6, fontSize: 12, color: '#6d28d9', lineHeight: 16 }, - reportUnavailable: { fontSize: 12, fontStyle: 'italic', color: '#991b1b' }, -}); +const createStyles = (c: ReturnType["colors"]) => + StyleSheet.create({ + container: { marginTop: 20, paddingTop: 20, borderTopWidth: 1, borderTopColor: c.border }, + title: { fontSize: 12, fontWeight: '700', color: c.textSecondary, marginBottom: 12, letterSpacing: 1.2, fontFamily: MONO }, + row: { flexDirection: 'column', backgroundColor: c.surface, borderColor: c.border, borderWidth: 1, padding: 12, borderRadius: 10, marginBottom: 8 }, + textContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, + message: { fontSize: 13, fontWeight: '600', color: c.text, fontFamily: MONO }, + time: { fontSize: 11, color: c.textMuted, fontFamily: MONO }, + reportCard: { marginTop: 10, backgroundColor: c.surfaceAlt, borderColor: c.border, borderWidth: 1, padding: 10, borderRadius: 8 }, + reportHeader: { flexDirection: 'row', alignItems: 'center', gap: 4, marginBottom: 4 }, + reportTitle: { fontSize: 10, fontWeight: '700', color: c.info, textTransform: 'uppercase', letterSpacing: 1, fontFamily: MONO }, + reportSummary: { fontSize: 12, color: c.textSecondary, lineHeight: 18 }, + reportRecommendations: { marginTop: 6, fontSize: 11, color: c.text, lineHeight: 16, fontFamily: MONO }, + reportUnavailable: { fontSize: 11, fontStyle: 'italic', color: c.caution, fontFamily: MONO }, + }); \ No newline at end of file diff --git a/mobile/components/ErrorBanner.tsx b/mobile/components/ErrorBanner.tsx index 833e20a..3c38bc7 100644 --- a/mobile/components/ErrorBanner.tsx +++ b/mobile/components/ErrorBanner.tsx @@ -1,27 +1,33 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { ServerCrash } from 'lucide-react-native'; +import { useAppTheme } from '../theme/useTheme'; +import { MONO } from '../theme'; interface Props { title?: string; message?: string; } -export function ErrorBanner({ - title = "Connection Error", - message = "Unable to reach the server. Please check your connection and try again." +export function ErrorBanner({ + title = "CONNECTION ERROR", + message = "Unable to reach the server. Please check your connection and try again." }: Readonly) { + const { colors } = useAppTheme(); + const styles = createStyles(colors); + return ( - + {title} {message} ); } -const styles = StyleSheet.create({ - container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, - title: { marginTop: 15, fontSize: 20, fontWeight: 'bold', color: '#1f2937', textAlign: 'center' }, - message: { marginTop: 8, fontSize: 14, color: '#6b7280', textAlign: 'center', lineHeight: 20 } -}); \ No newline at end of file +const createStyles = (c: ReturnType["colors"]) => + StyleSheet.create({ + container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, + title: { marginTop: 15, fontSize: 18, fontWeight: 'bold', color: c.text, textAlign: 'center', fontFamily: MONO, letterSpacing: 1 }, + message: { marginTop: 8, fontSize: 14, color: c.textSecondary, textAlign: 'center', lineHeight: 20 } + }); \ No newline at end of file diff --git a/mobile/components/LoadingSkeleton.tsx b/mobile/components/LoadingSkeleton.tsx index 121a138..8a395f1 100644 --- a/mobile/components/LoadingSkeleton.tsx +++ b/mobile/components/LoadingSkeleton.tsx @@ -1,20 +1,26 @@ import React from 'react'; import { View, Text, ActivityIndicator, StyleSheet } from 'react-native'; +import { useAppTheme } from '../theme/useTheme'; +import { MONO } from '../theme'; interface Props { message?: string; } -export function LoadingSkeleton({ message = "Loading data..." }: Readonly) { +export function LoadingSkeleton({ message = "LOADING DATA..." }: Readonly) { + const { colors } = useAppTheme(); + const styles = createStyles(colors); + return ( - + {message} ); } -const styles = StyleSheet.create({ - container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, - text: { marginTop: 15, fontSize: 16, color: '#4b5563', fontWeight: '500', textAlign: 'center' } -}); \ No newline at end of file +const createStyles = (c: ReturnType["colors"]) => + StyleSheet.create({ + container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }, + text: { marginTop: 15, fontSize: 14, color: c.textSecondary, fontWeight: '500', textAlign: 'center', fontFamily: MONO, letterSpacing: 1 } + }); \ No newline at end of file diff --git a/mobile/context/WebSocketContext.tsx b/mobile/context/WebSocketContext.tsx index f2c50b7..ee2fa9c 100644 --- a/mobile/context/WebSocketContext.tsx +++ b/mobile/context/WebSocketContext.tsx @@ -14,14 +14,19 @@ import * as Device from "expo-device"; import { API_BASE_URL, MOBILE_WS_TOKEN } from "../constants/config"; import { useAlertStore } from '../store/useAlertStore'; import { usePreferencesStore } from '../store/usePreferencesStore'; +import { playAlarm } from "../audio/alarm"; // --- NOTIFICATION HANDLER --- Notifications.setNotificationHandler({ - handleNotification: async () => ({ - shouldShowAlert: usePreferencesStore.getState().notificationsEnabled, - shouldPlaySound: usePreferencesStore.getState().notificationsEnabled, - shouldSetBadge: false, - }), + handleNotification: async () => { + const enabled = usePreferencesStore.getState().notificationsEnabled; + return { + shouldShowBanner: enabled, + shouldShowList: enabled, + shouldPlaySound: enabled, + shouldSetBadge: false, + }; + }, }); // --- TYPES & INTERFACES --- @@ -89,13 +94,15 @@ export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children }); } if (Device.isDevice) { - const { status: existingStatus } = await Notifications.getPermissionsAsync(); - let finalStatus = existingStatus; - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync(); - finalStatus = status; + // SDK 54: expo-notifications no longer depends on expo-modules-core, so + // its NotificationPermissionsStatus typings resolve empty. Read the + // permission via `granted`, which is present at runtime. + const existingGranted = (await Notifications.getPermissionsAsync() as any).granted; + let granted = existingGranted; + if (!existingGranted) { + granted = (await Notifications.requestPermissionsAsync() as any).granted; } - if (finalStatus !== 'granted') { + if (!granted) { console.warn('Failed to get push token for push notification!'); return; } @@ -128,7 +135,13 @@ export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children ws.current.onmessage = (event: WebSocketMessageEvent) => { try { const message: any = JSON.parse(event.data); - const { notificationsEnabled } = usePreferencesStore.getState(); + const { notificationsEnabled, homeZoneId } = usePreferencesStore.getState(); + + // The operator can set a home zone so the phone only rings for their + // own area — a quake on the other side of the world stays silently + // visible in the activity feed. + const ringsForZone = (zoneId: number): boolean => + notificationsEnabled && (homeZoneId == null || zoneId === homeZoneId); // 🤖 AI Emergency Report (generated asynchronously by the local Ollama worker) if (message.type === "EMERGENCY_REPORT") { @@ -137,7 +150,7 @@ export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children setLastReport(report); useAlertStore.getState().addReport(report); - if (notificationsEnabled) { + if (ringsForZone(report.zone_id)) { Notifications.scheduleNotificationAsync({ content: { title: report.status === "COMPLETED" ? "🤖 AI Emergency Report" : "🤖 AI Report Unavailable", @@ -160,8 +173,9 @@ export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children setLastAlert(alert); useAlertStore.getState().addAlert(alert); - if (alert.type === "CRITICAL" && notificationsEnabled) { + if (alert.type === "CRITICAL" && ringsForZone(alert.zone_id)) { Vibration.vibrate(SOS_VIBRATION_PATTERN); + playAlarm(15000); Notifications.scheduleNotificationAsync({ content: { title: "⚠️ CRITICAL SEISMIC ALERT", diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 01d3e5a..bb798f1 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -9,19 +9,21 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^15.0.3", - "@react-navigation/native": "^7.1.8", "@tanstack/react-query": "^5.99.2", "axios": "^1.18.0", - "expo": "~57.0.10", + "expo": "~54.0.0", + "expo-asset": "~12.0.13", + "expo-audio": "~1.1.1", "expo-constants": "~18.0.13", "expo-device": "~8.0.10", "expo-font": "~14.0.12", - "expo-linking": "~8.0.11", + "expo-linking": "~8.0.12", + "expo-location": "~19.0.8", "expo-notifications": "~0.32.17", "expo-router": "~6.0.24", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", - "expo-web-browser": "~15.0.10", + "expo-web-browser": "~15.0.11", "lucide-react-native": "^0.563.0", "react": "19.1.0", "react-dom": "19.1.0", @@ -47,6 +49,20 @@ "typescript": "~5.9.2" } }, + "node_modules/@0no-co/graphql.web": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.3.tgz", + "integrity": "sha512-4gFGBdyaFmQ6n9euhp5JtIGS4ZeivwDr1tCPENUxTvy5wyv532yOtFCr9zzYAJh1s6uibgC+TRXUcay+mxzCoQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -71,20 +87,20 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -101,13 +117,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -255,14 +271,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -381,13 +397,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -480,12 +496,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -635,12 +651,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -791,12 +807,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -806,12 +822,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -870,13 +886,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -902,17 +918,33 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -984,6 +1016,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", @@ -1000,13 +1064,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1032,12 +1096,27 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1081,13 +1160,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1193,6 +1272,36 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", @@ -1209,6 +1318,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", @@ -1230,12 +1354,43 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1245,12 +1400,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1260,16 +1415,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1279,13 +1434,33 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1295,16 +1470,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1314,9 +1489,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1337,17 +1512,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", + "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -1356,17 +1531,17 @@ }, "node_modules/@babel/traverse--for-generate-function-map": { "name": "@babel/traverse", - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -1374,9 +1549,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -1428,9 +1603,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -1627,9 +1802,9 @@ } }, "node_modules/@expo/config-plugins/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1654,9 +1829,9 @@ } }, "node_modules/@expo/config/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1685,9 +1860,9 @@ } }, "node_modules/@expo/devtools": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-57.0.1.tgz", - "integrity": "sha512-GyUf+wFNkbttaX0jR7MZa9bm77U0IrLg6d2AjpxdyoXw/w4abHoXG0oFufwLMgP9zLTd5+Ct4X/ffNUTnlzZgg==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-0.1.8.tgz", + "integrity": "sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ==", "license": "MIT", "dependencies": { "chalk": "^4.1.2" @@ -1705,17 +1880,6 @@ } } }, - "node_modules/@expo/dom-webview": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/@expo/dom-webview/-/dom-webview-57.0.1.tgz", - "integrity": "sha512-lAKsME4SAq+8sf56oN0DX5TBYyruupoRxbWbD2xf9RnKY8y6x8eb9LCE5pxSN0qyWdqnp+0wmyWzDkKboThKAw==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, "node_modules/@expo/env": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.12.tgz", @@ -1729,20 +1893,13 @@ "getenv": "^2.0.0" } }, - "node_modules/@expo/expo-modules-macros-plugin": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@expo/expo-modules-macros-plugin/-/expo-modules-macros-plugin-0.6.1.tgz", - "integrity": "sha512-cpsLZE4rqkc1Y3eZTkxB98jrqY1YXgetmtxFt8q89jBRmk3quRuk1BZo+VcnCSObZardjg99r1k5xijEMONFGA==", - "license": "MIT" - }, "node_modules/@expo/fingerprint": { - "version": "0.20.6", - "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.20.6.tgz", - "integrity": "sha512-cmC/6BOPRbdKr77Mgjwszb8aM0hY2RKBpMRCmjSdn9zIcn2FGor/ic4fHVr46cQFa1G6RDGg1GyAjRw3US4CCQ==", + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.5.tgz", + "integrity": "sha512-mdVoAMcux1WlM6kd1RoWiHRNqKqS+J6mKmWQ/BKgeh937S/fcW58EE68O6nc4KDXtWi3PBeNHskOFcgyIuD4hw==", "license": "MIT", "dependencies": { - "@expo/env": "^2.4.2", - "@expo/spawn-async": "^1.8.0", + "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", @@ -1750,6 +1907,7 @@ "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", + "p-limit": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, @@ -1757,20 +1915,6 @@ "fingerprint": "bin/cli.js" } }, - "node_modules/@expo/fingerprint/node_modules/@expo/env": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.4.2.tgz", - "integrity": "sha512-28pqaEqwnmLduZ00Pq9HkSzE5wbj1MTwp5/n8nm8rD8MCjR9eUnVOwmNksPI3Be2ReAPO/DbPn1puy0mvoocsQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "debug": "^4.3.4", - "getenv": "^2.0.0" - }, - "engines": { - "node": ">=20.12.0" - } - }, "node_modules/@expo/fingerprint/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1820,12 +1964,12 @@ } }, "node_modules/@expo/image-utils": { - "version": "0.8.14", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.14.tgz", - "integrity": "sha512-5Sn+jG4Cw+shC2wDMXoqSAJnvERbiwzHn05FpWtD5IBflfTIs5gUmjzwiGVyjOdlMSQhgRrw/AymPbmO9h9mpQ==", + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.15.tgz", + "integrity": "sha512-3f5CgKJnJ8m4mp8VTcAFQqLrxof99RDr77Aa+7hgzDPg3ZotjLnofl77hf+COQRYi/kuqRYdYFgPIqOIOIdWJA==", "license": "MIT", "dependencies": { - "@expo/require-utils": "^55.0.5", + "@expo/require-utils": "^55.0.6", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", @@ -1834,98 +1978,10 @@ "semver": "^7.6.0" } }, - "node_modules/@expo/image-utils/node_modules/@expo/require-utils": { - "version": "55.0.6", - "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.6.tgz", - "integrity": "sha512-IqsRGPdGbF0lAIrg3XQ+GzcITYcsskvIRmFAjw2kBnr8RgSrRhFAJY1bKksukEUsuZy0knhTCVIIUce7hocqeA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.25.2", - "@babel/plugin-transform-modules-commonjs": "^7.24.8" - }, - "peerDependencies": { - "typescript": "^5.0.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@expo/image-utils/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@expo/inline-modules": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.1.4.tgz", - "integrity": "sha512-8bPSCm//dv8raYfrQ4x79rCX52vMw0QzmnYYl4eSOAvEbGvDPPRGGdbrhZZ7oihU+hI1sZNS5kYEgqODgFwfsw==", - "license": "MIT", - "dependencies": { - "@expo/config-plugins": "~57.0.6" - } - }, - "node_modules/@expo/inline-modules/node_modules/@expo/config-plugins": { - "version": "57.0.6", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-57.0.6.tgz", - "integrity": "sha512-7CmKrS5Rnu8aSZyNlxH2qzA7Ls1HEa4EQvEVOAkHDKPr1e4Cg/nz7I7dUl09QDTVjMvkKYDH4Th0DuAsgqASaw==", - "license": "MIT", - "dependencies": { - "@expo/config-types": "^57.0.2", - "@expo/json-file": "~11.0.1", - "@expo/plist": "^0.8.1", - "@expo/require-utils": "^57.0.4", - "@expo/sdk-runtime-versions": "^1.0.0", - "chalk": "^4.1.2", - "debug": "^4.3.5", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "semver": "^7.5.4", - "slugify": "^1.6.6", - "xcode": "^3.0.1", - "xml2js": "0.6.0" - } - }, - "node_modules/@expo/inline-modules/node_modules/@expo/config-types": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-57.0.2.tgz", - "integrity": "sha512-ewW08OonrcRIsRKIlFvvcmmafE5zemb1ocu3HkNwtVPyRtj2w42pZCAkMIROYpcVBaPnc3mDT9UZDzwXWC3i6g==", - "license": "MIT" - }, - "node_modules/@expo/inline-modules/node_modules/@expo/json-file": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-11.0.1.tgz", - "integrity": "sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "json5": "^2.2.3" - } - }, - "node_modules/@expo/inline-modules/node_modules/@expo/plist": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.8.1.tgz", - "integrity": "sha512-3gTReGIUm0oRaMClsAJYxBnVPCl6fVpsl8HS+DTVxDhW4GyVyxg9E/Znm3BvcHtUJ51RJJI14pC1wvrNilCRHw==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - } - }, - "node_modules/@expo/inline-modules/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1944,303 +2000,26 @@ "json5": "^2.2.3" } }, - "node_modules/@expo/local-build-cache-provider": { - "version": "57.0.5", - "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-57.0.5.tgz", - "integrity": "sha512-OwiNC0Uxu67TOH7TEQ94GLa4oNzNfbbItKGdy3QMfX+hCSZv2VvjcjRo5vttMHfXCnXa9KZIu3GBS9pvtDHWhA==", - "license": "MIT", - "dependencies": { - "@expo/config": "~57.0.6", - "chalk": "^4.1.2" - } - }, - "node_modules/@expo/local-build-cache-provider/node_modules/@expo/config": { - "version": "57.0.6", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-57.0.6.tgz", - "integrity": "sha512-VpMJpB/De/fb9bBFVVBiK6Ntg9lt0kAleLH9hcZz85CYRUQ3jVFVA8rNC5f8y4cp2+FiiPNFp62+kEOFI6pDiw==", - "license": "MIT", - "dependencies": { - "@expo/config-plugins": "~57.0.6", - "@expo/config-types": "^57.0.2", - "@expo/json-file": "^11.0.1", - "@expo/require-utils": "^57.0.4", - "deepmerge": "^4.3.1", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "resolve-workspace-root": "^2.0.0", - "semver": "^7.6.0", - "slugify": "^1.3.4" - } - }, - "node_modules/@expo/local-build-cache-provider/node_modules/@expo/config-plugins": { - "version": "57.0.6", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-57.0.6.tgz", - "integrity": "sha512-7CmKrS5Rnu8aSZyNlxH2qzA7Ls1HEa4EQvEVOAkHDKPr1e4Cg/nz7I7dUl09QDTVjMvkKYDH4Th0DuAsgqASaw==", - "license": "MIT", - "dependencies": { - "@expo/config-types": "^57.0.2", - "@expo/json-file": "~11.0.1", - "@expo/plist": "^0.8.1", - "@expo/require-utils": "^57.0.4", - "@expo/sdk-runtime-versions": "^1.0.0", - "chalk": "^4.1.2", - "debug": "^4.3.5", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "semver": "^7.5.4", - "slugify": "^1.6.6", - "xcode": "^3.0.1", - "xml2js": "0.6.0" - } - }, - "node_modules/@expo/local-build-cache-provider/node_modules/@expo/config-types": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-57.0.2.tgz", - "integrity": "sha512-ewW08OonrcRIsRKIlFvvcmmafE5zemb1ocu3HkNwtVPyRtj2w42pZCAkMIROYpcVBaPnc3mDT9UZDzwXWC3i6g==", - "license": "MIT" - }, - "node_modules/@expo/local-build-cache-provider/node_modules/@expo/json-file": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-11.0.1.tgz", - "integrity": "sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "json5": "^2.2.3" - } - }, - "node_modules/@expo/local-build-cache-provider/node_modules/@expo/plist": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.8.1.tgz", - "integrity": "sha512-3gTReGIUm0oRaMClsAJYxBnVPCl6fVpsl8HS+DTVxDhW4GyVyxg9E/Znm3BvcHtUJ51RJJI14pC1wvrNilCRHw==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - } - }, - "node_modules/@expo/local-build-cache-provider/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@expo/log-box": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-57.0.2.tgz", - "integrity": "sha512-ZsFyfIR7YCbQAdVLzuTUmMHofZC7ZS9ywYCJNPlLc78x59cI8GwXFEIVbRjjC0uJERpNtXx/tsNNnkhexXlzMw==", - "license": "MIT", - "dependencies": { - "@expo/dom-webview": "^57.0.1", - "anser": "^1.4.9", - "stacktrace-parser": "^0.1.10" - }, - "peerDependencies": { - "@expo/dom-webview": "^57.0.1", - "expo": "*", - "react": "*", - "react-native": "*" - } - }, "node_modules/@expo/metro": { - "version": "56.0.0", - "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-56.0.0.tgz", - "integrity": "sha512-5gIgQHtEpjjvsjKfVtIv23a98LLRV0/y07PDShEwYSytAMlE3FSF8RHXqtHc1sUJL6dn7hnuIBpIbrLXXuVi0A==", - "license": "MIT", - "dependencies": { - "metro": "0.84.4", - "metro-babel-transformer": "0.84.4", - "metro-cache": "0.84.4", - "metro-cache-key": "0.84.4", - "metro-config": "0.84.4", - "metro-core": "0.84.4", - "metro-file-map": "0.84.4", - "metro-minify-terser": "0.84.4", - "metro-resolver": "0.84.4", - "metro-runtime": "0.84.4", - "metro-source-map": "0.84.4", - "metro-symbolicate": "0.84.4", - "metro-transform-plugins": "0.84.4", - "metro-transform-worker": "0.84.4" - } - }, - "node_modules/@expo/metro-config": { - "version": "57.0.7", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.7.tgz", - "integrity": "sha512-bVfEkg4zF1cA62OqAdYXmFOooJ6TB/I+REi7Se6Ct+PbSC+89TwSqWXnYx34L08eIs4z+1ilgbATakTZpgefmQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~57.0.6", - "@expo/env": "~2.4.2", - "@expo/json-file": "~11.0.1", - "@expo/metro": "~56.0.0", - "@expo/require-utils": "^57.0.4", - "@expo/spawn-async": "^1.8.0", - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/remapping": "^2.3.5", - "@jridgewell/sourcemap-codec": "^1.5.5", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.36.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, - "node_modules/@expo/metro-config/node_modules/@expo/config": { - "version": "57.0.6", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-57.0.6.tgz", - "integrity": "sha512-VpMJpB/De/fb9bBFVVBiK6Ntg9lt0kAleLH9hcZz85CYRUQ3jVFVA8rNC5f8y4cp2+FiiPNFp62+kEOFI6pDiw==", - "license": "MIT", - "dependencies": { - "@expo/config-plugins": "~57.0.6", - "@expo/config-types": "^57.0.2", - "@expo/json-file": "^11.0.1", - "@expo/require-utils": "^57.0.4", - "deepmerge": "^4.3.1", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "resolve-workspace-root": "^2.0.0", - "semver": "^7.6.0", - "slugify": "^1.3.4" - } - }, - "node_modules/@expo/metro-config/node_modules/@expo/config-plugins": { - "version": "57.0.6", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-57.0.6.tgz", - "integrity": "sha512-7CmKrS5Rnu8aSZyNlxH2qzA7Ls1HEa4EQvEVOAkHDKPr1e4Cg/nz7I7dUl09QDTVjMvkKYDH4Th0DuAsgqASaw==", - "license": "MIT", - "dependencies": { - "@expo/config-types": "^57.0.2", - "@expo/json-file": "~11.0.1", - "@expo/plist": "^0.8.1", - "@expo/require-utils": "^57.0.4", - "@expo/sdk-runtime-versions": "^1.0.0", - "chalk": "^4.1.2", - "debug": "^4.3.5", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "semver": "^7.5.4", - "slugify": "^1.6.6", - "xcode": "^3.0.1", - "xml2js": "0.6.0" - } - }, - "node_modules/@expo/metro-config/node_modules/@expo/config-types": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-57.0.2.tgz", - "integrity": "sha512-ewW08OonrcRIsRKIlFvvcmmafE5zemb1ocu3HkNwtVPyRtj2w42pZCAkMIROYpcVBaPnc3mDT9UZDzwXWC3i6g==", - "license": "MIT" - }, - "node_modules/@expo/metro-config/node_modules/@expo/env": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.4.2.tgz", - "integrity": "sha512-28pqaEqwnmLduZ00Pq9HkSzE5wbj1MTwp5/n8nm8rD8MCjR9eUnVOwmNksPI3Be2ReAPO/DbPn1puy0mvoocsQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "debug": "^4.3.4", - "getenv": "^2.0.0" - }, - "engines": { - "node": ">=20.12.0" - } - }, - "node_modules/@expo/metro-config/node_modules/@expo/json-file": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-11.0.1.tgz", - "integrity": "sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ==", + "version": "54.2.0", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz", + "integrity": "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.20.0", - "json5": "^2.2.3" - } - }, - "node_modules/@expo/metro-config/node_modules/@expo/plist": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.8.1.tgz", - "integrity": "sha512-3gTReGIUm0oRaMClsAJYxBnVPCl6fVpsl8HS+DTVxDhW4GyVyxg9E/Znm3BvcHtUJ51RJJI14pC1wvrNilCRHw==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - } - }, - "node_modules/@expo/metro-config/node_modules/hermes-estree": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.1.tgz", - "integrity": "sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw==", - "license": "MIT" - }, - "node_modules/@expo/metro-config/node_modules/hermes-parser": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.1.tgz", - "integrity": "sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.36.1" - } - }, - "node_modules/@expo/metro-config/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@expo/metro-config/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@expo/metro-file-map": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.1.tgz", - "integrity": "sha512-8JXfVstZN7QnP4NianZZnlTVboOWR0sG8trUDNajOjnbGlPln29vponXM84tY+3tAHapz5/TxE53L0ixUwqPtA==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "fb-watchman": "^2.0.2", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3" } }, "node_modules/@expo/metro-runtime": { @@ -2266,370 +2045,6 @@ } } }, - "node_modules/@expo/metro/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@expo/metro/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@expo/metro/node_modules/hermes-estree": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", - "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", - "license": "MIT" - }, - "node_modules/@expo/metro/node_modules/hermes-parser": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", - "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.35.0" - } - }, - "node_modules/@expo/metro/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@expo/metro/node_modules/metro": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.4.tgz", - "integrity": "sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "accepts": "^2.0.0", - "ci-info": "^2.0.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "error-stack-parser": "^2.0.6", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "hermes-parser": "0.35.0", - "image-size": "^1.0.2", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "jsc-safe-url": "^0.2.2", - "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.84.4", - "metro-cache": "0.84.4", - "metro-cache-key": "0.84.4", - "metro-config": "0.84.4", - "metro-core": "0.84.4", - "metro-file-map": "0.84.4", - "metro-resolver": "0.84.4", - "metro-runtime": "0.84.4", - "metro-source-map": "0.84.4", - "metro-symbolicate": "0.84.4", - "metro-transform-plugins": "0.84.4", - "metro-transform-worker": "0.84.4", - "mime-types": "^3.0.1", - "nullthrows": "^1.1.1", - "serialize-error": "^2.1.0", - "source-map": "^0.5.6", - "throat": "^5.0.0", - "ws": "^7.5.10", - "yargs": "^17.6.2" - }, - "bin": { - "metro": "src/cli.js" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-babel-transformer": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.4.tgz", - "integrity": "sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.35.0", - "metro-cache-key": "0.84.4", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-cache": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.4.tgz", - "integrity": "sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==", - "license": "MIT", - "dependencies": { - "exponential-backoff": "^3.1.1", - "flow-enums-runtime": "^0.0.6", - "https-proxy-agent": "^7.0.5", - "metro-core": "0.84.4" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-cache-key": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.4.tgz", - "integrity": "sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-config": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.4.tgz", - "integrity": "sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==", - "license": "MIT", - "dependencies": { - "connect": "^3.6.5", - "flow-enums-runtime": "^0.0.6", - "jest-validate": "^29.7.0", - "metro": "0.84.4", - "metro-cache": "0.84.4", - "metro-core": "0.84.4", - "metro-runtime": "0.84.4", - "yaml": "^2.6.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-core": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.4.tgz", - "integrity": "sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "lodash.throttle": "^4.1.1", - "metro-resolver": "0.84.4" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-file-map": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.4.tgz", - "integrity": "sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "fb-watchman": "^2.0.0", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "nullthrows": "^1.1.1", - "walker": "^1.0.7" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-minify-terser": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.4.tgz", - "integrity": "sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "terser": "^5.15.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-resolver": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.4.tgz", - "integrity": "sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-runtime": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.4.tgz", - "integrity": "sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.0", - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-source-map": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.4.tgz", - "integrity": "sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-symbolicate": "0.84.4", - "nullthrows": "^1.1.1", - "ob1": "0.84.4", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-symbolicate": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.4.tgz", - "integrity": "sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-source-map": "0.84.4", - "nullthrows": "^1.1.1", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "bin": { - "metro-symbolicate": "src/index.js" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-transform-plugins": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.4.tgz", - "integrity": "sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/metro-transform-worker": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.4.tgz", - "integrity": "sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "metro": "0.84.4", - "metro-babel-transformer": "0.84.4", - "metro-cache": "0.84.4", - "metro-cache-key": "0.84.4", - "metro-minify-terser": "0.84.4", - "metro-source-map": "0.84.4", - "metro-transform-plugins": "0.84.4", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@expo/metro/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@expo/metro/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@expo/metro/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@expo/metro/node_modules/ob1": { - "version": "0.84.4", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.4.tgz", - "integrity": "sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, "node_modules/@expo/osascript": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.7.1.tgz", @@ -2678,16 +2093,16 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "54.0.8", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz", - "integrity": "sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==", + "version": "54.0.9", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.9.tgz", + "integrity": "sha512-3/Rmyzt8vduPjnSVHbnc0wYFrlhwLWn2g596rDyKcLGeqN2WTLJbzVeznsrUwyzhBNXgnTomZWO5HDzbZ/4E7g==", "license": "MIT", "dependencies": { - "@expo/config": "~12.0.13", - "@expo/config-plugins": "~54.0.4", + "@expo/config": "~12.0.14", + "@expo/config-plugins": "~54.0.5", "@expo/config-types": "^54.0.10", "@expo/image-utils": "^0.8.8", - "@expo/json-file": "^10.0.8", + "@expo/json-file": "^10.0.16", "@react-native/normalize-colors": "0.81.5", "debug": "^4.3.1", "resolve-from": "^5.0.0", @@ -2699,9 +2114,9 @@ } }, "node_modules/@expo/prebuild-config/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2711,9 +2126,9 @@ } }, "node_modules/@expo/require-utils": { - "version": "57.0.4", - "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-57.0.4.tgz", - "integrity": "sha512-e7xbg/9BTQcsZE/oErafZXtI7kh5IgfasLJ97J5sFSzX2cA74pDvdlhW1KHVSaDkQyQv6h1LSLhsY7dEeOk7hw==", + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.6.tgz", + "integrity": "sha512-IqsRGPdGbF0lAIrg3XQ+GzcITYcsskvIRmFAjw2kBnr8RgSrRhFAJY1bKksukEUsuZy0knhTCVIIUce7hocqeA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -2721,7 +2136,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { - "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" + "typescript": "^5.0.0 || ^5.0.0-0" }, "peerDependenciesMeta": { "typescript": { @@ -2770,6 +2185,12 @@ "react-native": "*" } }, + "node_modules/@expo/ws-tunnel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", + "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==", + "license": "MIT" + }, "node_modules/@expo/xcpretty": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.4.tgz", @@ -2874,12 +2295,24 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@ide/backoff": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", - "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", - "license": "MIT" - }, + "node_modules/@ide/backoff": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", + "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", + "license": "MIT" + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -2914,15 +2347,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2937,9 +2361,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -3063,22 +2487,6 @@ } } }, - "node_modules/@jest/core/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/create-cache-key-function": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", @@ -3232,36 +2640,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@jest/reporters/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -3420,9 +2798,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz", - "integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", "dev": true, "license": "MIT", "optional": true, @@ -3490,9 +2868,9 @@ } }, "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", "license": "MIT" }, "node_modules/@radix-ui/react-compose-refs": { @@ -3511,9 +2889,9 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3526,9 +2904,9 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3541,9 +2919,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3556,12 +2934,12 @@ } }, "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3592,9 +2970,9 @@ } }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3607,13 +2985,14 @@ } }, "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3626,12 +3005,12 @@ } }, "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -3643,14 +3022,11 @@ } } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3662,9 +3038,9 @@ } }, "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3686,54 +3062,77 @@ } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.2.tgz", - "integrity": "sha512-NNDZqOlNbH5SzgPks1jFDYH3234Rpa5e/nhZymxhIiBH3NcE3uD+rGj/HWXhH7nHF2ToGK6XbUpqy7nmJPeh+g==", + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.81.5.tgz", + "integrity": "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.86.2" + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.81.5" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" } }, - "node_modules/@react-native/babel-plugin-codegen/node_modules/@react-native/codegen": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.2.tgz", - "integrity": "sha512-xKkudsahUJ1n//55g4fXk5BStVqqmZlz8HQveL45ZxcfDnwvhuYe2GymksQANFsSN+slvrarjrfq8kIxJzbceA==", + "node_modules/@react-native/babel-preset": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.81.5.tgz", + "integrity": "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", - "@babel/parser": "^7.29.0", - "hermes-parser": "0.36.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "tinyglobby": "^0.2.15", - "yargs": "^17.6.2" + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.81.5", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" }, "peerDependencies": { "@babel/core": "*" } }, - "node_modules/@react-native/babel-plugin-codegen/node_modules/hermes-estree": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.0.tgz", - "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==", - "license": "MIT" - }, - "node_modules/@react-native/babel-plugin-codegen/node_modules/hermes-parser": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.0.tgz", - "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.36.0" - } - }, "node_modules/@react-native/codegen": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.81.5.tgz", @@ -3822,9 +3221,9 @@ } }, "node_modules/@react-native/community-cli-plugin/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3842,20 +3241,6 @@ "node": ">= 20.19.4" } }, - "node_modules/@react-native/debugger-shell": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.2.tgz", - "integrity": "sha512-/TaVJ2+gGajZPJGrFaObUQmHmlaxAlfmOPZicl6pNKDUjzSgFMpcLkdTOExvb+USYTVdGX1XwxXyvjQdUO2bvg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "fb-dotslash": "0.5.8" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, "node_modules/@react-native/dev-middleware": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.81.5.tgz", @@ -3879,9 +3264,9 @@ } }, "node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.4.tgz", - "integrity": "sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.6.tgz", + "integrity": "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA==", "license": "MIT", "dependencies": { "async-limiter": "~1.0.0" @@ -3912,17 +3297,17 @@ "license": "MIT" }, "node_modules/@react-navigation/bottom-tabs": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.16.1.tgz", - "integrity": "sha512-wjFATJmbq0K8B96Ax0JcK2+Eu7syfYvQ5qUd/tgcv8JuCYLwKKqojJMAl31qdjpKqFG09pQ6TSdEDHOek60CAA==", + "version": "7.18.16", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.16.tgz", + "integrity": "sha512-5tBFuxvsCQBvYauDZK6u/uPdD86i/caWgBzABlZfTh8W2J7YZSHjT9SWOsquSMmR6dpcNj6idMd4utCNzuOnZg==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.18", + "@react-navigation/elements": "^2.9.38", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { - "@react-navigation/native": "^7.2.4", + "@react-navigation/native": "^7.3.16", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3930,12 +3315,12 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.17.4", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.4.tgz", - "integrity": "sha512-Rv9E2oNNQEkPGpmu9q+vJwGJRSQR6LBg5L+Yo1QHjtwGbHUbjkIKOdYymDZoZYgNzX2OD4rAIlfuzbDKa3cCeA==", + "version": "7.21.12", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.21.12.tgz", + "integrity": "sha512-FopGMGy5df+IMqUYg9w7ygwK0oP4RaQbMnpb7VFP30+vZdd8MqsAGc+bZZwdiVSnzK9hzhgJtZV9XF0ZFlUYOQ==", "license": "MIT", "dependencies": { - "@react-navigation/routers": "^7.5.5", + "@react-navigation/routers": "^7.6.4", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -3949,9 +3334,9 @@ } }, "node_modules/@react-navigation/elements": { - "version": "2.9.18", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.18.tgz", - "integrity": "sha512-mKEvDr6CkCVYZSb8W9WubNseihL+1c8M7ktZJCTCbMk8rQgdQfkdRNwpSUQKspdGpUHCb9cyzvaiuzl1NtjVgw==", + "version": "2.9.38", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.38.tgz", + "integrity": "sha512-M/2HWDiSO4zO7VyfAnovrTPWOAzTC+1DnVT7ZWZW93+/KVRF9OYlqixIUpPenQ0iQh2j1EaQ4zOe/134Arisqw==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -3960,7 +3345,7 @@ }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.2.4", + "@react-navigation/native": "^7.3.16", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -3972,15 +3357,16 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.4.tgz", - "integrity": "sha512-eWC2D3JjhYLId2fVTZhhCiUpWIaPhO9XyEb7Wq8ElmOHyIODlbOzgZ0rKia02OIsDKr9BzZl2sK1dL70yMxDaw==", + "version": "7.3.16", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.3.16.tgz", + "integrity": "sha512-Wy/6mai2HpA5AEVFk7JFfvv4RUArwuN2UenP/fc3NK7kOyfSlNu1tmb/3JDPpMzPIRbF1MPB8PKAHjVB29qNEQ==", "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.17.4", + "@react-navigation/core": "^7.21.12", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", + "standard-navigation": "^0.0.8", "use-latest-callback": "^0.2.4" }, "peerDependencies": { @@ -3989,18 +3375,18 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.15.1", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.15.1.tgz", - "integrity": "sha512-kNrJggwoB/onC0MpZIuZ6qaqeAziFchz+W9txBzhd6qbWmB1OkPVUnu6fWgc6BQc7MeMf59djVmqgX+6kJU1Ug==", + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.18.8.tgz", + "integrity": "sha512-Abcdt2ndmbeNSzJCXggxduK7X9yvMIajPNPdHgxsAZaA+16aIhb7TXZ2kRXprStZX5TRg8x+8G07Ne3kFwIQDg==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.18", + "@react-navigation/elements": "^2.9.38", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.2.4", + "@react-navigation/native": "^7.3.16", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -4008,9 +3394,9 @@ } }, "node_modules/@react-navigation/routers": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.5.tgz", - "integrity": "sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.6.4.tgz", + "integrity": "sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w==", "license": "MIT", "dependencies": { "nanoid": "^3.3.11" @@ -4024,9 +3410,9 @@ "license": "MIT" }, "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", "license": "MIT" }, "node_modules/@sinonjs/commons": { @@ -4048,9 +3434,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.100.14", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz", - "integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==", + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", "license": "MIT", "funding": { "type": "github", @@ -4058,12 +3444,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.100.14", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz", - "integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==", + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.100.14" + "@tanstack/query-core": "5.101.4" }, "funding": { "type": "github", @@ -4260,12 +3646,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/@types/react": { @@ -4300,17 +3686,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", - "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.4", - "@typescript-eslint/type-utils": "8.59.4", - "@typescript-eslint/utils": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -4323,15 +3709,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.4", + "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", "engines": { @@ -4339,16 +3725,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", - "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/typescript-estree": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "engines": { @@ -4364,14 +3750,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", - "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.4", - "@typescript-eslint/types": "^8.59.4", + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "engines": { @@ -4386,14 +3772,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", - "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4" + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4404,9 +3790,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", - "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", "dev": true, "license": "MIT", "engines": { @@ -4421,15 +3807,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", - "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/typescript-estree": "8.59.4", - "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -4446,9 +3832,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", - "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", "dev": true, "license": "MIT", "engines": { @@ -4460,16 +3846,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", - "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.4", - "@typescript-eslint/tsconfig-utils": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4", + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -4511,13 +3897,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -4527,9 +3913,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -4540,16 +3926,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", - "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/typescript-estree": "8.59.4" + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4564,13 +3950,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", - "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4595,9 +3981,9 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", - "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -4913,6 +4299,29 @@ "win32" ] }, + "node_modules/@urql/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", + "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.13", + "wonka": "^6.3.2" + } + }, + "node_modules/@urql/exchange-retry": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz", + "integrity": "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==", + "license": "MIT", + "dependencies": { + "@urql/core": "^5.1.2", + "wonka": "^6.3.2" + }, + "peerDependencies": { + "@urql/core": "^5.0.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.13", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", @@ -4948,9 +4357,9 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4981,18 +4390,6 @@ "node": ">= 6.0.0" } }, - "node_modules/agent-cli-detector": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.5.tgz", - "integrity": "sha512-6xvLw0EGPuxoYGZeqyMV4AK+WoZ31jsqb7a5pln1BTf6oDFsrgvfCJT7E7y9oAqifJZhJ3fZ0J36H7o6JzrB0Q==", - "license": "MIT", - "bin": { - "agent-cli-detector": "dist/cli.js" - }, - "engines": { - "node": ">=18.18" - } - }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -5327,13 +4724,13 @@ } }, "node_modules/axios": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", - "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -5375,6 +4772,22 @@ "node": ">=8" } }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", @@ -5492,106 +4905,15 @@ "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-expo": { - "version": "57.0.5", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.5.tgz", - "integrity": "sha512-sz9ZBTiUAlu5P9VORVNKoeAaY2qKFQRC6eQV5Y8aMkqcorvXKEv97UeCkFMLmMBEPKA+QeWImREKkX9/H21WQA==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.20.5", - "@babel/helper-module-imports": "^7.25.9", - "@babel/plugin-proposal-decorators": "^7.12.9", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.28.6", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.25.2", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-plugin-codegen": "0.86.2", - "babel-plugin-react-compiler": "^1.0.0", - "babel-plugin-react-native-web": "~0.21.0", - "babel-plugin-syntax-hermes-parser": "^0.36.0", - "babel-plugin-transform-flow-enums": "^0.0.2", - "debug": "^4.3.4" - }, - "peerDependencies": { - "@babel/runtime": "^7.20.0", - "expo": "*", - "expo-widgets": "^57.0.7", - "react-refresh": ">=0.14.0 <1.0.0" - }, - "peerDependenciesMeta": { - "@babel/runtime": { - "optional": true - }, - "expo": { - "optional": true - }, - "expo-widgets": { - "optional": true - } - } - }, - "node_modules/babel-preset-expo/node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.36.1.tgz", - "integrity": "sha512-ycduwJbvdvIMmVvlAZqGggS+pm5Eu4Bk9pcV9Sm2Z4PJNRVsKkv0g7vHj+LeuC1gHTeF67sJXFOq61IlqCa2hA==", - "license": "MIT", - "dependencies": { - "hermes-parser": "0.36.1" - } - }, - "node_modules/babel-preset-expo/node_modules/hermes-estree": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.1.tgz", - "integrity": "sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw==", - "license": "MIT" - }, - "node_modules/babel-preset-expo/node_modules/hermes-parser": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.1.tgz", - "integrity": "sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.36.1" + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -5643,9 +4965,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", - "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -5654,6 +4976,35 @@ "node": ">=6.0.0" } }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "license": "MIT", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/better-opn/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -5713,9 +5064,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "funding": [ { "type": "opencollective", @@ -5732,11 +5083,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -5767,6 +5118,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5840,21 +5215,18 @@ } }, "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "funding": [ { "type": "opencollective", @@ -5897,6 +5269,15 @@ "node": ">=10" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/chrome-launcher": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", @@ -5930,10 +5311,19 @@ } }, "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "license": "MIT" + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/cjs-module-lexer": { "version": "1.4.3", @@ -6270,15 +5660,6 @@ "node": ">=8.0.0" } }, - "node_modules/css-tree/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/css-what": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", @@ -6520,6 +5901,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6565,6 +5955,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -6660,12 +6059,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dnssd-advertise": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.6.tgz", - "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==", - "license": "MIT" - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6782,9 +6175,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.401", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz", - "integrity": "sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==", + "version": "1.5.404", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.404.tgz", + "integrity": "sha512-3WJtd7/lVq2Jnuz6wed1l9+1ZD2u2Tet1/1NBc4Iedkmgbu+I7YuAqdAQ8T+VZtnwysMsAf3IqSq9D1gyZjA2g==", "license": "ISC" }, "node_modules/emittery": { @@ -6827,6 +6220,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-editor": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", + "integrity": "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -6837,13 +6239,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, "node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -6922,6 +6317,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -6941,9 +6355,9 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", - "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7009,15 +6423,18 @@ } }, "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", "dev": true, "license": "MIT", "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -7201,9 +6618,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", "dev": true, "license": "MIT", "dependencies": { @@ -7229,14 +6646,14 @@ } }, "node_modules/eslint-plugin-expo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-1.0.3.tgz", - "integrity": "sha512-C1v9NPvpDET36+7Klpp/+53Jl+VzOfpbDxpKtL/pAPhCDwTX0kW6Swo425PT0uc4AMT5jpQbB7hSKFjKOGMl4A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-1.1.0.tgz", + "integrity": "sha512-vPP0EPx7IA7ZfP49dY4rq9RV5jqkFWG+Pih3/oGjzIRjMI+ogcOE8i6isYkLXAdw/yvFV2BRZkTaQaiOGQqn6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "^8.29.1", - "@typescript-eslint/utils": "^8.29.1", + "@typescript-eslint/types": "^8.59.0", + "@typescript-eslint/utils": "^8.59.0", "eslint": "^9.24.0" }, "engines": { @@ -7247,9 +6664,9 @@ } }, "node_modules/eslint-plugin-expo/node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, "license": "MIT", "dependencies": { @@ -7259,7 +6676,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -7271,9 +6688,9 @@ } }, "node_modules/eslint-plugin-expo/node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { @@ -7284,9 +6701,9 @@ } }, "node_modules/eslint-plugin-expo/node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", "dependencies": { @@ -7295,8 +6712,8 @@ "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7696,32 +7113,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -7749,34 +7140,32 @@ } }, "node_modules/expo": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.10.tgz", - "integrity": "sha512-nirZEdsA4ZKkzOZX3sqzpArc2tnVAvdiFmm0gZRmIckl0FnSgC3RGp27JJZ2NzqkEaVZ5e06dFvX/NECdK5cIQ==", + "version": "54.0.36", + "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.36.tgz", + "integrity": "sha512-HMHp1H+actmnX85NJE6lILKzSJV6pTDNkwghq9EMOP3zTynjvBYVqJGSLlm6sEVzJCC5Z2ZiKgLvtsHrqlY0dg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^57.0.12", - "@expo/config": "~57.0.6", - "@expo/config-plugins": "~57.0.6", - "@expo/devtools": "~57.0.1", - "@expo/dom-webview": "~57.0.1", - "@expo/fingerprint": "^0.20.6", - "@expo/local-build-cache-provider": "^57.0.5", - "@expo/log-box": "^57.0.2", - "@expo/metro": "~56.0.0", - "@expo/metro-config": "~57.0.7", + "@expo/cli": "54.0.26", + "@expo/config": "~12.0.14", + "@expo/config-plugins": "~54.0.5", + "@expo/devtools": "0.1.8", + "@expo/fingerprint": "0.15.5", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "54.0.17", + "@expo/vector-icons": "^15.0.3", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~57.0.5", - "expo-asset": "~57.0.8", - "expo-constants": "~57.0.9", - "expo-file-system": "~57.0.1", - "expo-font": "~57.0.1", - "expo-keep-awake": "~57.0.1", - "expo-modules-autolinking": "~57.0.9", - "expo-modules-core": "~57.0.9", + "babel-preset-expo": "~54.0.12", + "expo-asset": "~12.0.13", + "expo-constants": "~18.0.13", + "expo-file-system": "~19.0.23", + "expo-font": "~14.0.12", + "expo-keep-awake": "~15.0.8", + "expo-modules-autolinking": "3.0.26", + "expo-modules-core": "3.0.30", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", - "whatwg-url-minimum": "^0.1.2" + "whatwg-url-without-unicode": "8.0.0-3" }, "bin": { "expo": "bin/cli", @@ -7787,9 +7176,7 @@ "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", - "react-dom": "*", "react-native": "*", - "react-native-web": "*", "react-native-webview": "*" }, "peerDependenciesMeta": { @@ -7799,12 +7186,6 @@ "@expo/metro-runtime": { "optional": true }, - "react-dom": { - "optional": true - }, - "react-native-web": { - "optional": true - }, "react-native-webview": { "optional": true } @@ -7820,13 +7201,13 @@ } }, "node_modules/expo-asset": { - "version": "57.0.8", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.8.tgz", - "integrity": "sha512-sGocG+Wd2WcZ1KjKyB2LfUZXzrBM0VHEATGcpJKF9T3HM56M/sMbH73vv+n85u5Zr0FkJjEbV1DWhGPimCR1QQ==", + "version": "12.0.13", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.13.tgz", + "integrity": "sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ==", "license": "MIT", "dependencies": { - "@expo/image-utils": "^0.11.4", - "expo-constants": "~57.0.8" + "@expo/image-utils": "^0.8.8", + "expo-constants": "~18.0.13" }, "peerDependencies": { "expo": "*", @@ -7834,60 +7215,18 @@ "react-native": "*" } }, - "node_modules/expo-asset/node_modules/@expo/env": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.4.2.tgz", - "integrity": "sha512-28pqaEqwnmLduZ00Pq9HkSzE5wbj1MTwp5/n8nm8rD8MCjR9eUnVOwmNksPI3Be2ReAPO/DbPn1puy0mvoocsQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "debug": "^4.3.4", - "getenv": "^2.0.0" - }, - "engines": { - "node": ">=20.12.0" - } - }, - "node_modules/expo-asset/node_modules/@expo/image-utils": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.11.4.tgz", - "integrity": "sha512-pn/4770DIEOcYZr484uazuwg20FX/qaDkeMRF6J+oxejynDmEmO8wLsCudaNShFE0BhyKGQTYrs2rsRhqrqESw==", - "license": "MIT", - "dependencies": { - "@expo/require-utils": "^57.0.4", - "@expo/spawn-async": "^1.8.0", - "chalk": "^4.0.0", - "getenv": "^2.0.0", - "jimp-compact": "0.16.1", - "parse-png": "^2.1.0", - "semver": "^7.6.0" - } - }, - "node_modules/expo-asset/node_modules/expo-constants": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.9.tgz", - "integrity": "sha512-Y47sGiF+U8fwicUSPdJPjB27PuU+FgLK4Mpai0ksZF5hv8jNN3HBqKuDNxsiu70uHBKf80TaR4gwMPh0pmETiQ==", + "node_modules/expo-audio": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/expo-audio/-/expo-audio-1.1.1.tgz", + "integrity": "sha512-CPCpJ+0AEHdzWROc0f00Zh6e+irLSl2ALos/LPvxEeIcJw1APfBa4DuHPkL4CQCWsVe7EnUjFpdwpqsEUWcP0g==", "license": "MIT", - "dependencies": { - "@expo/env": "~2.4.2" - }, "peerDependencies": { "expo": "*", + "expo-asset": "*", + "react": "*", "react-native": "*" } }, - "node_modules/expo-asset/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/expo-constants": { "version": "18.0.13", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", @@ -7914,16 +7253,6 @@ "expo": "*" } }, - "node_modules/expo-file-system": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.1.tgz", - "integrity": "sha512-w7/ERvQFrGP2apTO9lDtZ+O6JQIhfakL7+Xqzh+rfMO9B4LB4qwrz+YvLgir8KFRVX64JHBnRuYBVLY1oQZcqw==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "node_modules/expo-font": { "version": "14.0.12", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.12.tgz", @@ -7938,16 +7267,6 @@ "react-native": "*" } }, - "node_modules/expo-keep-awake": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.1.tgz", - "integrity": "sha512-28lkFImeXTS+bhAjuCFV7w7tW5bXg27BJVrxv+nC/nyYa86qEa0oFeHwqol6ha5k4pdVDQgBF09GM4A1k76Ssg==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*" - } - }, "node_modules/expo-linking": { "version": "8.0.12", "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz", @@ -7962,27 +7281,41 @@ "react-native": "*" } }, + "node_modules/expo-location": { + "version": "19.0.8", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-19.0.8.tgz", + "integrity": "sha512-H/FI75VuJ1coodJbbMu82pf+Zjess8X8Xkiv9Bv58ZgPKS/2ztjC1YO1/XMcGz7+s9DrbLuMIw22dFuP4HqneA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.9.tgz", - "integrity": "sha512-lj2nsAKMMRLXSFnGgaaQrWJ2fdSpLPc/bca6Rkiw4g8zYPn7qX4MRUJgavvzm/hBrzvMnlhXVgJtidOGuwBh+w==", + "version": "3.0.26", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.26.tgz", + "integrity": "sha512-WOaud6UKg16ciCOj8raKcMOoKFMHLXKI29U29yhgu1lf+Y7VxJyCktUcYo6AM+ccZ7zLD1uWZdMtgnpf+95OXA==", "license": "MIT", "dependencies": { - "@expo/require-utils": "^57.0.4", - "@expo/spawn-async": "^1.8.0", + "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", - "commander": "^7.2.0" + "commander": "^7.2.0", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, - "node_modules/expo-modules-jsi": { - "version": "57.0.4", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.4.tgz", - "integrity": "sha512-vt7FyqUqqFXiRVnBqYD7y+GSPTgeua5Ocoy0+SYt+RSHkZEA2Fyop7If3g1TYDzQObYybPRo7TG2Rle1XLaWFw==", + "node_modules/expo-modules-core": { + "version": "3.0.30", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.30.tgz", + "integrity": "sha512-a6IrpAn/Jbmwxi9L+hMmXKpNqnkUpoF7WHOpn02rVLyax2J0gB1vvCVE5rNydplEnt41Q6WxQwvcOjZaIkcSUg==", "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, "peerDependencies": { + "react": "*", "react-native": "*" } }, @@ -8078,15 +7411,15 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -8104,12 +7437,12 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", @@ -8121,14 +7454,28 @@ } } }, - "node_modules/expo-router/node_modules/@radix-ui/react-presence": { + "node_modules/expo-router/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/expo-router/node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -8146,12 +7493,12 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -8169,12 +7516,12 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", @@ -8187,20 +7534,22 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -8218,19 +7567,19 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -8304,37 +7653,35 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "57.0.12", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.12.tgz", - "integrity": "sha512-mmOcZJmyEDYtEtHr5e4mBr8O+Y2WBSIhqY8PL5uY3EjQts/5vxKNBYuUqIknGUvZHGpdIjAQ1IjaPNQiAf8uPQ==", + "version": "54.0.26", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.26.tgz", + "integrity": "sha512-BjsAoKINLEo3LRE+sDC6FCgjxuOWsyfOFOKz0txrbEcxSatzIjJDVuX8XaTdmeicZdcoN524yl1sfwCWfxhYMw==", "license": "MIT", "dependencies": { + "@0no-co/graphql.web": "^1.0.8", "@expo/code-signing-certificates": "^0.0.6", - "@expo/config": "~57.0.6", - "@expo/config-plugins": "~57.0.6", + "@expo/config": "~12.0.14", + "@expo/config-plugins": "~54.0.5", "@expo/devcert": "^1.2.1", - "@expo/env": "~2.4.2", - "@expo/image-utils": "^0.11.4", - "@expo/inline-modules": "^0.1.4", - "@expo/json-file": "^11.0.1", - "@expo/log-box": "^57.0.2", - "@expo/metro": "~56.0.0", - "@expo/metro-config": "~57.0.7", - "@expo/metro-file-map": "^57.0.1", - "@expo/osascript": "^2.7.1", - "@expo/package-manager": "^1.13.1", - "@expo/plist": "^0.8.1", - "@expo/prebuild-config": "^57.0.10", - "@expo/require-utils": "^57.0.4", - "@expo/router-server": "^57.0.5", - "@expo/schema-utils": "^57.0.2", - "@expo/spawn-async": "^1.8.0", - "@expo/ws-tunnel": "^2.0.0", - "@expo/xcpretty": "^4.4.4", - "@react-native/dev-middleware": "0.86.2", + "@expo/env": "~2.0.12", + "@expo/image-utils": "^0.8.8", + "@expo/json-file": "^10.0.16", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "~54.0.17", + "@expo/osascript": "^2.3.8", + "@expo/package-manager": "^1.9.10", + "@expo/plist": "^0.4.9", + "@expo/prebuild-config": "^54.0.9", + "@expo/schema-utils": "^0.1.9", + "@expo/spawn-async": "^1.7.2", + "@expo/ws-tunnel": "^1.0.1", + "@expo/xcpretty": "^4.3.0", + "@react-native/dev-middleware": "0.81.5", + "@urql/core": "^5.0.6", + "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", - "agent-cli-detector": "^0.1.2", "arg": "^5.0.2", + "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", @@ -8342,34 +7689,41 @@ "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", - "dnssd-advertise": "^1.1.4", - "expo-server": "^57.0.1", - "fetch-nodeshim": "^0.4.10", + "env-editor": "^0.4.1", + "expo-server": "^1.0.7", + "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", - "multitars": "^1.0.0", + "minimatch": "^9.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", - "picomatch": "^4.0.4", + "picomatch": "^4.0.3", + "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", + "qrcode-terminal": "0.11.0", + "require-from-string": "^2.0.2", + "requireg": "^0.2.2", + "resolve": "^1.22.2", "resolve-from": "^5.0.0", + "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", + "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", + "tar": "^7.5.2", "terminal-link": "^2.1.1", - "toqr": "^0.1.1", + "undici": "^6.18.2", "wrap-ansi": "^7.0.0", - "ws": "^8.12.1", - "zod": "^3.25.76" + "ws": "^8.12.1" }, "bin": { - "expo-internal": "main.js" + "expo-internal": "build/bin/cli" }, "peerDependencies": { "expo": "*", @@ -8385,310 +7739,162 @@ } } }, - "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "57.0.5", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.5.tgz", - "integrity": "sha512-vke39l0bo3H2q9JB/KXpAJ7HpscdTG3Mktbxanc8yn3riWzzSsnv0uxwZGZCrZrnDQzFxlLTXgbZrGkU26ng1w==", + "node_modules/expo/node_modules/@expo/json-file": { + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.16.tgz", + "integrity": "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==", "license": "MIT", "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "@expo/metro-runtime": "^57.0.8", - "expo": "*", - "expo-constants": "^57.0.9", - "expo-font": "^57.0.1", - "expo-router": "*", - "expo-server": "^57.0.1", - "react": "*", - "react-dom": "*", - "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" - }, - "peerDependenciesMeta": { - "@expo/metro-runtime": { - "optional": true - }, - "expo-router": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-server-dom-webpack": { - "optional": true - } + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" } }, - "node_modules/expo/node_modules/@expo/config": { - "version": "57.0.6", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-57.0.6.tgz", - "integrity": "sha512-VpMJpB/De/fb9bBFVVBiK6Ntg9lt0kAleLH9hcZz85CYRUQ3jVFVA8rNC5f8y4cp2+FiiPNFp62+kEOFI6pDiw==", + "node_modules/expo/node_modules/@expo/json-file/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "~57.0.6", - "@expo/config-types": "^57.0.2", - "@expo/json-file": "^11.0.1", - "@expo/require-utils": "^57.0.4", - "deepmerge": "^4.3.1", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "resolve-workspace-root": "^2.0.0", - "semver": "^7.6.0", - "slugify": "^1.3.4" + "@babel/highlight": "^7.10.4" } }, - "node_modules/expo/node_modules/@expo/config-plugins": { - "version": "57.0.6", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-57.0.6.tgz", - "integrity": "sha512-7CmKrS5Rnu8aSZyNlxH2qzA7Ls1HEa4EQvEVOAkHDKPr1e4Cg/nz7I7dUl09QDTVjMvkKYDH4Th0DuAsgqASaw==", + "node_modules/expo/node_modules/@expo/metro-config": { + "version": "54.0.17", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.17.tgz", + "integrity": "sha512-PQFgQCZGY0DffZUvBzJttDPreZfHrQakaBlKjnvOUMNXbDna+TYmg1IFZuIDUYJezLcdp+TvVFTLjNi1+mqaVw==", "license": "MIT", "dependencies": { - "@expo/config-types": "^57.0.2", - "@expo/json-file": "~11.0.1", - "@expo/plist": "^0.8.1", - "@expo/require-utils": "^57.0.4", - "@expo/sdk-runtime-versions": "^1.0.0", - "chalk": "^4.1.2", - "debug": "^4.3.5", + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~12.0.14", + "@expo/env": "~2.0.12", + "@expo/json-file": "~10.0.16", + "@expo/metro": "~54.2.0", + "@expo/spawn-async": "^1.7.2", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", "getenv": "^2.0.0", "glob": "^13.0.0", - "semver": "^7.5.4", - "slugify": "^1.6.6", - "xcode": "^3.0.1", - "xml2js": "0.6.0" - } - }, - "node_modules/expo/node_modules/@expo/config-types": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-57.0.2.tgz", - "integrity": "sha512-ewW08OonrcRIsRKIlFvvcmmafE5zemb1ocu3HkNwtVPyRtj2w42pZCAkMIROYpcVBaPnc3mDT9UZDzwXWC3i6g==", - "license": "MIT" - }, - "node_modules/expo/node_modules/@expo/env": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.4.2.tgz", - "integrity": "sha512-28pqaEqwnmLduZ00Pq9HkSzE5wbj1MTwp5/n8nm8rD8MCjR9eUnVOwmNksPI3Be2ReAPO/DbPn1puy0mvoocsQ==", - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "debug": "^4.3.4", - "getenv": "^2.0.0" + "hermes-parser": "^0.29.1", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.3", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" }, - "engines": { - "node": ">=20.12.0" - } - }, - "node_modules/expo/node_modules/@expo/image-utils": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.11.4.tgz", - "integrity": "sha512-pn/4770DIEOcYZr484uazuwg20FX/qaDkeMRF6J+oxejynDmEmO8wLsCudaNShFE0BhyKGQTYrs2rsRhqrqESw==", - "license": "MIT", - "dependencies": { - "@expo/require-utils": "^57.0.4", - "@expo/spawn-async": "^1.8.0", - "chalk": "^4.0.0", - "getenv": "^2.0.0", - "jimp-compact": "0.16.1", - "parse-png": "^2.1.0", - "semver": "^7.6.0" - } - }, - "node_modules/expo/node_modules/@expo/json-file": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-11.0.1.tgz", - "integrity": "sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "json5": "^2.2.3" - } - }, - "node_modules/expo/node_modules/@expo/plist": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.8.1.tgz", - "integrity": "sha512-3gTReGIUm0oRaMClsAJYxBnVPCl6fVpsl8HS+DTVxDhW4GyVyxg9E/Znm3BvcHtUJ51RJJI14pC1wvrNilCRHw==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - } - }, - "node_modules/expo/node_modules/@expo/prebuild-config": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.10.tgz", - "integrity": "sha512-myrS5NolFAQWD8g7QuqkettnkyJx3GRl603N6rlmqAMxmO5EU7sZu4EX2EsIKkva8QtpX84B0E17PsM9xXMsTQ==", - "license": "MIT", - "dependencies": { - "@expo/config": "~57.0.6", - "@expo/config-plugins": "~57.0.6", - "@expo/config-types": "^57.0.2", - "@expo/image-utils": "^0.11.4", - "@expo/json-file": "^11.0.1", - "@react-native/normalize-colors": "0.86.2", - "debug": "^4.3.1", - "expo-modules-autolinking": "~57.0.9", - "resolve-from": "^5.0.0", - "semver": "^7.6.0" - } - }, - "node_modules/expo/node_modules/@expo/schema-utils": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-57.0.2.tgz", - "integrity": "sha512-fMu/jyN0l1Wzv7XkeWR4IYCx1M8ryui3FdBNGrWwbRgJ7EhxXxK8E2jxP2W3pbgUwUY0V3hG8+GyfCZwny+Lxw==", - "license": "MIT" - }, - "node_modules/expo/node_modules/@expo/ws-tunnel": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.0.tgz", - "integrity": "sha512-j+JfTRdCk820J9dU0sA2SqshQIKFOMo7ED84w9MJFcebfbNQgsLztEY/SABDkGnjatrW4xGqnUhVRxSBVyCkXw==", - "license": "MIT", "peerDependencies": { - "ws": "^8.0.0" - } - }, - "node_modules/expo/node_modules/@react-native/debugger-frontend": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.2.tgz", - "integrity": "sha512-KGS1aV5F6cIqpnoIUhLBXyVzy1oAj8jBFGau6vX4Vy0HXRJN7p+68RU7x6NuyraHvQcR14ccMGT5TkFuNjQ4gA==", - "license": "BSD-3-Clause", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/expo/node_modules/@react-native/dev-middleware": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.2.tgz", - "integrity": "sha512-B7L0vKvg+IcEElT7Vpqh1xj5yJAqWUegjbP+bQRaorJMAYnv11GkliTnZV2AdTDfZQJWgOEx8i8LGkHkUg7bnA==", - "license": "MIT", - "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.86.2", - "@react-native/debugger-shell": "0.86.2", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.3.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "serve-static": "^1.16.2", - "ws": "^7.5.10" + "expo": "*" }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "peerDependenciesMeta": { + "expo": { + "optional": true + } } }, - "node_modules/expo/node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", - "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "node_modules/expo/node_modules/babel-preset-expo": { + "version": "54.0.12", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.12.tgz", + "integrity": "sha512-6xeSkdaixmQhWSYL7tfLu0pOS0BY+8ftwmdNSHtpEFSizrXYZkCjk/B6Dxr+6nwNRihixMcS0aBlWS1wlDl3pw==", "license": "MIT", - "engines": { - "node": ">=8.3.0" + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "0.81.5", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.0", + "babel-plugin-syntax-hermes-parser": "^0.29.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4", + "resolve-from": "^5.0.0" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "@babel/runtime": "^7.20.0", + "expo": "*", + "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { - "bufferutil": { + "@babel/runtime": { "optional": true }, - "utf-8-validate": { + "expo": { "optional": true } } }, - "node_modules/expo/node_modules/@react-native/normalize-colors": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", - "integrity": "sha512-EzPFc9Y6lzYOWeso2almwXI7f8+qReHxWvT+algsOczb2UhWXIWXDoSvkdwoSfiwwmGt/ijJgKJoeHlzPkLwRg==", - "license": "MIT" - }, - "node_modules/expo/node_modules/chromium-edge-launcher": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.3.0.tgz", - "integrity": "sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "^1.0.4" - } - }, - "node_modules/expo/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], + "node_modules/expo/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/expo/node_modules/expo-constants": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.9.tgz", - "integrity": "sha512-Y47sGiF+U8fwicUSPdJPjB27PuU+FgLK4Mpai0ksZF5hv8jNN3HBqKuDNxsiu70uHBKf80TaR4gwMPh0pmETiQ==", + "node_modules/expo/node_modules/expo-file-system": { + "version": "19.0.23", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.23.tgz", + "integrity": "sha512-MeGkid9OeNILfT/qonaXHp4f2c15xaB28U/bcN7pqZej0Kx0+6+V7e9ZIXpPHm07zVatxA+QkMTPQEGfmvVOxA==", "license": "MIT", - "dependencies": { - "@expo/env": "~2.4.2" - }, "peerDependencies": { "expo": "*", "react-native": "*" } }, - "node_modules/expo/node_modules/expo-font": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz", - "integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==", + "node_modules/expo/node_modules/expo-keep-awake": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz", + "integrity": "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==", "license": "MIT", - "dependencies": { - "fontfaceobserver": "^2.1.0" - }, "peerDependencies": { "expo": "*", - "react": "*", - "react-native": "*" + "react": "*" } }, - "node_modules/expo/node_modules/expo-modules-core": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.9.tgz", - "integrity": "sha512-8UL/70VjmN8jOG4Tugdq6JX63Za6Qnri8h5dqFuLsDL2JxmTSB/Fz7fyEhDSpjlxVFyiJo5uF2aYVrz7Vxj2ZQ==", + "node_modules/expo/node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", + "license": "MIT" + }, + "node_modules/expo/node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", "license": "MIT", "dependencies": { - "@expo/expo-modules-macros-plugin": "0.6.1", - "expo-modules-jsi": "~57.0.4", - "invariant": "^2.2.4" - }, - "peerDependencies": { - "react": "*", - "react-native": "*", - "react-native-worklets": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0" - }, - "peerDependenciesMeta": { - "react-native-worklets": { - "optional": true - } + "hermes-estree": "0.29.1" } }, - "node_modules/expo/node_modules/expo-server": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.1.tgz", - "integrity": "sha512-sBfVDH6dmKVHZxqUxbfkzS00PZELMZt1IpnHKxcOTMZtR/t7CtRAFrbXcisG+EyzeqHSVDacZT+1tbYfZt5D8w==", - "license": "MIT", + "node_modules/expo/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, "engines": { - "node": ">=20.16.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/expo/node_modules/picomatch": { @@ -8697,10 +7903,31 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/expo/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/expo/node_modules/semver": { @@ -8715,10 +7942,20 @@ "node": ">=10" } }, + "node_modules/expo/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/expo/node_modules/ws": { - "version": "8.21.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz", - "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -8771,18 +8008,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fb-dotslash": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", - "integrity": "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "dotslash": "bin/dotslash" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -8848,12 +8073,6 @@ "node": "*" } }, - "node_modules/fetch-nodeshim": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.10.tgz", - "integrity": "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==", - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8954,9 +8173,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -9023,6 +8242,15 @@ "node": ">= 6" } }, + "node_modules/freeport-async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", + "integrity": "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -9062,18 +8290,21 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -9206,9 +8437,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.1.tgz", + "integrity": "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==", "dev": true, "license": "MIT", "dependencies": { @@ -9279,12 +8510,12 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -9373,16 +8604,6 @@ "uglify-js": "^3.1.4" } }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -9570,6 +8791,26 @@ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -9667,6 +8908,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/inline-style-prefixer": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", @@ -9744,9 +8991,10 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -9813,9 +9061,9 @@ } }, "node_modules/is-bun-module/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -9902,6 +9150,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -10248,19 +9512,33 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "semver": "^7.5.4" }, "engines": { - "node": ">=8" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/istanbul-lib-report": { @@ -10293,16 +9571,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -10489,22 +9757,6 @@ } } }, - "node_modules/jest-config/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-config/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -10805,27 +10057,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runner/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-runner/node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/jest-runtime": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", @@ -10882,16 +10113,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-runtime/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-snapshot": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", @@ -10954,21 +10175,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-validate": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", @@ -10986,6 +10192,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-watcher": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", @@ -11049,9 +10267,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -11352,9 +10580,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11375,9 +10600,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11398,9 +10620,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11421,9 +10640,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11954,6 +11170,15 @@ "node": ">=20.19.4" } }, + "node_modules/metro-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/metro-symbolicate": { "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", @@ -11974,6 +11199,15 @@ "node": ">=20.19.4" } }, + "node_modules/metro-symbolicate/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/metro-transform-plugins": { "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", @@ -12015,6 +11249,21 @@ "node": ">=20.19.4" } }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -12062,12 +11311,13 @@ } }, "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/minimatch": { @@ -12086,7 +11336,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12098,7 +11347,19 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" } }, "node_modules/mkdirp": { @@ -12119,12 +11380,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/multitars": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.1.tgz", - "integrity": "sha512-Do9rHaSDfQ2Fk5Dpg2RjQ4M48Ol7rSq1gvKn/dXJYnhKEm8RRjjUvyiGDDEhJqb0hJPGjRaTB1kx6beqKO/i6Q==", - "license": "MIT" - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -12137,9 +11392,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -12193,10 +11448,16 @@ "dev": true, "license": "MIT" }, + "node_modules/nested-error-stacks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", + "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", + "license": "MIT" + }, "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", "dev": true, "license": "MIT", "dependencies": { @@ -12248,9 +11509,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.52", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", - "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "license": "MIT", "engines": { "node": ">=18" @@ -12502,15 +11763,19 @@ } }, "node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^1.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=4" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/open": { @@ -12657,13 +11922,14 @@ } }, "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.6", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" }, @@ -12678,7 +11944,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -12818,9 +12083,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -12964,9 +12229,9 @@ } }, "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -12983,7 +12248,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -13007,6 +12272,18 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -13111,7 +12388,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13134,6 +12410,14 @@ ], "license": "MIT" }, + "node_modules/qrcode-terminal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", + "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -13191,6 +12475,30 @@ "node": ">= 0.6" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -13241,9 +12549,9 @@ } }, "node_modules/react-is": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", - "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", "license": "MIT" }, "node_modules/react-native": { @@ -13351,9 +12659,9 @@ } }, "node_modules/react-native-reanimated/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -13524,9 +12832,9 @@ } }, "node_modules/react-native/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -13536,9 +12844,9 @@ } }, "node_modules/react-native/node_modules/ws": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.4.tgz", - "integrity": "sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.6.tgz", + "integrity": "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA==", "license": "MIT", "dependencies": { "async-limiter": "~1.0.0" @@ -13728,9 +13036,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", - "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" @@ -13757,6 +13065,28 @@ "node": ">=0.10.0" } }, + "node_modules/requireg": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz", + "integrity": "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==", + "dependencies": { + "nested-error-stacks": "~2.0.1", + "rc": "~1.2.7", + "resolve": "~1.7.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/requireg/node_modules/resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "license": "MIT", + "dependencies": { + "path-parse": "^1.0.5" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -13823,7 +13153,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -13842,6 +13171,27 @@ "node": ">=4" } }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -13989,9 +13339,9 @@ } }, "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -14229,15 +13579,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -14330,6 +13680,12 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -14355,9 +13711,9 @@ } }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -14373,24 +13729,16 @@ } }, "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -14461,6 +13809,15 @@ "node": ">=8" } }, + "node_modules/standard-navigation": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/standard-navigation/-/standard-navigation-0.0.8.tgz", + "integrity": "sha512-TyVbo7INUDWtsUWDFn8RR7kwR87U0S4xHfLfbbnyeC581TmmyqQ+eM+nPw8rQTSD8QitRVcYfPaSHr/QJiUy1g==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -14570,19 +13927,20 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -14592,16 +13950,16 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.2" }, "engines": { "node": ">= 0.4" @@ -14641,13 +13999,13 @@ } }, "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/strip-final-newline": { @@ -14753,6 +14111,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -14770,9 +14153,9 @@ } }, "node_modules/terser": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", - "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -14793,6 +14176,16 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -14863,9 +14256,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -14896,9 +14289,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -14934,12 +14327,6 @@ "node": ">=0.6" } }, - "node_modules/toqr": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/toqr/-/toqr-0.1.1.tgz", - "integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==", - "license": "MIT" - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14966,9 +14353,9 @@ "license": "Apache-2.0" }, "node_modules/ts-jest": { - "version": "29.4.11", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", - "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", "dev": true, "license": "MIT", "dependencies": { @@ -14978,7 +14365,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.8.0", + "semver": "^7.8.5", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -15070,6 +14457,16 @@ "json5": "lib/cli.js" } }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -15169,18 +14566,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" }, "engines": { "node": ">= 0.4" @@ -15262,10 +14659,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -15356,9 +14762,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "funding": [ { "type": "opencollective", @@ -15534,26 +14940,42 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/vaul/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/vaul/node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", @@ -15571,16 +14993,16 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", @@ -15598,14 +15020,14 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -15623,13 +15045,13 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -15647,13 +15069,12 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -15671,12 +15092,12 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -15694,12 +15115,12 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", @@ -16245,11 +15666,28 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/whatwg-url-minimum": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.2.tgz", - "integrity": "sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==", - "license": "MIT" + "node_modules/whatwg-url-without-unicode": { + "version": "8.0.0-3", + "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", + "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", + "license": "MIT", + "dependencies": { + "buffer": "^5.4.3", + "punycode": "^2.1.1", + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } }, "node_modules/which": { "version": "2.0.2", @@ -16334,13 +15772,13 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", @@ -16354,6 +15792,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wonka": { + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.6.tgz", + "integrity": "sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==", + "license": "MIT" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -16408,9 +15852,9 @@ } }, "node_modules/ws": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", - "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -16503,9 +15947,9 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -16533,7 +15977,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -16542,19 +15985,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/zustand": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", - "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/mobile/package.json b/mobile/package.json index 16f85a2..257d1ef 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -11,19 +11,21 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.3", - "@react-navigation/native": "^7.1.8", "@tanstack/react-query": "^5.99.2", "axios": "^1.18.0", - "expo": "~57.0.10", + "expo": "~54.0.0", + "expo-asset": "~12.0.13", + "expo-audio": "~1.1.1", "expo-constants": "~18.0.13", "expo-device": "~8.0.10", "expo-font": "~14.0.12", - "expo-linking": "~8.0.11", + "expo-linking": "~8.0.12", + "expo-location": "~19.0.8", "expo-notifications": "~0.32.17", "expo-router": "~6.0.24", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", - "expo-web-browser": "~15.0.10", + "expo-web-browser": "~15.0.11", "lucide-react-native": "^0.563.0", "react": "19.1.0", "react-dom": "19.1.0", diff --git a/mobile/store/usePreferencesStore.ts b/mobile/store/usePreferencesStore.ts index bb030fe..c9c82e3 100644 --- a/mobile/store/usePreferencesStore.ts +++ b/mobile/store/usePreferencesStore.ts @@ -3,13 +3,22 @@ import { create } from 'zustand'; interface PreferencesState { isOfflineMode: boolean; notificationsEnabled: boolean; + /** + * Operator's own area. When set, alarms (sound + vibration) are only raised + * for alerts hitting this zone — a quake on the other side of the world + * must not ring the phone. `null` = notify for every zone. + */ + homeZoneId: number | null; setOfflineMode: (status: boolean) => void; toggleNotifications: () => void; + setHomeZoneId: (zoneId: number | null) => void; } export const usePreferencesStore = create((set) => ({ isOfflineMode: false, notificationsEnabled: true, + homeZoneId: null, setOfflineMode: (status) => set({ isOfflineMode: status }), toggleNotifications: () => set((state) => ({ notificationsEnabled: !state.notificationsEnabled })), + setHomeZoneId: (zoneId) => set({ homeZoneId: zoneId }), })); \ No newline at end of file diff --git a/mobile/store/useThemeStore.ts b/mobile/store/useThemeStore.ts new file mode 100644 index 0000000..5790e78 --- /dev/null +++ b/mobile/store/useThemeStore.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand'; +import type { ThemeMode } from '../theme'; + +interface ThemeState { + /** "dark" = MIC mode (military command), "light" = RESEARCH mode (scientific). */ + themeMode: ThemeMode; + setThemeMode: (mode: ThemeMode) => void; + toggleTheme: () => void; +} + +export const useThemeStore = create((set) => ({ + themeMode: 'dark', + setThemeMode: (themeMode) => set({ themeMode }), + toggleTheme: () => set((state) => ({ themeMode: state.themeMode === 'dark' ? 'light' : 'dark' })), +})); diff --git a/mobile/theme/index.ts b/mobile/theme/index.ts new file mode 100644 index 0000000..b2e7172 --- /dev/null +++ b/mobile/theme/index.ts @@ -0,0 +1,108 @@ +/** + * QuakeGuard Design Tokens — Military (dark) ↔ Scientific (light) modes. + * + * Dark palette ("MIC mode"): near-black zinc surfaces, terminal accents — + * green = live/secure, amber = caution threshold, red = seismic alert. + * Light palette ("RESEARCH mode"): clean scientific surfaces with the same + * semantic accents shifted to higher-contrast shades for readable on white. + */ + +export type ThemeMode = "dark" | "light"; + +export interface AppColors { + // Backgrounds / surfaces + bg: string; + surface: string; + surfaceAlt: string; + border: string; + borderStrong: string; + + // Text + text: string; + textSecondary: string; + textMuted: string; + + // Accents (terminal semantics) + live: string; + caution: string; + alert: string; + info: string; + + // Chart specifics + gridline: string; + axis: string; + tick: string; +} + +export const darkColors: AppColors = { + // Backgrounds / surfaces + bg: "#09090b", // zinc-950 — app background + surface: "#101014", // cards / panels + surfaceAlt: "#18181b", // raised surfaces, callouts + border: "#27272a", // zinc-800 — hairline borders + borderStrong: "#3f3f46", // zinc-700 — axis / gridlines + + // Text + text: "#e4e4e7", // zinc-200 — primary + textSecondary: "#a1a1aa",// zinc-400 — technical / labels + textMuted: "#71717a", // zinc-500 — hints, timestamps + + // Accents (terminal semantics) + live: "#10b981", // emerald-500 — sensor heartbeat / SYSTEM SECURE + caution: "#f59e0b", // amber-500 — approaching threshold + alert: "#ef4444", // red-500 — seismic alert + info: "#38bdf8", // sky-400 — informational accent (AI report) + + // Chart specifics + gridline: "#27272a", + axis: "#3f3f46", + tick: "#a1a1aa", +}; + +export const lightColors: AppColors = { + // Backgrounds / surfaces (clean scientific paper look) + bg: "#f8fafc", // slate-50 — app background + surface: "#ffffff", // cards / panels + surfaceAlt: "#f1f5f9", // raised surfaces, callouts + border: "#e2e8f0", // slate-200 — hairline borders + borderStrong: "#cbd5e1", // slate-300 — axis / gridlines + + // Text + text: "#0f172a", // slate-900 — primary + textSecondary: "#475569",// slate-600 — technical / labels + textMuted: "#94a3b8", // slate-400 — hints, timestamps + + // Accents (darker for contrast on light) + live: "#059669", // emerald-600 — sensor heartbeat / SYSTEM SECURE + caution: "#d97706", // amber-600 — approaching threshold + alert: "#dc2626", // red-600 — seismic alert + info: "#0284c7", // sky-600 — informational accent (AI report) + + // Chart specifics (soft graph-paper lines) + gridline: "#e2e8f0", + axis: "#cbd5e1", + tick: "#64748b", +}; + +export const THEMES: Record = { + dark: darkColors, + light: lightColors, +}; + +/** Backwards-compatible alias: static contexts default to the MIC (dark) theme. */ +export const COLORS: AppColors = darkColors; + +/** Monospace stack used across the technical UI and the chart tick labels. */ +export const MONO = "'SpaceMono-Regular', 'Courier New', monospace"; + +export const FONTS = { + mono: "SpaceMono-Regular", +} as const; + +export const SPACING = { + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 20, +} as const; \ No newline at end of file diff --git a/mobile/theme/mapStyle.ts b/mobile/theme/mapStyle.ts new file mode 100644 index 0000000..c9c87c8 --- /dev/null +++ b/mobile/theme/mapStyle.ts @@ -0,0 +1,118 @@ +/** + * Dark map styling for the Google Maps provider (Android). + * Apple Maps on iOS does not support customMapStyle, so it stays standard + * while callouts/badges are themed dark. + */ +/** + * Light map styling for the Google Maps provider (Android). + * Complements `darkMapStyle` above so RESEARCH MODE matches the light theme + * instead of forcing the dark console onto operators who switched. + */ +export const lightMapStyle = [ + { + elementType: "geometry", + stylers: [{ color: "#f5f5f4" }], + }, + { + elementType: "labels.text.fill", + stylers: [{ color: "#52525b" }], + }, + { + elementType: "labels.text.stroke", + stylers: [{ color: "#fafaf9" }], + }, + { + featureType: "administrative", + elementType: "geometry", + stylers: [{ color: "#e4e4e7" }], + }, + { + featureType: "administrative.country", + elementType: "geometry.stroke", + stylers: [{ color: "#a1a1aa" }], + }, + { + featureType: "poi", + elementType: "geometry", + stylers: [{ color: "#e7e5e4" }], + }, + { + featureType: "road", + elementType: "geometry", + stylers: [{ color: "#ffffff" }], + }, + { + featureType: "road.highway", + elementType: "geometry", + stylers: [{ color: "#d6d3d1" }], + }, + { + featureType: "water", + elementType: "geometry", + stylers: [{ color: "#bae6fd" }], + }, +] as const; + +export const darkMapStyle = [ + { + elementType: "geometry", + stylers: [{ color: "#0f0f12" }], + }, + { + elementType: "labels.text.fill", + stylers: [{ color: "#a1a1aa" }], + }, + { + elementType: "labels.text.stroke", + stylers: [{ color: "#09090b" }], + }, + { + elementType: "labels.icon", + stylers: [{ visibility: "off" }], + }, + { + featureType: "administrative", + elementType: "geometry", + stylers: [{ color: "#27272a" }], + }, + { + featureType: "administrative.country", + elementType: "geometry.stroke", + stylers: [{ color: "#3f3f46" }], + }, + { + featureType: "administrative.province", + elementType: "geometry.stroke", + stylers: [{ color: "#3f3f46" }], + }, + { + featureType: "landscape", + elementType: "geometry", + stylers: [{ color: "#101014" }], + }, + { + featureType: "poi", + elementType: "geometry", + stylers: [{ color: "#18181b" }], + }, + { + featureType: "road", + elementType: "geometry", + stylers: [{ color: "#1c1c21" }], + }, + { + featureType: "road", + elementType: "geometry.stroke", + stylers: [{ color: "#27272a" }], + }, + { + featureType: "road.highway", + elementType: "geometry", + stylers: [{ color: "#232328" }], + }, + { + featureType: "water", + elementType: "geometry", + stylers: [{ color: "#0c0c10" }], + }, +] as const; diff --git a/mobile/theme/useTheme.ts b/mobile/theme/useTheme.ts new file mode 100644 index 0000000..bac8018 --- /dev/null +++ b/mobile/theme/useTheme.ts @@ -0,0 +1,14 @@ +import { THEMES, type AppColors, type ThemeMode } from "./index"; +import { useThemeStore } from "../store/useThemeStore"; + +export interface AppTheme { + mode: ThemeMode; + isDark: boolean; + colors: AppColors; +} + +/** Reactive access to the active palette (MIC dark / RESEARCH light). */ +export function useAppTheme(): AppTheme { + const mode = useThemeStore((state) => state.themeMode); + return { mode, isDark: mode === "dark", colors: THEMES[mode] }; +} \ No newline at end of file diff --git a/mobile/theme/victory.ts b/mobile/theme/victory.ts new file mode 100644 index 0000000..11b4198 --- /dev/null +++ b/mobile/theme/victory.ts @@ -0,0 +1,83 @@ +import type { AppColors } from "./index"; +import { MONO, darkColors } from "./index"; + +/** + * Builds the custom QuakeGuard Victory theme from a palette — military dark or + * research light. No VictoryTheme.material: a technical-instrument aesthetic + * with transparent canvas, hairline axes, faint horizontal gridlines only, and + * monospaced tick labels so the seismograph reads like a precision terminal. + */ +export function createQuakeGuardTheme(colors: AppColors) { + return { + chart: { + background: "transparent", + padding: { top: 8, bottom: 34, left: 46, right: 12 }, + }, + axis: { + style: { + axis: { + stroke: "transparent", + strokeWidth: 0, + }, + ticks: { + stroke: colors.axis, + strokeWidth: 1, + size: 4, + padding: 4, + }, + grid: { + stroke: "transparent", + strokeWidth: 0, + }, + tickLabels: { + fontFamily: MONO, + fontSize: 10, + fill: colors.tick, + padding: 6, + }, + }, + }, + dependentAxis: { + style: { + axis: { + stroke: "transparent", + strokeWidth: 0, + }, + ticks: { + stroke: colors.axis, + strokeWidth: 1, + size: 4, + padding: 4, + }, + // Weak horizontal gridlines only (the "paper" of the seismograph). + grid: { + stroke: colors.gridline, + strokeWidth: 1, + strokeDasharray: "4 6", + }, + tickLabels: { + fontFamily: MONO, + fontSize: 10, + fill: colors.tick, + padding: 6, + }, + }, + }, + line: { + style: { + data: { + stroke: colors.live, + strokeWidth: 2, + }, + labels: { + fontFamily: MONO, + fontSize: 10, + fill: colors.tick, + }, + }, + }, + }; +} + +/** MIC-mode (dark) theme, kept for static/backward-compatible usage. */ +export const quakeGuardTheme = createQuakeGuardTheme(darkColors); \ No newline at end of file diff --git a/mobile/utils/magnitude.ts b/mobile/utils/magnitude.ts new file mode 100644 index 0000000..336a3dc --- /dev/null +++ b/mobile/utils/magnitude.ts @@ -0,0 +1,28 @@ +/** + * Magnitude estimation — mirrors backend `src/worker.py:estimate_magnitude`. + * M_IoT = log10(PGA_calib) + b, with PGA = raw_value / SENSOR_SCALE. + */ + +export const SENSOR_SCALE = 100.0; // raw value -> m/s² +export const K_CALIBRATION = 1.6; // MyShake-style MEMS calibration factor +export const B_OFFSET = 3.0; // anchor: PGA 0.07 m/s² ≈ M3.85 + +export const ALERT_MAGNITUDE = 4.5; // worker publishes a CRITICAL alert at this +export const CAUTION_MAGNITUDE = 4.0; // amber band below the alert threshold + +export function estimateMagnitude(sensorValue: number): number { + const pga = sensorValue / SENSOR_SCALE; + const pgaCalib = pga / K_CALIBRATION; + if (pgaCalib <= 0) return 0; + const magnitude = Math.log10(pgaCalib) + B_OFFSET; + return Math.max(0, Math.min(magnitude, 9.9)); +} + +export type Threshold = "live" | "caution" | "alert"; + +export function thresholdOf(sensorValue: number): Threshold { + const magnitude = estimateMagnitude(sensorValue); + if (magnitude >= ALERT_MAGNITUDE) return "alert"; + if (magnitude >= CAUTION_MAGNITUDE) return "caution"; + return "live"; +}