diff --git a/.env.example b/.env.example deleted file mode 100644 index e9d2d3e..0000000 --- a/.env.example +++ /dev/null @@ -1,77 +0,0 @@ -# IRacing Display Environment Variables -# Copy this file to .env and update with your actual values - -################################# -# Database Configuration -################################# - -# QuestDB Connection (PostgreSQL wire protocol recommended) -QUESTDB_URL=Host=questdb;Port=8812;Database=qdb;Username=admin;Password=quest -QUESTDB_HOST=questdb - -# InfluxDB Configuration (backup/alternative) -INFLUXDB_ADMIN_USERNAME=admin -INFLUXDB_ADMIN_PASSWORD=your_secure_influxdb_password - -################################# -# Message Queue Configuration -################################# - -# RabbitMQ Configuration -# These credentials are used for both the default admin user and guest user -RABBITMQ_USER=admin -RABBITMQ_PASS=changeme - -################################# -# Monitoring & Dashboards -################################# - -# Grafana Configuration -GRAFANA_ADMIN_PASSWORD=admin123 - -################################# -# Reverse Proxy & Security -################################# - -# Traefik Dashboard Authentication -# Generate with: htpasswd -nb admin password -# Example below uses password "admin123" - CHANGE THIS! -TRAEFIK_DASHBOARD_AUTH=admin:$2y$10$OIGILfCpNGVIR8WfcWKXMOVnYkU72ABW9n/zN9xYz0KtEQdHTY9i2 - -# Domain Configuration -LOCAL_DOMAIN=pi.local -TAILSCALE_DOMAIN=your-tailscale-name - -################################# -# Container Registry & Updates -################################# - -# Watchtower Docker Registry Token -WATCHTOWER_DOCKER_TOKEN=your_docker_registry_token_here - -################################# -# SSL/TLS Configuration (Optional) -################################# - -# For Let's Encrypt automatic SSL (future enhancement) -# LETSENCRYPT_EMAIL=your-email@example.com -# CLOUDFLARE_API_TOKEN=your_cloudflare_token - -################################# -# Application URLs -################################# - -# Next.js Dashboard URLs -NEXT_PUBLIC_APP_URL=https://${LOCAL_DOMAIN:-pi.local}/dashboard -NEXT_PUBLIC_BACKEND_URL=https://${LOCAL_DOMAIN:-pi.local}/api - -################################# -# Development Configuration -################################# - -# Environment -NODE_ENV=production -DOTNET_ENVIRONMENT=Production - -# Network/Host Information -# HOST_IP=192.168.1.202 # Your Pi's IP address \ No newline at end of file diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..eb266e0 --- /dev/null +++ b/.env.template @@ -0,0 +1,41 @@ +RABBITMQ_URL=amqp://admin:changeme@rabbitmq:5672/ + +QUESTDB_HOST=questdb +QUESTDB_HTTP_PORT=9000 # ILP HTTP writes +QUESTDB_PG_PORT=8812 # PostgreSQL wire protocol (queries) +QUESTDB_USER=admin +QUESTDB_PASSWORD=quest +QUESTDB_DATABASE=qdb + +# Worker Configuration (auto-scales based on CPU count) +# WORKER_COUNT=20 # Optional: Defaults to CPU_COUNT * 1.25 (16 CPUs β†’ 20 workers) +FILE_QUEUE_SIZE=5000 # File processing queue depth + +# Batch Configuration +BATCH_SIZE_BYTES=16777216 +BATCH_SIZE_RECORDS=8000 # Records per batch +BATCH_TIMEOUT=10ms # Flush timeout + +# Go Runtime (auto-detects optimal values) +GOGC=200 # Garbage collection target (higher = less frequent GC) + +# RabbitMQ Configuration (pool size auto-matches worker count) +# RABBITMQ_POOL_SIZE=20 # Optional: Defaults to WORKER_COUNT (each worker needs a channel) +RABBITMQ_BATCH_SIZE=8000 # Messages per batch +RABBITMQ_BATCH_TIMEOUT=2ms # Batch flush timeout +RABBITMQ_PREFETCH_COUNT=50000 # Prefetch buffer +RABBITMQ_FRAME_SIZE=4194304 # 4MB frame size + +ENABLE_PPROF=false # Enable pprof profiler +PPROF_PORT=6060 # Profiler port + + +GRAFANA_ADMIN_PASSWORD=testing4564 + +TRAEFIK_DASHBOARD_AUTH=admin:$$2y$$10$$X7d.w.zKRV9cGAc5QZnw7uAMeZjKcaFVZmRJ6YZ8vF5Zh.D1Ge2Gi + + +# Local Network +LOCAL_DOMAIN=pi.local + +TAILSCALE_DOMAIN=your-device.tail12345.ts.net diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..db3afdf --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,104 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '28 16 * * 5' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: csharp + build-mode: none + - language: go + build-mode: autobuild + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # πŸ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/snyk-container.yml b/.github/workflows/snyk-container.yml index 9781840..36b8eef 100644 --- a/.github/workflows/snyk-container.yml +++ b/.github/workflows/snyk-container.yml @@ -83,7 +83,7 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v5 with: - context: ./telemetryService/telemetryService/ + context: ./telemetryService/golang/ platforms: linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/.gitignore b/.gitignore index 0f11bb9..11e28bd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ *.dll *.so *.dylib +tailscaled.* + +**/.claude # Test binary, built with `go test -c` *.test @@ -21,6 +24,7 @@ dynamic/tls.yml # Go workspace file go.work go.work.sum +tailscale/ # Environment and sensitive files .env @@ -28,7 +32,8 @@ go.work.sum .env.production .env.development **/.env* -!.env.example +!.env.template +*.pdf # Secrets and tokens *.token @@ -36,6 +41,7 @@ go.work.sum api_keys/ tokens/ credentials/ +auth_credentials.txt ingest/go/ibt_files/*.ibt *.DS_Store .DS_Store @@ -64,7 +70,6 @@ acme.json *.blk *.db *.influxdb -*.sum *.inf **/bin *.ibt @@ -228,3 +233,10 @@ keys/ telemetryService/kafka/* ingest/go/kafka-data/* + +# Failed batch persistence (temporary recovery files) +failed_batches/ +telemetryService/golang/benchmark_results/* +dashboard/dist/** +node_modules/ +learnings/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index d912003..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/modules.xml -/.idea.IRacing-Display.iml -/projectSettingsUpdater.xml -/contentModel.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.vscode/settings.json b/.vscode/settings.json index fb0c3be..d653ec2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,10 +3,25 @@ "editor.defaultFormatter": "biomejs.biome", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "quickfix.biome": "explicit", - "source.organizeImports.biome": "explicit" + "source.fixAll.biome": "explicit" }, "[go]": { "editor.defaultFormatter": "golang.go" + }, + "[csharp]": { + "editor.defaultFormatter": "ms-dotnettools.csharp" + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + }, + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "biome.configurationPath": "dashboard/biome.json", + "files.associations": { + "wrangler.json": "jsonc" } -} +} \ No newline at end of file diff --git a/MKCERT_SETUP.md b/MKCERT_SETUP.md deleted file mode 100644 index baf294b..0000000 --- a/MKCERT_SETUP.md +++ /dev/null @@ -1,133 +0,0 @@ -# mkcert SSL Setup for IRacing Telemetry Dashboard - -This guide explains how to set up trusted SSL certificates using mkcert instead of self-signed certificates. - -## Benefits of mkcert - -βœ… **No browser warnings** - Certificates are automatically trusted -βœ… **Works on all devices** (with some setup) -βœ… **Same nginx configuration** - No changes needed to existing setup -βœ… **Easy renewal** - Simple command to regenerate - -## Quick Setup (Raspberry Pi) - -### Option 1: Automated Installation -```bash -# Install mkcert automatically (detects your Pi's architecture) -./install-mkcert-rpi.sh - -# Generate certificates and start nginx -./setup-nginx.sh -``` - -### Option 2: Manual Installation - -**For Raspberry Pi 4/5 (64-bit):** -```bash -curl -JLO https://dl.filippo.io/mkcert/latest?for=linux/arm64 -chmod +x mkcert-v*-linux-arm64 -sudo mv mkcert-v*-linux-arm64 /usr/local/bin/mkcert -``` - -**For older Raspberry Pi (32-bit):** -```bash -curl -JLO https://dl.filippo.io/mkcert/latest?for=linux/arm -chmod +x mkcert-v*-linux-arm -sudo mv mkcert-v*-linux-arm /usr/local/bin/mkcert -``` - -**After installation:** -```bash -mkcert -install # Install root CA -./setup-nginx.sh # Generate certs and start nginx -``` - -## Accessing from Other Devices - -### Trust on Your Computer -To access the dashboard from your laptop/desktop without warnings: - -1. **Copy the root CA from your Pi:** - ```bash - # On your Pi, find the root CA location - mkcert -CAROOT - # Copy rootCA.pem to your computer - ``` - -2. **Install on your computer:** - - **macOS:** Double-click rootCA.pem β†’ Add to System keychain - - **Windows:** Double-click β†’ Install Certificate β†’ Local Machine β†’ Trusted Root - - **Linux:** Copy to `/usr/local/share/ca-certificates/` and run `sudo update-ca-certificates` - -### Trust on Mobile Devices -For phones/tablets to trust the certificates: - -1. Copy `rootCA.pem` to your mobile device -2. **iOS:** Settings β†’ General β†’ VPN & Device Management β†’ Install Profile -3. **Android:** Settings β†’ Security β†’ Install from storage - -## File Structure -``` -IRacing-Display/ -β”œβ”€β”€ install-mkcert-rpi.sh # Auto-installer for Raspberry Pi -β”œβ”€β”€ generate-ssl-certs.sh # Generate mkcert certificates -β”œβ”€β”€ setup-nginx.sh # Full nginx setup with mkcert -β”œβ”€β”€ nginx.conf # nginx configuration (unchanged) -└── ssl/ - β”œβ”€β”€ certs/ - β”‚ β”œβ”€β”€ nginx-selfsigned.crt # mkcert certificate - β”‚ └── dhparam.pem # DH parameters - └── private/ - └── nginx-selfsigned.key # mkcert private key -``` - -## Commands Reference - -```bash -# Check if mkcert is installed -mkcert -version - -# Check CA root location -mkcert -CAROOT - -# Generate certificates manually -mkcert -key-file ssl/private/nginx-selfsigned.key \ - -cert-file ssl/certs/nginx-selfsigned.crt \ - localhost 127.0.0.1 192.168.1.100 - -# Regenerate certificates (if IP changes) -./generate-ssl-certs.sh - -# Uninstall mkcert CA (removes trust) -mkcert -uninstall -``` - -## Troubleshooting - -### Still seeing warnings? -- Make sure you ran `mkcert -install` -- Check your IP address is included in the certificate -- Try regenerating: `./generate-ssl-certs.sh` - -### Certificate not trusted on other devices? -- Install the root CA (`rootCA.pem`) on each device -- Make sure the CA is added to the system trust store, not user - -### Can't find mkcert binary? -- Check architecture: `uname -m` -- Verify download URL matches your Pi model -- Try the automated installer: `./install-mkcert-rpi.sh` - -### Browser still showing "Not Secure"? -- Clear browser cache and restart -- Check certificate details in browser (should show "mkcert") -- Verify certificate includes your device's IP address - -## Access URLs - -After setup, access your dashboard at: -- `https://YOUR_PI_IP/` (Main dashboard) -- `https://YOUR_PI_IP/grafana/` (Grafana) -- `https://YOUR_PI_IP/questdb/` (QuestDB) - -No more security warnings! πŸŽ‰ \ No newline at end of file diff --git a/Makefile b/Makefile index 6466876..8cdf46c 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,10 @@ .PHONY: restart logs -export DOCKER_BUILDKIT=1 -export COMPOSE_DOCKER_CLI_BUILD=1 - restart: @echo "πŸš€ Restarting Docker services..." @docker compose --file docker-compose.yml down -v @docker compose --file docker-compose.yml build --no-cache - @docker compose --file docker-compose.yml up -d + @docker compose --file docker-compose.yml up --pull=always -d @echo "βœ… Done! Check logs with: make logs" restart-dev: @@ -22,3 +19,23 @@ restart-lite: @docker compose --file docker-compose.dev.yml build --no-cache @docker compose --file docker-compose.dev.yml up -d @echo "βœ… Done! Check logs with: make logs" + +restart-p: + @echo "πŸš€ Restarting Docker services..." + @podman compose --file docker-compose.yml down -v + @podman compose --file docker-compose.yml build --no-cache + @podman compose --file docker-compose.yml up -d + @echo "βœ… Done! Check logs with: make logs" + +restart-dev-p: + @echo "πŸš€ Restarting Docker services..." + @podman compose --file docker-compose.dev.yml down -v + @podman compose --file docker-compose.dev.yml build --no-cache + @podman compose --file docker-compose.dev.yml up -d + @echo "βœ… Done! Check logs with: make logs" + +restart-lite-p: + @echo "πŸš€ Restarting Dashboard..." + @podman compose --file docker-compose.dev.yml build --no-cache + @podman compose --file docker-compose.dev.yml up -d + @echo "βœ… Done! Check logs with: make logs" \ No newline at end of file diff --git a/README.md b/README.md index 2a4551b..26b1914 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,150 @@ -# IRacing-Display +# TelemetryStack -An IRacing display over local network +A self-hosted telemetry pipeline for iRacing. Parses `.ibt` files, stores time-series data in QuestDB, and serves an interactive dashboard with synchronised track maps and telemetry charts β€” built to run on a Raspberry Pi 5. -- Ingest takes the data out of Iracing and sends it over the local network to the telemtry service -- The telemetry then formats this data and saves it -- The dashboard then displays the data that is stored in the telemtry service +Inspired by how professional motorsport teams process data: direct ingestion, time-series storage, and analysis tooling β€” no unnecessary infrastructure. -## Infra +## Architecture -- Golang or C# data ingest -- RabbitMQ pub/sub -- C# telemetry service -- QuestDB database -- NextJS dashboard +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” protobuf/gRPC β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Go Ingest β”‚ ────────────────▢ β”‚ Telemetry Service │────────▢│ QuestDB β”‚ +β”‚ CLI (PC) β”‚ β”‚ (Go, Pi5) β”‚ ILP β”‚ (Pi5) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ REST API + β”‚ + β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” + β”‚ Dashboard β”‚ + β”‚ (React) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` -### Sources +**Why no message queue?** Professional teams (F1, WEC, IMSA) use direct ingestion at the garage level β€” sensor to logger to analysis tool. Queues only appear at factory scale when 50+ engineers need independent consumer offsets across the same data. With one producer and one consumer doing batch post-session processing, the source `.ibt` files on disk are already the durable buffer. -[IRacing data ingest](https://github.com/hiimkyle/vr2c20) +## What It Does -## Current stats -- Can ingest 1.3 million telemetry points in 9s with the golang ingest +- **Parses** iRacing `.ibt` binary telemetry (mmap zero-copy, 46+ channels at 60 Hz) +- **Stores** time-series data in QuestDB (speed, throttle, brake, RPM, GPS, tire temps, G-forces, fuel, and more) +- **Visualises** laps on an interactive dashboard with synchronised track maps and telemetry charts +- **Runs** on a Pi 5 behind Traefik with Tailscale access, Prometheus metrics, and Grafana dashboards -## Next step +## Stack -- [x] Add all the telemetry data to one bucket per track -- [x] look at better running in parallel -- [] Handle session num 0 meaning practice -- [] Create a store on the device to know what files have already been sent -- [] Better filter non ibt files +| Component | Tech | Role | +|---|---|---| +| **Ingest CLI** | Go | Reads `.ibt` files, mmap zero-copy parsing, batches protobuf | +| **Telemetry Service** | Go | Receives data, writes to QuestDB via ILP, serves REST API | +| **Database** | QuestDB | Time-series storage optimised for high-throughput telemetry | +| **Dashboard** | Vite + React + TanStack Router | Track maps (MapLibre), telemetry charts (Recharts), session browser | +| **Infrastructure** | Traefik, Prometheus, Grafana | Reverse proxy, metrics, monitoring | +| **Cloud** | Cloudflare Workers + D1 | Optional cloud deployment variant | +## Performance + +**Ingest** (Apple M4 Pro) +- ~31M ticks/sec parsing 10 key fields, ~2M ticks/sec for all 160+ fields +- Zero memory allocations per tick β€” mmap zero-copy reads +- iRacing records at 60 Hz β€” parser runs ~33,000x faster than real-time + +**Pipeline (Old queue benchmarks)** +- 32MB / 16K record batches, worker pool defaults to CPU cores + 25% +- QuestDB writes flush at 10K rows or every second +- Memory-aware auto-pause at 5GB on Pi + +Run `make bench` in `ingest/go/` for your hardware numbers. + +## Getting Started + +### Prerequisites + +- Docker & Docker Compose (or Podman) +- Go 1.25+ (for ingest CLI) +- Node.js 18+ / pnpm (for dashboard development) + +### Quick Start + +```bash +cp .env.template .env +# Edit .env with your settings + +# Dev mode (builds from source) +make restart-dev + +# Production mode (pre-built images from GHCR) +make restart +``` + +### Running Services Individually + +```bash +# Ingest β€” run on your PC where .ibt files are stored +cd ingest/go && go run ./cmd/ingest go /path/to/ibt/files + +# Dashboard β€” local dev server +cd dashboard && pnpm install && pnpm dev + +# Cloud deployment +cd cloud && npx wrangler dev +``` + +### Makefile Targets + +| Target | Description | +|---|---| +| `make restart` | Production: pull images and start | +| `make restart-dev` | Dev: build from source and start | +| `make restart-lite` | Dev: rebuild without wiping volumes | +| `make restart-p` / `restart-dev-p` / `restart-lite-p` | Same as above with Podman | + +## Project Structure + +``` +β”œβ”€β”€ ingest/go/ # Go CLI β€” .ibt parsing and protobuf serialisation +β”œβ”€β”€ telemetryService/ +β”‚ β”œβ”€β”€ golang/ # Go telemetry consumer and API server +β”‚ └── telemetryService/ # C# (.NET 8) alternative consumer +β”œβ”€β”€ dashboard/ # Vite + React + TanStack Router frontend +β”œβ”€β”€ cloud/ # Cloudflare Workers + D1 cloud variant +β”œβ”€β”€ e2e/ # End-to-end integration tests (Go) +β”œβ”€β”€ config/ # QuestDB, Prometheus, Grafana configs +β”œβ”€β”€ traefik/ # Reverse proxy routing and TLS +β”œβ”€β”€ docker-compose.yml # Production compose +β”œβ”€β”€ docker-compose.dev.yml # Dev compose (builds from source) +└── Makefile +``` + +## Service Endpoints (via Traefik) + +| Path | Service | +|---|---| +| `/dashboard` | Telemetry Dashboard | +| `/api` | Telemetry Service API | +| `/grafana` | Grafana | +| `/prometheus` | Prometheus | +| `/questdb` | QuestDB HTTP API | +| `/net-dash` | Traefik Dashboard | + +All services accessible on both the local domain and via Tailscale (with TLS). + +## Roadmap + +### Now +- [ ] Remove RabbitMQ β€” replace with direct HTTP/gRPC ingestion +- [ ] Handle session num 0 (practice sessions) +- [ ] Track which `.ibt` files have already been ingested +- [ ] Containerise the ingest CLI for e2e testing + +### Next +- [ ] ML pipeline β€” lap time prediction from partial lap data (XGBoost on QuestDB telemetry) +- [ ] Tire degradation curves β€” LSTM over stint data (lateral G, tire temps, speed, throttle) +- [ ] gRPC server streaming for live fan-out to multiple consumers +- [ ] Multicast learning implementation (PGM-style reliable UDP) + +### Later +- [ ] Reinforcement learning for pit strategy optimisation +- [ ] Multi-session comparison and trend analysis +- [ ] Cloud sync between Pi and Cloudflare deployment + +## License + +MIT β€” Oliver Parkinson diff --git a/cloud/.dockerignore b/cloud/.dockerignore new file mode 100644 index 0000000..384838c --- /dev/null +++ b/cloud/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.wrangler +server +src +*.toml +*.json +*.ts +migrations diff --git a/cloud/.gitignore b/cloud/.gitignore new file mode 100644 index 0000000..97c5e72 --- /dev/null +++ b/cloud/.gitignore @@ -0,0 +1,3 @@ +.wrangler/ +server +node_modules/ diff --git a/cloud/Dockerfile b/cloud/Dockerfile new file mode 100644 index 0000000..cee2d20 --- /dev/null +++ b/cloud/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:latest +COPY bin/server /server +EXPOSE 8080 +CMD ["/server"] diff --git a/cloud/Makefile b/cloud/Makefile new file mode 100644 index 0000000..c6d863d --- /dev/null +++ b/cloud/Makefile @@ -0,0 +1,42 @@ +.PHONY: dev fix-podman check-podman build-server build-server-prod + +build-server: + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o ./bin/server ./cmd/api + +build-server-prod: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/server ./cmd/api + +# Start wrangler dev, fixing podman runtime if needed +dev: build-server check-podman + pnpm dev + +build-frontend: + cd ../dashboard/ && pnpm run build + +deploy: build-frontend build-server-prod + pnpm run deploy + +# Check if podman VM has runc configured; fix automatically if not +check-podman: + @runtime=$$(podman info --format '{{.Host.OCIRuntime.Name}}' 2>/dev/null) && \ + if [ "$$runtime" = "runc" ]; then \ + echo "podman: runc runtime OK"; \ + else \ + echo "podman: runtime is '$$runtime', need runc β€” running fix-podman..."; \ + $(MAKE) fix-podman; \ + fi + +# Install runc in podman VM and set it as default runtime. +# Needed because workerd sends MemorySwappiness in container create requests, +# which crun rejects on cgroupv2. runc silently ignores it. +fix-podman: + @echo "Installing runc in podman VM..." + podman machine ssh -- "\ + curl -fsSL -o /usr/local/bin/runc \ + https://github.com/opencontainers/runc/releases/download/v1.2.5/runc.$$(podman machine ssh -- uname -m | sed 's/aarch64/arm64/;s/x86_64/amd64/') && \ + chmod +x /usr/local/bin/runc && \ + sudo mkdir -p /etc/containers && \ + printf '[engine]\nruntime = \"runc\"\n' | sudo tee /etc/containers/containers.conf > /dev/null" + @echo "Restarting podman machine..." + podman machine stop && podman machine start + @echo "Done. Runtime: $$(podman info --format '{{.Host.OCIRuntime.Name}}')" diff --git a/cloud/cmd/api/main.go b/cloud/cmd/api/main.go new file mode 100644 index 0000000..92a5383 --- /dev/null +++ b/cloud/cmd/api/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/ojparkinson/IRacing-Display/cloud/internal/handlers" +) + +func testHandler(w http.ResponseWriter, r *http.Request) { + country := os.Getenv("CLOUDFLARE_COUNTRY_A2") + location := os.Getenv("CLOUDFLARE_LOCATION") + region := os.Getenv("CLOUDFLARE_REGION") + + fmt.Fprintf(w, "Hi, I'm a container running in %s, %s, which is part of %s ", location, country, region) +} + +func main() { + var port string + var exists bool + if port, exists = os.LookupEnv("PORT"); !exists { + port = "8080" + } + + c := make(chan os.Signal, 10) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + terminate := false + go func() { + for range c { + if terminate { + os.Exit(0) + continue + } + + terminate = true + go func() { + time.Sleep(time.Minute) + os.Exit(0) + }() + } + }() + + mux := http.NewServeMux() + + h := &handlers.Handler{} + + mux.HandleFunc("/api/test", testHandler) + mux.HandleFunc("/api/geojson", h.GetGeojson) + mux.HandleFunc("/api/sessions", h.GetSessions) + mux.HandleFunc("/api/session/{sessionId}", h.GetSession) + + server := &http.Server{ + Addr: "0.0.0.0:" + port, + Handler: mux, + } + + fmt.Println("server started on 0.0.0.0:" + port) + log.Fatal(server.ListenAndServe()) +} diff --git a/cloud/go.mod b/cloud/go.mod new file mode 100644 index 0000000..fc6313a --- /dev/null +++ b/cloud/go.mod @@ -0,0 +1,3 @@ +module github.com/ojparkinson/IRacing-Display/cloud + +go 1.26.0 diff --git a/cloud/index.ts b/cloud/index.ts new file mode 100644 index 0000000..2c28c1e --- /dev/null +++ b/cloud/index.ts @@ -0,0 +1,36 @@ +import { Container } from "@cloudflare/containers"; + +export class IracingAPI extends Container { + defaultPort = 8080; + sleepAfter = "30s"; + + override onStart() { + console.log("iRacing API container started"); + } + + override onStop() { + console.log("iRacing API container stopped"); + } + + override onError(error: unknown) { + console.error("iRacing API container error:", error); + } +} + +interface Env { + IRACING_API: DurableObjectNamespace; + ASSETS: Fetcher; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + const container = env.IRACING_API.getByName("iracing-api"); + return await container.fetch(request); + } + + return env.ASSETS.fetch(request); + }, +}; diff --git a/cloud/internal/handlers/geojson.go b/cloud/internal/handlers/geojson.go new file mode 100644 index 0000000..ae69cd4 --- /dev/null +++ b/cloud/internal/handlers/geojson.go @@ -0,0 +1,7 @@ +// Pcagege handers +package handlers + +import "net/http" + +func (h *Handler) GetGeojson(w http.ResponseWriter, r *http.Request) { +} diff --git a/cloud/internal/handlers/handler.go b/cloud/internal/handlers/handler.go new file mode 100644 index 0000000..d563197 --- /dev/null +++ b/cloud/internal/handlers/handler.go @@ -0,0 +1,4 @@ +package handlers + +type Handler struct { +} diff --git a/cloud/internal/handlers/laps.go b/cloud/internal/handlers/laps.go new file mode 100644 index 0000000..ea547b3 --- /dev/null +++ b/cloud/internal/handlers/laps.go @@ -0,0 +1,7 @@ +package handlers + +import "net/http" + +func (h *Handler) GetLaps(w http.ResponseWriter, r *http.Request) { + +} diff --git a/cloud/internal/handlers/sessions.go b/cloud/internal/handlers/sessions.go new file mode 100644 index 0000000..c50d2f1 --- /dev/null +++ b/cloud/internal/handlers/sessions.go @@ -0,0 +1,16 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +func (h *Handler) GetSessions(w http.ResponseWriter, r *http.Request) { + +} +func (h *Handler) GetSession(w http.ResponseWriter, r *http.Request) { + sessionID := r.PathValue("sessionId") + + w.WriteHeader(200) + json.NewEncoder(w).Encode(sessionID) +} diff --git a/cloud/internal/handlers/sync.go b/cloud/internal/handlers/sync.go new file mode 100644 index 0000000..5ac8282 --- /dev/null +++ b/cloud/internal/handlers/sync.go @@ -0,0 +1 @@ +package handlers diff --git a/cloud/internal/handlers/telemetry.go b/cloud/internal/handlers/telemetry.go new file mode 100644 index 0000000..5ac8282 --- /dev/null +++ b/cloud/internal/handlers/telemetry.go @@ -0,0 +1 @@ +package handlers diff --git a/cloud/internal/middleware/auth.go b/cloud/internal/middleware/auth.go new file mode 100644 index 0000000..c870d7c --- /dev/null +++ b/cloud/internal/middleware/auth.go @@ -0,0 +1 @@ +package middleware diff --git a/cloud/internal/model/types.go b/cloud/internal/model/types.go new file mode 100644 index 0000000..0c3b4f2 --- /dev/null +++ b/cloud/internal/model/types.go @@ -0,0 +1 @@ +package model \ No newline at end of file diff --git a/cloud/internal/storage/d1.go b/cloud/internal/storage/d1.go new file mode 100644 index 0000000..7306f61 --- /dev/null +++ b/cloud/internal/storage/d1.go @@ -0,0 +1 @@ +package storage \ No newline at end of file diff --git a/cloud/migrations/0001_initial.sql b/cloud/migrations/0001_initial.sql new file mode 100644 index 0000000..643dba5 --- /dev/null +++ b/cloud/migrations/0001_initial.sql @@ -0,0 +1,24 @@ + CREATE TABLE sessions ( + session_id TEXT PRIMARY KEY, + track_name TEXT NOT NULL, + track_id TEXT NOT NULL, + session_name TEXT NOT NULL, + session_num TEXT NOT NULL, + car_id TEXT NOT NULL, + max_lap_id INTEGER NOT NULL DEFAULT 0, + last_updated TEXT NOT NULL, + synced_at TEXT NOT NULL + ); + + CREATE TABLE laps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL REFERENCES sessions(session_id), + lap_id INTEGER NOT NULL, + lap_time REAL, + tick_count INTEGER NOT NULL DEFAULT 0, + r2_key TEXT NOT NULL, + synced_at TEXT NOT NULL, + UNIQUE(session_id, lap_id) + ); + + CREATE INDEX idx_laps_session ON laps(session_id); \ No newline at end of file diff --git a/cloud/package.json b/cloud/package.json new file mode 100644 index 0000000..cb999e8 --- /dev/null +++ b/cloud/package.json @@ -0,0 +1,17 @@ +{ + "name": "iracing-api", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "@cloudflare/containers": "^0.1.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260101.0", + "wrangler": "^4.0.0" + } +} diff --git a/cloud/pnpm-lock.yaml b/cloud/pnpm-lock.yaml new file mode 100644 index 0000000..6f2eed8 --- /dev/null +++ b/cloud/pnpm-lock.yaml @@ -0,0 +1,887 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@cloudflare/containers': + specifier: ^0.1.0 + version: 0.1.0 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20260101.0 + version: 4.20260218.0 + wrangler: + specifier: ^4.0.0 + version: 4.66.0(@cloudflare/workers-types@4.20260218.0) + +packages: + + '@cloudflare/containers@0.1.0': + resolution: {integrity: sha512-nuXnmkpJZzbjYdguRI2hB0sw1QCBMWdNuGDNQwEiJSLebtKRFpBt/d6AStGjp+8wGD2plPbd2U/mQerYF9kzJg==} + + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.13.0': + resolution: {integrity: sha512-bT2rnecesLjDBHgouMEPW9EQ7iLE8OG58srMuCEpAGp75xabi6j124SdS8XZ+dzB3sYBW4iQvVeCTCbAnMMVtA==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ^1.20260213.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260217.0': + resolution: {integrity: sha512-t1KRT0j4gwLntixMoNujv/UaS89Q7+MPRhkklaSup5tNhl3zBZOIlasBUSir69eXetqLZu8sypx3i7zE395XXA==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260217.0': + resolution: {integrity: sha512-9pEZ15BmELt0Opy79LTxUvbo55QAI4GnsnsvmgBxaQlc4P0dC8iycBGxbOpegkXnRx/LFj51l2zunfTo0EdATg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260217.0': + resolution: {integrity: sha512-IrZfxQ4b/4/RDQCJsyoxKrCR+cEqKl81yZOirMOKoRrDOmTjn4evYXaHoLBh2PjUKY1Imly7ZiC6G1p0xNIOwg==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260217.0': + resolution: {integrity: sha512-RGU1wq69ym4sFBVWhQeddZrRrG0hJM/SlZ5DwVDga/zBJ3WXxcDsFAgg1dToDfildTde5ySXN7jAasSmWko9rg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260217.0': + resolution: {integrity: sha512-4T65u1321z1Zet9n7liQsSW7g3EXM5SWIT7kJ/uqkEtkPnIzZBIowMQgkvL5W9SpGZks9t3mTQj7hiUia8Gq9Q==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260218.0': + resolution: {integrity: sha512-E28uJNJb9J9pca3RaxjXm1JxAjp8td9/cudkY+IT8rio71NlshN7NKMe2Cr/6GN+RufbSnp+N3ZKP74xgUaL0A==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + miniflare@4.20260217.0: + resolution: {integrity: sha512-t2v02Vi9SUiiXoHoxLvsntli7N35e/35PuRAYEqHWtHOdDX3bqQ73dBQ0tI12/8ThCb2by2tVs7qOvgwn6xSBQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + workerd@1.20260217.0: + resolution: {integrity: sha512-6jVisS6wB6KbF+F9DVoDUy9p7MON8qZCFSaL8OcDUioMwknsUPFojUISu3/c30ZOZ24D4h7oqaahFc5C6huilw==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.66.0: + resolution: {integrity: sha512-b9RVIdKai0BXDuYg0iN0zwVnVbULkvdKGP7Bf1uFY2GhJ/nzDGqgwQbCwgDIOhmaBC8ynhk/p22M2jc8tJy+dQ==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260217.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + +snapshots: + + '@cloudflare/containers@0.1.0': {} + + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.13.0(unenv@2.0.0-rc.24)(workerd@1.20260217.0)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260217.0 + + '@cloudflare/workerd-darwin-64@1.20260217.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260217.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20260217.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260217.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20260217.0': + optional: true + + '@cloudflare/workers-types@4.20260218.0': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.14': {} + + blake3-wasm@2.1.5: {} + + cookie@1.1.1: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + fsevents@2.3.3: + optional: true + + kleur@4.1.5: {} + + miniflare@4.20260217.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.18.2 + workerd: 1.20260217.0 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + semver@7.7.4: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + supports-color@10.2.2: {} + + tslib@2.8.1: + optional: true + + undici@7.18.2: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + workerd@1.20260217.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260217.0 + '@cloudflare/workerd-darwin-arm64': 1.20260217.0 + '@cloudflare/workerd-linux-64': 1.20260217.0 + '@cloudflare/workerd-linux-arm64': 1.20260217.0 + '@cloudflare/workerd-windows-64': 1.20260217.0 + + wrangler@4.66.0(@cloudflare/workers-types@4.20260218.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.13.0(unenv@2.0.0-rc.24)(workerd@1.20260217.0) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260217.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260217.0 + optionalDependencies: + '@cloudflare/workers-types': 4.20260218.0 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.18.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.14 + cookie: 1.1.1 + youch-core: 0.3.3 diff --git a/cloud/src/index.ts b/cloud/src/index.ts new file mode 100644 index 0000000..2c28c1e --- /dev/null +++ b/cloud/src/index.ts @@ -0,0 +1,36 @@ +import { Container } from "@cloudflare/containers"; + +export class IracingAPI extends Container { + defaultPort = 8080; + sleepAfter = "30s"; + + override onStart() { + console.log("iRacing API container started"); + } + + override onStop() { + console.log("iRacing API container stopped"); + } + + override onError(error: unknown) { + console.error("iRacing API container error:", error); + } +} + +interface Env { + IRACING_API: DurableObjectNamespace; + ASSETS: Fetcher; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + const container = env.IRACING_API.getByName("iracing-api"); + return await container.fetch(request); + } + + return env.ASSETS.fetch(request); + }, +}; diff --git a/cloud/tsconfig.json b/cloud/tsconfig.json new file mode 100644 index 0000000..208da41 --- /dev/null +++ b/cloud/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/cloud/wrangler.toml b/cloud/wrangler.toml new file mode 100644 index 0000000..87405d7 --- /dev/null +++ b/cloud/wrangler.toml @@ -0,0 +1,33 @@ +name = "iracing-api" +main = "src/index.ts" +compatibility_date = "2026-02-18" + +[assets] +directory = "../dashboard/dist" +binding = "ASSETS" +not_found_handling = "single-page-application" + +[[containers]] +class_name = "IracingAPI" +image = "./Dockerfile" +max_instances = 1 + +[[durable_objects.bindings]] +class_name = "IracingAPI" +name = "IRACING_API" + +[[migrations]] +new_sqlite_classes = ["IracingAPI"] +tag = "v1" + +[[d1_databases]] +binding = "DB" +database_name = "telemetry" +database_id = "8b2a184a-f60c-4d0c-9acb-6e935a5bec0d" +preview_database_id = "local" # uses local sqlite in dev + +[observability] +enabled = true + +[dev] +container_engine = "unix:///var/folders/jd/_dwy1xxx1qn7jwgcxx53qxw80000gn/T/podman/podman-machine-default-api.sock" diff --git a/config/definitions.json b/config/definitions.json index 615a82e..13ccf57 100644 --- a/config/definitions.json +++ b/config/definitions.json @@ -1,45 +1,73 @@ { - "rabbit_version": "4.0.0", - "rabbitmq_version": "4.0.0", - "product_name": "RabbitMQ", - "product_version": "4.0.0", - "vhosts": [ - { - "name": "/" - } - ], - "topic_permissions": [], - "parameters": [], - "global_parameters": [], - "policies": [], - "queues": [ - { - "name": "telemetry_queue", - "vhost": "/", - "durable": true, - "auto_delete": false, - "arguments": {} - } - ], - "exchanges": [ - { - "name": "telemetry_topic", - "vhost": "/", - "type": "topic", - "durable": true, - "auto_delete": false, - "internal": false, - "arguments": {} - } - ], - "bindings": [ - { - "source": "telemetry_topic", - "vhost": "/", - "destination": "telemetry_queue", - "destination_type": "queue", - "routing_key": "telemetry.ticks", - "arguments": {} - } - ] -} \ No newline at end of file + "rabbit_version": "4.0.0", + "rabbitmq_version": "4.0.0", + "product_name": "RabbitMQ", + "product_version": "4.0.0", + "vhosts": [ + { + "name": "/" + } + ], + "topic_permissions": [], + "users": [ + { + "name": "admin", + "password_hash": "FTwEjnDO5R8xMjBYrEULfrr5Y3sT8Ky1D1qoJwqMeUPKQjr2", + "tags": ["administrator"] + }, + { + "name": "guest", + "password": "guest", + "tags": ["administrator"] + } + ], + "permissions": [ + { + "user": "admin", + "vhost": "/", + "configure": ".*", + "write": ".*", + "read": ".*" + }, + { + "user": "guest", + "vhost": "/", + "configure": ".*", + "write": ".*", + "read": ".*" + } + ], + "parameters": [], + "global_parameters": [], + "policies": [], + "queues": [ + { + "name": "telemetry_queue", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + } + ], + "exchanges": [ + { + "name": "telemetry_topic", + "vhost": "/", + "type": "topic", + "durable": true, + "auto_delete": false, + "internal": false, + "arguments": {} + } + ], + "bindings": [ + { + "source": "telemetry_topic", + "vhost": "/", + "destination": "telemetry_queue", + "destination_type": "queue", + "routing_key": "telemetry.ticks", + "arguments": {} + } + ] +} diff --git a/config/grafana-dashboards.yml b/config/grafana-dashboards.yml deleted file mode 100644 index 154d3f1..0000000 --- a/config/grafana-dashboards.yml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: 1 - -providers: - - name: 'iracing-dashboards' - orgId: 1 - folder: 'IRacing Telemetry' - type: file - disableDeletion: false - updateIntervalSeconds: 10 - allowUiUpdates: true - options: - path: /etc/grafana/provisioning/dashboards/files \ No newline at end of file diff --git a/config/grafana-datasources.yml b/config/grafana-datasources.yml deleted file mode 100644 index c7af75b..0000000 --- a/config/grafana-datasources.yml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: 1 - -datasources: - - name: Prometheus - type: prometheus - access: proxy - url: http://prometheus:9090 - uid: prometheus - isDefault: true - editable: true - - - name: Loki - type: loki - access: proxy - url: http://loki:3100 - uid: loki - editable: true \ No newline at end of file diff --git a/config/grafana/dashboards/files/data-flow-pipeline.json b/config/grafana/dashboards/files/data-flow-pipeline.json new file mode 100644 index 0000000..18b4197 --- /dev/null +++ b/config/grafana/dashboards/files/data-flow-pipeline.json @@ -0,0 +1,164 @@ +{ + "title": "Data Flow Pipeline", + "uid": "data-flow-pipeline", + "editable": true, + "timezone": "browser", + "refresh": "5s", + "time": { + "from": "now-15m", + "to": "now" + }, + "panels": [ + { + "title": "Record Throughput (Records/sec)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}, + "datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"}, + "targets": [ + { + "expr": "rate(ingest_records_processed_total[1m])", + "legendFormat": "Ingest Rate" + }, + { + "expr": "rate(telemetry_records_written_total[1m])", + "legendFormat": "DB Write Rate" + } + ], + "fieldConfig": { + "defaults": { + "unit": "rps", + "custom": {"drawStyle": "line", "lineInterpolation": "smooth", "showPoints": "auto"} + } + } + }, + { + "title": "File Processing Rate (Files/sec)", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, + "datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"}, + "targets": [ + { + "expr": "rate(ingest_files_processed_total[1m])", + "legendFormat": "Files/sec" + } + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "custom": { + "drawStyle": "points", + "pointSize": 5, + "showPoints": "always", + "lineWidth": 0 + } + } + } + }, + { + "title": "End-to-End Latency", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}, + "datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"}, + "targets": [ + { + "expr": "histogram_quantile(0.50, rate(ingest_file_processing_duration_seconds_bucket[1m]))", + "legendFormat": "P50 File Processing" + }, + { + "expr": "histogram_quantile(0.95, rate(ingest_file_processing_duration_seconds_bucket[1m]))", + "legendFormat": "P95 File Processing" + }, + { + "expr": "histogram_quantile(0.99, rate(ingest_file_processing_duration_seconds_bucket[1m]))", + "legendFormat": "P99 File Processing" + } + ], + "fieldConfig": { + "defaults": {"unit": "s"} + } + }, + { + "title": "Database Write Latency", + "type": "timeseries", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}, + "datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"}, + "targets": [ + { + "expr": "histogram_quantile(0.50, rate(telemetry_db_write_duration_seconds_bucket[1m]))", + "legendFormat": "P50 DB Write" + }, + { + "expr": "histogram_quantile(0.95, rate(telemetry_db_write_duration_seconds_bucket[1m]))", + "legendFormat": "P95 DB Write" + }, + { + "expr": "histogram_quantile(0.99, rate(telemetry_db_write_duration_seconds_bucket[1m]))", + "legendFormat": "P99 DB Write" + } + ], + "fieldConfig": { + "defaults": {"unit": "s"} + } + }, + { + "title": "Current Rates", + "type": "stat", + "gridPos": {"h": 4, "w": 6, "x": 0, "y": 16}, + "datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"}, + "targets": [ + { + "expr": "rate(ingest_records_processed_total[1m])", + "legendFormat": "Ingest Rate" + } + ], + "fieldConfig": { + "defaults": {"unit": "rps", "decimals": 0} + } + }, + { + "title": "Total Records Processed", + "type": "stat", + "gridPos": {"h": 4, "w": 6, "x": 6, "y": 16}, + "datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"}, + "targets": [ + { + "expr": "ingest_records_processed_total", + "legendFormat": "Total Records" + } + ], + "fieldConfig": { + "defaults": {"decimals": 0} + } + }, + { + "title": "DB Write Success Rate", + "type": "stat", + "gridPos": {"h": 4, "w": 6, "x": 12, "y": 16}, + "datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"}, + "targets": [ + { + "expr": "(rate(telemetry_records_written_total[5m]) / rate(telemetry_records_received_total[5m])) * 100", + "legendFormat": "Success Rate" + } + ], + "fieldConfig": { + "defaults": {"unit": "percent", "decimals": 2, "thresholds": {"steps": [{"color": "red", "value": 0}, {"color": "yellow", "value": 90}, {"color": "green", "value": 99}]}} + } + }, + { + "title": "Queue Depth", + "type": "stat", + "gridPos": {"h": 4, "w": 6, "x": 18, "y": 16}, + "datasource": {"type": "prometheus", "uid": "PBFA97CFB590B2093"}, + "targets": [ + { + "expr": "ingest_queue_depth", + "legendFormat": "Queue Depth" + } + ], + "fieldConfig": { + "defaults": {"decimals": 0, "thresholds": {"steps": [{"color": "green", "value": 0}, {"color": "yellow", "value": 100}, {"color": "red", "value": 500}]}} + } + } + ] +} diff --git a/config/grafana/dashboards/files/enhanced-iracing-telemetry-system.json b/config/grafana/dashboards/files/enhanced-iracing-telemetry-system.json index 73745c8..905a7a6 100644 --- a/config/grafana/dashboards/files/enhanced-iracing-telemetry-system.json +++ b/config/grafana/dashboards/files/enhanced-iracing-telemetry-system.json @@ -24,7 +24,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 2, "links": [], "liveNow": false, "panels": [ @@ -44,7 +43,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -77,14 +76,15 @@ }, "gridPos": { "h": 8, - "w": 4, + "w": 5, "x": 0, "y": 1 }, - "id": 2, + "id": 3, "options": { - "minVizHeight": 75, - "minVizWidth": 75, + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -93,26 +93,29 @@ "fields": "", "values": false }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto", - "text": {} + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, "pluginVersion": "10.4.8", "targets": [ { - "expr": "100 - (node_memory_MemAvailable_bytes{job=\"node-exporter\"} / node_memory_MemTotal_bytes{job=\"node-exporter\"} * 100)", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg((1 - rate(node_cpu_seconds_total{job='node-exporter',mode='idle'}[1m])) * 100)", + "legendFormat": "{{cpu}}", + "range": true, "refId": "A" } ], - "title": "Memory Usage", - "type": "gauge" + "title": "CPU Usage", + "type": "stat" }, { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, + "datasource": {}, "fieldConfig": { "defaults": { "color": { @@ -144,14 +147,15 @@ }, "gridPos": { "h": 8, - "w": 6, - "x": 4, + "w": 5, + "x": 5, "y": 1 }, - "id": 3, + "id": 2, "options": { - "minVizHeight": 75, - "minVizWidth": 75, + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -160,33 +164,29 @@ "fields": "", "values": false }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto", - "text": {} + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, "pluginVersion": "10.4.8", "targets": [ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "editorMode": "code", - "expr": "(1 - rate(node_cpu_seconds_total{job='node-exporter',mode='idle'}[5m])) * 100", - "legendFormat": "{{cpu}}", + "expr": "100 - (node_memory_MemAvailable_bytes{job=\"node-exporter\"} / node_memory_MemTotal_bytes{job=\"node-exporter\"} * 100)", + "legendFormat": "{{label_name}}", "range": true, "refId": "A" } ], - "title": "CPU Usage", - "type": "gauge" + "title": "Memory Usage", + "type": "stat" }, { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, + "datasource": {}, "fieldConfig": { "defaults": { "color": { @@ -224,8 +224,9 @@ }, "id": 4, "options": { - "minVizHeight": 75, - "minVizWidth": 75, + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -234,26 +235,29 @@ "fields": "", "values": false }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto", - "text": {} + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, "pluginVersion": "10.4.8", "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "100 - ((node_filesystem_avail_bytes{mountpoint=\"/\",fstype!=\"rootfs\"} / node_filesystem_size_bytes{mountpoint=\"/\",fstype!=\"rootfs\"}) * 100)", + "legendFormat": "{{label_name}}", + "range": true, "refId": "A" } ], "title": "Disk Usage", - "type": "gauge" + "type": "stat" }, { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, + "datasource": {}, "fieldConfig": { "defaults": { "color": { @@ -291,8 +295,9 @@ }, "id": 5, "options": { - "minVizHeight": 75, - "minVizWidth": 75, + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -301,10 +306,9 @@ "fields": "", "values": false }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto", - "text": {} + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, "pluginVersion": "10.4.8", "targets": [ @@ -315,18 +319,18 @@ }, "editorMode": "code", "expr": "avg(node_hwmon_temp_celsius{job=\"node-exporter\"})", - "legendFormat": "{{label_name}}", + "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "CPU Temperature", - "type": "gauge" + "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -406,18 +410,36 @@ "pluginVersion": "10.4.8", "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "up{job=\"node-exporter\"}", - "legendFormat": "Node Exporter", + "legendFormat": "{{job}}", + "range": true, "refId": "A" }, { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "up{job=\"questdb-metrics\"}", - "legendFormat": "QuestDB", + "legendFormat": "{{job}}", + "range": true, "refId": "B" }, { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "up{job=\"rabbitmq-metrics\"}", - "legendFormat": "RabbitMQ", + "legendFormat": "{{job}}", + "range": true, "refId": "C" } ], @@ -438,10 +460,7 @@ "type": "row" }, { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, + "datasource": {}, "fieldConfig": { "defaults": { "color": { @@ -521,7 +540,7 @@ }, "editorMode": "code", "expr": "rate(questdb_json_queries_total[1m])", - "legendFormat": "JSON Queries/sec", + "legendFormat": "__auto", "range": true, "refId": "A" }, @@ -541,7 +560,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -616,13 +635,26 @@ }, "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "questdb_http_connections", - "legendFormat": "HTTP Connections", + "interval": "", + "legendFormat": "__auto", + "range": true, "refId": "A" }, { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "questdb_pg_wire_connections", - "legendFormat": "PostgreSQL Connections", + "legendFormat": "__auto", + "range": true, "refId": "B" } ], @@ -632,7 +664,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -713,7 +745,7 @@ }, "editorMode": "code", "expr": "rate(questdb_json_queries_cache_hits_total[1m])", - "legendFormat": "Cache Hits/sec", + "legendFormat": "__auto", "range": true, "refId": "A" }, @@ -722,8 +754,10 @@ "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", "expr": "rate(questdb_json_queries_cache_misses_total[5m])", - "legendFormat": "Cache Misses/sec", + "legendFormat": "__auto", + "range": true, "refId": "B" } ], @@ -733,7 +767,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -814,7 +848,7 @@ }, "editorMode": "code", "expr": "rate(questdb_json_queries_completed_total[1m])", - "legendFormat": "Completed Queries/sec", + "legendFormat": "__auto", "range": true, "refId": "A" }, @@ -823,8 +857,10 @@ "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", "expr": "questdb_pg_wire_errors_total", - "legendFormat": "PostgreSQL Errors", + "legendFormat": "__auto", + "range": true, "refId": "B" } ], @@ -844,23 +880,10 @@ "title": "RabbitMQ Performance", "type": "row" }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 19 - }, - "id": 17, - "panels": [], - "title": "System Resource Usage", - "type": "row" - }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -905,7 +928,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null } ] }, @@ -917,7 +941,7 @@ "h": 8, "w": 6, "x": 0, - "y": 20 + "y": 19 }, "id": 31, "options": { @@ -938,8 +962,10 @@ "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", "expr": "rabbitmq_queues_declared_total", - "legendFormat": "Total Queues Declared", + "legendFormat": "{{job}}", + "range": true, "refId": "A" }, { @@ -949,7 +975,7 @@ }, "editorMode": "code", "expr": "rate(rabbitmq_connections_opened_total[1m])", - "legendFormat": "Connections Opened/sec", + "legendFormat": "{{job}}", "range": true, "refId": "B" }, @@ -958,8 +984,10 @@ "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", "expr": "rate(rabbitmq_channels_opened_total[5m])", - "legendFormat": "Channels Opened/sec", + "legendFormat": "{{job}}", + "range": true, "refId": "C" } ], @@ -969,7 +997,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -1014,7 +1042,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null } ] }, @@ -1026,7 +1055,7 @@ "h": 8, "w": 6, "x": 6, - "y": 20 + "y": 19 }, "id": 32, "options": { @@ -1043,13 +1072,25 @@ }, "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "rabbitmq_erlang_processes_used", - "legendFormat": "Erlang Processes Used", + "legendFormat": "{{job}}", + "range": true, "refId": "A" }, { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "rabbitmq_process_open_fds", - "legendFormat": "Open File Descriptors", + "legendFormat": "{{job}}", + "range": true, "refId": "B" } ], @@ -1059,7 +1100,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -1104,7 +1145,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null } ] }, @@ -1116,7 +1158,7 @@ "h": 8, "w": 6, "x": 12, - "y": 20 + "y": 19 }, "id": 33, "options": { @@ -1148,8 +1190,10 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "rate(rabbitmq_erlang_gc_reclaimed_bytes_total[5m])", - "legendFormat": "GC Reclaimed Bytes/sec", + "editorMode": "code", + "expr": "rate(rabbitmq_erlang_gc_reclaimed_bytes_total[1m])", + "legendFormat": "messages per sec", + "range": true, "refId": "B" } ], @@ -1159,7 +1203,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -1204,7 +1248,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null } ] }, @@ -1216,7 +1261,7 @@ "h": 8, "w": 6, "x": 18, - "y": 20 + "y": 19 }, "id": 34, "options": { @@ -1233,23 +1278,48 @@ }, "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "rabbitmq_process_resident_memory_bytes", - "legendFormat": "Memory Usage", + "legendFormat": "{{__name__}}", + "range": true, "refId": "A" }, { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "rabbitmq_disk_space_available_bytes", - "legendFormat": "Disk Space Available", + "legendFormat": "{{__name__}}", + "range": true, "refId": "B" } ], "title": "RabbitMQ Resource Usage", "type": "timeseries" }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 17, + "panels": [], + "title": "System Resource Usage", + "type": "row" + }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -1294,7 +1364,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null } ] }, @@ -1329,7 +1400,7 @@ }, "editorMode": "code", "expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[1m])) * 100)", - "legendFormat": "Total CPU Usage %", + "legendFormat": "Total CPU ", "range": true, "refId": "A" }, @@ -1358,7 +1429,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -1403,7 +1474,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null } ] }, @@ -1432,21 +1504,39 @@ }, "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes", "legendFormat": "Memory Used", + "range": true, "refId": "A" }, { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, "expr": "node_memory_Buffers_bytes", "legendFormat": "Buffers", "refId": "B" }, { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, "expr": "node_memory_Cached_bytes", "legendFormat": "Cache", "refId": "C" }, { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, "expr": "node_memory_MemAvailable_bytes", "legendFormat": "Available", "refId": "D" @@ -1458,7 +1548,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -1503,7 +1593,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null } ] }, @@ -1558,7 +1649,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -1603,7 +1694,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null } ] }, @@ -1704,7 +1796,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -1778,8 +1870,14 @@ }, "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", "expr": "rate(loki_ingester_samples_received_total[5m])", "legendFormat": "Log Ingestion Rate", + "range": true, "refId": "A" } ], @@ -1805,6 +1903,6 @@ "timezone": "", "title": "Enhanced IRacing Telemetry System", "uid": "enhanced-iracing-telemetry", - "version": 7, + "version": 3, "weekStart": "" } \ No newline at end of file diff --git a/config/grafana/dashboards/files/iracing-telemetry-system.json b/config/grafana/dashboards/files/iracing-telemetry-system.json index 44c5d91..905a7a6 100644 --- a/config/grafana/dashboards/files/iracing-telemetry-system.json +++ b/config/grafana/dashboards/files/iracing-telemetry-system.json @@ -24,7 +24,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 3, "links": [], "liveNow": false, "panels": [ @@ -36,7 +35,7 @@ "x": 0, "y": 0 }, - "id": 20, + "id": 1, "panels": [], "title": "System Overview", "type": "row" @@ -44,7 +43,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -77,14 +76,15 @@ }, "gridPos": { "h": 8, - "w": 6, + "w": 5, "x": 0, "y": 1 }, - "id": 2, + "id": 3, "options": { - "minVizHeight": 75, - "minVizWidth": 75, + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -93,26 +93,29 @@ "fields": "", "values": false }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto", - "text": {} + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, "pluginVersion": "10.4.8", "targets": [ { - "expr": "100 - (node_memory_MemAvailable_bytes{job=\"node-exporter\"} / node_memory_MemTotal_bytes{job=\"node-exporter\"} * 100)", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg((1 - rate(node_cpu_seconds_total{job='node-exporter',mode='idle'}[1m])) * 100)", + "legendFormat": "{{cpu}}", + "range": true, "refId": "A" } ], - "title": "Memory Usage", - "type": "gauge" + "title": "CPU Usage", + "type": "stat" }, { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, + "datasource": {}, "fieldConfig": { "defaults": { "color": { @@ -144,14 +147,15 @@ }, "gridPos": { "h": 8, - "w": 6, - "x": 6, + "w": 5, + "x": 5, "y": 1 }, - "id": 3, + "id": 2, "options": { - "minVizHeight": 75, - "minVizWidth": 75, + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -160,26 +164,29 @@ "fields": "", "values": false }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto", - "text": {} + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, "pluginVersion": "10.4.8", "targets": [ { - "expr": "100 - (avg(rate(node_cpu_seconds_total{job=\"node-exporter\",mode=\"idle\"}[1m])) * 100)", + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "100 - (node_memory_MemAvailable_bytes{job=\"node-exporter\"} / node_memory_MemTotal_bytes{job=\"node-exporter\"} * 100)", + "legendFormat": "{{label_name}}", + "range": true, "refId": "A" } ], - "title": "CPU Usage", - "type": "gauge" + "title": "Memory Usage", + "type": "stat" }, { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, + "datasource": {}, "fieldConfig": { "defaults": { "color": { @@ -211,14 +218,15 @@ }, "gridPos": { "h": 8, - "w": 6, - "x": 12, + "w": 4, + "x": 10, "y": 1 }, "id": 4, "options": { - "minVizHeight": 75, - "minVizWidth": 75, + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -227,25 +235,102 @@ "fields": "", "values": false }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto", - "text": {} + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, "pluginVersion": "10.4.8", "targets": [ { - "expr": "100 - ((node_filesystem_avail_bytes{job=\"node-exporter\",mountpoint=\"/\"} / node_filesystem_size_bytes{job=\"node-exporter\",mountpoint=\"/\"}) * 100)", + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "100 - ((node_filesystem_avail_bytes{mountpoint=\"/\",fstype!=\"rootfs\"} / node_filesystem_size_bytes{mountpoint=\"/\",fstype!=\"rootfs\"}) * 100)", + "legendFormat": "{{label_name}}", + "range": true, "refId": "A" } ], "title": "Disk Usage", - "type": "gauge" + "type": "stat" + }, + { + "datasource": {}, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 85, + "min": 30, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 65 + }, + { + "color": "red", + "value": 75 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 14, + "y": 1 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "avg(node_hwmon_temp_celsius{job=\"node-exporter\"})", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "CPU Temperature", + "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -260,7 +345,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 0, + "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, @@ -270,15 +355,12 @@ }, "insertNulls": false, "lineInterpolation": "linear", - "lineStyle": { - "fill": "solid" - }, "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, - "showPoints": "auto", + "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", @@ -302,7 +384,7 @@ } ] }, - "unit": "celsius" + "unit": "short" }, "overrides": [] }, @@ -312,7 +394,7 @@ "x": 18, "y": 1 }, - "id": 5, + "id": 6, "options": { "legend": { "calcs": [], @@ -325,20 +407,43 @@ "sort": "none" } }, + "pluginVersion": "10.4.8", "targets": [ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "editorMode": "code", - "expr": "node_hwmon_temp_celsius{job=\"node-exporter\"}", - "legendFormat": "CPU Temperature", + "expr": "up{job=\"node-exporter\"}", + "legendFormat": "{{job}}", "range": true, "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "up{job=\"questdb-metrics\"}", + "legendFormat": "{{job}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "up{job=\"rabbitmq-metrics\"}", + "legendFormat": "{{job}}", + "range": true, + "refId": "C" } ], - "title": "Temperature", + "title": "Service Status", "type": "timeseries" }, { @@ -349,113 +454,13 @@ "x": 0, "y": 9 }, - "id": 21, + "id": 7, "panels": [], - "title": "Service Health", + "title": "QuestDB Performance", "type": "row" }, { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [ - { - "options": { - "0": { - "color": "red", - "index": 1, - "text": "DOWN" - }, - "1": { - "color": "green", - "index": 0, - "text": "UP" - } - }, - "type": "value" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 0 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 10 - }, - "id": 6, - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true - }, - "pluginVersion": "10.4.8", - "targets": [ - { - "expr": "up{job=~\"node-exporter|prometheus|rabbitmq|questdb\"}", - "format": "table", - "refId": "A" - } - ], - "title": "Service Status", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true, - "__name__": true - }, - "indexByName": {}, - "renameByName": { - "Value": "Status", - "instance": "Service", - "job": "Job" - } - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, + "datasource": {}, "fieldConfig": { "defaults": { "color": { @@ -501,24 +506,20 @@ { "color": "green", "value": null - }, - { - "color": "red", - "value": 80 } ] }, - "unit": "bytes" + "unit": "reqps" }, "overrides": [] }, "gridPos": { "h": 8, - "w": 12, - "x": 12, + "w": 6, + "x": 0, "y": 10 }, - "id": 7, + "id": 8, "options": { "legend": { "calcs": [], @@ -527,42 +528,39 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, "targets": [ { - "expr": "rate(node_network_receive_bytes_total{job=\"node-exporter\",device!=\"lo\"}[1m])", - "legendFormat": "{{ device }} - Receive", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(questdb_json_queries_total[1m])", + "legendFormat": "__auto", + "range": true, "refId": "A" }, { - "expr": "rate(node_network_transmit_bytes_total{job=\"node-exporter\",device!=\"lo\"}[1m])", - "legendFormat": "{{ device }} - Transmit", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "rate(questdb_pg_wire_queries_total[5m])", + "legendFormat": "PostgreSQL Queries/sec", "refId": "B" } ], - "title": "Network I/O", + "title": "QuestDB Request Rate", "type": "timeseries" }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 18 - }, - "id": 22, - "panels": [], - "title": "IRacing Telemetry Services", - "type": "row" - }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -609,24 +607,20 @@ { "color": "green", "value": null - }, - { - "color": "red", - "value": 80 } ] }, - "unit": "percent" + "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 8, - "w": 12, - "x": 0, - "y": 19 + "w": 6, + "x": 6, + "y": 10 }, - "id": 8, + "id": 9, "options": { "legend": { "calcs": [], @@ -635,24 +629,42 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, "targets": [ { - "expr": "100 - (avg(rate(node_cpu_seconds_total{job=\"node-exporter\",mode=\"idle\"}[1m])) * 100)", - "legendFormat": "CPU Usage", + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "questdb_http_connections", + "interval": "", + "legendFormat": "__auto", + "range": true, "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "questdb_pg_wire_connections", + "legendFormat": "__auto", + "range": true, + "refId": "B" } ], - "title": "System CPU Usage Over Time", + "title": "QuestDB Connections", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { @@ -699,24 +711,20 @@ { "color": "green", "value": null - }, - { - "color": "red", - "value": 80 } ] }, - "unit": "bytes" + "unit": "short" }, "overrides": [] }, "gridPos": { "h": 8, - "w": 12, + "w": 6, "x": 12, - "y": 19 + "y": 10 }, - "id": 9, + "id": 10, "options": { "legend": { "calcs": [], @@ -725,46 +733,79 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, "targets": [ { - "expr": "node_memory_MemTotal_bytes{job=\"node-exporter\"}", - "legendFormat": "Total Memory", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(questdb_json_queries_cache_hits_total[1m])", + "legendFormat": "__auto", + "range": true, "refId": "A" }, { - "expr": "node_memory_MemAvailable_bytes{job=\"node-exporter\"}", - "legendFormat": "Available Memory", - "refId": "B" - }, - { - "expr": "node_memory_MemTotal_bytes{job=\"node-exporter\"} - node_memory_MemAvailable_bytes{job=\"node-exporter\"}", - "legendFormat": "Used Memory", - "refId": "C" + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(questdb_json_queries_cache_misses_total[5m])", + "legendFormat": "__auto", + "range": true, + "refId": "B" } ], - "title": "Memory Usage Over Time", + "title": "QuestDB Cache Performance", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "mode": "palette-classic" }, "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" }, - "inspect": false + "thresholdsStyle": { + "mode": "off" + } }, "mappings": [], "thresholds": { @@ -773,128 +814,1083 @@ { "color": "green", "value": null - }, - { - "color": "red", - "value": 80 } ] }, - "unit": "bytes" + "unit": "ops" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Load Average" - }, - "properties": [ - { - "id": "unit", - "value": "short" - } - ] + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 10 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(questdb_json_queries_completed_total[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "questdb_pg_wire_errors_total", + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "QuestDB Query Performance", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 30, + "panels": [], + "title": "RabbitMQ Performance", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - { - "matcher": { - "id": "byName", - "options": "Uptime" + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" }, - "properties": [ + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ { - "id": "unit", - "value": "dtdurations" + "color": "green", + "value": null } ] - } - ] + }, + "unit": "short" + }, + "overrides": [] }, "gridPos": { "h": 8, - "w": 24, + "w": 6, "x": 0, - "y": 27 + "y": 19 }, - "id": 10, + "id": 31, "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "showHeader": true + "tooltip": { + "mode": "single", + "sort": "none" + } }, - "pluginVersion": "10.4.8", "targets": [ { - "expr": "node_memory_MemTotal_bytes{job=\"node-exporter\"}", - "format": "table", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rabbitmq_queues_declared_total", + "legendFormat": "{{job}}", + "range": true, "refId": "A" }, { - "expr": "node_memory_MemAvailable_bytes{job=\"node-exporter\"}", - "format": "table", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(rabbitmq_connections_opened_total[1m])", + "legendFormat": "{{job}}", + "range": true, "refId": "B" }, { - "expr": "node_load1{job=\"node-exporter\"}", - "format": "table", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(rabbitmq_channels_opened_total[5m])", + "legendFormat": "{{job}}", + "range": true, "refId": "C" - }, - { - "expr": "node_time_seconds{job=\"node-exporter\"} - node_boot_time_seconds{job=\"node-exporter\"}", - "format": "table", - "refId": "D" } ], - "title": "System Information", - "transformations": [ - { - "id": "seriesToColumns", - "options": { - "byField": "instance" - } - }, - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true, - "__name__ 1": true, - "__name__ 2": true, - "__name__ 3": true, - "__name__ 4": true, - "job 1": true, - "job 2": true, - "job 3": true, - "job 4": true - }, - "indexByName": {}, - "renameByName": { - "Value #A": "Total Memory", - "Value #B": "Available Memory", - "Value #C": "Load Average", - "Value #D": "Uptime", - "instance": "Host" + "title": "RabbitMQ Overview", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } - } - } - ], - "type": "table" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 19 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rabbitmq_erlang_processes_used", + "legendFormat": "{{job}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rabbitmq_process_open_fds", + "legendFormat": "{{job}}", + "range": true, + "refId": "B" + } + ], + "title": "RabbitMQ Process Metrics", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "msgps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 19 + }, + "id": 33, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(rabbitmq_erlang_gc_runs_total[1m])", + "legendFormat": "Garbage Collections/sec", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(rabbitmq_erlang_gc_reclaimed_bytes_total[1m])", + "legendFormat": "messages per sec", + "range": true, + "refId": "B" + } + ], + "title": "RabbitMQ Performance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 19 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rabbitmq_process_resident_memory_bytes", + "legendFormat": "{{__name__}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rabbitmq_disk_space_available_bytes", + "legendFormat": "{{__name__}}", + "range": true, + "refId": "B" + } + ], + "title": "RabbitMQ Resource Usage", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 17, + "panels": [], + "title": "System Resource Usage", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[1m])) * 100)", + "legendFormat": "Total CPU ", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "avg(rate(node_cpu_seconds_total{mode=\"system\"}[5m])) * 100", + "legendFormat": "System CPU %", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "avg(rate(node_cpu_seconds_total{mode=\"user\"}[5m])) * 100", + "legendFormat": "User CPU %", + "refId": "C" + } + ], + "title": "System CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 28 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes", + "legendFormat": "Memory Used", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "node_memory_Buffers_bytes", + "legendFormat": "Buffers", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "node_memory_Cached_bytes", + "legendFormat": "Cache", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "expr": "node_memory_MemAvailable_bytes", + "legendFormat": "Available", + "refId": "D" + } + ], + "title": "System Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 36 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(node_network_receive_bytes_total{device!~\"lo|veth.*|docker.*|br-.*\"}[1m])", + "legendFormat": "{{device}} - Received", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "rate(node_network_transmit_bytes_total{device!~\"lo|veth.*|docker.*|br-.*\"}[5m])", + "legendFormat": "{{device}} - Transmitted", + "refId": "B" + } + ], + "title": "Network I/O", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 36 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(node_disk_read_bytes_total{device!~\"dm-.*|loop.*\"}[1m])", + "legendFormat": "{{device}} - Read", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(node_disk_written_bytes_total{device!~\"dm-.*|loop.*\"}[1m])", + "legendFormat": "{{device}} - Write", + "range": true, + "refId": "B" + } + ], + "title": "Disk I/O", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 20, + "panels": [], + "title": "Application Logs", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 45 + }, + "id": 21, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": true, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "expr": "{container=~\".+\"}", + "refId": "A" + } + ], + "title": "Recent Application Logs", + "type": "logs" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "vis": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "logs/sec" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 55 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rate(loki_ingester_samples_received_total[5m])", + "legendFormat": "Log Ingestion Rate", + "range": true, + "refId": "A" + } + ], + "title": "Log Ingestion Verification", + "type": "timeseries" } ], "refresh": "10s", - "revision": 1, "schemaVersion": 39, "tags": [ "iracing", "telemetry", - "containers", - "raspberry-pi" + "monitoring" ], "templating": { "list": [] @@ -905,8 +1901,8 @@ }, "timepicker": {}, "timezone": "", - "title": "IRacing Telemetry System Dashboard", - "uid": "iracing-telemetry-system", - "version": 4, + "title": "Enhanced IRacing Telemetry System", + "uid": "enhanced-iracing-telemetry", + "version": 3, "weekStart": "" } \ No newline at end of file diff --git a/config/grafana/dashboards/files/system-monitoring.json b/config/grafana/dashboards/files/system-monitoring.json index 62bda70..458e0d5 100644 --- a/config/grafana/dashboards/files/system-monitoring.json +++ b/config/grafana/dashboards/files/system-monitoring.json @@ -1,7 +1,6 @@ { - "dashboard": { - "id": null, - "title": "System Monitoring", + "id": null, + "title": "System Monitoring", "tags": ["system", "monitoring"], "timezone": "browser", "refresh": "30s", @@ -192,5 +191,4 @@ "schemaVersion": 36, "version": 1, "links": [] - } } \ No newline at end of file diff --git a/config/grafana/dashboards/files/telemetry-overview.json b/config/grafana/dashboards/files/telemetry-overview.json deleted file mode 100644 index 84d0fc1..0000000 --- a/config/grafana/dashboards/files/telemetry-overview.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "dashboard": { - "id": null, - "title": "IRacing Telemetry Overview", - "tags": ["iracing", "telemetry"], - "timezone": "browser", - "refresh": "5s", - "time": { - "from": "now-15m", - "to": "now" - }, - "panels": [ - { - "id": 1, - "title": "Speed (MPH)", - "type": "timeseries", - "targets": [ - { - "expr": "SELECT Speed FROM TelemetryTicks WHERE $__timeFilter(time) ORDER BY time", - "datasource": { - "type": "postgres", - "uid": "${DS_QUESTDB}" - }, - "format": "time_series", - "rawSql": "SELECT time as \"time\", Speed as \"Speed (MPH)\" FROM TelemetryTicks WHERE $__timeFilter(time) ORDER BY time" - } - ], - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "vis": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "unit": "velocitymph" - } - } - }, - { - "id": 2, - "title": "RPM", - "type": "timeseries", - "targets": [ - { - "datasource": { - "type": "postgres", - "uid": "${DS_QUESTDB}" - }, - "format": "time_series", - "rawSql": "SELECT time as \"time\", RPM as \"RPM\" FROM TelemetryTicks WHERE $__timeFilter(time) ORDER BY time" - } - ], - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "vis": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "unit": "rotrpm" - } - } - }, - { - "id": 3, - "title": "Throttle & Brake", - "type": "timeseries", - "targets": [ - { - "datasource": { - "type": "postgres", - "uid": "${DS_QUESTDB}" - }, - "format": "time_series", - "rawSql": "SELECT time as \"time\", Throttle as \"Throttle\", Brake as \"Brake\" FROM TelemetryTicks WHERE $__timeFilter(time) ORDER BY time" - } - ], - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 8 - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "vis": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "max": 1, - "min": 0, - "unit": "percentunit" - } - } - } - ], - "templating": { - "list": [] - }, - "schemaVersion": 36, - "version": 1, - "links": [] - } -} \ No newline at end of file diff --git a/config/grafana/dashboards/files/traefik-monitoring.json b/config/grafana/dashboards/files/traefik-monitoring.json new file mode 100644 index 0000000..5bab1bf --- /dev/null +++ b/config/grafana/dashboards/files/traefik-monitoring.json @@ -0,0 +1,2777 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 100, + "panels": [], + "title": "Traefik Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0, + "text": "DOWN" + }, + "1": { + "color": "green", + "index": 1, + "text": "UP" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 101, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "up{job=\"traefik\"}", + "legendFormat": "Status", + "range": true, + "refId": "A" + } + ], + "title": "Traefik Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 10 + }, + { + "color": "red", + "value": 50 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 102, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "count(traefik_service_server_up)", + "legendFormat": "Services", + "range": true, + "refId": "A" + } + ], + "title": "Active Services", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "red", + "value": 100 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 6, + "y": 1 + }, + "id": 103, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "count(traefik_router_requests_total)", + "legendFormat": "Routers", + "range": true, + "refId": "A" + } + ], + "title": "Active Routers", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 9, + "y": 1 + }, + "id": 104, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(increase(traefik_entrypoint_requests_total[24h]))", + "legendFormat": "Requests", + "range": true, + "refId": "A" + } + ], + "title": "Total Requests (24h)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 105, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_entrypoint_requests_total[1m])) by (entrypoint)", + "legendFormat": "{{entrypoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Request Rate by EntryPoint", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 5 + }, + "id": 106, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "traefik_entrypoint_open_connections", + "legendFormat": "{{entrypoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Open Connections", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 100 + }, + { + "color": "red", + "value": 500 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 5 + }, + "id": 107, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(traefik_service_request_duration_seconds_bucket[5m])) by (le)) * 1000", + "legendFormat": "p95", + "range": true, + "refId": "A" + } + ], + "title": "Response Time (p95)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 95 + }, + { + "color": "green", + "value": 99 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 6, + "y": 5 + }, + "id": 108, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "(sum(rate(traefik_service_requests_total{code=~\"2..\"}[5m])) / sum(rate(traefik_service_requests_total[5m]))) * 100", + "legendFormat": "Success Rate", + "range": true, + "refId": "A" + } + ], + "title": "Success Rate (2xx)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 10 + }, + { + "color": "red", + "value": 100 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 9, + "y": 5 + }, + "id": 109, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(increase(traefik_service_requests_total{code=~\"5..\"}[5m]))", + "legendFormat": "5xx Errors", + "range": true, + "refId": "A" + } + ], + "title": "Server Errors (5m)", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 110, + "panels": [], + "title": "HTTP Status Codes", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/4.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/5.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "id": 111, + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "sum" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_service_requests_total[1m])) by (code)", + "legendFormat": "{{code}}", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Status Codes Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/4.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/5.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 10 + }, + "id": 112, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "value", + "percent" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(increase(traefik_service_requests_total[5m])) by (code)", + "legendFormat": "{{code}}", + "range": true, + "refId": "A" + } + ], + "title": "Status Code Distribution (5m)", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 10 + }, + "id": 113, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_service_requests_total{code=~\"4..\"}[1m])) by (code)", + "legendFormat": "{{code}}", + "range": true, + "refId": "A" + } + ], + "title": "Client Errors (4xx)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 120, + "panels": [], + "title": "Service Performance", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 121, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_service_requests_total[1m])) by (service)", + "legendFormat": "{{service}}", + "range": true, + "refId": "A" + } + ], + "title": "Requests per Service", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 122, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.50, sum(rate(traefik_service_request_duration_seconds_bucket[5m])) by (le, service)) * 1000", + "legendFormat": "{{service}} - p50", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(traefik_service_request_duration_seconds_bucket[5m])) by (le, service)) * 1000", + "legendFormat": "{{service}} - p95", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum(rate(traefik_service_request_duration_seconds_bucket[5m])) by (le, service)) * 1000", + "legendFormat": "{{service}} - p99", + "range": true, + "refId": "C" + } + ], + "title": "Service Response Times (p50, p95, p99)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".*Up.*" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": ".*Down.*" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 123, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "traefik_service_server_up", + "legendFormat": "{{service}} - {{url}}", + "range": true, + "refId": "A" + } + ], + "title": "Service Health Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 124, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_service_request_bytes_total[1m])) by (service)", + "legendFormat": "{{service}} - Request", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_service_response_bytes_total[1m])) by (service)", + "legendFormat": "{{service}} - Response", + "range": true, + "refId": "B" + } + ], + "title": "Service Bandwidth (Request/Response)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 35 + }, + "id": 130, + "panels": [], + "title": "Router Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 36 + }, + "id": 131, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_router_requests_total[1m])) by (router)", + "legendFormat": "{{router}}", + "range": true, + "refId": "A" + } + ], + "title": "Requests per Router", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 36 + }, + "id": 132, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(traefik_router_request_duration_seconds_bucket[5m])) by (le, router)) * 1000", + "legendFormat": "{{router}} - p95", + "range": true, + "refId": "A" + } + ], + "title": "Router Response Time (p95)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 44 + }, + "id": 133, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_router_request_bytes_total[1m])) by (router)", + "legendFormat": "{{router}} - Request", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_router_response_bytes_total[1m])) by (router)", + "legendFormat": "{{router}} - Response", + "range": true, + "refId": "B" + } + ], + "title": "Router Bandwidth", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 44 + }, + "id": 134, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "traefik_router_open_connections", + "legendFormat": "{{router}}", + "range": true, + "refId": "A" + } + ], + "title": "Open Connections per Router", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 140, + "panels": [], + "title": "EntryPoint Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 53 + }, + "id": 141, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "traefik_entrypoint_open_connections", + "legendFormat": "{{entrypoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Open Connections by EntryPoint", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 53 + }, + "id": 142, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.50, sum(rate(traefik_entrypoint_request_duration_seconds_bucket[5m])) by (le, entrypoint)) * 1000", + "legendFormat": "{{entrypoint}} - p50", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(traefik_entrypoint_request_duration_seconds_bucket[5m])) by (le, entrypoint)) * 1000", + "legendFormat": "{{entrypoint}} - p95", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum(rate(traefik_entrypoint_request_duration_seconds_bucket[5m])) by (le, entrypoint)) * 1000", + "legendFormat": "{{entrypoint}} - p99", + "range": true, + "refId": "C" + } + ], + "title": "EntryPoint Response Times", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 61 + }, + "id": 143, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_entrypoint_request_bytes_total[1m])) by (entrypoint)", + "legendFormat": "{{entrypoint}} - Request", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_entrypoint_response_bytes_total[1m])) by (entrypoint)", + "legendFormat": "{{entrypoint}} - Response", + "range": true, + "refId": "B" + } + ], + "title": "EntryPoint Bandwidth", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 61 + }, + "id": 144, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_entrypoint_requests_total[1m])) by (entrypoint, code)", + "legendFormat": "{{entrypoint}} - {{code}}", + "range": true, + "refId": "A" + } + ], + "title": "EntryPoint Requests by Status Code", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 69 + }, + "id": 150, + "panels": [], + "title": "Traefik Access Logs", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 70 + }, + "id": 151, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": true, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "{container=\"traefik\"}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Traefik Access Logs", + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 82 + }, + "id": 152, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "sum by (status) (count_over_time({container=\"traefik\"} | json | __error__=\"\" [$__interval]))", + "legendFormat": "{{status}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Log Volume by Status Code", + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 82 + }, + "id": 153, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "sum by (RouterName) (count_over_time({container=\"traefik\"} | json | __error__=\"\" [$__interval]))", + "legendFormat": "{{RouterName}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs by Router", + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 90 + }, + "id": 154, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": true, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "{container=\"traefik\"} | json | DownstreamStatus >= 400", + "queryType": "range", + "refId": "A" + } + ], + "title": "Error Logs (4xx & 5xx)", + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 90 + }, + "id": 155, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Total", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "topk(10, sum by (RequestPath) (count_over_time({container=\"traefik\"} | json | __error__=\"\" [$__interval])))", + "legendFormat": "{{RequestPath}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Top 10 Request Paths", + "type": "timeseries" + } + ], + "refresh": "10s", + "schemaVersion": 39, + "tags": [ + "traefik", + "reverse-proxy", + "monitoring", + "logs" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Traefik Monitoring Dashboard", + "uid": "traefik-monitoring", + "version": 1, + "weekStart": "" +} diff --git a/config/grafana/datasources/datasources.yml b/config/grafana/datasources/datasources.yml index 6d370d7..df053d5 100644 --- a/config/grafana/datasources/datasources.yml +++ b/config/grafana/datasources/datasources.yml @@ -5,6 +5,7 @@ datasources: type: prometheus access: proxy url: http://prometheus:9090 + uid: PBFA97CFB590B2093 isDefault: true editable: true jsonData: @@ -17,6 +18,7 @@ datasources: url: questdb:8812 database: qdb user: admin + uid: questdb secureJsonData: password: quest jsonData: @@ -29,6 +31,7 @@ datasources: type: loki access: proxy url: http://loki:3100 + uid: loki editable: true jsonData: maxLines: 1000 \ No newline at end of file diff --git a/config/prometheus.yml b/config/prometheus.yml index a336a90..a60c3bb 100644 --- a/config/prometheus.yml +++ b/config/prometheus.yml @@ -22,3 +22,18 @@ scrape_configs: - targets: ['questdb:9003'] # QuestDB metrics port scrape_interval: 15s + - job_name: 'traefik' + static_configs: + - targets: ['traefik:8080'] # Traefik metrics endpoint + scrape_interval: 10s + + - job_name: 'ingest-service' + static_configs: + - targets: ['host.docker.internal:9091'] # Ingest service running on host + scrape_interval: 5s + + - job_name: 'telemetry-service' + static_configs: + - targets: ['telemetry-service:9092'] # Telemetry service metrics + scrape_interval: 5s + diff --git a/config/questdb.conf b/config/questdb.conf new file mode 100644 index 0000000..6d2c603 --- /dev/null +++ b/config/questdb.conf @@ -0,0 +1,122 @@ +# QuestDB Configuration File +# High-Performance Configuration for Pi5 (16GB RAM) + 7600X Architecture +# Optimized for 3-6 GB/hour sustained telemetry ingestion + +# ============================================================================= +# HTTP Interface (Web Console + HTTP Ingress) +# ============================================================================= +http.enabled=true +http.bind.to=0.0.0.0:9000 +# Increased connection pools for high concurrency +http.connection.pool.initial.capacity=16 +http.connection.string.pool.capacity=128 +http.multipart.header.buffer.size=1024 +http.multipart.idle.spin.count=10000 +http.request.header.buffer.size=65536 +# http.response.header.buffer.size=65536 +http.send.buffer.size=2097152 +http.static.index.file.name=index.html + +# ============================================================================= +# PostgreSQL Wire Protocol (Dashboard & Grafana READ Operations) +# ============================================================================= +pg.enabled=true +pg.net.bind.to=0.0.0.0:8812 +# High connection limits for concurrent dashboard users +pg.net.connection.limit=32 +pg.character.store.capacity=4096 +pg.connection.pool.capacity=32 +pg.password=quest +pg.user=admin +# Query optimization +pg.select.cache.enabled=true +pg.select.cache.block.count=128 +pg.insert.cache.enabled=true +pg.insert.cache.row.count=256000 + +# ============================================================================= +# InfluxDB Line Protocol over TCP (High-Throughput Telemetry Ingestion) +# ============================================================================= +line.tcp.enabled=true +line.tcp.net.bind.to=0.0.0.0:9009 +# Aggressive connection pooling for 7600X ingress +line.tcp.connection.pool.capacity=32 +line.tcp.net.connection.hint=true +pg.net.connection.limit=128 +line.tcp.net.connection.queue.timeout=300000 +# Large buffers for high-throughput ingestion (512KB for dedup-enabled batches) +line.tcp.recv.buffer.size=1048576 + +# ============================================================================= +# InfluxDB Line Protocol over HTTP (Alternative Ingress) +# ============================================================================= +line.http.enabled=true + +# ============================================================================= +# Performance & Memory Optimization (Pi5 16GB Tuning) +# ============================================================================= + +# Cairo Engine (High-Performance Time-Series Storage) +cairo.commit.lag=120000 +cairo.max.uncommitted.rows=50000 +cairo.writer.data.append.page.size=33554432 +# cairo.commit.mode=nosync +cairo.wal.enabled.default=true +# WAL optimization for balanced durability and performance +cairo.wal.segment.rollover.row.count=200000 +cairo.wal.apply.table.time.quota=100000 + +# Memory Management (Optimized for 16GB system) +cairo.sql.copy.root=/tmp/questdb +cairo.sql.backup.root=/var/lib/questdb/backup +cairo.sql.backup.mkdir.mode=764 + +# Shared Worker Pool (Optimized for Pi5 with 2 CPU limit) +shared.worker.count=2 + +shared.worker.yield.threshold=10 + + +# ============================================================================= +# Query Performance Optimization +# ============================================================================= + +# SQL Query Engine +cairo.sql.copy.buffer.size=1048576 + +cairo.sql.groupby.map.capacity=256 +cairo.sql.groupby.pool.capacity=256 + +# Join and Aggregation Optimization +cairo.sql.join.context.pool.capacity=16 +cairo.sql.max.negative.limit=10000 +cairo.sql.sort.key.page.size=2097152 +cairo.sql.sort.key.max.pages=128 +cairo.sql.sort.light.value.page.size=524288 +cairo.sql.sort.light.value.max.pages=512 + +# ============================================================================= +# I/O Optimization (PCIe SSD on Pi5) +# ============================================================================= + +# Page Cache Settings +cairo.writer.data.append.page.size=16777216 +cairo.writer.misc.append.page.size=4194304 + +# ============================================================================= +# Network & Protocol Optimization +# ============================================================================= + +# Circuit Breaker Pattern +net.test.connection.buffer.size=128 +circuit.breaker.throttle=100 + +# Metrics and Monitoring +metrics.enabled=true + +# ============================================================================= +# Security Settings +# ============================================================================= +# Note: In production, consider enabling authentication +# http.security.readonly=false +# pg.readonly.user.enabled=true \ No newline at end of file diff --git a/config/rabbitmq.conf b/config/rabbitmq.conf index 0263c0b..1275b09 100644 --- a/config/rabbitmq.conf +++ b/config/rabbitmq.conf @@ -1,37 +1,129 @@ +# RabbitMQ Configuration File +# High-Performance Configuration for Pi5 (16GB RAM) Message Broker +# Optimized for 7600X β†’ Pi5 telemetry streaming (3-6 GB/hour) + +# ============================================================================= +# Core Permissions and Security +# ============================================================================= default_permissions.read = .* default_permissions.configure = .* default_permissions.write = .* -listeners.tcp.default = 5672 +# Security - guest user management handled by automated init scripts +loopback_users = none -# Security - guest user management handled by init script -# loopback_users = none +# ============================================================================= +# Network Listeners and Protocols +# ============================================================================= +listeners.tcp.default = 5672 +# Management interface management.listener.port = 15672 management.listener.ssl = false +management.load_definitions = /etc/rabbitmq/definitions.json + +# Prometheus metrics endpoint +prometheus.tcp.port = 15692 +prometheus.return_per_object_metrics = false # Reduce metric overhead + +# ============================================================================= +# Memory Management (Pi5 16GB Optimization) +# ============================================================================= +# Use 80% of allocated container memory (4GB of 5GB limit) +vm_memory_high_watermark.relative = 0.8 +vm_memory_calculation_strategy = allocated # Better for containerized environments +vm_memory_high_watermark_paging_ratio = 0.9 # Start paging at 90% of memory limit -# Memory and performance tuning -vm_memory_high_watermark.relative = 0.7 -vm_memory_calculation_strategy = rss +# Disk space management for SSD +disk_free_limit.relative = 0.1 # Reserve 10% free space +disk_free_limit.absolute = 2GB -# TCP optimization for high throughput -tcp_listen_options.backlog = 4096 -tcp_listen_options.nodelay = true +# ============================================================================= +# High-Throughput Network Optimization +# ============================================================================= +# TCP settings optimized for 0.3ms latency to 7600X +tcp_listen_options.backlog = 8192 # Large connection backlog for high concurrency +tcp_listen_options.nodelay = true # Disable Nagle for low latency tcp_listen_options.linger.on = true tcp_listen_options.linger.timeout = 0 tcp_listen_options.keepalive = true -# Connection and channel limits -num_acceptors.tcp = 20 -channel_max = 4096 -frame_max = 1048576 +# Connection handling for high-performance workload +num_acceptors.tcp = 16 # More acceptors for concurrent connections +channel_max = 8192 # Match Go client configuration +connection_max = 2048 # High connection limit for multiple clients +frame_max = 4194304 # 4MB frames to match Go client -# Reduced heartbeat for faster detection -heartbeat = 30 +# ============================================================================= +# Message Processing Optimization +# ============================================================================= +# Heartbeat optimized for high-throughput, low-latency network +heartbeat = 60 # Match Go client settings (reduced overhead) -# Prometheus plugin configuration -prometheus.tcp.port = 15692 -prometheus.return_per_object_metrics = false +# Message store settings for high throughput +# msg_store_file_size_limit = 134217728 # Invalid property - removed +# msg_store_credit_disc_bound = {8000, 16000} # Invalid property - removed + +# Queue performance tuning +queue_master_locator = min-masters # Distribute queues evenly +queue_index_embed_msgs_below = 8192 # Embed larger messages for performance + +# ============================================================================= +# Flow Control for High Throughput +# ============================================================================= +# Credit flow settings optimized for large message batches +# credit_flow_default_credit = {800, 400} # Invalid property - removed + +# ============================================================================= +# ARM Processor Optimization (Pi5 Specific) +# ============================================================================= +# Disable HiPE compilation (not beneficial on ARM) +hipe_compile = false + +# ============================================================================= +# Logging (Production Optimized) +# ============================================================================= +# Console logging +log.console = true +log.console.level = info +log.console.formatter = json + +# File logging with rotation for SSD longevity +log.file = /var/log/rabbitmq/rabbit.log +log.file.level = info +log.file.formatter = json +log.file.rotation.count = 5 # Keep fewer log files +log.file.rotation.size = 52428800 # 50MB log files + +# Reduce connection logging overhead in high-throughput scenarios +log.connection.level = warning # Only log connection issues + +# ============================================================================= +# Advanced Performance Settings +# ============================================================================= +# Garbage collection and memory tuning for ARM64 +collect_statistics_interval = 10000 # Collect stats every 10 seconds (less overhead) + +# Scheduler settings for 4-core Pi5 +# +S 4:4 # Invalid in rabbitmq.conf - moved to docker env + +# ============================================================================= +# Storage Optimization (PCIe SSD) +# ============================================================================= +# Message store index optimization +# msg_store_index_module = rabbit_msg_store_ets_index # Invalid property - removed +# queue_index_max_journal_entries = 32768 # Invalid property - removed -# Load definitions on startup -management.load_definitions = /etc/rabbitmq/definitions.json \ No newline at end of file +# ============================================================================= +# Performance Notes +# ============================================================================= +# This configuration supports: +# - Message throughput: 200,000-800,000 messages/second +# - Large message batches: 32MB Protocol Buffer batches from 7600X +# - High connection concurrency: 20 connections from Go ingest service +# - Memory usage: 3-4GB (within 5GB container limit) +# - CPU utilization: 60-80% of Pi5's 4 cores during peak load +# +# Network flow optimized for: +# 7600X Ingest β†’ RabbitMQ (Pi5) β†’ Telemetry Service (Pi5) β†’ QuestDB (Pi5) +# Expected latency: <5ms for message routing with 0.3ms base network latency \ No newline at end of file diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile index 05c645f..0da9a15 100644 --- a/dashboard/Dockerfile +++ b/dashboard/Dockerfile @@ -1,62 +1,11 @@ - -FROM node:23-alpine AS base - -ARG TARGETPLATFORM -ARG BUILDPLATFORM - +FROM node:23-alpine AS build WORKDIR /app - -FROM base AS deps -RUN apk add --no-cache libc6-compat - -COPY package.json package-lock.json* ./ - -RUN npm ci --ignore-scripts && npm cache clean --force - -FROM base AS builder -WORKDIR /app - -COPY --from=deps /app/node_modules ./node_modules - +COPY package.json package-lock.json ./ +RUN npm ci COPY . . - -ENV NEXT_TELEMETRY_DISABLED=1 -ENV NODE_ENV=production -ENV ANALYZE=false - -ENV NODE_OPTIONS="--max-old-space-size=1024" - +ENV VITE_BASE_PATH=/dashboard/ RUN npm run build -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 nextjs - -COPY --from=builder /app/public ./public - -RUN mkdir .next && chown nextjs:nodejs .next - -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ - -COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next -COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules - -USER nextjs - -EXPOSE 3000 - -ENV HOSTNAME="0.0.0.0" -ENV PORT=3000 - -HEALTHCHECK --interval=60s --timeout=30s --start-period=80s --retries=3 \ - CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" || exit 1 - -ENV NODE_OPTIONS="--max-old-space-size=512" - -CMD ["sh", "-c", "if [ -f server.js ]; then node server.js; else npm start; fi"] \ No newline at end of file +FROM devforth/spa-to-http:latest +COPY --from=build /app/dist/ . +CMD ["--base-path", "/dashboard", "--brotli"] diff --git a/dashboard/app/[sessionId]/page.tsx b/dashboard/app/[sessionId]/page.tsx deleted file mode 100644 index 557c3d4..0000000 --- a/dashboard/app/[sessionId]/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { notFound } from "next/navigation"; -import { Suspense } from "react"; -import { getLaps, getTelemetryData } from "@/lib/questDb"; -import TelemetryPage from "../../components/TelemetryPage"; -import ClientWrapper from "../../components/ClientWrapper"; - -// Force dynamic rendering - prevent build-time execution -export const dynamic = "force-dynamic"; -export const revalidate = 0; - -interface PageProps { - params: Promise<{ sessionId: string }>; - searchParams: Promise<{ lapId?: string }>; -} - -export default async function SessionPage({ params, searchParams }: PageProps) { - const { sessionId } = await params; - const { lapId } = await searchParams; - - // Default to lap 1 if no lapId provided - const currentLapId = lapId ? Number.parseInt(lapId, 10) : 1; - - if (isNaN(currentLapId)) { - notFound(); - } - - // Fetch data on the server at RUNTIME (not build time) - try { - console.log( - `πŸ“Š Fetching data at RUNTIME for session: ${sessionId}, lap: ${currentLapId}`, - ); - - const [telemetryData, availableLaps] = await Promise.all([ - getTelemetryData(sessionId, currentLapId), - getLaps(sessionId), - ]); - - if (!telemetryData) { - notFound(); - } - - console.log( - `βœ… Successfully fetched telemetry data with ${telemetryData.dataWithGPSCoordinates?.length || 0} points`, - ); - - return ( - }> - }> - - - - ); - } catch (error) { - console.error("Error fetching telemetry data at runtime:", error); - notFound(); - } -} - -function TelemetryLoadingSkeleton() { - return ( -
- {/* Sidebar Skeleton */} -
-
-
-
-
-
-
-
-
-
-
-
-
- - {/* Main Content Skeleton */} -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); -} diff --git a/dashboard/app/api/health/route.ts b/dashboard/app/api/health/route.ts index bb3fbbf..2f6c1f1 100644 --- a/dashboard/app/api/health/route.ts +++ b/dashboard/app/api/health/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { getQuestDBHealth } from "@/lib/questDb"; +import { getQuestDBHealth } from "../../../lib/questDb"; export async function GET() { try { @@ -14,16 +14,16 @@ export async function GET() { details: healthResult.details, }); } - return NextResponse.json( - { - status: "error", - message: healthResult.message, - timestamp: new Date().toISOString(), - database: "QuestDB", - details: healthResult.details, - }, - { status: 503 }, - ); + return NextResponse.json( + { + status: "error", + message: healthResult.message, + timestamp: new Date().toISOString(), + database: "QuestDB", + details: healthResult.details, + }, + { status: 503 }, + ); } catch (error) { console.error("Health check failed:", error); return NextResponse.json( diff --git a/dashboard/app/api/questdb-status/route.ts b/dashboard/app/api/questdb-status/route.ts index 819c220..21c8cf0 100644 --- a/dashboard/app/api/questdb-status/route.ts +++ b/dashboard/app/api/questdb-status/route.ts @@ -1,5 +1,9 @@ import { NextResponse } from "next/server"; -import { getQuestDBHealth, getQuestDBStats, getSessions } from "@/lib/questDb"; +import { + getQuestDBHealth, + getQuestDBStats, + getSessions, +} from "../../../lib/questDb"; export async function GET() { const startTime = Date.now(); @@ -39,9 +43,9 @@ export async function GET() { database: stats, sessions: sessionsResult, environment: { - nodeEnv: process.env.NODE_ENV, - questdbHost: process.env.QUESTDB_HOST || "default", - questdbPort: process.env.QUESTDB_PORT || "default", + // nodeEnv: import.meta.env.MODE, + // questdbHost: process.env.QUESTDB_HOST || "default", + // questdbPort: process.env.QUESTDB_PORT || "default", }, }; @@ -50,7 +54,7 @@ export async function GET() { if (healthResult.healthy) { return NextResponse.json(response); } - return NextResponse.json(response, { status: 503 }); + return NextResponse.json(response, { status: 503 }); } catch (error) { console.error("❌ QuestDB status check failed:", error); diff --git a/dashboard/app/api/tiles/[...params]/route.ts b/dashboard/app/api/tiles/[...params]/route.ts new file mode 100644 index 0000000..46fcc25 --- /dev/null +++ b/dashboard/app/api/tiles/[...params]/route.ts @@ -0,0 +1,116 @@ +import { createHash } from "node:crypto"; +import { type NextRequest, NextResponse } from "next/server"; + +const TILE_SOURCES = { + dark: "https://cartodb-basemaps-a.global.ssl.fastly.net/dark_all", + light: "https://cartodb-basemaps-a.global.ssl.fastly.net/light_all", + satellite: + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile", +}; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ params: string[] }> }, +) { + try { + const resolvedParams = await params; + const [theme, z, x, y] = resolvedParams.params; + + // Validate parameters + if (!theme || !z || !x || !y) { + return NextResponse.json( + { error: "Invalid tile parameters" }, + { status: 400 }, + ); + } + + // Validate theme + if (!(theme in TILE_SOURCES)) { + return NextResponse.json({ error: "Invalid theme" }, { status: 400 }); + } + + // Validate numeric parametersk + const zNum = Number.parseInt(z, 10); + const xNum = Number.parseInt(x, 10); + const yNum = Number.parseInt(y, 10); + + if (Number.isNaN(zNum) || Number.isNaN(xNum) || Number.isNaN(yNum)) { + return NextResponse.json( + { error: "Invalid tile coordinates" }, + { status: 400 }, + ); + } + + // Construct tile URL + const baseUrl = TILE_SOURCES[theme as keyof typeof TILE_SOURCES]; + const tileUrl = + theme === "satellite" + ? `${baseUrl}/${z}/${y}/${x}` + : `${baseUrl}/${z}/${x}/${y}.png`; + + // Generate ETag for this tile based on URL (tiles never change) + const etag = `"${createHash("md5").update(tileUrl).digest("hex")}"`; + + // Check if client has cached version + const clientETag = request.headers.get("if-none-match"); + if (clientETag === etag) { + return new NextResponse(null, { + status: 304, + headers: { + ETag: etag, + "Cache-Control": "public, max-age=86400, immutable", + }, + }); + } + + // Fetch the tile with caching headers + const response = await fetch(tileUrl, { + headers: { + "User-Agent": "IRacing-Display/1.0", + Referer: request.headers.get("referer") || "", + "Accept-Encoding": "gzip, deflate, br", + }, + }); + + if (!response.ok) { + return NextResponse.json( + { error: "Failed to fetch tile" }, + { status: response.status }, + ); + } + + const tileData = await response.arrayBuffer(); + const contentType = response.headers.get("content-type") || "image/png"; + + // Return the tile with aggressive caching headers + return new NextResponse(tileData, { + status: 200, + headers: { + "Content-Type": contentType, + "Cache-Control": "public, max-age=86400, s-maxage=86400, immutable", + ETag: etag, + "Last-Modified": new Date().toUTCString(), + "Cross-Origin-Resource-Policy": "cross-origin", + "Access-Control-Allow-Origin": "*", + Vary: "Accept-Encoding", + }, + }); + } catch (error) { + console.error("Tile proxy error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} + +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); +} diff --git a/dashboard/app/globe/page.tsx b/dashboard/app/globe/page.tsx deleted file mode 100644 index 71d2c8b..0000000 --- a/dashboard/app/globe/page.tsx +++ /dev/null @@ -1,525 +0,0 @@ -"use client"; - -import { - Activity, - Clock, - Gauge, - type LucideIcon, - MapPin, - Navigation, - Target, -} from "lucide-react"; -import dynamic from "next/dynamic"; -import type React from "react"; -import { useEffect, useRef, useState } from "react"; - -const Globe = dynamic(() => import("react-globe.gl"), { - ssr: false, - loading: () => ( -
-
Loading Globe...
-
- ), -}); - -interface Track { - track_id: number; - track_name: string; - location: string; - latitude: number; - longitude: number; - track_config_length: number; - corners_per_lap: number; - category: TrackCategory; - track_type_text: string; -} - -interface TrackPoint extends Track { - lat: number; - lng: number; - size: number; - color: string; -} - -type TrackCategory = - | "road" - | "oval" - | "dirt" - | "street" - | "dirt_oval" - | "dirt_road"; - -interface GeoJSONFeature { - type: "Feature"; - properties: { - [key: string]: any; - }; - geometry: { - type: string; - coordinates: number[][][] | number[][][][] | number[]; - }; -} - -interface CountriesData { - features: GeoJSONFeature[]; -} - -interface StatCardProps { - icon: LucideIcon; - label: string; - value: string | number; - color?: string; -} - -interface TrackPopupProps { - track: Track | null; - onClose: () => void; -} - -const mockTracks = [] as Track[]; - -const getCountriesData = async (): Promise => { - try { - const response = await fetch( - "https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson", - ); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error("Error loading countries:", error); - return { features: [] }; - } -}; - -const getTrackTypeColor = (category: TrackCategory): string => { - const colors: Record = { - road: "#f59e0b", - oval: "#ef4444", - dirt: "#a78bfa", - street: "#10b981", - dirt_oval: "#fff", - dirt_road: "#b9109c", - }; - return colors[category] || "#6b7280"; -}; - -const formatDate = (dateString: string): string => { - return new Date(dateString).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); -}; - -const generateTooltipContent = (point: TrackPoint): string => { - return ` -
-
${point.track_name}
-
${point.location}
-
Length: ${point.track_config_length}km
-
- `; -}; - -const StatCard: React.FC = ({ - icon: Icon, - label, - value, - color = "text-amber-400", -}) => ( -
-
- - - {label} - -
-
{value}
-
-); - -const TrackPopup: React.FC = ({ track, onClose }) => { - if (!track) return null; - - return ( -
-
-
-

- {track.track_name} -

-

- - {track.location} -

-
- -
- -
- {track.track_type_text} -
- -
-
-
LENGTH
-
- {track.track_config_length}km -
-
-
-
CORNERS
-
- {track.corners_per_lap} -
-
-
- - -
- ); -}; - -const SpyDashboardGlobe: React.FC = () => { - const [selectedTrack, setSelectedTrack] = useState(null); - const [currentTime, setCurrentTime] = useState(new Date()); - const [countriesData, setCountriesData] = useState({ - features: [], - }); - const [isAutoRotating, setIsAutoRotating] = useState(true); - const [dimensions, setDimensions] = useState({ width: 1200, height: 800 }); - const [isMounted, setIsMounted] = useState(false); - const globeRef = useRef(null); - const autoRotateRef = useRef(null); - const lastInteractionRef = useRef(0); - const rotationAngleRef = useRef(0); - const isDraggingRef = useRef(false); - - useEffect(() => { - setIsMounted(true); - }, []); - - useEffect(() => { - const timer = setInterval(() => setCurrentTime(new Date()), 1000); - return () => clearInterval(timer); - }, []); - - useEffect(() => { - if (!isMounted) return; - - const updateDimensions = () => { - setDimensions({ - width: window.innerWidth, - height: window.innerHeight - 80, - }); - }; - - updateDimensions(); - - window.addEventListener("resize", updateDimensions); - return () => window.removeEventListener("resize", updateDimensions); - }, [isMounted]); - - useEffect(() => { - getCountriesData().then(setCountriesData); - }, []); - - const trackPoints: TrackPoint[] = mockTracks.map((track) => ({ - ...track, - lat: track.latitude, - lng: track.longitude, - size: 5, - color: getTrackTypeColor(track.category), - })); - - const totalSessions = mockTracks.reduce((sum) => sum, 0); - const totalTracks = mockTracks.length; - const totalDistance = mockTracks.reduce( - (sum, track) => sum + track.track_config_length, - 0, - ); - - useEffect(() => { - if (!globeRef.current || !isAutoRotating || !isMounted) return; - - const globe = globeRef.current; - - const autoRotate = (): void => { - const now = Date.now(); - if (!isDraggingRef.current && now - lastInteractionRef.current > 3000) { - rotationAngleRef.current += 0.3; - try { - globe.pointOfView( - { - lat: 0, - lng: rotationAngleRef.current, - altitude: 2.5, - }, - 0, - ); - } catch (error) { - console.error("error setting auto rotate: ", error); - } - } - }; - - autoRotateRef.current = setInterval(autoRotate, 100); - - return () => { - if (autoRotateRef.current) { - clearInterval(autoRotateRef.current); - autoRotateRef.current = null; - } - }; - }, [isAutoRotating, isMounted]); - - useEffect(() => { - if (!isMounted) return; - - const handleMouseDown = () => { - isDraggingRef.current = true; - lastInteractionRef.current = Date.now(); - }; - - const handleMouseUp = () => { - isDraggingRef.current = false; - lastInteractionRef.current = Date.now(); - }; - - const handleMouseMove = () => { - if (isDraggingRef.current) { - lastInteractionRef.current = Date.now(); - } - }; - - document.addEventListener("mousedown", handleMouseDown); - document.addEventListener("mouseup", handleMouseUp); - document.addEventListener("mousemove", handleMouseMove); - - return () => { - document.removeEventListener("mousedown", handleMouseDown); - document.removeEventListener("mouseup", handleMouseUp); - document.removeEventListener("mousemove", handleMouseMove); - }; - }, [isMounted]); - - const handleGlobeInteraction = (): void => { - lastInteractionRef.current = Date.now(); - }; - - const toggleAutoRotation = (): void => { - setIsAutoRotating(!isAutoRotating); - }; - - const handleTrackClick = (point: any): void => { - lastInteractionRef.current = Date.now(); - setSelectedTrack(point as Track); - }; - - const handleClosePopup = (): void => { - setSelectedTrack(null); - }; - - if (!isMounted) { - return ( -
-
- Initializing Telemetry Command... -
-
- ); - } - - return ( -
-
-
-
- -
-

- TELEMETRY COMMAND -

-

- Global Track Operations -

-
-
- -
-
-
- MISSION TIME -
-
- {currentTime.toLocaleTimeString("en-US", { hour12: false })} -
-
-
-
- ONLINE -
-
-
-
- - - -
- "rgba(74, 85, 104, 0.4)"} - polygonSideColor={() => "rgba(74, 85, 104, 0.1)"} - polygonStrokeColor={() => "#9ca3af"} - polygonAltitude={0.005} - pointsData={trackPoints} - pointLat="lat" - pointLng="lng" - pointColor="color" - pointAltitude={0.01} - pointRadius={0.1} - onPointClick={handleTrackClick} - enablePointerInteraction={true} - onGlobeReady={() => { - if (globeRef.current) { - globeRef.current.pointOfView({ altitude: 2.5 }); - } - }} - onGlobeClick={handleGlobeInteraction} - onPointHover={handleGlobeInteraction} - /> -
- - {selectedTrack && ( - - )} - -
-
-
- -
-
- ); -}; - -export default SpyDashboardGlobe; diff --git a/dashboard/app/layout.tsx b/dashboard/app/layout.tsx deleted file mode 100644 index f383ca8..0000000 --- a/dashboard/app/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Telemetry Dashboard", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/dashboard/app/page.tsx b/dashboard/app/page.tsx deleted file mode 100644 index 7f9093e..0000000 --- a/dashboard/app/page.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import SessionSelector from "@/components/SessionSelector"; -import { getSessions } from "@/lib/questDb"; -import ClientWrapper from "@/components/ClientWrapper"; - -// Force dynamic rendering - prevent build-time execution -export const dynamic = "force-dynamic"; -export const revalidate = 0; - -export default async function HomePage() { - let sessions: string | any[] = []; - let errorMessage = null; - - try { - console.log("🏠 Home page: Fetching sessions at RUNTIME..."); - sessions = await getSessions(); - console.log(`🏠 Home page: Found ${sessions.length} sessions`); - } catch (error) { - console.error("🏠 Home page: Error loading sessions:", error); - errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - } - - return ( - -
- {/* Sidebar */} -
- {/* Logo/Brand */} -
-
-
-
-
-
-

iRacing

-

Telemetry

-
-
-
- - {/* Navigation */} - - - {/* Status */} -
-
-
- - {errorMessage ? 'Offline' : 'Connected'} - -
-
-
- - {/* Main Content */} -
- {/* Header */} -
-
- Dashboard - / - Sessions -
-
- - {/* Main Content */} -
- {/* Page Title */} -
-

Sessions

-

Manage and analyze your telemetry sessions

-
- - {/* Content */} -
- {errorMessage ? ( -
-
-
-
-
-
-
-
-

- Database Connection Error -

-

- Unable to connect to QuestDB. Please check your configuration. -

-
- - Show error details - -
- - {errorMessage} - -
-
-
-
-
- ) : sessions.length > 0 ? ( - - ) : ( -
-
-
-
-

No sessions found

-

- Import telemetry data to get started with session analysis. -

-
- )} - - {/* System Status */} - {sessions.length > 0 && ( -
- {/* Database Status */} -
-
-

Database

-
-
-

- {errorMessage ? 'Offline' : 'Online'} -

-

QuestDB Connection

-
- - {/* Sessions Count */} -
-
-

Sessions

-
-
-

- {sessions.length} -

-

Available for analysis

-
- - {/* Processing Status */} -
-
-

Processing

-
-
-

Active

-

Runtime dynamic

-
-
- )} -
-
-
-
-
- ); -} diff --git a/dashboard/biome.json b/dashboard/biome.json index 57ec841..37bd169 100644 --- a/dashboard/biome.json +++ b/dashboard/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", "vcs": { "enabled": false, "clientKind": "git", @@ -7,13 +7,7 @@ }, "files": { "ignoreUnknown": false, - "includes": [ - "**", - "!**/.next/**", - "!**/node_modules/**", - "!**/dist/**", - "!**/build/**" - ] + "includes": ["**", "!**/.next", "!**/node_modules", "!**/dist", "!**/build"] }, "formatter": { "enabled": true, @@ -27,7 +21,9 @@ "noUnusedVariables": "error", "noUnusedImports": "error" }, - "nursery": "off", + "nursery": { + "useSortedClasses": "error" + }, "style": { "noParameterAssign": "error", "useAsConstAssertion": "error", @@ -54,5 +50,10 @@ "organizeImports": "on" } } + }, + "css": { + "parser": { + "tailwindDirectives": true + } } } diff --git a/dashboard/components/ClientWrapper.tsx b/dashboard/components/ClientWrapper.tsx index 827d7ff..13465d1 100644 --- a/dashboard/components/ClientWrapper.tsx +++ b/dashboard/components/ClientWrapper.tsx @@ -1,5 +1,3 @@ -"use client"; - import { useEffect, useState } from "react"; interface ClientWrapperProps { @@ -7,7 +5,10 @@ interface ClientWrapperProps { fallback?: React.ReactNode; } -export default function ClientWrapper({ children, fallback }: ClientWrapperProps) { +export default function ClientWrapper({ + children, + fallback, +}: ClientWrapperProps) { const [hasMounted, setHasMounted] = useState(false); useEffect(() => { @@ -19,4 +20,4 @@ export default function ClientWrapper({ children, fallback }: ClientWrapperProps } return <>{children}; -} \ No newline at end of file +} diff --git a/dashboard/components/InfoBox.tsx b/dashboard/components/InfoBox.tsx index 5e82fe2..2c58e7a 100644 --- a/dashboard/components/InfoBox.tsx +++ b/dashboard/components/InfoBox.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { memo, useCallback, useMemo } from "react"; import { CartesianGrid, Line, @@ -9,39 +9,44 @@ import { XAxis, YAxis, } from "recharts"; -import type { TelemetryDataPoint } from "@/lib/types"; +import type { TelemetryDataPoint } from "../lib/types"; interface InfoBoxProps { telemetryData: any[]; lapId: string; + selectedMetric?: string; + setSelectedMetric?: (metric: string) => void; } -export const InfoBox = ({ telemetryData, lapId }: InfoBoxProps) => { +export const InfoBox = memo(function InfoBox({ + telemetryData, + lapId, +}: InfoBoxProps) { return ( -
-

Telemetry Details

-
-
-
Lap
-
{lapId}
+
+
+
+
+
Lap
+
{lapId}
-
-
Lap Time
-
+
+
Lap Time
+
{telemetryData[telemetryData.length - 1].LapCurrentLapTime?.toFixed( 2, ) || "0.00"}
-
-
Position
-
+
+
Position
+
{telemetryData[telemetryData.length - 1].PlayerCarPosition || 0}
-
-
Fuel
-
+
+
Fuel
+
{telemetryData[telemetryData.length - 1].FuelLevel?.toFixed(1) || 0}{" "} L
@@ -49,12 +54,10 @@ export const InfoBox = ({ telemetryData, lapId }: InfoBoxProps) => {
); -}; +}); export const TelemetryChart = ({ selectedMetric, - setSelectedMetric, - availableMetrics, telemetryData, selectedIndex, onIndexChange, @@ -93,25 +96,8 @@ export const TelemetryChart = ({ return (
-
-

Telemetry Data

-
- - -
+
+

Telemetry Data

@@ -138,7 +124,7 @@ export const TelemetryChart = ({ if (active && payload && payload.length) { const dataPoint = payload[0].payload as TelemetryDataPoint; return ( -
+

Time: {dataPoint.sessionTime.toFixed(2)}s

@@ -155,7 +141,7 @@ export const TelemetryChart = ({
); } - return <>; + return; }} /> @@ -217,10 +203,10 @@ export const TelemetryChart = ({
{/* Metric info boxes with additional information */} -
-
-

Brake

-

+

+
+

Brake

+

{telemetryData.length > 0 && selectedIndex !== null && selectedIndex >= 0 @@ -228,9 +214,9 @@ export const TelemetryChart = ({ : "0.0"}

-
-

LapDistPct

-

+

+

LapDistPct

+

{telemetryData.length > 0 && selectedIndex !== null && selectedIndex >= 0 @@ -238,9 +224,9 @@ export const TelemetryChart = ({ : "0.0"}

-
-

Speed

-

+

+

Speed

+

{telemetryData.length > 0 && selectedIndex !== null && selectedIndex >= 0 @@ -248,9 +234,9 @@ export const TelemetryChart = ({ : "277.8"}

-
-

Throttle

-

+

+

Throttle

+

{telemetryData.length > 0 && selectedIndex !== null && selectedIndex >= 0 diff --git a/dashboard/components/OptimizedTrackMap.tsx b/dashboard/components/OptimizedTrackMap.tsx index e349606..19eb163 100644 --- a/dashboard/components/OptimizedTrackMap.tsx +++ b/dashboard/components/OptimizedTrackMap.tsx @@ -1,38 +1,34 @@ "use client"; -import Feature from "ol/Feature"; -import { LineString, Point } from "ol/geom"; -import TileLayer from "ol/layer/Tile"; -import VectorLayer from "ol/layer/Vector"; -import OlMap from "ol/Map"; -import { fromLonLat } from "ol/proj"; -import VectorSource from "ol/source/Vector"; -import XYZ from "ol/source/XYZ"; -import { Circle, Fill, Stroke, Style } from "ol/style"; -import View from "ol/View"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import type { TelemetryDataPoint } from "@/lib/types"; -import { usePerformanceMonitor } from "@/hooks/usePerformanceMonitor"; +// Tree-shaken OpenLayers imports for better performance +import Feature from "ol/Feature.js"; +import { LineString, Point } from "ol/geom.js"; +import TileLayer from "ol/layer/Tile.js"; +import VectorLayer from "ol/layer/Vector.js"; +import OlMap from "ol/Map.js"; +import { fromLonLat } from "ol/proj.js"; +import VectorSource from "ol/source/Vector.js"; +import XYZ from "ol/source/XYZ.js"; +import { Circle, Fill, Stroke, Style } from "ol/style.js"; +import View from "ol/View.js"; +import { memo, useCallback, useEffect, useMemo, useRef } from "react"; +import type { TelemetryDataPoint } from "../lib/types"; interface OptimizedTrackMapProps { dataWithCoordinates: TelemetryDataPoint[]; selectedPointIndex: number; onPointClick?: (index: number) => void; selectedMetric?: string; + setSelectedMetric: (metric: string) => void; + onMetricChange?: (metric: string) => void; } -const MAP_THEMES = { - dark: { - name: "Dark", - url: "https://{a-d}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", - }, -}; - -export default function OptimizedTrackMap({ +export const OptimizedTrackMap = memo(function OptimizedTrackMap({ dataWithCoordinates, selectedPointIndex, onPointClick, selectedMetric = "Speed", + setSelectedMetric, }: OptimizedTrackMapProps) { const mapRef = useRef(null); const mapInstanceRef = useRef(null); @@ -40,23 +36,12 @@ export default function OptimizedTrackMap({ const markerSourceRef = useRef(null); const trackRenderedRef = useRef(false); - const [mapTheme, setMapTheme] = useState("dark"); - const [displayMetric, setDisplayMetric] = useState( - selectedMetric || "Speed", - ); - - // PERFORMANCE FIX: Color cache to avoid recalculating same colors const colorCacheRef = useRef>(new Map()); - // Performance monitoring (enable during development) - const perfMonitor = usePerformanceMonitor("OptimizedTrackMap", process.env.NODE_ENV === 'development'); - - // Color mapping function for different metrics with caching const getColorForMetric = useCallback( (value: number, metric: string, minVal: number, maxVal: number): string => { if (!value || minVal === maxVal) return "#888888"; - // PERFORMANCE FIX: Use cache to avoid recalculating same colors const cacheKey = `${metric}-${value}-${minVal}-${maxVal}`; const cached = colorCacheRef.current.get(cacheKey); if (cached) return cached; @@ -66,16 +51,16 @@ export default function OptimizedTrackMap({ switch (metric) { case "Speed": - if (normalized < 0.3) color = "#ef4444"; // Red for low speed - else if (normalized < 0.6) color = "#f97316"; // Orange for medium - else if (normalized < 0.8) color = "#eab308"; // Yellow for medium-high - else color = "#22c55e"; // Green for high speed + if (normalized < 0.3) color = "#ef4444"; + else if (normalized < 0.6) color = "#f97316"; + else if (normalized < 0.8) color = "#eab308"; + else color = "#22c55e"; break; case "Throttle": - color = `rgb(0, ${Math.round(150 + 105 * normalized)}, 0)`; // Green gradient + color = `rgb(0, ${Math.round(150 + 105 * normalized)}, 0)`; break; case "Brake": - color = `rgb(${Math.round(150 + 105 * normalized)}, 0, 0)`; // Red gradient + color = `rgb(${Math.round(150 + 105 * normalized)}, 0, 0)`; break; case "Gear": { const gearColors = [ @@ -88,15 +73,16 @@ export default function OptimizedTrackMap({ "#ec4899", "#f59e0b", ]; - color = gearColors[Math.min(Math.floor(normalized * 8), 7)] || "#888888"; + color = + gearColors[Math.min(Math.floor(normalized * 8), 7)] || "#888888"; break; } case "RPM": - color = `rgb(${Math.round(255 * normalized)}, ${Math.round(100 + 155 * (1 - normalized))}, 255)`; // Purple-pink gradient + color = `rgb(${Math.round(255 * normalized)}, ${Math.round(100 + 155 * (1 - normalized))}, 255)`; break; case "SteeringWheelAngle": { const absNormalized = Math.abs(normalized - 0.5) * 2; - color = `rgb(${Math.round(150 + 105 * absNormalized)}, 0, ${Math.round(150 + 105 * absNormalized)})`; // Purple for steering + color = `rgb(${Math.round(150 + 105 * absNormalized)}, 0, ${Math.round(150 + 105 * absNormalized)})`; break; } default: @@ -105,8 +91,7 @@ export default function OptimizedTrackMap({ // Cache the result colorCacheRef.current.set(cacheKey, color); - - // Limit cache size to prevent memory leaks + if (colorCacheRef.current.size > 10000) { const firstKey = colorCacheRef.current.keys().next().value; colorCacheRef.current.delete(firstKey!); @@ -117,7 +102,6 @@ export default function OptimizedTrackMap({ [], ); - // Memoize the static track data - this should NEVER change once created const staticTrackData = useMemo(() => { if (!dataWithCoordinates?.length) return null; @@ -139,29 +123,43 @@ export default function OptimizedTrackMap({ maxLon: Math.max(...validGPSPoints.map((p) => p.Lon)), }, }; - }, [dataWithCoordinates]); // Only recalculate if data completely changes + }, [dataWithCoordinates]); - // Initialize map ONCE - never again useEffect(() => { if (!mapRef.current || mapInstanceRef.current || !staticTrackData) return; - console.log("ONE-TIME: Initializing optimized track map..."); - const racingLineSource = new VectorSource(); const markerSource = new VectorSource(); racingLineSourceRef.current = racingLineSource; markerSourceRef.current = markerSource; + const tileUrl = window.location.href.includes("dashboard") + ? "/dashboard/api/tiles/dark/{z}/{x}/{y}" + : "/api/tiles/dark/{z}/{x}/{y}"; + const baseLayer = new TileLayer({ source: new XYZ({ - url: MAP_THEMES[mapTheme].url, + url: tileUrl, + crossOrigin: "anonymous", + maxZoom: 20, + minZoom: 5, + transition: 250, + cacheSize: 512, + reprojectionErrorThreshold: 0.5, }), + preload: 1, + useInterimTilesOnError: true, }); - // Create racing line layer (STATIC - track never re-renders, only colors change) const racingLineLayer = new VectorLayer({ source: racingLineSource, + style: new Style({ + stroke: new Stroke({ + color: "#888888", + width: 3, + }), + }), }); // Create marker layer (DYNAMIC - only this updates) @@ -169,7 +167,7 @@ export default function OptimizedTrackMap({ source: markerSource, style: new Style({ image: new Circle({ - radius: 10, // 20% smaller (was 12, now 10) + radius: 5, // 20% smaller (was 12, now 10) fill: new Fill({ color: "#ffff00", }), @@ -207,7 +205,7 @@ export default function OptimizedTrackMap({ markerSourceRef.current = null; trackRenderedRef.current = false; }; - }, [staticTrackData, mapTheme]); // Only re-init if track data completely changes + }, [staticTrackData]); // ONE-TIME: Render the static racing line useEffect(() => { @@ -280,7 +278,7 @@ export default function OptimizedTrackMap({ source.addFeature(lineFeature); } - // Fit view to track bounds + // Fit view to track bounds and warm tile cache if (mapInstanceRef.current) { const view = mapInstanceRef.current.getView(); view.fit(source.getExtent(), { @@ -297,7 +295,12 @@ export default function OptimizedTrackMap({ ); }, [staticTrackData]); // Only render if track data changes - // DYNAMIC: Update ONLY the marker position with throttling + useEffect(() => { + if (staticTrackData) { + colorCacheRef.current.clear(); + } + }, [staticTrackData]); + const lastMarkerUpdateRef = useRef(0); useEffect(() => { if (!markerSourceRef.current || !staticTrackData || selectedPointIndex < 0) @@ -306,14 +309,12 @@ export default function OptimizedTrackMap({ const point = staticTrackData.validGPSPoints[selectedPointIndex]; if (!point) return; - // PERFORMANCE FIX: Throttle marker updates to 60fps max const now = performance.now(); - if (now - lastMarkerUpdateRef.current < 16) { // 60fps = 16.67ms + if (now - lastMarkerUpdateRef.current < 16) { return; } lastMarkerUpdateRef.current = now; - // Use requestAnimationFrame for smooth marker movement requestAnimationFrame(() => { if (!markerSourceRef.current) return; @@ -332,11 +333,11 @@ export default function OptimizedTrackMap({ // No console log here to avoid spam - this runs frequently }, [selectedPointIndex, staticTrackData]); // Updates frequently but only affects marker - // EFFICIENT: Update only line colors when metric changes (no track re-rendering) + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { if (!racingLineSourceRef.current || !mapInstanceRef.current) return; - console.log("Updating racing line colors for metric:", displayMetric); + console.log("Updating racing line colors for metric:", selectedMetric); // PERFORMANCE FIX: Clear color cache when metric changes to avoid stale colors colorCacheRef.current.clear(); @@ -357,20 +358,24 @@ export default function OptimizedTrackMap({ // Batch style updates to avoid layout thrashing features.forEach((feature) => { - const metricValue = feature.get(displayMetric) || 0; - const minVal = feature.get(`${displayMetric}_min`) || 0; - const maxVal = feature.get(`${displayMetric}_max`) || 1; + const metricValue = feature.get(selectedMetric) || 0; + const minVal = feature.get(`${selectedMetric}_min`) || 0; + const maxVal = feature.get(`${selectedMetric}_max`) || 1; const color = getColorForMetric( metricValue, - displayMetric, + selectedMetric, minVal, maxVal, ); // Reuse existing style object if possible const existingStyle = feature.getStyle(); - if (existingStyle && typeof existingStyle !== 'function' && Array.isArray(existingStyle) === false) { + if ( + existingStyle && + typeof existingStyle !== "function" && + Array.isArray(existingStyle) === false + ) { const stroke = (existingStyle as Style).getStroke(); if (stroke) { stroke.setColor(color); @@ -385,7 +390,7 @@ export default function OptimizedTrackMap({ color: color, width: 3, }), - }) + }), ); }); @@ -396,7 +401,7 @@ export default function OptimizedTrackMap({ // Use requestAnimationFrame for smooth updates requestAnimationFrame(updateStyles); } - }, [displayMetric, getColorForMetric]); + }, [getColorForMetric, staticTrackData, selectedMetric]); // Handle click events useEffect(() => { @@ -421,13 +426,13 @@ export default function OptimizedTrackMap({ if (!staticTrackData) { return ( -

+
-
-
+
+
-

No GPS data available

-

+

No GPS data available

+

This session may not contain GPS coordinates or they may be invalid.

@@ -436,184 +441,163 @@ export default function OptimizedTrackMap({ } return ( - <> -
-
- {displayMetric === "Speed" && "Speed (km/h)"} - {displayMetric === "Throttle" && "Throttle (%)"} - {displayMetric === "Brake" && "Brake (%)"} - {displayMetric === "Gear" && "Gear"} - {displayMetric === "RPM" && "RPM"} - {displayMetric === "SteeringWheelAngle" && "Steering (deg)"} -
-
- {displayMetric === "Speed" && ( - <> -
-
- Low Speed -
-
-
- Medium Speed -
-
-
- High Speed -
- - )} - {displayMetric === "Throttle" && ( - <> -
-
- 0% Throttle -
-
-
- 100% Throttle -
- - )} - {displayMetric === "Brake" && ( - <> -
-
- 0% Brake -
-
-
- 100% Brake -
- - )} - {displayMetric === "Gear" && ( - <> -
-
- Low Gear -
-
-
- High Gear -
- - )} - {displayMetric === "RPM" && ( - <> -
-
- Low RPM -
-
-
- High RPM -
- - )} - {displayMetric === "SteeringWheelAngle" && ( - <> -
-
- Straight -
-
-
- Full Lock -
- - )} -
-
- - {/* Controls */} -
- {/* Zoom controls */} -
- - -
- - {/* Metric selector */} - -
- +
- + +
+
+
+
+ + +
+
+ +
+
+ {selectedMetric === "Speed" && "Speed (km/h)"} + {selectedMetric === "Throttle" && "Throttle (%)"} + {selectedMetric === "Brake" && "Brake (%)"} + {selectedMetric === "Gear" && "Gear"} + {selectedMetric === "RPM" && "RPM"} + {selectedMetric === "SteeringWheelAngle" && "Steering (deg)"} +
+ {selectedMetric === "Speed" && ( + <> +
+
+ Low Speed +
+
+
+ Medium Speed +
+
+
+ High Speed +
+ + )} + {selectedMetric === "Throttle" && ( + <> +
+
+ 0% Throttle +
+
+
+ 100% Throttle +
+ + )} + {selectedMetric === "Brake" && ( + <> +
+
+ 0% Brake +
+
+
+ 100% Brake +
+ + )} + {selectedMetric === "Gear" && ( + <> +
+
+ Low Gear +
+
+
+ High Gear +
+ + )} + {selectedMetric === "RPM" && ( + <> +
+
+ Low RPM +
+
+
+ High RPM +
+ + )} + {selectedMetric === "SteeringWheelAngle" && ( + <> +
+
+ Straight +
+
+
+ Full Lock +
+ + )} +
+
+
+
); -} +}); diff --git a/dashboard/components/ProfessionalTelemetryCharts.tsx b/dashboard/components/ProfessionalTelemetryCharts.tsx deleted file mode 100644 index 7fb4003..0000000 --- a/dashboard/components/ProfessionalTelemetryCharts.tsx +++ /dev/null @@ -1,281 +0,0 @@ -"use client"; - -import React, { useCallback, useMemo, useRef, useEffect } from "react"; -import { - CartesianGrid, - Line, - LineChart, - ResponsiveContainer, - XAxis, - YAxis, - ReferenceLine, - Tooltip, -} from "recharts"; -import type { TelemetryDataPoint } from "@/lib/types"; - -interface ProfessionalTelemetryChartsProps { - telemetryData: TelemetryDataPoint[]; - selectedIndex: number; - onHover: (index: number) => void; - onIndexChange: (index: number) => void; - onMouseLeave?: () => void; -} - -const ProfessionalTelemetryCharts = React.memo(function ProfessionalTelemetryCharts({ - telemetryData, - selectedIndex, - onHover, - onIndexChange, - onMouseLeave, -}: ProfessionalTelemetryChartsProps) { - const chartData = useMemo(() => { - // Sample data for better performance - show every nth point for charts - // This reduces 39K points to ~4K points for rendering while maintaining shape - const sampleRate = Math.max(1, Math.floor(telemetryData.length / 4000)); - - // PERFORMANCE FIX: Build sampled data and index mapping efficiently - const sampledData: (TelemetryDataPoint & { originalIndex: number; index: number; lapDistance: number })[] = []; - const originalToSampledIndex = new Map(); - - telemetryData.forEach((point, originalIndex) => { - if (originalIndex % sampleRate === 0) { - const sampledIndex = sampledData.length; - sampledData.push({ - ...point, - originalIndex, // Store original index efficiently - index: sampledIndex, - lapDistance: (point.LapDistPct / 100) * 5.5, // Approximate track distance in km - }); - originalToSampledIndex.set(originalIndex, sampledIndex); - } - }); - - return { sampledData, originalToSampledIndex }; - }, [telemetryData]); - - // Throttle hover events for better performance - const throttledHoverRef = useRef(null); - const lastHoverIndex = useRef(-1); - - // Cleanup throttled events on unmount - useEffect(() => { - return () => { - if (throttledHoverRef.current) { - cancelAnimationFrame(throttledHoverRef.current); - } - }; - }, []); - - const handleMouseMove = useCallback( - (data: any) => { - if (data && data.activeTooltipIndex !== undefined && chartData.sampledData[data.activeTooltipIndex]) { - const originalIndex = chartData.sampledData[data.activeTooltipIndex].originalIndex; - - // Skip if same index to prevent unnecessary updates - if (lastHoverIndex.current === originalIndex) return; - - // Throttle hover events to 30fps (33ms) for better performance - if (throttledHoverRef.current) { - cancelAnimationFrame(throttledHoverRef.current); - } - throttledHoverRef.current = requestAnimationFrame(() => { - lastHoverIndex.current = originalIndex; - onHover(originalIndex); - }); - } - }, - [onHover, chartData.sampledData], - ); - - const handleChartClick = useCallback( - (data: any) => { - if (data && data.activeTooltipIndex !== undefined && chartData.sampledData[data.activeTooltipIndex]) { - // Use original index from the sampled data - const originalIndex = chartData.sampledData[data.activeTooltipIndex].originalIndex; - onIndexChange(originalIndex); - } - }, - [onIndexChange, chartData.sampledData], - ); - - // Memoize chart configurations to prevent recreations - const chartConfigs = useMemo(() => [ - { - title: "Speed", - dataKey: "Speed", - color: "#ef4444", - unit: "km/h", - yDomain: [0, 300], - height: 120, - }, - { - title: "Throttle", - dataKey: "Throttle", - color: "#22c55e", - unit: "%", - yDomain: [0, 100], - height: 100, - }, - { - title: "Brake", - dataKey: "Brake", - color: "#f97316", - unit: "%", - yDomain: [0, 100], - height: 100, - }, - { - title: "Gear", - dataKey: "Gear", - color: "#8b5cf6", - unit: "", - yDomain: [0, 8], - height: 80, - }, - { - title: "RPM", - dataKey: "RPM", - color: "#06b6d4", - unit: "", - yDomain: [0, 8000], - height: 100, - }, - { - title: "Steering", - dataKey: "SteeringWheelAngle", - color: "#ec4899", - unit: "deg", - yDomain: [-180, 180], - height: 100, - }, - ], []); - - // Memoize tooltip component - const CustomTooltip = useCallback(({ active, payload, label }: any) => { - if (active && payload && payload.length) { - const dataPoint = payload[0].payload as TelemetryDataPoint & { lapDistance?: number }; - return ( -
-

- Distance: {dataPoint.lapDistance?.toFixed(2)} km -

-

- Time: {dataPoint.sessionTime?.toFixed(2)}s -

-
- ); - } - return null; - }, []); - - // PERFORMANCE FIX: Memoize reference line position to prevent jumping - const referenceLineDistance = useMemo(() => { - if (selectedIndex < 0 || !telemetryData[selectedIndex]) return null; - - // Try to find exact sampled point first - const sampledIndex = chartData.originalToSampledIndex.get(selectedIndex); - if (sampledIndex !== undefined && chartData.sampledData[sampledIndex]) { - return chartData.sampledData[sampledIndex].lapDistance; - } - - // Fallback: calculate from original data - const originalPoint = telemetryData[selectedIndex]; - if (originalPoint?.LapDistPct !== undefined) { - return (originalPoint.LapDistPct / 100) * 5.5; - } - - return null; - }, [selectedIndex, chartData.originalToSampledIndex, chartData.sampledData, telemetryData]); - - return ( -
-
- Telemetry Data -
- - {chartConfigs.map((config) => ( -
-
- - {config.title} - - {config.unit} -
- -
- - - - - - } /> - - - - {/* FIXED: Memoized reference line to prevent jumping */} - {referenceLineDistance !== null && ( - - )} - - -
-
- ))} - - {/* Distance markers */} -
- 0 km - 1 km - 2 km - 3 km - 4 km - 5 km -
-
- ); -}); - -ProfessionalTelemetryCharts.displayName = 'ProfessionalTelemetryCharts'; - -export default ProfessionalTelemetryCharts; \ No newline at end of file diff --git a/dashboard/components/SessionSelector.tsx b/dashboard/components/SessionSelector.tsx index 626e207..22a63fd 100644 --- a/dashboard/components/SessionSelector.tsx +++ b/dashboard/components/SessionSelector.tsx @@ -1,11 +1,10 @@ -"use client"; +import { Link } from "@tanstack/react-router"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; - -interface Session { +export interface Session { + last_updated: string; + max_lap_id: string; session_id: string; - last_updated: Date; + session_name: string; track_name: string; } @@ -14,15 +13,6 @@ interface SessionSelectorProps { } export default function SessionSelector({ sessions }: SessionSelectorProps) { - const router = useRouter(); - const [selectedSession, setSelectedSession] = useState(""); - - const handleSessionSelect = (sessionId: string) => { - if (sessionId) { - router.push(`/${sessionId}?lapId=1`); - } - }; - const formatDate = (date: Date) => { return new Intl.DateTimeFormat("en-US", { year: "numeric", @@ -43,86 +33,109 @@ export default function SessionSelector({ sessions }: SessionSelectorProps) { if (diffDays > 0) return `${diffDays}d ago`; if (diffHours > 0) return `${diffHours}h ago`; if (diffMinutes > 0) return `${diffMinutes}m ago`; - return 'Just now'; + return "Just now"; }; // Group sessions by track for better organization - const sessionsByTrack = sessions.reduce((acc, session) => { - const track = session.track_name || 'Unknown Track'; - if (!acc[track]) acc[track] = []; - acc[track].push(session); - return acc; - }, {} as Record); + const sessionsByTrack = sessions.reduce( + (acc, session) => { + const track = session.track_name || "Unknown Track"; + if (!acc[track]) acc[track] = []; + acc[track].push(session); + return acc; + }, + {} as Record, + ); return (
{/* Sessions Grid */}
{sessions.map((session) => ( - + ))}
{/* Quick Filter */} {Object.keys(sessionsByTrack).length > 1 && ( -
-

Filter by Track

+
+

+ Filter by Track +

- {Object.entries(sessionsByTrack).map(([trackName, trackSessions]) => ( -
-
- {trackName} - - {trackSessions.length} - + {Object.entries(sessionsByTrack).map( + ([trackName, trackSessions]) => ( +
+
+ {trackName} + + {trackSessions.length} + +
-
- ))} + ), + )}
)} diff --git a/dashboard/components/TelemetryChart.tsx b/dashboard/components/TelemetryChart.tsx new file mode 100644 index 0000000..755b41f --- /dev/null +++ b/dashboard/components/TelemetryChart.tsx @@ -0,0 +1,113 @@ +import { useCallback } from "react"; +import { + CartesianGrid, + Line, + LineChart, + ReferenceLine, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import type { TelemetryDataPoint } from "../lib/types"; +import type { chartConfig } from "./TelemetryCharts"; + +interface TelemetryChartProps { + config: chartConfig; + chartData: TelemetryDataPoint[]; + ReferenceLineX: number; + onHover?: (index: number | null) => void; +} + +export const TelemetryChart = ({ + config, + chartData, + ReferenceLineX, + onHover, +}: TelemetryChartProps) => { + const CustomTooltip = useCallback(({ active, payload }: any) => { + if (active && payload && payload.length) { + const dataPoint = payload[0].payload as TelemetryDataPoint; + + return ( +
+

+ Distance: {dataPoint.LapDistPct?.toFixed(1)} % +

+

+ Time: {dataPoint.sessionTime?.toFixed(2)}s +

+
+ ); + } + return null; + }, []); + + return ( +
+
+ + {config.title} + + {config.unit} +
+ +
+ + {}} + margin={{ top: 5, right: 5, left: 5, bottom: 5 }} + syncId="telemetry-charts" + onMouseMove={(e) => { + if (onHover) { + onHover(e.activeIndex as number); + } + }} + > + + + + } /> + + + + + + +
+
+ ); +}; diff --git a/dashboard/components/TelemetryCharts.tsx b/dashboard/components/TelemetryCharts.tsx new file mode 100644 index 0000000..7613dd2 --- /dev/null +++ b/dashboard/components/TelemetryCharts.tsx @@ -0,0 +1,105 @@ +"use client"; + +import React, { useMemo } from "react"; +import type { TelemetryDataPoint } from "../lib/types"; +import { TelemetryChart } from "./TelemetryChart"; + +export type chartConfig = { + title: string; + dataKey: string; + color: string; + unit: string; + yDomain: number[]; + height: number; +}; + +interface ProfessionalTelemetryChartsProps { + telemetryData: TelemetryDataPoint[]; + onMouseLeave?: () => void; + onHover?: (index: number | null) => void; +} + +const ProfessionalTelemetryCharts = React.memo( + function ProfessionalTelemetryCharts({ + telemetryData, + onMouseLeave, + onHover, + }: ProfessionalTelemetryChartsProps) { + const chartConfigs = useMemo( + () => [ + { + title: "Speed", + dataKey: "Speed", + color: "#ef4444", + unit: "km/h", + yDomain: [0, 300], + height: 120, + }, + { + title: "Throttle", + dataKey: "Throttle", + color: "#22c55e", + unit: "%", + yDomain: [0, 100], + height: 100, + }, + { + title: "Brake", + dataKey: "Brake", + color: "#f97316", + unit: "%", + yDomain: [0, 100], + height: 100, + }, + { + title: "Gear", + dataKey: "Gear", + color: "#8b5cf6", + unit: "", + yDomain: [0, 8], + height: 80, + }, + { + title: "RPM", + dataKey: "RPM", + color: "#06b6d4", + unit: "", + yDomain: [0, 8000], + height: 100, + }, + { + title: "Steering", + dataKey: "SteeringWheelAngle", + color: "#ec4899", + unit: "deg", + yDomain: [-180, 180], + height: 100, + }, + ], + [], + ); + + return ( + // biome-ignore lint/a11y/noStaticElementInteractions: na +
+
+ Telemetry Data +
+ + {chartConfigs.map((config) => ( + + ))} +
+ ); + }, +); + +ProfessionalTelemetryCharts.displayName = "ProfessionalTelemetryCharts"; + +export default ProfessionalTelemetryCharts; diff --git a/dashboard/components/TelemetryPage.tsx b/dashboard/components/TelemetryPage.tsx index 6aeb0ed..89987aa 100644 --- a/dashboard/components/TelemetryPage.tsx +++ b/dashboard/components/TelemetryPage.tsx @@ -1,316 +1,97 @@ -"use client"; - -import { usePathname, useRouter } from "next/navigation"; -import { useMemo, useState, useCallback, useRef, useEffect, useDeferredValue } from "react"; -import { InfoBox, TelemetryChart } from "@/components/InfoBox"; -import TrackView from "@/components/TrackView"; -import ProfessionalTelemetryCharts from "@/components/ProfessionalTelemetryCharts"; -import { useTrackPosition } from "@/hooks/useTrackPosition"; -import type { TelemetryRes } from "@/lib/Fetch"; -import type { TelemetryDataPoint } from "@/lib/types"; +import { useNavigate } from "@tanstack/react-router"; +import React, { useState } from "react"; +import useSWR from "swr"; +import { InfoBox } from "../components/InfoBox"; +import { fetcherBR, type TelemetryRes } from "../lib/Fetch"; +import type { TelemetryDataPoint } from "../lib/types"; +import { useChartHover } from "../hooks/useChartHover"; +import { useTelemetryData } from "../hooks/useTelemetryData"; +import { + GPSAnalysisPanel, + TelemetryMapSection, + TelemetrySidebar, + TelemetryStatsHeader, +} from "./telemetry"; + +const ProfessionalTelemetryCharts = React.lazy( + () => import("./TelemetryCharts"), +); interface TelemetryPageProps { initialTelemetryData: TelemetryRes; - availableLaps: Array<{ lap_id: number }>; + availableLaps?: Array; sessionId: string; currentLapId: number; } -const availableMetrics: string[] = [ - "LapDistPct", - "Speed", - "Throttle", - "Brake", - "Gear", - "RPM", - "SteeringWheelAngle", - "LapCurrentLapTime", - "PlayerCarPosition", - "FuelLevel", -]; - export default function TelemetryPage({ initialTelemetryData, availableLaps, sessionId, currentLapId, }: TelemetryPageProps) { - const router = useRouter(); - const pathname = usePathname(); - + const nav = useNavigate(); const [selectedMetric, setSelectedMetric] = useState("Speed"); - const [isScrubbing, setIsScrubbing] = useState(false); - const [hoverIndex, setHoverIndex] = useState(-1); - - // Extract processed data from the server response - wrap in useMemo to fix dependency warning - const dataWithGPSCoordinates = useMemo(() => { - return initialTelemetryData?.dataWithGPSCoordinates || []; - }, [initialTelemetryData?.dataWithGPSCoordinates]); - - const trackBounds = initialTelemetryData?.trackBounds || null; - const processError = initialTelemetryData?.processError || null; - - const { - selectedIndex, - selectedLapPct, - handlePointSelection, - getTrackDisplayPoint, - } = useTrackPosition(dataWithGPSCoordinates as TelemetryDataPoint[]); - - // Derive track information from data - const trackInfo = useMemo(() => { - if (dataWithGPSCoordinates.length === 0) return null; - - const firstPoint = dataWithGPSCoordinates[0]; - return { - trackName: firstPoint?.TrackName || "Unknown Track", - sessionNum: firstPoint?.SessionNum || sessionId, - }; - }, [dataWithGPSCoordinates, sessionId]); - - // Memoize track point click handler - const handleTrackPointClick = useCallback((index: number) => { - handlePointSelection(index); - setIsScrubbing(true); - setTimeout(() => setIsScrubbing(false), 500); - }, [handlePointSelection]); - - // Debounced hover handling for smoother interactions - const hoverTimeoutRef = useRef(null); - - const handleChartHover = useCallback((index: number) => { - // Clear any pending hover timeout - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - } - - // More aggressive debouncing for heavy datasets - hoverTimeoutRef.current = setTimeout(() => { - setHoverIndex(index); - }, 16); // 16ms debounce (~60fps) for better performance with 39K points - }, []); - - const handleChartClick = useCallback((index: number) => { - // Clear any pending hover timeouts on click - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - } - - handlePointSelection(index); - setIsScrubbing(true); - setTimeout(() => setIsScrubbing(false), 300); - setHoverIndex(-1); // Clear hover state after selection - }, [handlePointSelection]); - - const handleChartMouseLeave = useCallback(() => { - // Clear any pending hover timeouts - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - } - - // Debounce mouse leave to prevent flickering - hoverTimeoutRef.current = setTimeout(() => { - setHoverIndex(-1); - }, 50); // Slightly longer delay for mouse leave - }, []); - - // Cleanup timeouts on unmount - useEffect(() => { - return () => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - } - }; - }, []); - - // Defer hover index updates to prevent blocking the main thread - const deferredHoverIndex = useDeferredValue(hoverIndex); - - // Get display index (hover takes precedence over selection for preview) - const displayIndex = useMemo(() => { - return deferredHoverIndex >= 0 ? deferredHoverIndex : selectedIndex; - }, [deferredHoverIndex, selectedIndex]); - - // Memoize the telemetry data to prevent unnecessary recalculations - const memoizedTelemetryData = useMemo(() => { - return dataWithGPSCoordinates as TelemetryDataPoint[]; - }, [dataWithGPSCoordinates]); + const { hoveredIndex, handleChartHover, handleChartMouseLeave } = + useChartHover(); + const { dataWithGPSCoordinates, trackInfo, hoverCoordinates } = + useTelemetryData(initialTelemetryData, sessionId, hoveredIndex); + + const { data: racingLineData } = useSWR( + `/api/sessions/${sessionId}/laps/${currentLapId}/geojson`, + fetcherBR, + ); const handleLapChange = (newLapId: string) => { - const params = new URLSearchParams(); - params.set("lapId", newLapId); - router.push(pathname + "?" + params.toString()); + nav({ to: ".", search: () => ({ lapId: newLapId }) }); }; - if (processError) { - return ( -
- {/* Sidebar */} -
- {/* Logo/Brand */} -
-
-
-
-
-
-

iRacing

-

Telemetry

-
-
-
-
- - {/* Main Content */} -
- {/* Header */} -
-
- Dashboard - / - Sessions -
-
- - {/* Error Content */} -
-
-
-
-
-

- Error Loading Telemetry Data -

-

{processError}

-
-
-
-
- ); - } - return ( -
- {/* Sidebar */} -
- {/* Logo/Brand */} -
-
-
-
-
-
-

iRacing

-

Telemetry

-
-
-
- - {/* Navigation */} - - -
- - {/* Main Content */} -
- - {/* Main Content */} -
- {/* Key Stats Section */} +
+ + +
+
{dataWithGPSCoordinates.length > 0 && ( -
-
-
-
Track
-
{trackInfo?.trackName || "Unknown"}
-
-
-
GPS Points
-
{dataWithGPSCoordinates.length.toLocaleString()}
-
-
-
Max Speed
-
- {Math.max(...dataWithGPSCoordinates.map(p => p.Speed || 0)).toFixed(0)} km/h -
-
-
-
- {deferredHoverIndex >= 0 ? "Preview" : "Selected"} -
- {displayIndex >= 0 && dataWithGPSCoordinates[displayIndex] ? ( -
- {dataWithGPSCoordinates[displayIndex].Speed?.toFixed(0) || "0"} km/h -
- ) : ( -
--
- )} -
-
-
+ )} - {/* Main Content Grid - Track Map + Professional Telemetry Charts */} -
- {/* Track Map */} -
+
+ +
{dataWithGPSCoordinates.length > 0 ? ( - - ) : ( -
-
-
-
-
-

No GPS data available

-

- This session may not contain GPS coordinates or they may be invalid. -

-
-
- )} -
- - {/* Professional Telemetry Charts */} -
- {memoizedTelemetryData.length > 0 ? ( ) : ( -
+
-
-
+
+
-

No telemetry data available

-

+

+ No telemetry data available +

+

Loading telemetry charts...

@@ -319,11 +100,12 @@ export default function TelemetryPage({
- {/* Info Boxes */} {dataWithGPSCoordinates.length > 0 && ( )} @@ -335,49 +117,3 @@ export default function TelemetryPage({
); } - - -function GPSAnalysisPanel({ data }: { data: any[] }) { - const totalDistance = data.reduce( - (sum, point) => sum + (point.distanceFromPrev || 0), - 0, - ); - const avgSpeed = - data.reduce((sum, point) => sum + (point.Speed || 0), 0) / data.length; - const maxSpeed = Math.max(...data.map((point) => point.Speed || 0)); - const minSpeed = Math.min(...data.map((point) => point.Speed || 0)); - - const corners = data.filter((point) => point.sectionType === "corner"); - - return ( -
-

GPS Track Analysis

-
-
-
Total Distance
-
- {(totalDistance / 1000).toFixed(2)} km -
-
-
-
Average Speed
-
{avgSpeed.toFixed(1)} km/h
-
-
-
Speed Range
-
- {minSpeed.toFixed(0)} - {maxSpeed.toFixed(0)} -
-
km/h
-
-
-
Corner Points
-
{corners.length.toLocaleString()}
-
- {((corners.length / data.length) * 100).toFixed(1)}% of lap -
-
-
-
- ); -} diff --git a/dashboard/components/TrackView.tsx b/dashboard/components/TrackView.tsx deleted file mode 100644 index f194895..0000000 --- a/dashboard/components/TrackView.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import type { TelemetryDataPoint } from "@/lib/types"; -import OptimizedTrackMap from "./OptimizedTrackMap"; - -interface TrackViewProps { - dataWithCoordinates: TelemetryDataPoint[]; - selectedPointIndex: number; - onPointClick?: (index: number) => void; - selectedMetric?: string; -} - -export default function TrackView({ - dataWithCoordinates, - selectedPointIndex, - onPointClick, - selectedMetric = "Speed", -}: TrackViewProps) { - - return ( - - ); -} diff --git a/dashboard/components/telemetry/GPSAnalysisPanel.tsx b/dashboard/components/telemetry/GPSAnalysisPanel.tsx new file mode 100644 index 0000000..a49c8a5 --- /dev/null +++ b/dashboard/components/telemetry/GPSAnalysisPanel.tsx @@ -0,0 +1,52 @@ +import type { TelemetryDataPoint } from "../../lib/types"; + +export function GPSAnalysisPanel({ data }: { data: TelemetryDataPoint[] }) { + const totalDistance = data.reduce( + (sum, point) => sum + (point.distanceFromPrev || 0), + 0, + ); + const avgSpeed = + data.reduce((sum, point) => sum + (point.Speed || 0), 0) / data.length; + const maxSpeed = Math.max(...data.map((point) => point.Speed || 0)); + const minSpeed = Math.min(...data.map((point) => point.Speed || 0)); + + const corners = data.filter((point) => point.sectionType === "corner"); + + return ( +
+

+ GPS Track Analysis +

+
+
+
Total Distance
+
+ {(totalDistance / 1000).toFixed(2)} km +
+
+
+
Average Speed
+
+ {avgSpeed.toFixed(1)} km/h +
+
+
+
Speed Range
+
+ {minSpeed.toFixed(0)} - {maxSpeed.toFixed(0)} +
+
km/h
+
+
+
Corner Points
+
+ {corners.length.toLocaleString()} +
+
+ {((corners.length / data.length) * 100).toFixed(1)}% of lap +
+
+
+
+ ); +} diff --git a/dashboard/components/telemetry/HoverMarker.tsx b/dashboard/components/telemetry/HoverMarker.tsx new file mode 100644 index 0000000..e9afa9b --- /dev/null +++ b/dashboard/components/telemetry/HoverMarker.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { MapMarker, MarkerContent } from "../ui/map"; + +export const HoverMarker = React.memo(function HoverMarker({ + longitude, + latitude, +}: { + longitude: number; + latitude: number; +}) { + return ( + + +
+ + + ); +}); diff --git a/dashboard/components/telemetry/RacingLine.tsx b/dashboard/components/telemetry/RacingLine.tsx new file mode 100644 index 0000000..53f8c85 --- /dev/null +++ b/dashboard/components/telemetry/RacingLine.tsx @@ -0,0 +1,56 @@ +import React, { useEffect } from "react"; +import { useMap } from "../ui/map"; + +function RacingLine({ + dataWithGPSCoordinates, +}: { + dataWithGPSCoordinates: GeoJSON.FeatureCollection; +}) { + const { map, isLoaded } = useMap(); + + useEffect(() => { + if (!isLoaded || !map || !dataWithGPSCoordinates?.features) return; + + try { + if (!map.getSource("racing-line")) { + map.addSource("racing-line", { + type: "geojson", + data: dataWithGPSCoordinates, + }); + } + + if (!map.getLayer("racing-line-layer")) { + map.addLayer({ + id: "racing-line-layer", + type: "line", + source: "racing-line", + paint: { + "line-color": ["get", "color"], + "line-width": 4, + "line-opacity": 1, + }, + layout: { + "line-cap": "round", + "line-join": "round", + }, + }); + } + } catch { + // style not ready yet + } + + return () => { + try { + if (map.getLayer("racing-line-layer")) + map.removeLayer("racing-line-layer"); + if (map.getSource("racing-line")) map.removeSource("racing-line"); + } catch { + // ignore β€” map may already be removed + } + }; + }, [isLoaded, map, dataWithGPSCoordinates]); + + return null; +} + +export const MemoizedRacingLine = React.memo(RacingLine); diff --git a/dashboard/components/telemetry/TelemetryMapSection.tsx b/dashboard/components/telemetry/TelemetryMapSection.tsx new file mode 100644 index 0000000..6d000fe --- /dev/null +++ b/dashboard/components/telemetry/TelemetryMapSection.tsx @@ -0,0 +1,91 @@ +import { useMemo } from "react"; +import { Card } from "../ui/card"; +import type { MapStyleOption } from "../ui/map"; +import { + MapControls, + MapRoute, + NewMap as MapUI, +} from "../ui/map"; +import { HoverMarker } from "./HoverMarker"; +import { MemoizedRacingLine } from "./RacingLine"; + +const darkTileStyle: MapStyleOption = { + version: 8, + sources: { + satellite: { + type: "raster", + tiles: [ + "https://cartodb-basemaps-a.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", + ], + tileSize: 256, + }, + }, + layers: [ + { + id: "satellite", + type: "raster", + source: "satellite", + }, + ], +}; + +const mapStyles = { light: darkTileStyle }; + +import type { TelemetryDataPoint } from "../../lib/types"; + +interface TelemetryMapSectionProps { + dataWithGPSCoordinates: TelemetryDataPoint[]; + racingLineData: GeoJSON.FeatureCollection | undefined; + hoverCoordinates: { lon: number; lat: number } | null; +} + +export function TelemetryMapSection({ + dataWithGPSCoordinates, + racingLineData, + hoverCoordinates, +}: TelemetryMapSectionProps) { + const routeCoordinates = useMemo( + () => + dataWithGPSCoordinates.map((data) => [data.Lon, data.Lat] as [number, number]), + [dataWithGPSCoordinates], + ); + + return ( +
+ + {dataWithGPSCoordinates[0].Lon !== undefined && ( + + + {racingLineData && ( + + )} + + {hoverCoordinates && ( + + )} + + )} + +
+ ); +} diff --git a/dashboard/components/telemetry/TelemetrySidebar.tsx b/dashboard/components/telemetry/TelemetrySidebar.tsx new file mode 100644 index 0000000..fa2252f --- /dev/null +++ b/dashboard/components/telemetry/TelemetrySidebar.tsx @@ -0,0 +1,65 @@ +import { Link } from "@tanstack/react-router"; + +interface TelemetrySidebarProps { + availableLaps?: number[]; + currentLapId: number; + sessionId: string; + onLapChange: (lapId: string) => void; +} + +export function TelemetrySidebar({ + availableLaps, + currentLapId, + sessionId, + onLapChange, +}: TelemetrySidebarProps) { + return ( +
+
+ +
+
+
+
+
+

iRacing

+

Telemetry

+
+
+ +
+ + +
+ ); +} diff --git a/dashboard/components/telemetry/TelemetryStatsHeader.tsx b/dashboard/components/telemetry/TelemetryStatsHeader.tsx new file mode 100644 index 0000000..08dce36 --- /dev/null +++ b/dashboard/components/telemetry/TelemetryStatsHeader.tsx @@ -0,0 +1,46 @@ +import { formatTime } from "../../lib/formatters"; + +interface TelemetryStatsHeaderProps { + trackName: string; + gpsPointCount: number; + maxSpeed: number; + lapTime: number | undefined; +} + +export function TelemetryStatsHeader({ + trackName, + gpsPointCount, + maxSpeed, + lapTime, +}: TelemetryStatsHeaderProps) { + return ( +
+
+
+
Track
+
+ {trackName || "Unknown"} +
+
+
+
GPS Points
+
+ {gpsPointCount.toLocaleString()} +
+
+
+
Max Speed
+
+ {maxSpeed.toFixed(0)} km/h +
+
+
+
Lap time
+
+ {lapTime ? formatTime(lapTime) : "0.00"} +
+
+
+
+ ); +} diff --git a/dashboard/components/telemetry/index.ts b/dashboard/components/telemetry/index.ts new file mode 100644 index 0000000..981b08f --- /dev/null +++ b/dashboard/components/telemetry/index.ts @@ -0,0 +1,6 @@ +export { TelemetrySidebar } from "./TelemetrySidebar"; +export { TelemetryStatsHeader } from "./TelemetryStatsHeader"; +export { TelemetryMapSection } from "./TelemetryMapSection"; +export { MemoizedRacingLine } from "./RacingLine"; +export { GPSAnalysisPanel } from "./GPSAnalysisPanel"; +export { HoverMarker } from "./HoverMarker"; diff --git a/dashboard/components/theme-provider.tsx b/dashboard/components/theme-provider.tsx new file mode 100644 index 0000000..7c8090f --- /dev/null +++ b/dashboard/components/theme-provider.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type * as React from "react"; + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children}; +} diff --git a/dashboard/components/trackMap.tsx b/dashboard/components/trackMap.tsx deleted file mode 100644 index d9cd3c8..0000000 --- a/dashboard/components/trackMap.tsx +++ /dev/null @@ -1,402 +0,0 @@ -"use client"; - -import Feature from "ol/Feature"; -import { LineString, Point } from "ol/geom"; -import TileLayer from "ol/layer/Tile"; -import VectorLayer from "ol/layer/Vector"; -import OlMap from "ol/Map"; -import { fromLonLat } from "ol/proj"; -import VectorSource from "ol/source/Vector"; -import XYZ from "ol/source/XYZ"; -import { Circle, Fill, Stroke, Style, Text } from "ol/style"; -import View from "ol/View"; -import { useEffect, useRef } from "react"; -import type { TelemetryDataPoint } from "@/lib/types"; - -interface TrackMapProps { - dataWithCoordinates: TelemetryDataPoint[]; - selectedPointIndex: number; - selectedLapPct: number; - isScrubbing: boolean; - getTrackDisplayPoint: () => TelemetryDataPoint | null; -} - -export default function GPSTrackMap({ - dataWithCoordinates, - selectedPointIndex, - isScrubbing, -}: TrackMapProps) { - const mapRef = useRef(null); - const mapInstanceRef = useRef(null); - - const mainLineSourceRef = useRef(null); - const speedSegmentsSourceRef = useRef(null); - const carPositionSourceRef = useRef(null); - const selectedMarkerSourceRef = useRef(null); - - useEffect(() => { - if (!mapRef.current || mapInstanceRef.current) return; - - console.log("Initializing progressive track map..."); - - const mainLineSource = new VectorSource(); - const speedSegmentsSource = new VectorSource(); - const carPositionSource = new VectorSource(); - const selectedMarkerSource = new VectorSource(); - - mainLineSourceRef.current = mainLineSource; - speedSegmentsSourceRef.current = speedSegmentsSource; - carPositionSourceRef.current = carPositionSource; - selectedMarkerSourceRef.current = selectedMarkerSource; - - const baseLayer = new TileLayer({ - source: new XYZ({ - url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", - attributions: "Β© OpenStreetMap contributors", - }), - }); - - const mainLineLayer = new VectorLayer({ - source: mainLineSource, - style: new Style({ - stroke: new Stroke({ - color: "#ed0909", - width: 10, - }), - }), - }); - - const speedSegmentsLayer = new VectorLayer({ - source: speedSegmentsSource, - }); - - const carPositionLayer = new VectorLayer({ - source: carPositionSource, - style: new Style({ - image: new Circle({ - radius: 8, - fill: new Fill({ - color: "#00ff00", - }), - stroke: new Stroke({ - color: "#000000", - width: 2, - }), - }), - }), - }); - - const selectedMarkerLayer = new VectorLayer({ - source: selectedMarkerSource, - style: new Style({ - image: new Circle({ - radius: 10, - fill: new Fill({ - color: "#00ffff", - }), - stroke: new Stroke({ - color: "#000000", - width: 2, - }), - }), - }), - }); - - const map = new OlMap({ - target: mapRef.current, - layers: [ - baseLayer, - mainLineLayer, - speedSegmentsLayer, - carPositionLayer, - selectedMarkerLayer, - ], - view: new View({ - center: fromLonLat([9.2808, 45.6162]), - zoom: 15, - maxZoom: 20, - minZoom: 5, - }), - }); - - mapInstanceRef.current = map; - - console.log("Progressive track map initialized"); - - return () => { - if (mapInstanceRef.current) { - mapInstanceRef.current.setTarget(undefined); - mapInstanceRef.current = null; - } - mainLineSourceRef.current = null; - speedSegmentsSourceRef.current = null; - carPositionSourceRef.current = null; - selectedMarkerSourceRef.current = null; - }; - }, []); - - // biome-ignore lint/correctness/useExhaustiveDependencies: <> - useEffect(() => { - if ( - !mapInstanceRef.current || - !mainLineSourceRef.current || - !speedSegmentsSourceRef.current || - !carPositionSourceRef.current - ) { - return; - } - - if (dataWithCoordinates.length === 0) { - return; - } - - console.log( - "Updating racing line with", - dataWithCoordinates.length, - "points", - ); - - mainLineSourceRef.current.clear(); - speedSegmentsSourceRef.current.clear(); - carPositionSourceRef.current.clear(); - - try { - const validGPSPoints = dataWithCoordinates.filter( - (point) => point.Lat && point.Lon && point.Lat !== 0 && point.Lon !== 0, - ); - - if (validGPSPoints.length === 0) { - console.warn("No valid GPS coordinates found"); - return; - } - - console.log("Valid GPS points:", validGPSPoints.length); - - const lineCoordinates = validGPSPoints.map((point) => - fromLonLat([point.Lon, point.Lat]), - ); - - const mainLineFeature = new Feature({ - geometry: new LineString(lineCoordinates), - }); - - mainLineSourceRef.current.addFeature(mainLineFeature); - - let segmentsAdded = 0; - for (let i = 0; i < validGPSPoints.length - 1; i++) { - const point = validGPSPoints[i]; - const nextPoint = validGPSPoints[i + 1]; - - if (typeof point.Speed !== "number" || isNaN(point.Speed)) { - console.warn(`Invalid speed at point ${i}:`, point.Speed); - continue; - } - - const segmentCoords = [ - fromLonLat([point.Lon, point.Lat]), - fromLonLat([nextPoint.Lon, nextPoint.Lat]), - ]; - - const segmentFeature = new Feature({ - geometry: new LineString(segmentCoords), - }); - - const speedColor = getSpeedColor(point.Speed); - segmentFeature.setStyle( - new Style({ - stroke: new Stroke({ - color: speedColor, - width: 3, - }), - }), - ); - - speedSegmentsSourceRef.current.addFeature(segmentFeature); - segmentsAdded++; - } - - console.log(`Added ${segmentsAdded} speed-colored segments`); - - const startPoint = validGPSPoints[0]; - if (startPoint) { - const carFeature = new Feature({ - geometry: new Point(fromLonLat([startPoint.Lon, startPoint.Lat])), - }); - carPositionSourceRef.current.addFeature(carFeature); - } - - if (!isScrubbing) { - const geometry = mainLineFeature.getGeometry(); - if (geometry) { - mapInstanceRef.current.getView().fit(geometry.getExtent(), { - padding: [100, 100, 100, 100], - duration: 1000, - maxZoom: 18, - }); - } - } - - console.log("Racing line updated successfully"); - } catch (error) { - console.error("Error updating racing line:", error); - } - }, [dataWithCoordinates, isScrubbing]); - - useEffect(() => { - if (!selectedMarkerSourceRef.current || dataWithCoordinates.length === 0) { - return; - } - - selectedMarkerSourceRef.current.clear(); - - const validIndex = Math.min( - Math.max(0, selectedPointIndex), - dataWithCoordinates.length - 1, - ); - - const selectedPoint = dataWithCoordinates[validIndex]; - - if (selectedPoint && selectedPoint.Lat && selectedPoint.Lon) { - const markerCoords = fromLonLat([selectedPoint.Lon, selectedPoint.Lat]); - - const markerFeature = new Feature({ - geometry: new Point(markerCoords), - }); - - const displayText = `${selectedPoint.LapDistPct.toFixed(1)}% - ${selectedPoint.Speed.toFixed(1)}kph`; - - markerFeature.setStyle( - new Style({ - image: new Circle({ - radius: 10, - fill: new Fill({ - color: "#00ffff", - }), - stroke: new Stroke({ - color: "#000000", - width: 2, - }), - }), - text: new Text({ - text: displayText, - offsetY: -20, - font: "14px sans-serif", - fill: new Fill({ - color: "#ffffff", - }), - stroke: new Stroke({ - color: "#000000", - width: 3, - }), - }), - }), - ); - - selectedMarkerSourceRef.current.addFeature(markerFeature); - - if (isScrubbing && mapInstanceRef.current) { - mapInstanceRef.current.getView().animate({ - center: markerCoords, - duration: 300, - }); - } - } - }, [selectedPointIndex, dataWithCoordinates, isScrubbing]); - - const getSpeedColor = (speed: number): string => { - const normalizedSpeed = Math.min(speed / 300, 1); - - if (normalizedSpeed < 0.3) { - return "#ff0000"; - } else if (normalizedSpeed < 0.6) { - return "#ffff00"; - } else { - return "#00ff00"; - } - }; - - const handleZoomIn = (): void => { - if (mapInstanceRef.current) { - const view = mapInstanceRef.current.getView(); - const currentZoom = view.getZoom(); - if (currentZoom && currentZoom < 20) { - view.animate({ - zoom: currentZoom + 0.5, - duration: 250, - }); - } - } - }; - - const handleZoomOut = (): void => { - if (mapInstanceRef.current) { - const view = mapInstanceRef.current.getView(); - const currentZoom = view.getZoom(); - if (currentZoom && currentZoom > 5) { - view.animate({ - zoom: currentZoom - 0.5, - duration: 250, - }); - } - } - }; - - return ( -
-
- - -
- -
-
-
- Low Speed -
-
-
- Medium Speed -
-
-
- High Speed -
-
- -
-
GPS Points: {dataWithCoordinates.length}
-
Map: {mapInstanceRef.current ? "Initialized" : "Not ready"}
-
- Main Line: {mainLineSourceRef.current?.getFeatures().length || 0} -
-
- Speed Segments:{" "} - {speedSegmentsSourceRef.current?.getFeatures().length || 0} -
-
- -
-
- ); -} diff --git a/dashboard/components/ui/card.tsx b/dashboard/components/ui/card.tsx new file mode 100644 index 0000000..135bbc4 --- /dev/null +++ b/dashboard/components/ui/card.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; + +import { cn } from "../../lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/dashboard/components/ui/map/Map.tsx b/dashboard/components/ui/map/Map.tsx new file mode 100644 index 0000000..2ce7d23 --- /dev/null +++ b/dashboard/components/ui/map/Map.tsx @@ -0,0 +1,164 @@ +"use client"; + +import MapLibreGL from "maplibre-gl"; +import "maplibre-gl/dist/maplibre-gl.css"; +import { useTheme } from "next-themes"; +import { + forwardRef, + type ReactNode, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; + +import { MapContext } from "./MapContext"; + +export type MapStyleOption = string | MapLibreGL.StyleSpecification; + +type MapProps = { + children?: ReactNode; + /** Custom map styles for light and dark themes. Overrides the default Carto styles. */ + styles?: { + light?: MapStyleOption; + dark?: MapStyleOption; + }; + /** Map projection type. Use `{ type: "globe" }` for 3D globe view. */ + projection?: MapLibreGL.ProjectionSpecification; +} & Omit; + +export type MapRef = MapLibreGL.Map; + +const defaultStyles = { + dark: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json", + light: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", +}; + +const DefaultLoader = () => ( +
+
+ + + +
+
+); + +export const NewMap = forwardRef(function MapFunc( + { children, styles, projection, ...props }, + ref, +) { + const containerRef = useRef(null); + const [mapInstance, setMapInstance] = useState(null); + const [isLoaded, setIsLoaded] = useState(false); + const [isStyleLoaded, setIsStyleLoaded] = useState(false); + const { resolvedTheme } = useTheme(); + const currentStyleRef = useRef(null); + const styleTimeoutRef = useRef | null>(null); + + const mapStyles = useMemo( + () => ({ + dark: styles?.dark ?? defaultStyles.dark, + light: styles?.light ?? defaultStyles.light, + }), + [styles], + ); + + useImperativeHandle(ref, () => mapInstance as MapLibreGL.Map, [mapInstance]); + + const clearStyleTimeout = useCallback(() => { + if (styleTimeoutRef.current) { + clearTimeout(styleTimeoutRef.current); + styleTimeoutRef.current = null; + } + }, []); + + useEffect(() => { + if (!containerRef.current) return; + + const initialStyle = + resolvedTheme === "dark" ? mapStyles.dark : mapStyles.light; + currentStyleRef.current = initialStyle; + + const map = new MapLibreGL.Map({ + container: containerRef.current, + style: initialStyle, + renderWorldCopies: false, + attributionControl: { + compact: true, + }, + ...props, + }); + + const styleDataHandler = () => { + clearStyleTimeout(); + // Delay to ensure style is fully processed before allowing layer operations + // This is a workaround to avoid race conditions with the style loading + styleTimeoutRef.current = setTimeout(() => { + setIsStyleLoaded(true); + if (projection) { + map.setProjection(projection); + } + }, 150); + }; + const loadHandler = () => setIsLoaded(true); + + map.on("load", loadHandler); + map.on("styledata", styleDataHandler); + setMapInstance(map); + + return () => { + clearStyleTimeout(); + map.off("load", loadHandler); + map.off("styledata", styleDataHandler); + map.remove(); + setIsLoaded(false); + setIsStyleLoaded(false); + setMapInstance(null); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + clearStyleTimeout, + resolvedTheme, + mapStyles.dark, + mapStyles.light, + projection, + ]); + + useEffect(() => { + if (!mapInstance || !resolvedTheme) return; + + const newStyle = + resolvedTheme === "dark" ? mapStyles.dark : mapStyles.light; + + if (currentStyleRef.current === newStyle) return; + + clearStyleTimeout(); + currentStyleRef.current = newStyle; + setIsStyleLoaded(false); + + mapInstance.setStyle(newStyle, { diff: true }); + }, [mapInstance, resolvedTheme, mapStyles, clearStyleTimeout]); + + const isLoading = !isLoaded || !isStyleLoaded; + + const contextValue = useMemo( + () => ({ + map: mapInstance, + isLoaded: isLoaded && isStyleLoaded, + }), + [mapInstance, isLoaded, isStyleLoaded], + ); + + return ( + +
+ {isLoading && } + {/* SSR-safe: children render only when map is loaded on client */} + {mapInstance && children} +
+
+ ); +}); diff --git a/dashboard/components/ui/map/MapClusterLayer.tsx b/dashboard/components/ui/map/MapClusterLayer.tsx new file mode 100644 index 0000000..0794f9b --- /dev/null +++ b/dashboard/components/ui/map/MapClusterLayer.tsx @@ -0,0 +1,322 @@ +"use client"; + +import type MapLibreGL from "maplibre-gl"; +import { useEffect, useId, useRef } from "react"; + +import { useMap } from "./MapContext"; + +type MapClusterLayerProps< + P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties, +> = { + /** GeoJSON FeatureCollection data or URL to fetch GeoJSON from */ + data: string | GeoJSON.FeatureCollection; + /** Maximum zoom level to cluster points on (default: 14) */ + clusterMaxZoom?: number; + /** Radius of each cluster when clustering points in pixels (default: 50) */ + clusterRadius?: number; + /** Colors for cluster circles: [small, medium, large] based on point count (default: ["#51bbd6", "#f1f075", "#f28cb1"]) */ + clusterColors?: [string, string, string]; + /** Point count thresholds for color/size steps: [medium, large] (default: [100, 750]) */ + clusterThresholds?: [number, number]; + /** Color for unclustered individual points (default: "#3b82f6") */ + pointColor?: string; + /** Callback when an unclustered point is clicked */ + onPointClick?: ( + feature: GeoJSON.Feature, + coordinates: [number, number], + ) => void; + /** Callback when a cluster is clicked. If not provided, zooms into the cluster */ + onClusterClick?: ( + clusterId: number, + coordinates: [number, number], + pointCount: number, + ) => void; +}; + +export function MapClusterLayer< + P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties, +>({ + data, + clusterMaxZoom = 14, + clusterRadius = 50, + clusterColors = ["#51bbd6", "#f1f075", "#f28cb1"], + clusterThresholds = [100, 750], + pointColor = "#3b82f6", + onPointClick, + onClusterClick, +}: MapClusterLayerProps

) { + const { map, isLoaded } = useMap(); + const id = useId(); + const sourceId = `cluster-source-${id}`; + const clusterLayerId = `clusters-${id}`; + const clusterCountLayerId = `cluster-count-${id}`; + const unclusteredLayerId = `unclustered-point-${id}`; + + const stylePropsRef = useRef({ + clusterColors, + clusterThresholds, + pointColor, + }); + + // Add source and layers on mount + useEffect(() => { + if (!isLoaded || !map) return; + + try { + // Add clustered GeoJSON source + map.addSource(sourceId, { + type: "geojson", + data, + cluster: true, + clusterMaxZoom, + clusterRadius, + }); + + // Add cluster circles layer + map.addLayer({ + id: clusterLayerId, + type: "circle", + source: sourceId, + filter: ["has", "point_count"], + paint: { + "circle-color": [ + "step", + ["get", "point_count"], + clusterColors[0], + clusterThresholds[0], + clusterColors[1], + clusterThresholds[1], + clusterColors[2], + ], + "circle-radius": [ + "step", + ["get", "point_count"], + 20, + clusterThresholds[0], + 30, + clusterThresholds[1], + 40, + ], + }, + }); + + // Add cluster count text layer + map.addLayer({ + id: clusterCountLayerId, + type: "symbol", + source: sourceId, + filter: ["has", "point_count"], + layout: { + "text-field": "{point_count_abbreviated}", + "text-size": 12, + }, + paint: { + "text-color": "#fff", + }, + }); + + // Add unclustered point layer + map.addLayer({ + id: unclusteredLayerId, + type: "circle", + source: sourceId, + filter: ["!", ["has", "point_count"]], + paint: { + "circle-color": pointColor, + "circle-radius": 6, + }, + }); + } catch { + // style not ready yet + } + + return () => { + try { + if (map.getLayer(clusterCountLayerId)) + map.removeLayer(clusterCountLayerId); + if (map.getLayer(unclusteredLayerId)) + map.removeLayer(unclusteredLayerId); + if (map.getLayer(clusterLayerId)) map.removeLayer(clusterLayerId); + if (map.getSource(sourceId)) map.removeSource(sourceId); + } catch { + // ignore + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoaded, map, sourceId]); + + // Update source data when data prop changes (only for non-URL data) + useEffect(() => { + if (!isLoaded || !map || typeof data === "string") return; + + try { + const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource; + if (source) { + source.setData(data); + } + } catch { + // style not ready + } + }, [isLoaded, map, data, sourceId]); + + // Update layer styles when props change + useEffect(() => { + if (!isLoaded || !map) return; + + try { + const prev = stylePropsRef.current; + const colorsChanged = + prev.clusterColors !== clusterColors || + prev.clusterThresholds !== clusterThresholds; + + // Update cluster layer colors and sizes + if (map.getLayer(clusterLayerId) && colorsChanged) { + map.setPaintProperty(clusterLayerId, "circle-color", [ + "step", + ["get", "point_count"], + clusterColors[0], + clusterThresholds[0], + clusterColors[1], + clusterThresholds[1], + clusterColors[2], + ]); + map.setPaintProperty(clusterLayerId, "circle-radius", [ + "step", + ["get", "point_count"], + 20, + clusterThresholds[0], + 30, + clusterThresholds[1], + 40, + ]); + } + + // Update unclustered point layer color + if ( + map.getLayer(unclusteredLayerId) && + prev.pointColor !== pointColor + ) { + map.setPaintProperty(unclusteredLayerId, "circle-color", pointColor); + } + + stylePropsRef.current = { + clusterColors, + clusterThresholds, + pointColor, + }; + } catch { + // style not ready + } + }, [ + isLoaded, + map, + clusterLayerId, + unclusteredLayerId, + clusterColors, + clusterThresholds, + pointColor, + ]); + + // Handle click events + useEffect(() => { + if (!isLoaded || !map) return; + + // Cluster click handler - zoom into cluster + const handleClusterClick = async ( + e: MapLibreGL.MapMouseEvent & { + features?: MapLibreGL.MapGeoJSONFeature[]; + }, + ) => { + const features = map.queryRenderedFeatures(e.point, { + layers: [clusterLayerId], + }); + if (!features.length) return; + + const feature = features[0]; + const clusterId = feature.properties?.cluster_id as number; + const pointCount = feature.properties?.point_count as number; + const coordinates = (feature.geometry as GeoJSON.Point).coordinates as [ + number, + number, + ]; + + if (onClusterClick) { + onClusterClick(clusterId, coordinates, pointCount); + } else { + // Default behavior: zoom to cluster expansion zoom + const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource; + const zoom = await source.getClusterExpansionZoom(clusterId); + map.easeTo({ + center: coordinates, + zoom, + }); + } + }; + + // Unclustered point click handler + const handlePointClick = ( + e: MapLibreGL.MapMouseEvent & { + features?: MapLibreGL.MapGeoJSONFeature[]; + }, + ) => { + if (!onPointClick || !e.features?.length) return; + + const feature = e.features[0]; + const coordinates = ( + feature.geometry as GeoJSON.Point + ).coordinates.slice() as [number, number]; + + // Handle world copies + while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) { + coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360; + } + + onPointClick( + feature as unknown as GeoJSON.Feature, + coordinates, + ); + }; + + // Cursor style handlers + const handleMouseEnterCluster = () => { + map.getCanvas().style.cursor = "pointer"; + }; + const handleMouseLeaveCluster = () => { + map.getCanvas().style.cursor = ""; + }; + const handleMouseEnterPoint = () => { + if (onPointClick) { + map.getCanvas().style.cursor = "pointer"; + } + }; + const handleMouseLeavePoint = () => { + map.getCanvas().style.cursor = ""; + }; + + map.on("click", clusterLayerId, handleClusterClick); + map.on("click", unclusteredLayerId, handlePointClick); + map.on("mouseenter", clusterLayerId, handleMouseEnterCluster); + map.on("mouseleave", clusterLayerId, handleMouseLeaveCluster); + map.on("mouseenter", unclusteredLayerId, handleMouseEnterPoint); + map.on("mouseleave", unclusteredLayerId, handleMouseLeavePoint); + + return () => { + map.off("click", clusterLayerId, handleClusterClick); + map.off("click", unclusteredLayerId, handlePointClick); + map.off("mouseenter", clusterLayerId, handleMouseEnterCluster); + map.off("mouseleave", clusterLayerId, handleMouseLeaveCluster); + map.off("mouseenter", unclusteredLayerId, handleMouseEnterPoint); + map.off("mouseleave", unclusteredLayerId, handleMouseLeavePoint); + }; + }, [ + isLoaded, + map, + clusterLayerId, + unclusteredLayerId, + sourceId, + onClusterClick, + onPointClick, + ]); + + return null; +} diff --git a/dashboard/components/ui/map/MapContext.tsx b/dashboard/components/ui/map/MapContext.tsx new file mode 100644 index 0000000..2fcbe19 --- /dev/null +++ b/dashboard/components/ui/map/MapContext.tsx @@ -0,0 +1,19 @@ +"use client"; + +import MapLibreGL from "maplibre-gl"; +import { createContext, useContext } from "react"; + +export type MapContextValue = { + map: MapLibreGL.Map | null; + isLoaded: boolean; +}; + +export const MapContext = createContext(null); + +export function useMap() { + const context = useContext(MapContext); + if (!context) { + throw new Error("useMap must be used within a Map component"); + } + return context; +} diff --git a/dashboard/components/ui/map/MapControls.tsx b/dashboard/components/ui/map/MapControls.tsx new file mode 100644 index 0000000..e0739aa --- /dev/null +++ b/dashboard/components/ui/map/MapControls.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { Loader2, Locate, Maximize, Minus, Plus } from "lucide-react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; + +import { cn } from "../../../lib/utils"; +import { useMap } from "./MapContext"; + +type MapControlsProps = { + /** Position of the controls on the map (default: "bottom-right") */ + position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; + /** Show zoom in/out buttons (default: true) */ + showZoom?: boolean; + /** Show compass button to reset bearing (default: false) */ + showCompass?: boolean; + /** Show locate button to find user's location (default: false) */ + showLocate?: boolean; + /** Show fullscreen toggle button (default: false) */ + showFullscreen?: boolean; + /** Additional CSS classes for the controls container */ + className?: string; + /** Callback with user coordinates when located */ + onLocate?: (coords: { longitude: number; latitude: number }) => void; +}; + +const positionClasses = { + "top-left": "top-2 left-2", + "top-right": "top-2 right-2", + "bottom-left": "bottom-2 left-2", + "bottom-right": "bottom-10 right-2", +}; + +function ControlGroup({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +
+ ); +} + +function ControlButton({ + onClick, + label, + children, + disabled = false, +}: { + onClick: () => void; + label: string; + children: React.ReactNode; + disabled?: boolean; +}) { + return ( + + ); +} + +function CompassButton({ onClick }: { onClick: () => void }) { + const { isLoaded, map } = useMap(); + const compassRef = useRef(null); + + useEffect(() => { + if (!isLoaded || !map || !compassRef.current) return; + + const compass = compassRef.current; + + const updateRotation = () => { + const bearing = map.getBearing(); + const pitch = map.getPitch(); + compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`; + }; + + map.on("rotate", updateRotation); + map.on("pitch", updateRotation); + updateRotation(); + + return () => { + map.off("rotate", updateRotation); + map.off("pitch", updateRotation); + }; + }, [isLoaded, map]); + + return ( + + {/** biome-ignore lint/a11y/noSvgWithoutTitle: */} + + + + + + + + ); +} + +export function MapControls({ + position = "bottom-right", + showZoom = true, + showCompass = false, + showLocate = false, + showFullscreen = false, + className, + onLocate, +}: MapControlsProps) { + const { map, isLoaded } = useMap(); + const [waitingForLocation, setWaitingForLocation] = useState(false); + + const handleZoomIn = useCallback(() => { + map?.zoomTo(map.getZoom() + 1, { duration: 300 }); + }, [map]); + + const handleZoomOut = useCallback(() => { + map?.zoomTo(map.getZoom() - 1, { duration: 300 }); + }, [map]); + + const handleResetBearing = useCallback(() => { + map?.resetNorthPitch({ duration: 300 }); + }, [map]); + + const handleLocate = useCallback(() => { + setWaitingForLocation(true); + if ("geolocation" in navigator) { + navigator.geolocation.getCurrentPosition( + (pos) => { + const coords = { + longitude: pos.coords.longitude, + latitude: pos.coords.latitude, + }; + map?.flyTo({ + center: [coords.longitude, coords.latitude], + zoom: 14, + duration: 1500, + }); + onLocate?.(coords); + setWaitingForLocation(false); + }, + (error) => { + console.error("Error getting location:", error); + setWaitingForLocation(false); + }, + ); + } + }, [map, onLocate]); + + const handleFullscreen = useCallback(() => { + const container = map?.getContainer(); + if (!container) return; + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + container.requestFullscreen(); + } + }, [map]); + + if (!isLoaded) return null; + + return ( +
+ {showZoom && ( + + + + + + + + + )} + {showCompass && ( + + + + )} + {showLocate && ( + + + {waitingForLocation ? ( + + ) : ( + + )} + + + )} + {showFullscreen && ( + + + + + + )} +
+ ); +} diff --git a/dashboard/components/ui/map/MapMarker.tsx b/dashboard/components/ui/map/MapMarker.tsx new file mode 100644 index 0000000..daad533 --- /dev/null +++ b/dashboard/components/ui/map/MapMarker.tsx @@ -0,0 +1,152 @@ +"use client"; + +import MapLibreGL, { type MarkerOptions } from "maplibre-gl"; +import { + createContext, + type ReactNode, + useContext, + useEffect, + useMemo, +} from "react"; + +import { useMap } from "./MapContext"; + +type MarkerContextValue = { + marker: MapLibreGL.Marker; + map: MapLibreGL.Map | null; +}; + +export const MarkerContext = createContext(null); + +export function useMarkerContext() { + const context = useContext(MarkerContext); + if (!context) { + throw new Error("Marker components must be used within MapMarker"); + } + return context; +} + +type MapMarkerProps = { + /** Longitude coordinate for marker position */ + longitude: number; + /** Latitude coordinate for marker position */ + latitude: number; + /** Marker subcomponents (MarkerContent, MarkerPopup, MarkerTooltip, MarkerLabel) */ + children: ReactNode; + /** Callback when marker is clicked */ + onClick?: (e: MouseEvent) => void; + /** Callback when mouse enters marker */ + onMouseEnter?: (e: MouseEvent) => void; + /** Callback when mouse leaves marker */ + onMouseLeave?: (e: MouseEvent) => void; + /** Callback when marker drag starts (requires draggable: true) */ + onDragStart?: (lngLat: { lng: number; lat: number }) => void; + /** Callback during marker drag (requires draggable: true) */ + onDrag?: (lngLat: { lng: number; lat: number }) => void; + /** Callback when marker drag ends (requires draggable: true) */ + onDragEnd?: (lngLat: { lng: number; lat: number }) => void; +} & Omit; + +export function MapMarker({ + longitude, + latitude, + children, + onClick, + onMouseEnter, + onMouseLeave, + onDragStart, + onDrag, + onDragEnd, + draggable = false, + ...markerOptions +}: MapMarkerProps) { + const { map } = useMap(); + + const marker = useMemo(() => { + const markerInstance = new MapLibreGL.Marker({ + ...markerOptions, + element: document.createElement("div"), + draggable, + }).setLngLat([longitude, latitude]); + + const handleClick = (e: MouseEvent) => onClick?.(e); + const handleMouseEnter = (e: MouseEvent) => onMouseEnter?.(e); + const handleMouseLeave = (e: MouseEvent) => onMouseLeave?.(e); + + markerInstance.getElement()?.addEventListener("click", handleClick); + markerInstance + .getElement() + ?.addEventListener("mouseenter", handleMouseEnter); + markerInstance + .getElement() + ?.addEventListener("mouseleave", handleMouseLeave); + + const handleDragStart = () => { + const lngLat = markerInstance.getLngLat(); + onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat }); + }; + const handleDrag = () => { + const lngLat = markerInstance.getLngLat(); + onDrag?.({ lng: lngLat.lng, lat: lngLat.lat }); + }; + const handleDragEnd = () => { + const lngLat = markerInstance.getLngLat(); + onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat }); + }; + + markerInstance.on("dragstart", handleDragStart); + markerInstance.on("drag", handleDrag); + markerInstance.on("dragend", handleDragEnd); + + return markerInstance; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!map) return; + + marker.addTo(map); + + return () => { + marker.remove(); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [map]); + + if ( + marker.getLngLat().lng !== longitude || + marker.getLngLat().lat !== latitude + ) { + marker.setLngLat([longitude, latitude]); + } + if (marker.isDraggable() !== draggable) { + marker.setDraggable(draggable); + } + + const currentOffset = marker.getOffset(); + const newOffset = markerOptions.offset ?? [0, 0]; + const [newOffsetX, newOffsetY] = Array.isArray(newOffset) + ? newOffset + : [newOffset.x, newOffset.y]; + if (currentOffset.x !== newOffsetX || currentOffset.y !== newOffsetY) { + marker.setOffset(newOffset); + } + + if (marker.getRotation() !== markerOptions.rotation) { + marker.setRotation(markerOptions.rotation ?? 0); + } + if (marker.getRotationAlignment() !== markerOptions.rotationAlignment) { + marker.setRotationAlignment(markerOptions.rotationAlignment ?? "auto"); + } + if (marker.getPitchAlignment() !== markerOptions.pitchAlignment) { + marker.setPitchAlignment(markerOptions.pitchAlignment ?? "auto"); + } + + return ( + + {children} + + ); +} diff --git a/dashboard/components/ui/map/MapPopup.tsx b/dashboard/components/ui/map/MapPopup.tsx new file mode 100644 index 0000000..29ad500 --- /dev/null +++ b/dashboard/components/ui/map/MapPopup.tsx @@ -0,0 +1,116 @@ +"use client"; + +import MapLibreGL, { type PopupOptions } from "maplibre-gl"; +import { X } from "lucide-react"; +import { type ReactNode, useEffect, useMemo, useRef } from "react"; +import { createPortal } from "react-dom"; + +import { cn } from "../../../lib/utils"; +import { useMap } from "./MapContext"; + +type MapPopupProps = { + /** Longitude coordinate for popup position */ + longitude: number; + /** Latitude coordinate for popup position */ + latitude: number; + /** Callback when popup is closed */ + onClose?: () => void; + /** Popup content */ + children: ReactNode; + /** Additional CSS classes for the popup container */ + className?: string; + /** Show a close button in the popup (default: false) */ + closeButton?: boolean; +} & Omit; + +export function MapPopup({ + longitude, + latitude, + onClose, + children, + className, + closeButton = false, + ...popupOptions +}: MapPopupProps) { + const { map } = useMap(); + const popupOptionsRef = useRef(popupOptions); + const container = useMemo(() => document.createElement("div"), []); + + const popup = useMemo(() => { + const popupInstance = new MapLibreGL.Popup({ + offset: 16, + ...popupOptions, + closeButton: false, + }) + .setMaxWidth("none") + .setLngLat([longitude, latitude]); + + return popupInstance; + // biome-ignore lint/correctness/useExhaustiveDependencies: + }, []); + + useEffect(() => { + if (!map) return; + + const onCloseProp = () => onClose?.(); + popup.on("close", onCloseProp); + + popup.setDOMContent(container); + popup.addTo(map); + + return () => { + popup.off("close", onCloseProp); + if (popup.isOpen()) { + popup.remove(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [map]); + + if (popup.isOpen()) { + const prev = popupOptionsRef.current; + + if ( + popup.getLngLat().lng !== longitude || + popup.getLngLat().lat !== latitude + ) { + popup.setLngLat([longitude, latitude]); + } + + if (prev.offset !== popupOptions.offset) { + popup.setOffset(popupOptions.offset ?? 16); + } + if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) { + popup.setMaxWidth(popupOptions.maxWidth ?? "none"); + } + popupOptionsRef.current = popupOptions; + } + + const handleClose = () => { + popup.remove(); + onClose?.(); + }; + + return createPortal( +
+ {closeButton && ( + + )} + {children} +
, + container, + ); +} diff --git a/dashboard/components/ui/map/MapRoute.tsx b/dashboard/components/ui/map/MapRoute.tsx new file mode 100644 index 0000000..9feae06 --- /dev/null +++ b/dashboard/components/ui/map/MapRoute.tsx @@ -0,0 +1,161 @@ +"use client"; + +import type MapLibreGL from "maplibre-gl"; +import { useEffect, useId } from "react"; + +import { useMap } from "./MapContext"; + +type MapRouteProps = { + /** Optional unique identifier for the route layer */ + id?: string; + /** Array of [longitude, latitude] coordinate pairs defining the route */ + coordinates: [number, number][]; + /** Line color as CSS color value (default: "#4285F4") */ + color?: string; + /** Line width in pixels (default: 3) */ + width?: number; + /** Line opacity from 0 to 1 (default: 0.8) */ + opacity?: number; + /** Dash pattern [dash length, gap length] for dashed lines */ + dashArray?: [number, number]; + /** Callback when the route line is clicked */ + onClick?: () => void; + /** Callback when mouse enters the route line */ + onMouseEnter?: () => void; + /** Callback when mouse leaves the route line */ + onMouseLeave?: () => void; + /** Whether the route is interactive - shows pointer cursor on hover (default: true) */ + interactive?: boolean; +}; + +export function MapRoute({ + id: propId, + coordinates, + color = "#4285F4", + width = 3, + opacity = 0.8, + dashArray, + onClick, + onMouseEnter, + onMouseLeave, + interactive = true, +}: MapRouteProps) { + const { map, isLoaded } = useMap(); + const autoId = useId(); + const id = propId ?? autoId; + const sourceId = `route-source-${id}`; + const layerId = `route-layer-${id}`; + + // Add source and layer on mount + useEffect(() => { + if (!isLoaded || !map) return; + + try { + map.addSource(sourceId, { + type: "geojson", + data: { + type: "Feature", + properties: {}, + geometry: { type: "LineString", coordinates: [] }, + }, + }); + + map.addLayer({ + id: layerId, + type: "line", + source: sourceId, + layout: { "line-join": "round", "line-cap": "round" }, + paint: { + "line-color": color, + "line-width": width, + "line-opacity": opacity, + ...(dashArray && { "line-dasharray": dashArray }), + }, + }); + } catch { + // style not ready yet β€” will retry on next effect run + } + + return () => { + try { + if (map.getLayer(layerId)) map.removeLayer(layerId); + if (map.getSource(sourceId)) map.removeSource(sourceId); + } catch { + // ignore + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoaded, map]); + + // When coordinates change, update the source data + useEffect(() => { + if (!isLoaded || !map || coordinates.length < 2) return; + + try { + const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource; + if (source) { + source.setData({ + type: "Feature", + properties: {}, + geometry: { type: "LineString", coordinates }, + }); + } + } catch { + // style not ready + } + }, [isLoaded, map, coordinates, sourceId]); + + useEffect(() => { + if (!isLoaded || !map) return; + + try { + if (!map.getLayer(layerId)) return; + + map.setPaintProperty(layerId, "line-color", color); + map.setPaintProperty(layerId, "line-width", width); + map.setPaintProperty(layerId, "line-opacity", opacity); + if (dashArray) { + map.setPaintProperty(layerId, "line-dasharray", dashArray); + } + } catch { + // style not ready + } + }, [isLoaded, map, layerId, color, width, opacity, dashArray]); + + // Handle click and hover events + useEffect(() => { + if (!isLoaded || !map || !interactive) return; + + const handleClick = () => { + onClick?.(); + }; + const handleMouseEnter = () => { + map.getCanvas().style.cursor = "pointer"; + onMouseEnter?.(); + }; + const handleMouseLeave = () => { + map.getCanvas().style.cursor = ""; + onMouseLeave?.(); + }; + + map.on("click", layerId, handleClick); + map.on("mouseenter", layerId, handleMouseEnter); + map.on("mouseleave", layerId, handleMouseLeave); + + return () => { + map.off("click", layerId, handleClick); + map.off("mouseenter", layerId, handleMouseEnter); + map.off("mouseleave", layerId, handleMouseLeave); + }; + }, [ + isLoaded, + map, + layerId, + onClick, + onMouseEnter, + onMouseLeave, + interactive, + ]); + + return null; +} diff --git a/dashboard/components/ui/map/MarkerContent.tsx b/dashboard/components/ui/map/MarkerContent.tsx new file mode 100644 index 0000000..965c93e --- /dev/null +++ b/dashboard/components/ui/map/MarkerContent.tsx @@ -0,0 +1,31 @@ +"use client"; + +import type { ReactNode } from "react"; +import { createPortal } from "react-dom"; + +import { cn } from "../../../lib/utils"; +import { useMarkerContext } from "./MapMarker"; + +type MarkerContentProps = { + /** Custom marker content. Defaults to a blue dot if not provided */ + children?: ReactNode; + /** Additional CSS classes for the marker container */ + className?: string; +}; + +function DefaultMarkerIcon() { + return ( +
+ ); +} + +export function MarkerContent({ children, className }: MarkerContentProps) { + const { marker } = useMarkerContext(); + + return createPortal( +
+ {children || } +
, + marker.getElement(), + ); +} diff --git a/dashboard/components/ui/map/MarkerLabel.tsx b/dashboard/components/ui/map/MarkerLabel.tsx new file mode 100644 index 0000000..618d21e --- /dev/null +++ b/dashboard/components/ui/map/MarkerLabel.tsx @@ -0,0 +1,38 @@ +"use client"; + +import type { ReactNode } from "react"; + +import { cn } from "../../../lib/utils"; + +type MarkerLabelProps = { + /** Label text content */ + children: ReactNode; + /** Additional CSS classes for the label */ + className?: string; + /** Position of the label relative to the marker (default: "top") */ + position?: "top" | "bottom"; +}; + +export function MarkerLabel({ + children, + className, + position = "top", +}: MarkerLabelProps) { + const positionClasses = { + top: "bottom-full mb-1", + bottom: "top-full mt-1", + }; + + return ( +
+ {children} +
+ ); +} diff --git a/dashboard/components/ui/map/MarkerPopup.tsx b/dashboard/components/ui/map/MarkerPopup.tsx new file mode 100644 index 0000000..73e1ce3 --- /dev/null +++ b/dashboard/components/ui/map/MarkerPopup.tsx @@ -0,0 +1,93 @@ +"use client"; + +import MapLibreGL, { type PopupOptions } from "maplibre-gl"; +import { X } from "lucide-react"; +import { type ReactNode, useEffect, useMemo, useRef } from "react"; +import { createPortal } from "react-dom"; + +import { cn } from "../../../lib/utils"; +import { useMarkerContext } from "./MapMarker"; + +type MarkerPopupProps = { + /** Popup content */ + children: ReactNode; + /** Additional CSS classes for the popup container */ + className?: string; + /** Show a close button in the popup (default: false) */ + closeButton?: boolean; +} & Omit; + +export function MarkerPopup({ + children, + className, + closeButton = false, + ...popupOptions +}: MarkerPopupProps) { + const { marker, map } = useMarkerContext(); + const container = useMemo(() => document.createElement("div"), []); + const prevPopupOptions = useRef(popupOptions); + + const popup = useMemo(() => { + const popupInstance = new MapLibreGL.Popup({ + offset: 16, + ...popupOptions, + closeButton: false, + }) + .setMaxWidth("none") + .setDOMContent(container); + + return popupInstance; + // eslint-disable-next-line react-hooks/exhaustive-deps + // biome-ignore lint/correctness/useExhaustiveDependencies: + }, []); + + useEffect(() => { + if (!map) return; + + popup.setDOMContent(container); + marker.setPopup(popup); + + return () => { + marker.setPopup(null); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [map, container, marker.setPopup, popup]); + + if (popup.isOpen()) { + const prev = prevPopupOptions.current; + + if (prev.offset !== popupOptions.offset) { + popup.setOffset(popupOptions.offset ?? 16); + } + if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) { + popup.setMaxWidth(popupOptions.maxWidth ?? "none"); + } + + prevPopupOptions.current = popupOptions; + } + + const handleClose = () => popup.remove(); + + return createPortal( +
+ {closeButton && ( + + )} + {children} +
, + container, + ); +} diff --git a/dashboard/components/ui/map/MarkerTooltip.tsx b/dashboard/components/ui/map/MarkerTooltip.tsx new file mode 100644 index 0000000..7b852d3 --- /dev/null +++ b/dashboard/components/ui/map/MarkerTooltip.tsx @@ -0,0 +1,84 @@ +"use client"; + +import MapLibreGL, { type PopupOptions } from "maplibre-gl"; +import { type ReactNode, useEffect, useMemo, useRef } from "react"; +import { createPortal } from "react-dom"; + +import { cn } from "../../../lib/utils"; +import { useMarkerContext } from "./MapMarker"; + +type MarkerTooltipProps = { + /** Tooltip content */ + children: ReactNode; + /** Additional CSS classes for the tooltip container */ + className?: string; +} & Omit; + +export function MarkerTooltip({ + children, + className, + ...popupOptions +}: MarkerTooltipProps) { + const { marker, map } = useMarkerContext(); + const container = useMemo(() => document.createElement("div"), []); + const prevTooltipOptions = useRef(popupOptions); + + const tooltip = useMemo(() => { + const tooltipInstance = new MapLibreGL.Popup({ + offset: 16, + ...popupOptions, + closeOnClick: true, + closeButton: false, + }).setMaxWidth("none"); + + return tooltipInstance; + // eslint-disable-next-line react-hooks/exhaustive-deps + // biome-ignore lint/correctness/useExhaustiveDependencies: + }, []); + + useEffect(() => { + if (!map) return; + + tooltip.setDOMContent(container); + + const handleMouseEnter = () => { + tooltip.setLngLat(marker.getLngLat()).addTo(map); + }; + const handleMouseLeave = () => tooltip.remove(); + + marker.getElement()?.addEventListener("mouseenter", handleMouseEnter); + marker.getElement()?.addEventListener("mouseleave", handleMouseLeave); + + return () => { + marker.getElement()?.removeEventListener("mouseenter", handleMouseEnter); + marker.getElement()?.removeEventListener("mouseleave", handleMouseLeave); + tooltip.remove(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [map]); + + if (tooltip.isOpen()) { + const prev = prevTooltipOptions.current; + + if (prev.offset !== popupOptions.offset) { + tooltip.setOffset(popupOptions.offset ?? 16); + } + if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) { + tooltip.setMaxWidth(popupOptions.maxWidth ?? "none"); + } + + prevTooltipOptions.current = popupOptions; + } + + return createPortal( +
+ {children} +
, + container, + ); +} diff --git a/dashboard/components/ui/map/index.ts b/dashboard/components/ui/map/index.ts new file mode 100644 index 0000000..d4a4baa --- /dev/null +++ b/dashboard/components/ui/map/index.ts @@ -0,0 +1,11 @@ +export { NewMap, type MapRef, type MapStyleOption } from "./Map"; +export { useMap } from "./MapContext"; +export { MapMarker } from "./MapMarker"; +export { MarkerContent } from "./MarkerContent"; +export { MarkerPopup } from "./MarkerPopup"; +export { MarkerTooltip } from "./MarkerTooltip"; +export { MarkerLabel } from "./MarkerLabel"; +export { MapPopup } from "./MapPopup"; +export { MapControls } from "./MapControls"; +export { MapRoute } from "./MapRoute"; +export { MapClusterLayer } from "./MapClusterLayer"; diff --git a/dashboard/docs/RACING_LINE.md b/dashboard/docs/RACING_LINE.md new file mode 100644 index 0000000..d52ec35 --- /dev/null +++ b/dashboard/docs/RACING_LINE.md @@ -0,0 +1,20 @@ +# Racing line + +Displays the line the driver took around the track. There can be different colour overlays, the top ones are, speed, throttle and brake. This clearly shows what inputs the driver made. + +Although the racing line is a key visual part it's only one piece of the puzzle for allowing a race engineer to analyse the lap data. So to advance the user experience, the user should be able to hover over any part of the racing line and it will put a point on the racing line and it will save the the point in a hook which the charts on the page will use and show a line and dot where the racing line point is on the chart. This allows users to see the values at each and every point. + +## Implementation + +As part of the map libre migration I want to review how this is done, mainly doing a lot more of the calculations on the golang backend. To do that I need to sure up the interfaces for the endpoints. Currently, I have the racing line being shown on the map. So in theory it just needs colour. + + +## Future prospects + +In the future I want to be able to compare different laps on the same page, so it will require two racing lines and two lines on each chart, when you hover it will show the point and the values on each chart so it's easy to compare for the race engineer. + +I also want to look at giving the user the option of selecting a sector, so s1, s2, s3. that then allows the user to first see the sector time difference and then focus in one where their driver was either faster or slower. Selecting a sector will zoom the user slightly closer and reframe to having a centre of the middle of the sector, this can be done through getting the coordinates of that sector and getting the cords from the middle. The chart should also focus in on just the data from that sector. + +## Tech notes + +The frontend must be smooth to use when transitioning. It's important to do heavy computation on the BE/DB as they are more optimised for doing it. Make sure that we aren't sending data that is not needed to the frontend. \ No newline at end of file diff --git a/dashboard/hooks/useChartHover.ts b/dashboard/hooks/useChartHover.ts new file mode 100644 index 0000000..e3091d1 --- /dev/null +++ b/dashboard/hooks/useChartHover.ts @@ -0,0 +1,31 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +export function useChartHover() { + const [hoveredIndex, setHoveredIndex] = useState(null); + const hoverFrameRef = useRef(null); + + const handleChartHover = useCallback((index: number | null) => { + if (hoverFrameRef.current) { + cancelAnimationFrame(hoverFrameRef.current); + } + hoverFrameRef.current = requestAnimationFrame(() => { + setHoveredIndex(index); + }); + }, []); + + const handleChartMouseLeave = useCallback(() => { + if (hoverFrameRef.current) { + cancelAnimationFrame(hoverFrameRef.current); + } + }, []); + + useEffect(() => { + return () => { + if (hoverFrameRef.current) { + cancelAnimationFrame(hoverFrameRef.current); + } + }; + }, []); + + return { hoveredIndex, handleChartHover, handleChartMouseLeave }; +} diff --git a/dashboard/hooks/useGPSTelemetryData.tsx b/dashboard/hooks/useGPSTelemetryData.tsx index 7976f11..4f1da8f 100644 --- a/dashboard/hooks/useGPSTelemetryData.tsx +++ b/dashboard/hooks/useGPSTelemetryData.tsx @@ -1,9 +1,9 @@ -import { RefObject, useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import type { TelemetryDataPoint } from "@/lib/types"; export function useGPSTelemetryData( telemetry: any[] | undefined, - trackName?: string, + _trackName?: string, ) { const [processError, setProcessError] = useState(null); const [trackBounds, setTrackBounds] = useState<{ @@ -214,7 +214,7 @@ function detectTrackSections(data: any[]): any[] { } export async function fetchTrackBoundaries( - trackName: string, + _trackName: string, bounds: { minLat: number; maxLat: number; diff --git a/dashboard/hooks/useLapPosition.tsx b/dashboard/hooks/useLapPosition.tsx index 582ae45..d66773a 100644 --- a/dashboard/hooks/useLapPosition.tsx +++ b/dashboard/hooks/useLapPosition.tsx @@ -3,7 +3,7 @@ import type { TelemetryDataPoint } from "@/lib/types"; export function useLapPosition( telemetryData: TelemetryDataPoint[], - targetLapDistPct?: number, + _targetLapDistPct?: number, ) { const lastFoundIndex = useRef(0); diff --git a/dashboard/hooks/usePerformanceMonitor.ts b/dashboard/hooks/usePerformanceMonitor.ts index 8c542b7..1b09d07 100644 --- a/dashboard/hooks/usePerformanceMonitor.ts +++ b/dashboard/hooks/usePerformanceMonitor.ts @@ -1,69 +1,78 @@ "use client"; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef } from "react"; interface PerformanceMetrics { - renderTime: number; - frameRate: number; - memoryUsage?: number; + renderTime: number; + frameRate: number; + memoryUsage?: number; } -export function usePerformanceMonitor(componentName: string, enabled: boolean = true) { - const lastFrameTimeRef = useRef(performance.now()); - const frameCountRef = useRef(0); - const fpsRef = useRef(60); - const renderStartRef = useRef(0); +export function usePerformanceMonitor(componentName: string, enabled = true) { + const lastFrameTimeRef = useRef(performance.now()); + const frameCountRef = useRef(0); + const fpsRef = useRef(60); + const renderStartRef = useRef(0); - useEffect(() => { - if (!enabled) return; + useEffect(() => { + if (!enabled) return; - renderStartRef.current = performance.now(); + renderStartRef.current = performance.now(); - return () => { - const renderTime = performance.now() - renderStartRef.current; - - // Calculate FPS - const currentTime = performance.now(); - const deltaTime = currentTime - lastFrameTimeRef.current; - - if (deltaTime >= 1000) { // Update every second - fpsRef.current = Math.round((frameCountRef.current * 1000) / deltaTime); - frameCountRef.current = 0; - lastFrameTimeRef.current = currentTime; + return () => { + const renderTime = performance.now() - renderStartRef.current; - const metrics: PerformanceMetrics = { - renderTime, - frameRate: fpsRef.current, - }; + // Calculate FPS + const currentTime = performance.now(); + const deltaTime = currentTime - lastFrameTimeRef.current; - // Add memory usage if available - if ('memory' in performance) { - metrics.memoryUsage = Math.round((performance as any).memory.usedJSHeapSize / 1024 / 1024); - } + if (deltaTime >= 1000) { + // Update every second + fpsRef.current = Math.round((frameCountRef.current * 1000) / deltaTime); + frameCountRef.current = 0; + lastFrameTimeRef.current = currentTime; - console.log(`πŸš€ ${componentName} Performance:`, { - renderTime: `${renderTime.toFixed(2)}ms`, - fps: `${metrics.frameRate}fps`, - memory: metrics.memoryUsage ? `${metrics.memoryUsage}MB` : 'N/A' - }); + const metrics: PerformanceMetrics = { + renderTime, + frameRate: fpsRef.current, + }; - // Warn about performance issues - if (renderTime > 16) { - console.warn(`⚠️ ${componentName} slow render: ${renderTime.toFixed(2)}ms (target: <16ms)`); - } - - if (metrics.frameRate < 30) { - console.warn(`⚠️ ${componentName} low FPS: ${metrics.frameRate}fps (target: >30fps)`); - } - } + // Add memory usage if available + if ("memory" in performance) { + metrics.memoryUsage = Math.round( + (performance as any).memory.usedJSHeapSize / 1024 / 1024, + ); + } - frameCountRef.current++; - }; - }, [componentName, enabled]); + console.log(`πŸš€ ${componentName} Performance:`, { + renderTime: `${renderTime.toFixed(2)}ms`, + fps: `${metrics.frameRate}fps`, + memory: metrics.memoryUsage ? `${metrics.memoryUsage}MB` : "N/A", + }); - return { - getCurrentFPS: () => fpsRef.current, - markRenderStart: () => { renderStartRef.current = performance.now(); }, - markRenderEnd: () => performance.now() - renderStartRef.current - }; -} \ No newline at end of file + // Warn about performance issues + if (renderTime > 16) { + console.warn( + `⚠️ ${componentName} slow render: ${renderTime.toFixed(2)}ms (target: <16ms)`, + ); + } + + if (metrics.frameRate < 30) { + console.warn( + `⚠️ ${componentName} low FPS: ${metrics.frameRate}fps (target: >30fps)`, + ); + } + } + + frameCountRef.current++; + }; + }, [componentName, enabled]); + + return { + getCurrentFPS: () => fpsRef.current, + markRenderStart: () => { + renderStartRef.current = performance.now(); + }, + markRenderEnd: () => performance.now() - renderStartRef.current, + }; +} diff --git a/dashboard/hooks/useSvgTrack.tsx b/dashboard/hooks/useSvgTrack.tsx index 89794ca..7366c44 100644 --- a/dashboard/hooks/useSvgTrack.tsx +++ b/dashboard/hooks/useSvgTrack.tsx @@ -27,6 +27,7 @@ export function useSvgTrack() { if (pathElement) { setTrackPath(pathElement); + // biome-ignore lint/style/noNonNullAssertion: cool svgContainerRef.current!.innerHTML = svgText; setSvgLoaded(true); diff --git a/dashboard/hooks/useTelemetryData.ts b/dashboard/hooks/useTelemetryData.ts new file mode 100644 index 0000000..45f1316 --- /dev/null +++ b/dashboard/hooks/useTelemetryData.ts @@ -0,0 +1,46 @@ +import { useMemo } from "react"; +import type { TelemetryRes } from "../lib/Fetch"; +import type { TelemetryDataPoint } from "../lib/types"; + +export function useTelemetryData( + initialTelemetryData: TelemetryRes, + sessionId: string, + hoveredIndex: number | null, +) { + const dataWithGPSCoordinates = useMemo(() => { + return initialTelemetryData?.dataWithGPSCoordinates || []; + }, [initialTelemetryData?.dataWithGPSCoordinates]); + + const trackInfo = useMemo(() => { + if (dataWithGPSCoordinates.length === 0) return null; + + const firstPoint = dataWithGPSCoordinates[0] as TelemetryDataPoint; + const lastPoint = dataWithGPSCoordinates[ + dataWithGPSCoordinates.length - 1 + ] as TelemetryDataPoint; + return { + lapTime: lastPoint?.LapCurrentLapTime, + trackName: firstPoint?.TrackName || "Unknown Track", + sessionNum: firstPoint?.SessionNum || sessionId, + maxSpeed: Math.max( + ...dataWithGPSCoordinates.map((p) => p.Speed || 0), + ), + }; + }, [dataWithGPSCoordinates, sessionId]); + + const hoverCoordinates = useMemo(() => { + if ( + hoveredIndex === null || + hoveredIndex < 0 || + hoveredIndex >= dataWithGPSCoordinates.length + ) { + return null; + } + return { + lon: dataWithGPSCoordinates[hoveredIndex].Lon, + lat: dataWithGPSCoordinates[hoveredIndex].Lat, + }; + }, [dataWithGPSCoordinates, hoveredIndex]); + + return { dataWithGPSCoordinates, trackInfo, hoverCoordinates }; +} diff --git a/dashboard/hooks/useTelemetryData.tsx b/dashboard/hooks/useTelemetryData.tsx deleted file mode 100644 index 5d71b03..0000000 --- a/dashboard/hooks/useTelemetryData.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { type RefObject, useEffect, useMemo, useState } from "react"; -import { TelemetryDataPoint } from "@/lib/types"; - -export function useTelemetryData( - telemetry: any[] | undefined, - trackPath: SVGPathElement | null, - startFinishPosition: number, - svgContainerRef: RefObject, - isClockwise = 0, -) { - const [processError, setProcessError] = useState(null); - - const dataWithCoordinates = useMemo(() => { - if (!telemetry?.length || !trackPath || !svgContainerRef.current) { - return []; - } - - try { - const pathElement = svgContainerRef.current.querySelector( - "#track-outline", - ) as SVGPathElement; - if (!pathElement) return []; - - const totalLength = pathElement.getTotalLength(); - - const rotationAngle = 90; - const insideTrackFactor = 0.8; - const verticalOffset = -0; - const horizontalOffset = 780; - - const minSpeed = Math.min(...telemetry.map((p) => p.Speed)); - const maxSpeed = Math.max(...telemetry.map((p) => p.Speed)); - const speedRange = maxSpeed - minSpeed; - - return telemetry.map((point, index) => { - const adjustedLapPct = (point.LapDistPct / 100) % 1.0; - - let pathPosition; - - if (isClockwise) { - pathPosition = - (startFinishPosition + adjustedLapPct * totalLength) % totalLength; - } else { - pathPosition = - (startFinishPosition - adjustedLapPct * totalLength + totalLength) % - totalLength; - } - - const basePoint = pathElement.getPointAtLength(pathPosition); - - let offsetX = 0; - let offsetY = 0; - - const steeringFactor = point.SteeringWheelAngle / 10; - - if (Math.abs(steeringFactor) > 0.01) { - const stepSize = totalLength / 1; - const prevPosition = Math.max(0, pathPosition - stepSize); - const nextPosition = Math.min(totalLength, pathPosition + stepSize); - - const prevPoint = pathElement.getPointAtLength(prevPosition); - const nextPoint = pathElement.getPointAtLength(nextPosition); - - const dirX = nextPoint.x - prevPoint.x; - const dirY = nextPoint.y - prevPoint.y; - const dirLength = Math.sqrt(dirX * dirX + dirY * dirY); - const normDirX = dirLength > 0 ? dirX / dirLength : 0; - const normDirY = dirLength > 0 ? dirY / dirLength : 0; - - const perpDirX = -normDirY; - const perpDirY = normDirX; - - const speedFactor = point.Speed / 250; - const throttleFactor = point.Throttle / 100; - const brakeFactor = point.Brake / 100; - - let offsetMagnitude = Math.min(18, Math.abs(steeringFactor * 12)); - - offsetMagnitude *= 1 + speedFactor * 0.5; - - if (brakeFactor > 0.3) { - offsetMagnitude *= 1 - brakeFactor * 0.7; - } - - if (throttleFactor > 0.7 && Math.abs(steeringFactor) > 0.2) { - offsetMagnitude *= 1 + throttleFactor * 0.8; - } - - offsetX = - perpDirX * - offsetMagnitude * - Math.sign(steeringFactor) * - insideTrackFactor; - offsetY = - perpDirY * - offsetMagnitude * - Math.sign(steeringFactor) * - insideTrackFactor; - } - - if (speedRange > 0) { - const speedFactor = (point.Speed - minSpeed) / speedRange; - const speedAdjustment = 0.8 + 0.5 * speedFactor; - - offsetX *= speedAdjustment; - offsetY *= speedAdjustment; - } - - const radians = (rotationAngle * Math.PI) / 180; - const rotatedX = - (basePoint.x + offsetX) * Math.cos(radians) - - (basePoint.y + offsetY) * Math.sin(radians); - const rotatedY = - (basePoint.x + offsetX) * Math.sin(radians) + - (basePoint.y + offsetY) * Math.cos(radians); - - const finalX = rotatedX + horizontalOffset; - const finalY = rotatedY + verticalOffset; - - return { - ...point, - coordinates: [finalY, finalX] as [number, number], - pathPosition, - originalIndex: index, - speed: point.Speed, - }; - }); - } catch (error) { - console.error("Error calculating coordinates:", error); - setProcessError("Error processing telemetry data with track path."); - return []; - } - }, [telemetry, trackPath, startFinishPosition, svgContainerRef, isClockwise]); - - return { - dataWithCoordinates, - processError, - }; -} diff --git a/dashboard/hooks/useTelemetrySync.tsx b/dashboard/hooks/useTelemetrySync.tsx index abad294..eab7656 100644 --- a/dashboard/hooks/useTelemetrySync.tsx +++ b/dashboard/hooks/useTelemetrySync.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import type { TelemetryDataPoint } from "@/lib/types"; +import type { TelemetryDataPoint } from "../lib/types"; /** * Hook to synchronize chart selection with track position @@ -89,7 +89,8 @@ export function useTelemetrySync( previousSelectionRef.current = selectedIndex; setIsProcessingSync(false); - }, [selectedIndex, telemetryData, onIndexChange]); + // biome-ignore lint/correctness/useExhaustiveDependencies: yeah + }, [selectedIndex, onIndexChange, isProcessingSync, findCorrespondingPoint]); return { syncedIndex, diff --git a/dashboard/hooks/useTrackPosition.tsx b/dashboard/hooks/useTrackPosition.tsx index 31b8128..4f4f9d7 100644 --- a/dashboard/hooks/useTrackPosition.tsx +++ b/dashboard/hooks/useTrackPosition.tsx @@ -1,16 +1,16 @@ -import { useCallback, useState, useMemo, useRef } from "react"; -import type { TelemetryDataPoint } from "@/lib/types"; +import { useCallback, useMemo, useRef, useState } from "react"; +import type { TelemetryDataPoint } from "../lib/types"; -/** - * Custom hook to manage track position synchronization with chart - */ +/* deprecated */ export function useTrackPosition(telemetryData: TelemetryDataPoint[]) { - const [selectedIndex, setSelectedIndex] = useState(0); - const [selectedLapPct, setSelectedLapPct] = useState(0); - + const [selectedPosition, setSelectedPosition] = useState<{ + index: number; + lapPct: number; + }>({ index: 0, lapPct: 0 }); + // Cache for expensive lookups const lookupCacheRef = useRef>(new Map()); - + // Memoize sorted data for faster lookups const sortedData = useMemo(() => { if (!telemetryData?.length) return []; @@ -33,9 +33,11 @@ export function useTrackPosition(telemetryData: TelemetryDataPoint[]) { } const clickedPoint = telemetryData[index]; - setSelectedIndex(index); - - setSelectedLapPct(clickedPoint.LapDistPct); + // Single state update - prevents render thrashing + setSelectedPosition({ + index, + lapPct: clickedPoint.LapDistPct, + }); }, [telemetryData], ); @@ -51,21 +53,23 @@ export function useTrackPosition(telemetryData: TelemetryDataPoint[]) { } // Check cache first - const cacheKey = Math.floor(selectedLapPct * 1000); // Round to 3 decimal places for caching + const cacheKey = Math.floor(selectedPosition.lapPct * 1000); // Round to 3 decimal places for caching if (lookupCacheRef.current.has(cacheKey)) { - return lookupCacheRef.current.get(cacheKey)!; + return lookupCacheRef.current.get(cacheKey); } // Use binary search for faster lookup in sorted data let left = 0; let right = sortedData.length - 1; let bestPoint = sortedData[0]; - let minDistance = Math.abs(sortedData[0].LapDistPct - selectedLapPct); + let minDistance = Math.abs( + sortedData[0].LapDistPct - selectedPosition.lapPct, + ); while (left <= right) { const mid = Math.floor((left + right) / 2); const point = sortedData[mid]; - const distance = Math.abs(point.LapDistPct - selectedLapPct); + const distance = Math.abs(point.LapDistPct - selectedPosition.lapPct); const wrappedDistance = Math.min(distance, 100 - distance); if (wrappedDistance < minDistance) { @@ -73,7 +77,7 @@ export function useTrackPosition(telemetryData: TelemetryDataPoint[]) { bestPoint = point; } - if (point.LapDistPct < selectedLapPct) { + if (point.LapDistPct < selectedPosition.lapPct) { left = mid + 1; } else { right = mid - 1; @@ -82,19 +86,19 @@ export function useTrackPosition(telemetryData: TelemetryDataPoint[]) { // Cache the result lookupCacheRef.current.set(cacheKey, bestPoint); - + // Limit cache size to prevent memory leaks if (lookupCacheRef.current.size > 1000) { const firstKey = lookupCacheRef.current.keys().next().value; - lookupCacheRef.current.delete(firstKey); + lookupCacheRef.current.delete(firstKey!); } return bestPoint; - }, [sortedData, selectedLapPct]); + }, [sortedData, selectedPosition.lapPct]); return { - selectedIndex, - selectedLapPct, + selectedIndex: selectedPosition.index, + selectedLapPct: selectedPosition.lapPct, handlePointSelection, getTrackDisplayPoint, }; diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..55a8542 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + \ No newline at end of file diff --git a/dashboard/lib/Fetch.tsx b/dashboard/lib/Fetch.tsx index d38d0d4..2ea2548 100644 --- a/dashboard/lib/Fetch.tsx +++ b/dashboard/lib/Fetch.tsx @@ -1,8 +1,7 @@ -import type { QueryResult } from "pg"; import type { TelemetryDataPoint } from "./types"; -export const processIRacingDataWithGPS = ({ rows }: QueryResult) => { - const sortedData = [...rows].sort((a, b) => { +export const processIRacingDataWithGPS = (data: any) => { + const sortedData = [...data].sort((a, b) => { const timeA = a.session_time !== undefined ? a.session_time : 0; const timeB = b.session_time !== undefined ? b.session_time : 0; return timeA - timeB; @@ -321,3 +320,40 @@ export const fetcher = async (url: string): Promise => { throw new Error("An unexpected error occurred"); } }; + +export const fetcherBR = async (url: string): Promise => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout + + try { + const response = await fetch(url, { + signal: controller.signal, + headers: { + Accept: "application/json", + "Accept-Encoding": "br", + "Content-Type": "application/json", + }, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status} - ${response.statusText}`, + ); + } + + return await response.json(); + } catch (error) { + clearTimeout(timeoutId); + + if (error instanceof Error) { + if (error.name === "AbortError") { + throw new Error("Request timeout - please try again"); + } + throw error; + } + + throw new Error("An unexpected error occurred"); + } +}; diff --git a/dashboard/lib/TrackService.ts b/dashboard/lib/TrackService.ts index a91da0c..2c7373e 100644 --- a/dashboard/lib/TrackService.ts +++ b/dashboard/lib/TrackService.ts @@ -1,14 +1,18 @@ // TrackService.ts - Dynamic approach following track shape import Feature from "ol/Feature"; import { LineString, Point } from "ol/geom"; -import { transform } from "ol/proj"; import type VectorSource from "ol/source/Vector"; /** * Creates a racing line feature based on telemetry data */ export function createRacingLine( - telemetryData: any[], + telemetryData: Array<{ + LapDistPct: number; + VelocityX: number; + VelocityY: number; + Speed: number; + }>, source: VectorSource, ): Feature | null { if (!telemetryData || telemetryData.length === 0) { @@ -139,11 +143,11 @@ function projectToTrackCoordinates( const normalizedDistance = lapDistPct / 100; // Track dimensions and center - const centerX = trackWidth / 2; - const centerY = trackHeight / 2; + const _centerX = trackWidth / 2; + const _centerY = trackHeight / 2; // Calculate angle around the track (0 to 2Ο€) - const angle = normalizedDistance * 2 * Math.PI; + const _angle = normalizedDistance * 2 * Math.PI; // Start/finish line is at the bottom-right of the track const startFinishX = 1115; @@ -151,7 +155,8 @@ function projectToTrackCoordinates( // Determine where on the track this point is // Using a custom shape that follows Monza's actual layout - let x, y; + let x: number; + let y: number; if (normalizedDistance < 0.1) { // Start/finish straight @@ -224,7 +229,7 @@ export function getTrackPoints(numPoints = 100): number[][] { export function mapLapDistanceToTrackPoint( lapDistPct: number, - numPoints = 100, + _numPoints = 100, ): number[] { const trackWidth = 1556; const trackHeight = 783; diff --git a/dashboard/lib/formatters.ts b/dashboard/lib/formatters.ts new file mode 100644 index 0000000..c6a5f1c --- /dev/null +++ b/dashboard/lib/formatters.ts @@ -0,0 +1,18 @@ +export function formatTime(totalSeconds: number | undefined) { + if (!totalSeconds) return "--"; + const minutes = Math.floor(totalSeconds / 60); + const remainingSeconds = totalSeconds % 60; + + const seconds = Math.floor(remainingSeconds); + const milliseconds = Math.round((remainingSeconds % 1) * 1000); + + const padTo2Digits = (num: number) => { + return num.toString().padStart(2, "0"); + }; + + const paddedMinutes = padTo2Digits(minutes); + const paddedSeconds = padTo2Digits(seconds); + const paddedMilliseconds = milliseconds > 0 ? `.${milliseconds}` : ""; + + return `${paddedMinutes}:${paddedSeconds}${paddedMilliseconds}`; +} diff --git a/dashboard/lib/questDb.ts b/dashboard/lib/questDb.ts index 2c15370..b41409c 100644 --- a/dashboard/lib/questDb.ts +++ b/dashboard/lib/questDb.ts @@ -1,4 +1,5 @@ -import { Client } from "pg"; +// import { Client } from "pg"; +import type { Session } from "@/components/SessionSelector"; import { processIRacingDataWithGPS, type TelemetryRes } from "./Fetch"; interface QuestDBConfig { @@ -15,12 +16,12 @@ class QuestDBClient { constructor() { this.config = { host: - process.env.QUESTDB_HOST || - (process.env.NODE_ENV === "production" ? "questdb" : "127.0.0.1"), - port: Number.parseInt(process.env.QUESTDB_PORT || "8812", 10), - user: process.env.QUESTDB_USER || "admin", - password: process.env.QUESTDB_PASSWORD || "quest", - database: process.env.QUESTDB_DATABASE || "qdb", + import.meta.env.MODE || + (import.meta.env.MODE === "production" ? "questdb" : "127.0.0.1"), + port: Number.parseInt(import.meta.env.VITE_QUESTDB_PORT || "8812", 10), + user: import.meta.env.VITE_QUESTDB_USER || "admin", + password: import.meta.env.VITE_QUESTDB_PASSWORD || "quest", + database: import.meta.env.VITE_QUESTDB_DATABASE || "qdb", }; } @@ -46,11 +47,6 @@ class QuestDBClient { maxRetries = 3, baseDelay = 1000, ): Promise { - if (process.env.NEXT_PHASE === "phase-production-build") { - console.log("πŸ—οΈ Build mode detected - skipping database operation"); - throw new Error("Database operations disabled during build"); - } - let lastError: Error | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { @@ -108,7 +104,7 @@ class QuestDBClient { return await this.executeWithRetry(async (client) => { const query = ` SELECT * FROM TelemetryTicks - WHERE session_id = $1 AND lap_id = $2 + WHERE session_id = $1 AND lap_id = $2 AND session_type = 'Race' ORDER BY session_time ASC `; @@ -150,7 +146,7 @@ class QuestDBClient { .map((row) => ({ lap_id: Number.parseInt(row.lap_id, 10), })) - .sort(({ lap_id }, b) => lap_id - b.lap_id); + .sort(({ lap_id }, b) => lap_id - b.lap_id); console.log(`Found ${laps.length} laps for session ${sessionId}`); return laps; @@ -163,18 +159,15 @@ class QuestDBClient { } } - async getSessions(): Promise< - Array<{ session_id: string; last_updated: Date }> - > { + async getSessions(): Promise { try { return await this.executeWithRetry(async (client) => { const query = ` SELECT DISTINCT session_id, - track_name, - MAX(timestamp) as last_updated + track_name, session_name, MAX(lap_id), MAX(timestamp) as last_updated FROM TelemetryTicks + WHERE session_name = 'RACE' and lap_id > 0 ORDER BY last_updated DESC - LIMIT 50 `; const result = await client.query(query); @@ -263,7 +256,7 @@ class QuestDBClient { FROM TelemetryTicks `); totalRows = Number.parseInt(rowsResult.rows[0]?.row_count || "0", 10); - } catch (e) { + } catch (_e) { // Table might not exist, that's okay console.log("TelemetryTicks table not found or empty"); } diff --git a/dashboard/lib/trackParser.ts b/dashboard/lib/trackParser.ts index 166ce4d..f98f32b 100644 --- a/dashboard/lib/trackParser.ts +++ b/dashboard/lib/trackParser.ts @@ -77,9 +77,9 @@ function parsePathCommands(pathString: string): PathCommand[] { switch (command.toUpperCase()) { case "M": // Move to if (i + 2 < tokens.length) { - const x = parseFloat(tokens[i + 1]); - const y = parseFloat(tokens[i + 2]); - if (!isNaN(x) && !isNaN(y)) { + const x = Number.parseFloat(tokens[i + 1]); + const y = Number.parseFloat(tokens[i + 2]); + if (!Number.isNaN(x) && !Number.isNaN(y)) { currentX = command === "M" ? x : currentX + x; currentY = command === "M" ? y : currentY + y; commands.push({ type: "M", x: currentX, y: currentY }); @@ -92,9 +92,9 @@ function parsePathCommands(pathString: string): PathCommand[] { case "L": // Line to if (i + 2 < tokens.length) { - const x = parseFloat(tokens[i + 1]); - const y = parseFloat(tokens[i + 2]); - if (!isNaN(x) && !isNaN(y)) { + const x = Number.parseFloat(tokens[i + 1]); + const y = Number.parseFloat(tokens[i + 2]); + if (!Number.isNaN(x) && !Number.isNaN(y)) { currentX = command === "L" ? x : currentX + x; currentY = command === "L" ? y : currentY + y; commands.push({ type: "L", x: currentX, y: currentY }); @@ -107,20 +107,20 @@ function parsePathCommands(pathString: string): PathCommand[] { case "C": // Cubic Bezier curve if (i + 6 < tokens.length) { - const x1 = parseFloat(tokens[i + 1]); - const y1 = parseFloat(tokens[i + 2]); - const x2 = parseFloat(tokens[i + 3]); - const y2 = parseFloat(tokens[i + 4]); - const x = parseFloat(tokens[i + 5]); - const y = parseFloat(tokens[i + 6]); + const x1 = Number.parseFloat(tokens[i + 1]); + const y1 = Number.parseFloat(tokens[i + 2]); + const x2 = Number.parseFloat(tokens[i + 3]); + const y2 = Number.parseFloat(tokens[i + 4]); + const x = Number.parseFloat(tokens[i + 5]); + const y = Number.parseFloat(tokens[i + 6]); if ( - !isNaN(x1) && - !isNaN(y1) && - !isNaN(x2) && - !isNaN(y2) && - !isNaN(x) && - !isNaN(y) + !Number.isNaN(x1) && + !Number.isNaN(y1) && + !Number.isNaN(x2) && + !Number.isNaN(y2) && + !Number.isNaN(x) && + !Number.isNaN(y) ) { if (command === "c") { // Relative coordinates @@ -355,10 +355,10 @@ export function mapLapDistanceToTrackPoint( if (trackPoints.length === 0) return [0, 0]; // Ensure the percentage is between 0 and 1 - lapDistPct = Math.max(0, Math.min(1, lapDistPct)); + const checkedLapDistPct = Math.max(0, Math.min(1, lapDistPct)); // Calculate the index in the points array - const pointIndex = lapDistPct * (trackPoints.length - 1); + const pointIndex = checkedLapDistPct * (trackPoints.length - 1); const lowerIndex = Math.floor(pointIndex); const upperIndex = Math.min(lowerIndex + 1, trackPoints.length - 1); diff --git a/dashboard/lib/utils.ts b/dashboard/lib/utils.ts index c19776e..dcdf786 100644 --- a/dashboard/lib/utils.ts +++ b/dashboard/lib/utils.ts @@ -1,4 +1,10 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + export interface LapRow { lap_id: string; - [key: string]: any; // For any other properties +} + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); } diff --git a/dashboard/next.config.mjs b/dashboard/next.config.mjs deleted file mode 100644 index ba3a789..0000000 --- a/dashboard/next.config.mjs +++ /dev/null @@ -1,149 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - output: process.env.NODE_ENV === "production" ? "standalone" : undefined, - serverExternalPackages: ["pg"], - reactStrictMode: true, - - compiler: { - removeConsole: - process.env.NODE_ENV === "production" - ? { - exclude: ["error", "warn"], - } - : false, - }, - - // Image optimization - images: { - formats: ["image/webp", "image/avif"], - minimumCacheTTL: 86400, // 24 hours - dangerouslyAllowSVG: true, - contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", - }, - - // Headers for caching and performance - async headers() { - return [ - { - source: "/api/:path*", - headers: [ - { key: "Access-Control-Allow-Credentials", value: "true" }, - { key: "Access-Control-Allow-Origin", value: "*" }, - { - key: "Access-Control-Allow-Methods", - value: "GET,OPTIONS,PATCH,DELETE,POST,PUT", - }, - { - key: "Access-Control-Allow-Headers", - value: - "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version", - }, - // Cache API responses for 30 seconds - { - key: "Cache-Control", - value: "public, s-maxage=30, stale-while-revalidate=60", - }, - ], - }, - { - source: "/:path*-tiles/:path*", - headers: [ - { key: "Access-Control-Allow-Origin", value: "*" }, - { key: "Access-Control-Allow-Methods", value: "GET" }, - { key: "Cache-Control", value: "public, max-age=86400, immutable" }, - ], - }, - { - source: "/_next/static/:path*", - headers: [ - { - key: "Cache-Control", - value: "public, max-age=31536000, immutable", - }, - ], - }, - { - source: "/static/:path*", - headers: [ - { - key: "Cache-Control", - value: "public, max-age=31536000, immutable", - }, - ], - }, - // Security headers - { - source: "/(.*)", - headers: [ - { - key: "X-DNS-Prefetch-Control", - value: "on", - }, - { - key: "X-Frame-Options", - value: "DENY", - }, - { - key: "X-Content-Type-Options", - value: "nosniff", - }, - { - key: "Referrer-Policy", - value: "origin-when-cross-origin", - }, - ], - }, - ]; - }, - - // Optimized rewrites with caching - async rewrites() { - return [ - { - source: "/osm-tiles/:z/:x/:y.png", - destination: "https://tile.openstreetmap.org/:z/:x/:y.png", - }, - { - source: "/carto-dark/:z/:x/:y.png", - destination: - "https://cartodb-basemaps-a.global.ssl.fastly.net/dark_all/:z/:x/:y.png", - }, - { - source: "/carto-dark-nolabels/:z/:x/:y.png", - destination: - "https://cartodb-basemaps-a.global.ssl.fastly.net/dark_nolabels/:z/:x/:y.png", - }, - ]; - }, - - // Redirect configuration for performance - async redirects() { - return [ - { - source: "/telemetry", - destination: "/", - permanent: false, - }, - ]; - }, - - // PoweredByHeader removal for security and performance - poweredByHeader: false, - - // Compression - compress: true, - - // Generate ETags for better caching - generateEtags: true, - - // Optimize page extensions - pageExtensions: ["tsx", "ts", "jsx", "js"], - - // Environment variables for optimization - env: { - OPTIMIZE_IMAGES: "true", - MINIMIZE_JS: process.env.NODE_ENV === "production" ? "true" : "false", - }, -}; - -export default nextConfig; diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 1978bd8..d3365aa 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -1,8093 +1,4525 @@ { - "name": "dashboard", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "dashboard", - "version": "0.1.0", - "dependencies": { - "@questdb/nodejs-client": "^4.0.1", - "@tailwindcss/postcss": "^4.1.11", - "autoprefixer": "^10.4.21", - "lucide-react": "^0.539.0", - "next": "^15.4.6", - "ol": "^10.6.1", - "pg": "^8.16.3", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-globe.gl": "^2.35.0", - "recharts": "^3.1.2", - "swr": "^2.3.6", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@biomejs/biome": "2.1.4", - "@next/bundle-analyzer": "^15.4.6", - "@playwright/test": "^1.54.2", - "@types/d3": "^7.4.3", - "@types/node": "^24", - "@types/pg": "^8.15.5", - "@types/react": "^19", - "@types/react-dom": "^19", - "@types/three": "^0.179.0", - "cross-env": "^10.0.0", - "eslint": "^9", - "eslint-config-next": "15.4.6", - "playwright": "^1.54.2", - "postcss": "^8.5.6", - "tailwindcss": "^4.1.11", - "typescript": "^5" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@biomejs/biome": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.4.tgz", - "integrity": "sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.1.4", - "@biomejs/cli-darwin-x64": "2.1.4", - "@biomejs/cli-linux-arm64": "2.1.4", - "@biomejs/cli-linux-arm64-musl": "2.1.4", - "@biomejs/cli-linux-x64": "2.1.4", - "@biomejs/cli-linux-x64-musl": "2.1.4", - "@biomejs/cli-win32-arm64": "2.1.4", - "@biomejs/cli-win32-x64": "2.1.4" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.4.tgz", - "integrity": "sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.4.tgz", - "integrity": "sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.4.tgz", - "integrity": "sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.4.tgz", - "integrity": "sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.4.tgz", - "integrity": "sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.4.tgz", - "integrity": "sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.4.tgz", - "integrity": "sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.4.tgz", - "integrity": "sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@dimforge/rapier3d-compat": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", - "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", - "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@epic-web/invariant": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "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/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz", - "integrity": "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@next/bundle-analyzer": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.4.6.tgz", - "integrity": "sha512-LZWqTQgIpfhblT77VVc1r4qtHJY1pfZOAIx8zNtliU7L3pMjpNrG4rYWikJ7AyAI/RgYyt2sCVWqkeOZmFp7Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "webpack-bundle-analyzer": "4.10.1" - } - }, - "node_modules/@next/bundle-analyzer/node_modules/webpack-bundle-analyzer": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", - "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "commander": "^7.2.0", - "debounce": "^1.2.1", - "escape-string-regexp": "^4.0.0", - "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", - "is-plain-object": "^5.0.0", - "opener": "^1.5.2", - "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/@next/env": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.6.tgz", - "integrity": "sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.6.tgz", - "integrity": "sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", - "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", - "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", - "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", - "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", - "integrity": "sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.6.tgz", - "integrity": "sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", - "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", - "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@petamoriken/float16": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.1.tgz", - "integrity": "sha512-j+ejhYwY6PeB+v1kn7lZFACUIG97u90WxMuGosILFsl9d4Ovi0sjk0GlPfoEcx+FzvXZDAfioD+NGnnPamXgMA==" - }, - "node_modules/@playwright/test": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", - "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.54.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@questdb/nodejs-client": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@questdb/nodejs-client/-/nodejs-client-4.0.1.tgz", - "integrity": "sha512-V5lLTZKHU1MwQIO23SXBVeI/dvK6PENDuAuvU+J6mhX2h1FtaObuGxeHeTeu7ZBDyBDA1v5ery500b0v87o8Ew==", - "license": "Apache-2.0", - "dependencies": { - "undici": "^7.8.0" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", - "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^10.0.3", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", - "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", - "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "postcss": "^8.4.41", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@turf/boolean-point-in-polygon": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.2.0.tgz", - "integrity": "sha512-lvEOjxeXIp+wPXgl9kJA97dqzMfNexjqHou+XHVcfxQgolctoJiRYmcVCWGpiZ9CBf/CJha1KmD1qQoRIsjLaA==", - "license": "MIT", - "dependencies": { - "@turf/helpers": "^7.2.0", - "@turf/invariant": "^7.2.0", - "@types/geojson": "^7946.0.10", - "point-in-polygon-hao": "^1.1.0", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" - } - }, - "node_modules/@turf/helpers": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.2.0.tgz", - "integrity": "sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==", - "license": "MIT", - "dependencies": { - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" - } - }, - "node_modules/@turf/invariant": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.2.0.tgz", - "integrity": "sha512-kV4u8e7Gkpq+kPbAKNC21CmyrXzlbBgFjO1PhrHPgEdNqXqDawoZ3i6ivE3ULJj2rSesCjduUaC/wyvH/sNr2Q==", - "license": "MIT", - "dependencies": { - "@turf/helpers": "^7.2.0", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" - } - }, - "node_modules/@tweenjs/tween.js": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", - "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", - "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", - "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", - "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", - "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/geojson": { - "version": "7946.0.14", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", - "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/pg": { - "version": "8.15.5", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", - "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/rbush": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz", - "integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==" - }, - "node_modules/@types/react": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", - "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@types/stats.js": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", - "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.179.0", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.179.0.tgz", - "integrity": "sha512-VgbFG2Pgsm84BqdegZzr7w2aKbQxmgzIu4Dy7/75ygiD/0P68LKmp5ie08KMPNqGTQwIge8s6D1guZf1RnZE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": "*", - "@webgpu/types": "*", - "fflate": "~0.8.2", - "meshoptimizer": "~0.22.0" - } - }, - "node_modules/@types/three/node_modules/@tweenjs/tween.js": { - "version": "23.1.3", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/@types/webxr": { - "version": "0.5.22", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz", - "integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", - "integrity": "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz", - "integrity": "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz", - "integrity": "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz", - "integrity": "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz", - "integrity": "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz", - "integrity": "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz", - "integrity": "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz", - "integrity": "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz", - "integrity": "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz", - "integrity": "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz", - "integrity": "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz", - "integrity": "sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz", - "integrity": "sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz", - "integrity": "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.9" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz", - "integrity": "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", - "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", - "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@webgpu/types": { - "version": "0.1.61", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.61.tgz", - "integrity": "sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/accessor-fn": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", - "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "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/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-env": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz", - "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" - }, - "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo-voronoi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-geo-voronoi/-/d3-geo-voronoi-2.1.0.tgz", - "integrity": "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-delaunay": "6", - "d3-geo": "3", - "d3-tricontour": "1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-octree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", - "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", - "license": "MIT" - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-tricontour": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d3-tricontour/-/d3-tricontour-1.0.2.tgz", - "integrity": "sha512-HIRxHzHagPtUPNabjOlfcyismJYIsc+Xlq4mlsts4e8eAcwyq9Tgk/sYdyhlBpQ0MHwVquc/8j+e29YjXnmxeA==", - "license": "ISC", - "dependencies": { - "d3-delaunay": "6", - "d3-scale": "4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-bind-mapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz", - "integrity": "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==", - "license": "MIT", - "dependencies": { - "accessor-fn": "1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true, - "license": "MIT" - }, - "node_modules/earcut": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", - "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", - "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" - }, - "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", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-toolkit": { - "version": "1.39.9", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.9.tgz", - "integrity": "sha512-9OtbkZmTA2Qc9groyA1PUNeb6knVTkvB2RSdr/LcJXDL8IdEakaxwXLHXa7VX/Wj0GmdMJPR3WhnPGhiP3E+qg==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", - "@eslint/plugin-kit": "^0.3.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.6.tgz", - "integrity": "sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "15.4.6", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" - }, - "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/float-tooltip": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", - "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", - "license": "MIT", - "dependencies": { - "d3-selection": "2 - 3", - "kapsule": "^1.16", - "preact": "10" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/frame-ticker": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/frame-ticker/-/frame-ticker-1.0.3.tgz", - "integrity": "sha512-E0X2u2JIvbEMrqEg5+4BpTqaD22OwojJI63K7MdKHdncjtAhGRbCR8nJCr2vwEt9NWBPCPcu70X9smPviEBy8Q==", - "license": "MIT", - "dependencies": { - "simplesignal": "^2.1.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/geotiff": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", - "integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==", - "dependencies": { - "@petamoriken/float16": "^3.4.7", - "lerc": "^3.0.0", - "pako": "^2.0.4", - "parse-headers": "^2.0.2", - "quick-lru": "^6.1.1", - "web-worker": "^1.2.0", - "xml-utils": "^1.0.2", - "zstddec": "^0.1.0" - }, - "engines": { - "node": ">=10.19" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globe.gl": { - "version": "2.43.0", - "resolved": "https://registry.npmjs.org/globe.gl/-/globe.gl-2.43.0.tgz", - "integrity": "sha512-iOPADfA/J0s0LrU8BnxXeDH2iTpZBCvmQfyl+DVN/RSSoxLxtNpK1xc6c5eBqc9mc+rC263sGiqOIA3/HTGLgQ==", - "license": "MIT", - "dependencies": { - "@tweenjs/tween.js": "18 - 25", - "accessor-fn": "1", - "kapsule": "^1.16", - "three": ">=0.154 <1", - "three-globe": "^2.44", - "three-render-objects": "^1.40" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/h3-js": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.2.1.tgz", - "integrity": "sha512-HYiUrq5qTRFqMuQu3jEHqxXLk1zsSJiby9Lja/k42wHjabZG7tN9rOuzT/PEFf+Wa7rsnHLMHRWIu0mgcJ0ewQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=4", - "npm": ">=3", - "yarn": ">=1.3.0" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/index-array-by": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", - "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "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", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jerrypick": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", - "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/kapsule": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", - "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", - "license": "MIT", - "dependencies": { - "lodash-es": "4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/lerc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", - "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lucide-react": { - "version": "0.539.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz", - "integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/meshoptimizer": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", - "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "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" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-postinstall": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz", - "integrity": "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/next/-/next-15.4.6.tgz", - "integrity": "sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==", - "license": "MIT", - "dependencies": { - "@next/env": "15.4.6", - "@swc/helpers": "0.5.15", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.4.6", - "@next/swc-darwin-x64": "15.4.6", - "@next/swc-linux-arm64-gnu": "15.4.6", - "@next/swc-linux-arm64-musl": "15.4.6", - "@next/swc-linux-x64-gnu": "15.4.6", - "@next/swc-linux-x64-musl": "15.4.6", - "@next/swc-win32-arm64-msvc": "15.4.6", - "@next/swc-win32-x64-msvc": "15.4.6", - "sharp": "^0.34.3" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT" - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ol": { - "version": "10.6.1", - "resolved": "https://registry.npmjs.org/ol/-/ol-10.6.1.tgz", - "integrity": "sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg==", - "license": "BSD-2-Clause", - "dependencies": { - "@types/rbush": "4.0.0", - "earcut": "^3.0.0", - "geotiff": "^2.1.3", - "pbf": "4.0.1", - "rbush": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/openlayers" - } - }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true, - "license": "(WTFPL OR MIT)", - "bin": { - "opener": "bin/opener-bin.js" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "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" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-headers": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", - "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/pbf": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", - "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", - "dependencies": { - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/playwright": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", - "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.54.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", - "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/point-in-polygon-hao": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz", - "integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==", - "license": "MIT", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/polished": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", - "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.17.8" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/preact": { - "version": "10.27.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.0.tgz", - "integrity": "sha512-/DTYoB6mwwgPytiqQTh/7SFRL98ZdiD8Sk8zIUVOxtwq4oWcwrcd1uno9fE/zZmUaUrFNYzbH14CPebOz9tZQw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/protocol-buffers-schema": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" - }, - "node_modules/punycode": { - "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" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-lru": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", - "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/quickselect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" - }, - "node_modules/rbush": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", - "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", - "dependencies": { - "quickselect": "^3.0.0" - } - }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.1" - } - }, - "node_modules/react-globe.gl": { - "version": "2.35.0", - "resolved": "https://registry.npmjs.org/react-globe.gl/-/react-globe.gl-2.35.0.tgz", - "integrity": "sha512-ZCUjM/4rw0UvDyiZq75hyMT29OOMCYTfmy9XWRXyMn1c5npsOLXMOxQ1LLhGNd1y7psNMcnq8VqlzOz/SLzkEQ==", - "license": "MIT", - "dependencies": { - "globe.gl": "^2.43", - "prop-types": "15", - "react-kapsule": "^2.5" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-kapsule": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz", - "integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==", - "license": "MIT", - "dependencies": { - "jerrypick": "^1.1.1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": ">=16.13.1" - } - }, - "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/recharts": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.1.2.tgz", - "integrity": "sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g==", - "license": "MIT", - "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^10.1.1", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.1.1", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/resolve-protobuf-schema": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", - "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", - "dependencies": { - "protocol-buffers-schema": "^3.3.1" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "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": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "devOptional": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simplesignal": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/simplesignal/-/simplesignal-2.1.7.tgz", - "integrity": "sha512-PEo2qWpUke7IMhlqiBxrulIFvhJRLkl1ih52Rwa+bPjzhJepcd4GIjn2RiQmFSx3dQvsEAgF0/lXMwMN7vODaA==", - "license": "MIT" - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swr": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", - "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "license": "MIT" - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/three": { - "version": "0.179.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.179.1.tgz", - "integrity": "sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw==", - "license": "MIT" - }, - "node_modules/three-conic-polygon-geometry": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/three-conic-polygon-geometry/-/three-conic-polygon-geometry-2.1.2.tgz", - "integrity": "sha512-NaP3RWLJIyPGI+zyaZwd0Yj6rkoxm4FJHqAX1Enb4L64oNYLCn4bz1ESgOEYavgcUwCNYINu1AgEoUBJr1wZcA==", - "license": "MIT", - "dependencies": { - "@turf/boolean-point-in-polygon": "^7.2", - "d3-array": "1 - 3", - "d3-geo": "1 - 3", - "d3-geo-voronoi": "2", - "d3-scale": "1 - 4", - "delaunator": "5", - "earcut": "3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "three": ">=0.72.0" - } - }, - "node_modules/three-geojson-geometry": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/three-geojson-geometry/-/three-geojson-geometry-2.1.1.tgz", - "integrity": "sha512-dC7bF3ri1goDcihYhzACHOBQqu7YNNazYLa2bSydVIiJUb3jDFojKSy+gNj2pMkqZNSVjssSmdY9zlmnhEpr1w==", - "license": "MIT", - "dependencies": { - "d3-geo": "1 - 3", - "d3-interpolate": "1 - 3", - "earcut": "3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "three": ">=0.72.0" - } - }, - "node_modules/three-globe": { - "version": "2.44.0", - "resolved": "https://registry.npmjs.org/three-globe/-/three-globe-2.44.0.tgz", - "integrity": "sha512-ZDZgGf06xSP2WfKxZgXBl1TjiSutzNhBK9vGMmy7Nupaujia5as75MmhV2VBVQL8iN0nAblXVnnXepfLNC93qA==", - "license": "MIT", - "dependencies": { - "@tweenjs/tween.js": "18 - 25", - "accessor-fn": "1", - "d3-array": "3", - "d3-color": "3", - "d3-geo": "3", - "d3-interpolate": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "data-bind-mapper": "1", - "frame-ticker": "1", - "h3-js": "4", - "index-array-by": "1", - "kapsule": "^1.16", - "three-conic-polygon-geometry": "2", - "three-geojson-geometry": "2", - "three-slippy-map-globe": "1", - "tinycolor2": "1" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "three": ">=0.154" - } - }, - "node_modules/three-render-objects": { - "version": "1.40.4", - "resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.40.4.tgz", - "integrity": "sha512-Ukpu1pei3L5r809izvjsZxwuRcYLiyn6Uvy3lZ9bpMTdvj3i6PeX6w++/hs2ZS3KnEzGjb6YvTvh4UQuwHTDJg==", - "license": "MIT", - "dependencies": { - "@tweenjs/tween.js": "18 - 25", - "accessor-fn": "1", - "float-tooltip": "^1.7", - "kapsule": "^1.16", - "polished": "4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "three": ">=0.168" - } - }, - "node_modules/three-slippy-map-globe": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/three-slippy-map-globe/-/three-slippy-map-globe-1.0.3.tgz", - "integrity": "sha512-Y9WCA/tTL8yH8FHVSXVQss/P0V36utTNhuixzFPj0Bs0SXxO+Vui133oAQmMpx4BLXYZpWZwcqHM2i3MfFlYWw==", - "license": "MIT", - "dependencies": { - "d3-geo": "1 - 3", - "d3-octree": "^1.1", - "d3-scale": "1 - 4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "three": ">=0.154" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.13.0.tgz", - "integrity": "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/unrs-resolver": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz", - "integrity": "sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/JounQin" - }, - "optionalDependencies": { - "@unrs/resolver-binding-darwin-arm64": "1.7.2", - "@unrs/resolver-binding-darwin-x64": "1.7.2", - "@unrs/resolver-binding-freebsd-x64": "1.7.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.7.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.7.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.7.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-x64-musl": "1.7.2", - "@unrs/resolver-binding-wasm32-wasi": "1.7.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.7.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.7.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/web-worker": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", - "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-utils": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.1.tgz", - "integrity": "sha512-Dn6vJ1Z9v1tepSjvnCpwk5QqwIPcEFKdgnjqfYOABv1ngSofuAhtlugcUC3ehS1OHdgDWSG6C5mvj+Qm15udTQ==" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "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/yocto-queue": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zstddec": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", - "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==" - } - } + "name": "dashboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dashboard", + "version": "0.1.0", + "dependencies": { + "@tailwindcss/postcss": "^4.1.16", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-router": "^1.147.3", + "@tanstack/react-router-devtools": "^1.149.0", + "autoprefixer": "^10.4.21", + "lucide-react": "^0.562.0", + "maplibre-gl": "^5.15.0", + "next-themes": "^0.4.6", + "ol": "^10.6.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "recharts": "^3.3.0", + "swr": "^2.3.8", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@biomejs/biome": "2.3.2", + "@playwright/test": "^1.56.1", + "@tanstack/router-plugin": "^1.149.0", + "@types/node": "^24.10.15", + "@types/react": "^19.2.7", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^5.1.2", + "playwright": "^1.56.1", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "^5", + "vite": "npm:rolldown-vite@latest" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.2.tgz", + "integrity": "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.2", + "@biomejs/cli-darwin-x64": "2.3.2", + "@biomejs/cli-linux-arm64": "2.3.2", + "@biomejs/cli-linux-arm64-musl": "2.3.2", + "@biomejs/cli-linux-x64": "2.3.2", + "@biomejs/cli-linux-x64-musl": "2.3.2", + "@biomejs/cli-win32-arm64": "2.3.2", + "@biomejs/cli-win32-x64": "2.3.2" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz", + "integrity": "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.2.tgz", + "integrity": "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.2.tgz", + "integrity": "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.2.tgz", + "integrity": "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.2.tgz", + "integrity": "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.2.tgz", + "integrity": "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.2.tgz", + "integrity": "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz", + "integrity": "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", + "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", + "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.1" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.4.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.4.1.tgz", + "integrity": "sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.2.tgz", + "integrity": "sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.2.0.tgz", + "integrity": "sha512-bxrk/kQUwWXZgmqYgwOCnZCMONCRi3MJMqJdza4T3E4AeR5i+VyMnaJ8iDWtWxdfEAJRtrzIOeJtxZSy5mFrFA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.4", + "@types/geojson-vt": "3.2.5", + "@types/supercluster": "^7.1.3", + "geojson-vt": "^4.0.2", + "pbf": "^4.0.1", + "supercluster": "^8.0.1" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.101.0.tgz", + "integrity": "sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.101.0.tgz", + "integrity": "sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@petamoriken/float16": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz", + "integrity": "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==", + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.53.tgz", + "integrity": "sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.53.tgz", + "integrity": "sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", + "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.16" + } + }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", + "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", + "license": "MIT" + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", + "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-x64": "4.1.16", + "@tailwindcss/oxide-freebsd-x64": "4.1.16", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-x64-musl": "4.1.16", + "@tailwindcss/oxide-wasm32-wasi": "4.1.16", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz", + "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz", + "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz", + "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz", + "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz", + "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz", + "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz", + "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz", + "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz", + "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz", + "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", + "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz", + "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.16.tgz", + "integrity": "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.16", + "@tailwindcss/oxide": "4.1.16", + "postcss": "^8.4.41", + "tailwindcss": "4.1.16" + } + }, + "node_modules/@tailwindcss/postcss/node_modules/tailwindcss": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", + "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", + "license": "MIT" + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tanstack/history": { + "version": "1.145.7", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.145.7.tgz", + "integrity": "sha512-gMo/ReTUp0a3IOcZoI3hH6PLDC2R/5ELQ7P2yu9F6aEkA0wSQh+Q4qzMrtcKvF2ut0oE+16xWCGDo/TdYd6cEQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.147.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.147.3.tgz", + "integrity": "sha512-Fp9DoszYiIJclwxU43kyP/cqcWD418DPmV6yhmIOuVedsSMnfh2g7uRQ+bOoaWn996JjuU9yt/x48h66aCQSQA==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.145.7", + "@tanstack/react-store": "^0.8.0", + "@tanstack/router-core": "1.147.1", + "isbot": "^5.1.22", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-router-devtools": { + "version": "1.149.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.149.0.tgz", + "integrity": "sha512-QJ6epMhRKTS8WrBmcMFjK1v+jDaimMQuySCSNA8NR1ZROKv3xx0gY8AjyVVgQ1h78HSXXRMYH3aql2kWYjc31g==", + "license": "MIT", + "dependencies": { + "@tanstack/router-devtools-core": "1.149.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.147.3", + "@tanstack/router-core": "^1.147.1", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/router-core": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.0.tgz", + "integrity": "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.8.0", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.147.1", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.147.1.tgz", + "integrity": "sha512-yf8o3CNgJVGO5JnIqiTe0y2eChxEM0w7TrEs1VSumL/zz2bQroYGNr1mOXJ2VeN+7YfJJwjEqq71P5CzWwMzRg==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.145.7", + "@tanstack/store": "^0.8.0", + "cookie-es": "^2.0.0", + "seroval": "^1.4.1", + "seroval-plugins": "^1.4.0", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-devtools-core": { + "version": "1.149.0", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.149.0.tgz", + "integrity": "sha512-dy9xb8U9VWAavqKM0sTFhAs2ufVs3d/cGSbqczIgBcAKCjjbsAng1gV4ezPXmfF1pa+2MW6n7SViXsxxvtCRiw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16", + "tiny-invariant": "^1.3.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/router-core": "^1.147.1", + "csstype": "^3.0.10" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-generator": { + "version": "1.149.0", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.149.0.tgz", + "integrity": "sha512-H+SZbJ9j4G+y/329LlRLb//4sBdPNQpuMddb/rgkfoRZpdztm9Ejm9EEbMJB0rkNDrSgfSPOZ6VtJbndYH/AQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/router-core": "1.147.1", + "@tanstack/router-utils": "1.143.11", + "@tanstack/virtual-file-routes": "1.145.4", + "prettier": "^3.5.0", + "recast": "^0.23.11", + "source-map": "^0.7.4", + "tsx": "^4.19.2", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-generator/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@tanstack/router-plugin": { + "version": "1.149.0", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.149.0.tgz", + "integrity": "sha512-DYPScneHZ0fm3FJyDhkUCW0w0dOopAKvep57n/Ft2b3RrHSeSeJB/cJWgsUvpcYJfpywkyOLyqVLMoiDvLoG/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.147.1", + "@tanstack/router-generator": "1.149.0", + "@tanstack/router-utils": "1.143.11", + "@tanstack/virtual-file-routes": "1.145.4", + "babel-dead-code-elimination": "^1.0.11", + "chokidar": "^3.6.0", + "unplugin": "^2.1.2", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rsbuild/core": ">=1.0.2", + "@tanstack/react-router": "^1.147.3", + "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", + "vite-plugin-solid": "^2.11.10", + "webpack": ">=5.92.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "vite": { + "optional": true + }, + "vite-plugin-solid": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-plugin/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@tanstack/router-utils": { + "version": "1.143.11", + "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.143.11.tgz", + "integrity": "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.28.5", + "ansis": "^4.1.0", + "diff": "^8.0.2", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.15" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.0.tgz", + "integrity": "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-file-routes": { + "version": "1.145.4", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.145.4.tgz", + "integrity": "sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.15.tgz", + "integrity": "sha512-BgjLoRuSr0MTI5wA6gMw9Xy0sFudAaUuvrnjgGx9wZ522fYYLA5SYJ+1Y30vTcJEG+DRCyDHx/gzQVfofYzSdg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/rbush": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz", + "integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.11.tgz", + "integrity": "sha512-mwq3W3e/pKSI6TG8lXMiDWvEi1VXYlSBlJlB3l+I0bAb5u1RNUl88udos85eOPNK3m5EXK9uO7d2g08pesTySQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", + "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.221", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.221.tgz", + "integrity": "sha512-/1hFJ39wkW01ogqSyYoA4goOXOtMRy6B+yvA1u42nnsEGtHzIzmk93aPISumVQeblj47JUHLC9coCjUxb1EvtQ==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, + "node_modules/geotiff": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", + "integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==", + "license": "MIT", + "dependencies": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.2.0", + "xml-utils": "^1.0.2", + "zstddec": "^0.1.0" + }, + "engines": { + "node": ">=10.19" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isbot": { + "version": "5.1.32", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.32.tgz", + "integrity": "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", + "license": "Apache-2.0" + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/maplibre-gl": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.15.0.tgz", + "integrity": "sha512-pPeu/t4yPDX/+Uf9ibLUdmaKbNMlGxMAX+tBednYukol2qNk2TZXAlhdohWxjVvTO3is8crrUYv3Ok02oAaKzA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.0.7", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^24.4.1", + "@maplibre/mlt": "^1.1.2", + "@maplibre/vt-pbf": "^4.2.0", + "@types/geojson": "^7946.0.16", + "@types/geojson-vt": "3.2.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.2", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.4", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ol": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/ol/-/ol-10.6.1.tgz", + "integrity": "sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg==", + "license": "BSD-2-Clause", + "dependencies": { + "@types/rbush": "4.0.0", + "earcut": "^3.0.0", + "geotiff": "^2.1.3", + "pbf": "4.0.1", + "rbush": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openlayers" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/rbush": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", + "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", + "license": "MIT", + "dependencies": { + "quickselect": "^3.0.0" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/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/recharts": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz", + "integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", + "integrity": "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.101.0", + "@rolldown/pluginutils": "1.0.0-beta.53" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-x64": "1.0.0-beta.53", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.53", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.53", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.53", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.53", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.53", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.53" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/seroval": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz", + "integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.4.2.tgz", + "integrity": "sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/swr": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.8.tgz", + "integrity": "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "name": "rolldown-vite", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.3.1.tgz", + "integrity": "sha512-LYzdNAjRHhF2yA4JUQm/QyARyi216N2rpJ0lJZb8E9FU2y5v6Vk+xq/U4XBOxMefpWixT5H3TslmAHm1rqIq2w==", + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.101.0", + "fdir": "^6.5.0", + "lightningcss": "^1.30.2", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rolldown": "1.0.0-beta.53", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml-utils": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", + "integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==", + "license": "CC0-1.0" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zstddec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", + "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==", + "license": "MIT AND BSD-3-Clause" + } + } } diff --git a/dashboard/package.json b/dashboard/package.json index 19cfc03..95e5d50 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,79 +1,70 @@ { - "name": "dashboard", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build --turbopack", - "start": "next start", - "lint": "next lint", - "analyze": "cross-env ANALYZE=true next build", - "build:profile": "next build --profile", - "build:debug": "next build --debug", - "lighthouse": "lhci autorun", - "perf": "npm run build && npm run start", - "test": "playwright test", - "test:headed": "playwright test --headed", - "test:debug": "playwright test --debug", - "test:ui": "playwright test --ui", - "test:chromium": "playwright test --project=chromium", - "test:firefox": "playwright test --project=firefox", - "test:webkit": "playwright test --project=webkit", - "test:mobile": "playwright test --project='Mobile Chrome' --project='Mobile Safari'", - "test:report": "playwright show-report test-results/html", - "test:install": "playwright install" - }, - "dependencies": { - "@questdb/nodejs-client": "^4.0.1", - "@tailwindcss/postcss": "^4.1.11", - "autoprefixer": "^10.4.21", - "lucide-react": "^0.539.0", - "next": "^15.4.6", - "ol": "^10.6.1", - "pg": "^8.16.3", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-globe.gl": "^2.35.0", - "recharts": "^3.1.2", - "swr": "^2.3.6", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@biomejs/biome": "2.1.4", - "@next/bundle-analyzer": "^15.4.6", - "@playwright/test": "^1.54.2", - "@types/d3": "^7.4.3", - "@types/node": "^24", - "@types/pg": "^8.15.5", - "@types/react": "^19", - "@types/react-dom": "^19", - "@types/three": "^0.179.0", - "cross-env": "^10.0.0", - "eslint": "^9", - "eslint-config-next": "15.4.6", - "playwright": "^1.54.2", - "postcss": "^8.5.6", - "tailwindcss": "^4.1.11", - "typescript": "^5" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" - }, - "browserslist": { - "production": [ - ">0.3%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "overrides": { - "react": "^19.1.1", - "react-dom": "^19.1.1" - } + "name": "dashboard", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "build": "tsc -b && vite build", + "lighthouse": "lhci autorun", + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:debug": "playwright test --debug", + "test:ui": "playwright test --ui", + "test:chromium": "playwright test --project=chromium", + "test:firefox": "playwright test --project=firefox", + "test:webkit": "playwright test --project=webkit", + "test:mobile": "playwright test --project='Mobile Chrome' --project='Mobile Safari'", + "test:report": "playwright show-report test-results/html", + "test:install": "playwright install", + "lint": "biome check --write" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.1.16", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-router": "^1.147.3", + "autoprefixer": "^10.4.21", + "lucide-react": "^0.562.0", + "maplibre-gl": "^5.15.0", + "next-themes": "^0.4.6", + "ol": "^10.6.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "recharts": "^3.3.0", + "swr": "^2.3.8", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@biomejs/biome": "2.3.2", + "@tanstack/react-router-devtools": "^1.149.0", + "@tanstack/router-plugin": "^1.149.0", + "@types/node": "^24.10.15", + "@types/react": "^19.2.7", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^5.1.2", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "^5", + "vite": "npm:rolldown-vite@latest" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + }, + "browserslist": { + "production": [ + ">0.3%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "overrides": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + } } diff --git a/dashboard/playwright.config.ts b/dashboard/playwright.config.ts deleted file mode 100644 index dafb765..0000000 --- a/dashboard/playwright.config.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -/** - * @see https://playwright.dev/docs/test-configuration - */ -export default defineConfig({ - testDir: './tests/e2e', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [ - ['html', { outputFolder: 'test-results/html' }], - ['json', { outputFile: 'test-results/test-results.json' }], - ['junit', { outputFile: 'test-results/results.xml' }] - ], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - - /* Take screenshot only when test fails */ - screenshot: 'only-on-failure', - - /* Record video only when test fails */ - video: 'retain-on-failure', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - - /* Test against mobile viewports. */ - { - name: 'Mobile Chrome', - use: { ...devices['Pixel 5'] }, - }, - { - name: 'Mobile Safari', - use: { ...devices['iPhone 12'] }, - }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npm run dev', - url: 'http://localhost:3000', - reuseExistingServer: !process.env.CI, - timeout: 120000, - }, - - /* Global setup and teardown */ - globalSetup: require.resolve('./tests/global-setup.ts'), - - /* Test timeout */ - timeout: 30000, - expect: { - /* Maximum time expect() should wait for the condition to be met */ - timeout: 10000 - }, - - /* Output directories */ - outputDir: 'test-results/artifacts', -}); \ No newline at end of file diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml new file mode 100644 index 0000000..59a29f4 --- /dev/null +++ b/dashboard/pnpm-lock.yaml @@ -0,0 +1,3204 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/postcss': + specifier: ^4.1.16 + version: 4.1.16 + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.2.1(rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@tanstack/react-router': + specifier: ^1.147.3 + version: 1.163.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) + lucide-react: + specifier: ^0.562.0 + version: 0.562.0(react@19.2.0) + maplibre-gl: + specifier: ^5.15.0 + version: 5.15.0 + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + ol: + specifier: ^10.6.1 + version: 10.6.1 + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + recharts: + specifier: ^3.3.0 + version: 3.3.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react-is@16.13.1)(react@19.2.0)(redux@5.0.1) + swr: + specifier: ^2.3.8 + version: 2.4.0(react@19.2.0) + tailwind-merge: + specifier: ^3.4.0 + version: 3.5.0 + devDependencies: + '@biomejs/biome': + specifier: 2.3.2 + version: 2.3.2 + '@tanstack/react-router-devtools': + specifier: ^1.149.0 + version: 1.163.2(@tanstack/react-router@1.163.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.163.2)(csstype@3.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/router-plugin': + specifier: ^1.149.0 + version: 1.163.2(@tanstack/react-router@1.163.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@types/node': + specifier: ^24.10.15 + version: 24.10.15 + '@types/react': + specifier: ^19.2.7 + version: 19.2.7 + '@types/react-dom': + specifier: ^19 + version: 19.1.9(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.2 + version: 5.1.4(rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + tailwindcss: + specifier: ^4.1.18 + version: 4.2.1 + typescript: + specifier: ^5 + version: 5.9.2 + vite: + specifier: npm:rolldown-vite@latest + version: rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.3.2': + resolution: {integrity: sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.2': + resolution: {integrity: sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.2': + resolution: {integrity: sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.2': + resolution: {integrity: sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.3.2': + resolution: {integrity: sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.3.2': + resolution: {integrity: sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.3.2': + resolution: {integrity: sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.3.2': + resolution: {integrity: sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.2': + resolution: {integrity: sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mapbox/geojson-rewind@0.5.2': + resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} + hasBin: true + + '@mapbox/jsonlint-lines-primitives@2.0.2': + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} + engines: {node: '>= 0.6'} + + '@mapbox/point-geometry@1.1.0': + resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} + + '@mapbox/tiny-sdf@2.0.7': + resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==} + + '@mapbox/unitbezier@0.0.1': + resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} + + '@mapbox/vector-tile@2.0.4': + resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==} + + '@mapbox/whoots-js@3.1.0': + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} + engines: {node: '>=6.0.0'} + + '@maplibre/maplibre-gl-style-spec@24.4.1': + resolution: {integrity: sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==} + hasBin: true + + '@maplibre/mlt@1.1.2': + resolution: {integrity: sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==} + + '@maplibre/vt-pbf@4.2.0': + resolution: {integrity: sha512-bxrk/kQUwWXZgmqYgwOCnZCMONCRi3MJMqJdza4T3E4AeR5i+VyMnaJ8iDWtWxdfEAJRtrzIOeJtxZSy5mFrFA==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@oxc-project/runtime@0.101.0': + resolution: {integrity: sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.101.0': + resolution: {integrity: sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==} + + '@petamoriken/float16@3.9.2': + resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} + + '@reduxjs/toolkit@2.9.0': + resolution: {integrity: sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@rolldown/binding-android-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.53': + resolution: {integrity: sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.53': + resolution: {integrity: sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': + resolution: {integrity: sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': + resolution: {integrity: sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': + resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': + resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': + resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': + resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': + resolution: {integrity: sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': + resolution: {integrity: sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': + resolution: {integrity: sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@tailwindcss/node@4.1.16': + resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.1.16': + resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.16': + resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.16': + resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.16': + resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.16': + resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==} + engines: {node: '>= 10'} + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.1.16': + resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==} + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/history@1.161.4': + resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==} + engines: {node: '>=20.19'} + + '@tanstack/react-router-devtools@1.163.2': + resolution: {integrity: sha512-gk/tC+vx8eoNNIM27vfb/bZTXQjpopw7tZA4WkRQWLh9A8PG3V6QjMQysbPcRRO5m7KtdCbTk51ZG4ERi0J1kA==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/react-router': ^1.163.2 + '@tanstack/router-core': ^1.163.2 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + peerDependenciesMeta: + '@tanstack/router-core': + optional: true + + '@tanstack/react-router@1.163.2': + resolution: {integrity: sha512-1LosUlpL2mRMWxUZXmkEg5+Br5P5j9TrLngqRgHVbZoFkjnbcj1x9fQN2OVLrBv9Npw97NRsHeJljnAH/c7oSw==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.1': + resolution: {integrity: sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.163.2': + resolution: {integrity: sha512-mD0Pav6kcpS317XSJN+wCZaxLLngDhlwgzPNca56dWCp8YKPEvhhj/Zdl+LdRlJQ2VJ5BOy7FbOV1hErc9Nj5Q==} + engines: {node: '>=20.19'} + + '@tanstack/router-devtools-core@1.163.2': + resolution: {integrity: sha512-IrbSK30AtMOgCLXTbvhnVsU6BGjhwB8EjfZIQLtUPDvFsP0RH3/2ZiWRA3a0EsQhxkl+fxIVByVP7wgyRzkZPQ==} + engines: {node: '>=20.19'} + peerDependencies: + '@tanstack/router-core': ^1.163.2 + csstype: ^3.0.10 + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-generator@1.163.2': + resolution: {integrity: sha512-6LjU3+8iKEgt8iOaYCmCnQCs0jsOhc7z8fa1yAYlj3s82uYWv3g5CB9mwv8wZXblXBQWOl+hW4PI6WNjP/CK9w==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.163.2': + resolution: {integrity: sha512-SrVILMz/c15RYWxIMG+bf/glLbP/O9DUxOg0E7bo9pooBxGPvgWSlEzHNjhVekLhK5l7fiuQZzKsfksVeIEqDA==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + '@tanstack/react-router': ^1.163.2 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.10 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.161.4': + resolution: {integrity: sha512-r8TpjyIZoqrXXaf2DDyjd44gjGBoyE+/oEaaH68yLI9ySPO1gUWmQENZ1MZnmBnpUGN24NOZxdjDLc8npK0SAw==} + engines: {node: '>=20.19'} + + '@tanstack/store@0.9.1': + resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} + + '@tanstack/virtual-file-routes@1.161.4': + resolution: {integrity: sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w==} + engines: {node: '>=20.19'} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/geojson-vt@3.2.5': + resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/node@24.10.15': + resolution: {integrity: sha512-BgjLoRuSr0MTI5wA6gMw9Xy0sFudAaUuvrnjgGx9wZ522fYYLA5SYJ+1Y30vTcJEG+DRCyDHx/gzQVfofYzSdg==} + + '@types/rbush@4.0.0': + resolution: {integrity: sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==} + + '@types/react-dom@19.1.9': + resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + + '@types/supercluster@7.1.3': + resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@vitejs/plugin-react@5.1.4': + resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.26.2: + resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001743: + resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.0: + resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==} + engines: {node: '>=8'} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + earcut@3.0.2: + resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} + + electron-to-chromium@1.5.221: + resolution: {integrity: sha512-/1hFJ39wkW01ogqSyYoA4goOXOtMRy6B+yvA1u42nnsEGtHzIzmk93aPISumVQeblj47JUHLC9coCjUxb1EvtQ==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + + es-toolkit@1.39.10: + resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + geojson-vt@4.0.2: + resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} + + geotiff@2.1.3: + resolution: {integrity: sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==} + engines: {node: '>=10.19'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + immer@10.1.3: + resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isbot@5.1.35: + resolution: {integrity: sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg==} + engines: {node: '>=18'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-stringify-pretty-compact@4.0.0: + resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kdbush@4.0.2: + resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} + + lerc@3.0.0: + resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.562.0: + resolution: {integrity: sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + maplibre-gl@5.15.0: + resolution: {integrity: sha512-pPeu/t4yPDX/+Uf9ibLUdmaKbNMlGxMAX+tBednYukol2qNk2TZXAlhdohWxjVvTO3is8crrUYv3Ok02oAaKzA==} + engines: {node: '>=16.14.0', npm: '>=8.1.0'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + murmurhash-js@1.0.0: + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + ol@10.6.1: + resolution: {integrity: sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + parse-headers@2.0.6: + resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pbf@4.0.1: + resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + potpack@2.1.0: + resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + protocol-buffers-schema@3.6.0: + resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} + + quick-lru@6.1.2: + resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} + engines: {node: '>=12'} + + quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + + rbush@4.0.1: + resolution: {integrity: sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==} + + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + recharts@3.3.0: + resolution: {integrity: sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + + rolldown-vite@7.3.1: + resolution: {integrity: sha512-LYzdNAjRHhF2yA4JUQm/QyARyi216N2rpJ0lJZb8E9FU2y5v6Vk+xq/U4XBOxMefpWixT5H3TslmAHm1rqIq2w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + rolldown@1.0.0-beta.53: + resolution: {integrity: sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + seroval-plugins@1.5.0: + resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.0: + resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} + engines: {node: '>=10'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + supercluster@8.0.1: + resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} + + swr@2.4.0: + resolution: {integrity: sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.1.16: + resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyqueue@3.0.0: + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + xml-utils@1.10.2: + resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zstddec@0.1.0: + resolution: {integrity: sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@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 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@2.3.2': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.2 + '@biomejs/cli-darwin-x64': 2.3.2 + '@biomejs/cli-linux-arm64': 2.3.2 + '@biomejs/cli-linux-arm64-musl': 2.3.2 + '@biomejs/cli-linux-x64': 2.3.2 + '@biomejs/cli-linux-x64-musl': 2.3.2 + '@biomejs/cli-win32-arm64': 2.3.2 + '@biomejs/cli-win32-x64': 2.3.2 + + '@biomejs/cli-darwin-arm64@2.3.2': + optional: true + + '@biomejs/cli-darwin-x64@2.3.2': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.2': + optional: true + + '@biomejs/cli-linux-arm64@2.3.2': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.2': + optional: true + + '@biomejs/cli-linux-x64@2.3.2': + optional: true + + '@biomejs/cli-win32-arm64@2.3.2': + optional: true + + '@biomejs/cli-win32-x64@2.3.2': + optional: true + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mapbox/geojson-rewind@0.5.2': + dependencies: + get-stream: 6.0.1 + minimist: 1.2.8 + + '@mapbox/jsonlint-lines-primitives@2.0.2': {} + + '@mapbox/point-geometry@1.1.0': {} + + '@mapbox/tiny-sdf@2.0.7': {} + + '@mapbox/unitbezier@0.0.1': {} + + '@mapbox/vector-tile@2.0.4': + dependencies: + '@mapbox/point-geometry': 1.1.0 + '@types/geojson': 7946.0.16 + pbf: 4.0.1 + + '@mapbox/whoots-js@3.1.0': {} + + '@maplibre/maplibre-gl-style-spec@24.4.1': + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/unitbezier': 0.0.1 + json-stringify-pretty-compact: 4.0.0 + minimist: 1.2.8 + quickselect: 3.0.0 + rw: 1.3.3 + tinyqueue: 3.0.0 + + '@maplibre/mlt@1.1.2': + dependencies: + '@mapbox/point-geometry': 1.1.0 + + '@maplibre/vt-pbf@4.2.0': + dependencies: + '@mapbox/point-geometry': 1.1.0 + '@mapbox/vector-tile': 2.0.4 + '@types/geojson-vt': 3.2.5 + '@types/supercluster': 7.1.3 + geojson-vt: 4.0.2 + pbf: 4.0.1 + supercluster: 8.0.1 + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/runtime@0.101.0': {} + + '@oxc-project/types@0.101.0': {} + + '@petamoriken/float16@3.9.2': {} + + '@reduxjs/toolkit@2.9.0(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1))(react@19.2.0)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 10.1.3 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.0 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1) + + '@rolldown/binding-android-arm64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.53': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.53': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.53': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.53': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.53': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@standard-schema/spec@1.0.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@tailwindcss/node@4.1.16': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.16 + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.1.16': + optional: true + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.16': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.16': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.16': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.1.16': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-x64': 4.1.16 + '@tailwindcss/oxide-freebsd-x64': 4.1.16 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.16 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-x64-musl': 4.1.16 + '@tailwindcss/oxide-wasm32-wasi': 4.1.16 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/postcss@4.1.16': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 + postcss: 8.5.6 + tailwindcss: 4.1.16 + + '@tailwindcss/vite@4.2.1(rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + + '@tanstack/history@1.161.4': {} + + '@tanstack/react-router-devtools@1.163.2(@tanstack/react-router@1.163.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.163.2)(csstype@3.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/react-router': 1.163.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/router-devtools-core': 1.163.2(@tanstack/router-core@1.163.2)(csstype@3.2.3) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@tanstack/router-core': 1.163.2 + transitivePeerDependencies: + - csstype + + '@tanstack/react-router@1.163.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/history': 1.161.4 + '@tanstack/react-store': 0.9.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/router-core': 1.163.2 + isbot: 5.1.35 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-store@0.9.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/store': 0.9.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) + + '@tanstack/router-core@1.163.2': + dependencies: + '@tanstack/history': 1.161.4 + '@tanstack/store': 0.9.1 + cookie-es: 2.0.0 + seroval: 1.5.0 + seroval-plugins: 1.5.0(seroval@1.5.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/router-devtools-core@1.163.2(@tanstack/router-core@1.163.2)(csstype@3.2.3)': + dependencies: + '@tanstack/router-core': 1.163.2 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + tiny-invariant: 1.3.3 + optionalDependencies: + csstype: 3.2.3 + + '@tanstack/router-generator@1.163.2': + dependencies: + '@tanstack/router-core': 1.163.2 + '@tanstack/router-utils': 1.161.4 + '@tanstack/virtual-file-routes': 1.161.4 + prettier: 3.8.1 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.21.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.163.2(@tanstack/react-router@1.163.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.28.5) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@tanstack/router-core': 1.163.2 + '@tanstack/router-generator': 1.163.2 + '@tanstack/router-utils': 1.161.4 + '@tanstack/virtual-file-routes': 1.161.4 + chokidar: 3.6.0 + unplugin: 2.3.11 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.163.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + vite: rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.161.4': + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + ansis: 4.2.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.3 + pathe: 2.0.3 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.9.1': {} + + '@tanstack/virtual-file-routes@1.161.4': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/geojson-vt@3.2.5': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/geojson@7946.0.16': {} + + '@types/node@24.10.15': + dependencies: + undici-types: 7.16.0 + + '@types/rbush@4.0.0': {} + + '@types/react-dom@19.1.9(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + + '@types/react@19.2.7': + dependencies: + csstype: 3.2.3 + + '@types/supercluster@7.1.3': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/use-sync-external-store@0.0.6': {} + + '@vitejs/plugin-react@5.1.4(rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + + acorn@8.15.0: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.26.2 + caniuse-lite: 1.0.30001743 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.10.0: {} + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.26.2: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001743 + electron-to-chromium: 1.5.221 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.2) + + caniuse-lite@1.0.30001743: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + + clsx@2.1.1: {} + + convert-source-map@2.0.0: {} + + cookie-es@2.0.0: {} + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + dequal@2.0.3: {} + + detect-libc@2.1.0: {} + + diff@8.0.3: {} + + earcut@3.0.2: {} + + electron-to-chromium@1.5.221: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + es-toolkit@1.39.10: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + esprima@4.0.1: {} + + eventemitter3@5.0.1: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fraction.js@4.3.7: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + geojson-vt@4.0.2: {} + + geotiff@2.1.3: + dependencies: + '@petamoriken/float16': 3.9.2 + lerc: 3.0.0 + pako: 2.1.0 + parse-headers: 2.0.6 + quick-lru: 6.1.2 + web-worker: 1.5.0 + xml-utils: 1.10.2 + zstddec: 0.1.0 + + get-stream@6.0.1: {} + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + gl-matrix@3.4.4: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + + graceful-fs@4.2.11: {} + + immer@10.1.3: {} + + internmap@2.0.3: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isbot@5.1.35: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-stringify-pretty-compact@4.0.0: {} + + json5@2.2.3: {} + + kdbush@4.0.2: {} + + lerc@3.0.0: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.0 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.0 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.562.0(react@19.2.0): + dependencies: + react: 19.2.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + maplibre-gl@5.15.0: + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/point-geometry': 1.1.0 + '@mapbox/tiny-sdf': 2.0.7 + '@mapbox/unitbezier': 0.0.1 + '@mapbox/vector-tile': 2.0.4 + '@mapbox/whoots-js': 3.1.0 + '@maplibre/maplibre-gl-style-spec': 24.4.1 + '@maplibre/mlt': 1.1.2 + '@maplibre/vt-pbf': 4.2.0 + '@types/geojson': 7946.0.16 + '@types/geojson-vt': 3.2.5 + '@types/supercluster': 7.1.3 + earcut: 3.0.2 + geojson-vt: 4.0.2 + gl-matrix: 3.4.4 + kdbush: 4.0.2 + murmurhash-js: 1.0.0 + pbf: 4.0.1 + potpack: 2.1.0 + quickselect: 3.0.0 + supercluster: 8.0.1 + tinyqueue: 3.0.0 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + murmurhash-js@1.0.0: {} + + nanoid@3.3.11: {} + + next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + node-releases@2.0.21: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + ol@10.6.1: + dependencies: + '@types/rbush': 4.0.0 + earcut: 3.0.2 + geotiff: 2.1.3 + pbf: 4.0.1 + rbush: 4.0.1 + + pako@2.1.0: {} + + parse-headers@2.0.6: {} + + pathe@2.0.3: {} + + pbf@4.0.1: + dependencies: + resolve-protobuf-schema: 2.1.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + potpack@2.1.0: {} + + prettier@3.8.1: {} + + protocol-buffers-schema@3.6.0: {} + + quick-lru@6.1.2: {} + + quickselect@3.0.0: {} + + rbush@4.0.1: + dependencies: + quickselect: 3.0.0 + + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.0 + use-sync-external-store: 1.5.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + redux: 5.0.1 + + react-refresh@0.18.0: {} + + react@19.2.0: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + recharts@3.3.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react-is@16.13.1)(react@19.2.0)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.9.0(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1))(react@19.2.0) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.39.10 + eventemitter3: 5.0.1 + immer: 10.1.3 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.5.0(react@19.2.0) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + + reselect@5.1.1: {} + + resolve-pkg-maps@1.0.0: {} + + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.0 + + rolldown-vite@7.3.1(@types/node@24.10.15)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0): + dependencies: + '@oxc-project/runtime': 0.101.0 + fdir: 6.5.0(picomatch@4.0.3) + lightningcss: 1.30.2 + picomatch: 4.0.3 + postcss: 8.5.6 + rolldown: 1.0.0-beta.53 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.15 + esbuild: 0.27.3 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.21.0 + + rolldown@1.0.0-beta.53: + dependencies: + '@oxc-project/types': 0.101.0 + '@rolldown/pluginutils': 1.0.0-beta.53 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.53 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.53 + '@rolldown/binding-darwin-x64': 1.0.0-beta.53 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.53 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.53 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.53 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.53 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.53 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.53 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.53 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.53 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.53 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.53 + + rw@1.3.3: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + seroval-plugins@1.5.0(seroval@1.5.0): + dependencies: + seroval: 1.5.0 + + seroval@1.5.0: {} + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + supercluster@8.0.1: + dependencies: + kdbush: 4.0.2 + + swr@2.4.0(react@19.2.0): + dependencies: + dequal: 2.0.3 + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + + tailwind-merge@3.5.0: {} + + tailwindcss@4.1.16: {} + + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyqueue@3.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.2: {} + + undici-types@7.16.0: {} + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.1.3(browserslist@4.26.2): + dependencies: + browserslist: 4.26.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.5.0(react@19.2.0): + dependencies: + react: 19.2.0 + + use-sync-external-store@1.6.0(react@19.2.0): + dependencies: + react: 19.2.0 + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + web-worker@1.5.0: {} + + webpack-virtual-modules@0.6.2: {} + + xml-utils@1.10.2: {} + + yallist@3.1.1: {} + + zod@3.25.76: {} + + zstddec@0.1.0: {} diff --git a/dashboard/src/main.tsx b/dashboard/src/main.tsx new file mode 100644 index 0000000..10926c7 --- /dev/null +++ b/dashboard/src/main.tsx @@ -0,0 +1,26 @@ +import { createRouter, RouterProvider } from "@tanstack/react-router"; +import ReactDOM from "react-dom/client"; +import "./styles.css"; +import { routeTree } from "./routeTree.gen"; + +// Set up a Router instance +const router = createRouter({ + routeTree, + defaultPreload: "intent", + scrollRestoration: true, + basepath: import.meta.env.BASE_URL.replace(/\/$/, "") || "/", +}); + +// Register things for typesafety +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +const rootElement = document.getElementById("app")!; + +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + root.render(); +} diff --git a/dashboard/src/routeTree.gen.ts b/dashboard/src/routeTree.gen.ts new file mode 100644 index 0000000..d03e0d1 --- /dev/null +++ b/dashboard/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SessionIdRouteImport } from './routes/$sessionId' +import { Route as IndexRouteImport } from './routes/index' + +const SessionIdRoute = SessionIdRouteImport.update({ + id: '/$sessionId', + path: '/$sessionId', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/$sessionId': typeof SessionIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/$sessionId': typeof SessionIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/$sessionId': typeof SessionIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/$sessionId' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/$sessionId' + id: '__root__' | '/' | '/$sessionId' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + SessionIdRoute: typeof SessionIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/$sessionId': { + id: '/$sessionId' + path: '/$sessionId' + fullPath: '/$sessionId' + preLoaderRoute: typeof SessionIdRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + SessionIdRoute: SessionIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/dashboard/src/routes/$sessionId.tsx b/dashboard/src/routes/$sessionId.tsx new file mode 100644 index 0000000..8fd7b8a --- /dev/null +++ b/dashboard/src/routes/$sessionId.tsx @@ -0,0 +1,149 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; +import useSWR from "swr"; +import TelemetryPage from "../../components/TelemetryPage"; +import { processIRacingDataWithGPS, type TelemetryRes } from "../../lib/Fetch"; + +export const Route = createFileRoute("/$sessionId")({ + component: SessionPage, + validateSearch: ( + search: Record, + ): { + lapId: string; + } => ({ + lapId: (search?.lapId as string) || "1", + }), +}); + +export const fetcher = (url: string) => + fetch(url, {headers: { + Accept: "application/json", + "Accept-Encoding": "br", + "Content-Type": "application/json" + }}).then(async (res) => { + return processIRacingDataWithGPS(await res.json()); + }); + +const lapsFetcher = (url: string) => + fetch(url).then((res) => { + return res.json() as unknown as Array; + }); + +export default function SessionPage() { + const { sessionId } = Route.useParams(); + const { lapId } = Route.useSearch(); + + const { + data: telemetryData, + error, + isLoading, + } = useSWR( + `/api/sessions/${sessionId}/laps/${lapId}`, + fetcher, + ); + + const { data: availableLaps } = useSWR, Error>( + `/api/sessions/${sessionId}/laps`, + lapsFetcher, + ); + + // Default to lap 1 if no lapId provided + const currentLapId = lapId ? Number.parseInt(lapId, 10) : 1; + + + if (error) return ; + if (isLoading || telemetryData === undefined) + return ; + + return ( + + ); +} + +function DatabaseUnavailableError() { + return ( +
+
+
+
+ + Error + + +
+
+

+ Database Unavailable +

+

+ The telemetry database is not running or accessible. Please start the + Docker Compose stack to access telemetry data. +

+
+

+ To start the system: +

+ + docker compose up -d + +
+
+ + ← Back to Dashboard + +
+
+
+ ); +} + +function TelemetryLoadingSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/dashboard/src/routes/-layout.tsx b/dashboard/src/routes/-layout.tsx new file mode 100644 index 0000000..62de226 --- /dev/null +++ b/dashboard/src/routes/-layout.tsx @@ -0,0 +1,18 @@ +import { ThemeProvider } from "../../components/theme-provider"; +import "./globals.css"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} diff --git a/dashboard/src/routes/__root.tsx b/dashboard/src/routes/__root.tsx new file mode 100644 index 0000000..c7d949c --- /dev/null +++ b/dashboard/src/routes/__root.tsx @@ -0,0 +1,13 @@ +import { createRootRoute, Outlet } from "@tanstack/react-router"; + +export const Route = createRootRoute({ + component: DashboardPage, +}); + +export default function DashboardPage() { + return ( +
+ +
+ ); +} diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx new file mode 100644 index 0000000..cb7b6e1 --- /dev/null +++ b/dashboard/src/routes/index.tsx @@ -0,0 +1,193 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; +import useSWR from "swr"; +import SessionSelector, { + type Session, +} from "../../components/SessionSelector"; + +// import { fetcher } from "@/lib/Fetch"; + +const fetcher = (url: string) => + fetch(url).then((res) => res.json() as unknown as Session[]); + +export const Route = createFileRoute("/")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { + data: sessions, + error: errorMessage, + isLoading, + } = useSWR("/api/sessions", fetcher); + + if (isLoading) return
Loading...
; + return ( + <> +
+
+ +
+
+
+
+
+

iRacing

+

Telemetry

+
+
+ +
+ + + +
+
+
+ + {errorMessage ? "Offline" : "Connected"} + +
+
+
+
+
+
+ Dashboard + / + Sessions +
+
+ +
+
+

Sessions

+

+ Manage and analyze your telemetry sessions +

+
+ +
+ {sessions === undefined ? ( +
+
+
+
+
+
+
+
+

+ Database Connection Error +

+

+ The telemetry database is not running. Start the Docker + Compose stack to access telemetry data. +

+
+

+ To start the system: +

+ + docker compose up -d + +

+ This will start QuestDB, RabbitMQ, and all required + services. +

+
+
+ + Show technical error details + +
+ + {errorMessage?.message} + +
+
+
+
+
+ ) : sessions.length > 0 ? ( + + ) : ( +
+
+
+
+

+ No sessions found +

+

+ Import telemetry data to get started with session analysis. +

+
+ )} + + {sessions !== undefined && sessions.length > 0 && ( +
+
+
+

+ Database +

+
+
+

+ {errorMessage ? "Offline" : "Online"} +

+

QuestDB Connection

+
+ +
+
+

+ Sessions +

+
+
+

+ {sessions.length} +

+

+ Available for analysis +

+
+ +
+
+

+ Processing +

+
+
+

+ Active +

+

Runtime dynamic

+
+
+ )} +
+
+
+ + ); +} diff --git a/dashboard/app/globals.css b/dashboard/src/styles.css similarity index 93% rename from dashboard/app/globals.css rename to dashboard/src/styles.css index 1232af8..62ef5f2 100644 --- a/dashboard/app/globals.css +++ b/dashboard/src/styles.css @@ -57,6 +57,13 @@ --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; } + + .maplibregl-popup-content { + @apply bg-transparent! shadow-none! p-0! rounded-none!; + } + .maplibregl-popup-tip { + @apply hidden!; + } } @layer base { @@ -86,5 +93,5 @@ .ol-compass, .ol-zoom-in, .ol-zoom-out { - display: none !important; + display: none; } diff --git a/dashboard/tailwind.config.ts b/dashboard/tailwind.config.ts index dd18935..eba76dd 100644 --- a/dashboard/tailwind.config.ts +++ b/dashboard/tailwind.config.ts @@ -73,7 +73,6 @@ const config = { }, }, }, - plugins: [require("tailwindcss-animate")], } satisfies Config; export default config; diff --git a/dashboard/tests/e2e/homepage.spec.ts b/dashboard/tests/e2e/homepage.spec.ts deleted file mode 100644 index 8a54162..0000000 --- a/dashboard/tests/e2e/homepage.spec.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { DashboardHelpers, PerformanceHelpers } from '../utils/test-helpers'; - -test.describe('IRacing Telemetry Dashboard - Homepage', () => { - let dashboardHelpers: DashboardHelpers; - let performanceHelpers: PerformanceHelpers; - - test.beforeEach(async ({ page }) => { - dashboardHelpers = new DashboardHelpers(page); - performanceHelpers = new PerformanceHelpers(page); - }); - - test.describe('Layout and Design', () => { - test('should display the new Vercel-inspired homepage layout', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForDashboardReady(); - - // Check main layout structure - await expect(page.locator('.min-h-screen.bg-black')).toBeVisible(); - - // Check header with breadcrumbs - const header = page.locator('header'); - await expect(header).toBeVisible(); - await expect(header.locator('h1')).toContainText('iRacing Telemetry'); - await expect(header.locator('p')).toContainText('Dashboard / Sessions'); - - // Check connection status indicator in header - const connectionStatus = header.locator('[data-connection-status]'); - await expect(connectionStatus).toBeVisible(); - - // Check main content area - const main = page.locator('main'); - await expect(main).toBeVisible(); - await expect(main.locator('h2')).toContainText('Select Session'); - }); - - test('should display system status cards with proper styling', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForDashboardReady(); - - // Check system status section exists - const statusSection = page.locator('text=System Status').locator('..'); - await expect(statusSection).toBeVisible(); - - // Check for three status cards - const statusCards = page.locator('.border.border-gray-800.bg-gray-950\\/50.rounded-lg.p-4'); - await expect(statusCards).toHaveCount(3); - - // Verify card content structure - const firstCard = statusCards.first(); - await expect(firstCard.locator('h3')).toBeVisible(); - await expect(firstCard.locator('.h-2.w-2.rounded-full')).toBeVisible(); // Status dot - await expect(firstCard.locator('.text-xs.text-gray-500')).toBeVisible(); // Description - }); - - test('should handle responsive design across different screen sizes', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - - const responsiveResults = await dashboardHelpers.testResponsiveBreakpoints(); - - // Verify no horizontal overflow on any breakpoint - responsiveResults.forEach(result => { - expect(result.isResponsive, `Layout should be responsive at ${result.name} (${result.width}px)`).toBe(true); - }); - - // Test specific responsive behaviors - await page.setViewportSize({ width: 375, height: 667 }); // Mobile - await expect(page.locator('.lg\\:grid-cols-3')).toBeVisible(); // Grid should be responsive - - await page.setViewportSize({ width: 1024, height: 768 }); // Desktop - const sidebar = page.locator('.hidden.lg\\:block.w-64'); - await expect(sidebar).toBeVisible(); // Sidebar space should be visible on large screens - }); - }); - - test.describe('Session Selection Functionality', () => { - test('should display session cards when sessions are available', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForSessionsToLoad(); - - // Check if we have sessions or appropriate message - const hasError = await dashboardHelpers.hasErrorMessage(); - - if (!hasError) { - // If no error, we should either have session cards or "no sessions" message - const sessionCards = page.locator('[data-testid="session-card"]'); - const noSessionsMessage = page.locator('text=No sessions found'); - - const sessionCardCount = await sessionCards.count(); - const hasNoSessionsMessage = await noSessionsMessage.isVisible(); - - expect(sessionCardCount > 0 || hasNoSessionsMessage).toBeTruthy(); - - if (sessionCardCount > 0) { - // Test session card structure - const firstCard = sessionCards.first(); - await expect(firstCard.locator('h4')).toBeVisible(); // Session ID - await expect(firstCard.locator('.h-2.w-2.rounded-full')).toBeVisible(); // Status dot - await expect(firstCard.locator('text=Track:')).toBeVisible(); - await expect(firstCard.locator('text=Date:')).toBeVisible(); - } - } - }); - - test('should handle session selection and navigation', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForSessionsToLoad(); - - const hasError = await dashboardHelpers.hasErrorMessage(); - - if (!hasError) { - const sessionCards = page.locator('button[class*="border-gray-800"][class*="bg-gray-950"]'); - const cardCount = await sessionCards.count(); - - if (cardCount > 0) { - // Click on first session card - await sessionCards.first().click(); - - // Should navigate to session page (this would need actual session data) - // For now, we'll just verify the click doesn't cause errors - await page.waitForLoadState('networkidle'); - - // Check for navigation or stay on page with updated state - const currentUrl = page.url(); - expect(currentUrl).toBeTruthy(); - } - } - }); - - test('should display legacy dropdown when expanded', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForSessionsToLoad(); - - // Find and click the legacy view toggle - const legacyToggle = page.locator('summary:has-text("Show all sessions")'); - await expect(legacyToggle).toBeVisible(); - - await legacyToggle.click(); - - // Check that dropdown becomes visible - const dropdown = page.locator('select[name="selectSession"]'); - await expect(dropdown).toBeVisible(); - }); - }); - - test.describe('Error Handling', () => { - test('should display proper error message when database is unreachable', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForSessionsToLoad(); - - const hasError = await dashboardHelpers.hasErrorMessage(); - - if (hasError) { - // Check error message structure - const errorContainer = page.locator('.border-red-800.bg-red-950'); - await expect(errorContainer).toBeVisible(); - - // Check error content - await expect(errorContainer.locator('text=Database Connection Error')).toBeVisible(); - await expect(errorContainer.locator('text=Unable to connect to QuestDB')).toBeVisible(); - - // Check troubleshooting section - await expect(errorContainer.locator('text=Troubleshooting:')).toBeVisible(); - await expect(errorContainer.locator('text=Ensure QuestDB container is running')).toBeVisible(); - - // Check collapsible error details - const errorDetails = errorContainer.locator('details'); - await expect(errorDetails).toBeVisible(); - - await errorDetails.locator('summary').click(); - await expect(errorDetails.locator('code')).toBeVisible(); - } - }); - - test('should display "no sessions" message when database is empty', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForSessionsToLoad(); - - const noSessionsMessage = page.locator('text=No sessions found'); - const hasNoSessions = await noSessionsMessage.isVisible(); - - if (hasNoSessions) { - const container = noSessionsMessage.locator('..'); - await expect(container.locator('text=Make sure telemetry data has been imported')).toBeVisible(); - - // Check for empty state icon - const emptyIcon = container.locator('.w-12.h-12'); - await expect(emptyIcon).toBeVisible(); - } - }); - }); - - test.describe('Performance', () => { - test('should load within acceptable time limits', async ({ page }) => { - const metrics = await performanceHelpers.measurePageLoad(); - - // Basic performance assertions - expect(metrics.totalLoadTime).toBeLessThan(5000); // 5 seconds max load time - expect(metrics.fcp).toBeLessThan(2500); // First contentful paint under 2.5s - - // Log performance metrics for monitoring - console.log('Performance metrics:', metrics); - }); - - test('should not have console errors during normal operation', async ({ page }) => { - const consoleErrors = await performanceHelpers.getConsoleErrors(); - - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForDashboardReady(); - - // Filter out expected warnings/errors (like 404s for missing data) - const criticalErrors = consoleErrors.filter(error => - !error.includes('404') && - !error.includes('Failed to fetch') && - !error.includes('QuestDB') - ); - - expect(criticalErrors.length).toBe(0); - }); - }); - - test.describe('Accessibility', () => { - test('should have proper heading hierarchy', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForDashboardReady(); - - // Check heading structure - const h1 = page.locator('h1'); - await expect(h1).toHaveCount(1); - await expect(h1).toContainText('iRacing Telemetry'); - - const h2Elements = page.locator('h2'); - await expect(h2Elements.first()).toContainText('Select Session'); - - // Verify other headings exist and are properly structured - const h3Elements = page.locator('h3'); - expect(await h3Elements.count()).toBeGreaterThan(0); - }); - - test('should have proper color contrast for text', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForDashboardReady(); - - // Check main text colors are visible - const mainText = page.locator('.text-white'); - await expect(mainText.first()).toBeVisible(); - - const grayText = page.locator('.text-gray-400'); - await expect(grayText.first()).toBeVisible(); - }); - - test('should support keyboard navigation', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForDashboardReady(); - - // Test tab navigation through interactive elements - await page.keyboard.press('Tab'); - - // Check that focus is visible on interactive elements - const focusedElement = page.locator(':focus'); - await expect(focusedElement).toBeVisible(); - }); - }); - - test.describe('Visual Regression', () => { - test('should match homepage visual snapshot', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForDashboardReady(); - - // Wait for any animations to settle - await page.waitForTimeout(1000); - - // Take full page screenshot for visual comparison - await expect(page).toHaveScreenshot('homepage.png', { - fullPage: true, - threshold: 0.2, - animations: 'disabled' - }); - }); - - test('should handle different session states visually', async ({ page }) => { - await dashboardHelpers.navigateToHomepage(); - await dashboardHelpers.waitForSessionsToLoad(); - - // Test different states: error, empty, with sessions - await page.waitForTimeout(1000); - - await expect(page.locator('main')).toHaveScreenshot('session-state.png', { - threshold: 0.2, - animations: 'disabled' - }); - }); - }); -}); \ No newline at end of file diff --git a/dashboard/tests/e2e/session-analysis.spec.ts b/dashboard/tests/e2e/session-analysis.spec.ts deleted file mode 100644 index 738b384..0000000 --- a/dashboard/tests/e2e/session-analysis.spec.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { DashboardHelpers } from '../utils/test-helpers'; - -test.describe('Session Analysis Page', () => { - let dashboardHelpers: DashboardHelpers; - - test.beforeEach(async ({ page }) => { - dashboardHelpers = new DashboardHelpers(page); - }); - - test.describe('Session Page Layout', () => { - test('should display telemetry page when session exists', async ({ page }) => { - // Navigate to a mock session (this would need actual test data) - await page.goto('/test-session-1?lapId=1'); - await page.waitForLoadState('networkidle'); - - // Check for telemetry page elements (adjust selectors based on actual implementation) - const pageTitle = page.locator('h1'); - await expect(pageTitle).toContainText('iRacing Telemetry Dashboard'); - - // Check for main components that should exist - const trackMap = page.locator('[data-testid="track-map"]'); - const telemetryChart = page.locator('[data-testid="telemetry-chart"]'); - - // At least one of these should be visible if session exists - const hasTrackMap = await trackMap.isVisible(); - const hasChart = await telemetryChart.isVisible(); - const hasErrorMessage = await page.locator('text=Error Loading Telemetry Data').isVisible(); - - expect(hasTrackMap || hasChart || hasErrorMessage).toBeTruthy(); - }); - - test('should handle invalid session IDs gracefully', async ({ page }) => { - await page.goto('/invalid-session-id'); - await page.waitForLoadState('networkidle'); - - // Should either redirect to homepage or show appropriate error - const currentUrl = page.url(); - const isHomepage = currentUrl.endsWith('/') || currentUrl.includes('invalid-session'); - expect(isHomepage).toBeTruthy(); - }); - }); - - test.describe('Track Map Interaction', () => { - test('should display GPS track visualization when data is available', async ({ page }) => { - await page.goto('/test-session-1?lapId=1'); - await page.waitForLoadState('networkidle'); - - // Look for track map container - const trackMapSection = page.locator('text=GPS Track Map').locator('..'); - await expect(trackMapSection).toBeVisible(); - - // Check for either track visualization or no data message - const hasTrackData = await page.locator('canvas, svg').first().isVisible(); - const hasNoDataMessage = await page.locator('text=No GPS data available').isVisible(); - - expect(hasTrackData || hasNoDataMessage).toBeTruthy(); - }); - - test('should allow interaction with track points', async ({ page }) => { - await page.goto('/test-session-1?lapId=1'); - await page.waitForLoadState('networkidle'); - - // If track visualization exists, test interaction - const trackVisualization = page.locator('canvas, svg').first(); - const isVisible = await trackVisualization.isVisible(); - - if (isVisible) { - // Test clicking on track (coordinates would need to be adjusted) - await trackVisualization.click({ position: { x: 100, y: 100 } }); - - // Should update current selection info - const selectionInfo = page.locator('text=Current Selection').locator('..'); - await expect(selectionInfo).toBeVisible(); - } - }); - }); - - test.describe('Telemetry Data Display', () => { - test('should show telemetry metrics when available', async ({ page }) => { - await page.goto('/test-session-1?lapId=1'); - await page.waitForLoadState('networkidle'); - - // Check for telemetry data sections - const dataQuality = page.locator('text=Data Quality').locator('..'); - const trackInfo = page.locator('text=Track Information').locator('..'); - - await expect(dataQuality).toBeVisible(); - await expect(trackInfo).toBeVisible(); - - // Check for GPS analysis if data exists - const gpsAnalysis = page.locator('text=GPS Track Analysis'); - const hasAnalysis = await gpsAnalysis.isVisible(); - - if (hasAnalysis) { - // Should show metrics like distance, speed, etc. - const analysisPanel = gpsAnalysis.locator('..'); - await expect(analysisPanel.locator('text=Total Distance')).toBeVisible(); - await expect(analysisPanel.locator('text=Average Speed')).toBeVisible(); - } - }); - - test('should allow metric selection and chart updates', async ({ page }) => { - await page.goto('/test-session-1?lapId=1'); - await page.waitForLoadState('networkidle'); - - // Look for metric selector dropdown - const metricSelector = page.locator('select').first(); - const isVisible = await metricSelector.isVisible(); - - if (isVisible) { - // Test changing metric selection - await metricSelector.selectOption('Throttle'); - await page.waitForTimeout(500); // Allow chart to update - - // Chart should update (this would need actual chart testing) - const chartArea = page.locator('[data-testid="telemetry-chart"]'); - await expect(chartArea).toBeVisible(); - } - }); - }); - - test.describe('Session Navigation', () => { - test('should allow lap selection', async ({ page }) => { - await page.goto('/test-session-1?lapId=1'); - await page.waitForLoadState('networkidle'); - - // Look for lap selector - const lapSelector = page.locator('select[name="sessionSelect"]'); - const isVisible = await lapSelector.isVisible(); - - if (isVisible) { - // Test lap selection - const options = await lapSelector.locator('option').count(); - if (options > 1) { - await lapSelector.selectOption({ index: 1 }); - await page.waitForLoadState('networkidle'); - - // URL should update with new lap ID - const url = page.url(); - expect(url).toContain('lapId='); - } - } - }); - - test('should maintain state during navigation', async ({ page }) => { - await page.goto('/test-session-1?lapId=1'); - await page.waitForLoadState('networkidle'); - - // Get initial state - const sessionInfo = page.locator('text=Session:').locator('..'); - const isVisible = await sessionInfo.isVisible(); - - if (isVisible) { - const initialSession = await sessionInfo.textContent(); - - // Navigate back to homepage and back to session - await page.goBack(); - await page.goForward(); - - // Session should be preserved - const finalSession = await sessionInfo.textContent(); - expect(finalSession).toBe(initialSession); - } - }); - }); - - test.describe('Error States', () => { - test('should handle missing telemetry data gracefully', async ({ page }) => { - await page.goto('/empty-session?lapId=1'); - await page.waitForLoadState('networkidle'); - - // Should show appropriate empty state - const errorMessage = page.locator('text=Error Loading Telemetry Data'); - const noDataMessage = page.locator('text=No GPS data available'); - - const hasError = await errorMessage.isVisible(); - const hasNoData = await noDataMessage.isVisible(); - - expect(hasError || hasNoData).toBeTruthy(); - }); - - test('should provide useful error messages', async ({ page }) => { - await page.goto('/error-session?lapId=1'); - await page.waitForLoadState('networkidle'); - - // Look for error message with helpful context - const errorElements = page.locator('[class*="bg-red"], text*="Error"'); - const count = await errorElements.count(); - - if (count > 0) { - const errorText = await errorElements.first().textContent(); - expect(errorText).toBeTruthy(); - expect(errorText!.length).toBeGreaterThan(10); // Should have meaningful message - } - }); - }); -}); \ No newline at end of file diff --git a/dashboard/tests/fixtures/test-data.ts b/dashboard/tests/fixtures/test-data.ts deleted file mode 100644 index 66e3eb8..0000000 --- a/dashboard/tests/fixtures/test-data.ts +++ /dev/null @@ -1,293 +0,0 @@ -/** - * Test data fixtures for IRacing Telemetry Dashboard - */ - -export const mockSessions = [ - { - session_id: 'mock-session-monza-001', - last_updated: new Date('2024-01-15T14:30:00Z'), - track_name: 'Monza' - }, - { - session_id: 'mock-session-spa-001', - last_updated: new Date('2024-01-14T09:15:00Z'), - track_name: 'Spa-Francorchamps' - }, - { - session_id: 'mock-session-silverstone-001', - last_updated: new Date('2024-01-13T16:45:00Z'), - track_name: 'Silverstone' - }, - { - session_id: 'mock-session-nurburgring-001', - last_updated: new Date('2024-01-12T11:20:00Z'), - track_name: 'NΓΌrburgring' - }, - { - session_id: 'mock-session-imola-001', - last_updated: new Date('2024-01-11T13:30:00Z'), - track_name: 'Imola' - } -]; - -export const mockTelemetryData = [ - { - LapDistPct: 0.0, - Speed: 45.2, - Throttle: 0.8, - Brake: 0.0, - Gear: 2, - RPM: 4500, - SteeringWheelAngle: 0.1, - LapCurrentLapTime: 0.0, - PlayerCarPosition: 1, - FuelLevel: 95.5, - Lat: 45.6205, - Lon: 9.2897, - SessionTime: 0.0, - TrackName: 'Monza' - }, - { - LapDistPct: 0.1, - Speed: 78.3, - Throttle: 1.0, - Brake: 0.0, - Gear: 3, - RPM: 6200, - SteeringWheelAngle: -0.2, - LapCurrentLapTime: 5.2, - PlayerCarPosition: 1, - FuelLevel: 95.3, - Lat: 45.6208, - Lon: 9.2902, - SessionTime: 5.2, - TrackName: 'Monza' - }, - { - LapDistPct: 0.2, - Speed: 125.7, - Throttle: 1.0, - Brake: 0.0, - Gear: 5, - RPM: 7800, - SteeringWheelAngle: 0.05, - LapCurrentLapTime: 12.1, - PlayerCarPosition: 1, - FuelLevel: 95.0, - Lat: 45.6212, - Lon: 9.2908, - SessionTime: 12.1, - TrackName: 'Monza' - } -]; - -export const mockTrackBounds = { - minLat: 45.6180, - maxLat: 45.6230, - minLon: 9.2850, - maxLon: 9.2950 -}; - -export const mockSystemStatus = { - database: { - status: 'connected', - message: 'QuestDB Connected', - responseTime: 45 - }, - processing: { - status: 'active', - mode: 'Runtime Dynamic', - lastUpdate: new Date('2024-01-15T14:30:00Z') - }, - sessions: { - total: 25, - recent: 5, - lastSession: new Date('2024-01-15T14:30:00Z') - } -}; - -export const mockErrorStates = { - databaseError: { - error: true, - message: 'Connection timeout: Unable to connect to QuestDB at localhost:8812', - code: 'CONNECTION_TIMEOUT' - }, - noSessions: { - error: false, - sessions: [], - message: 'No telemetry sessions found in database' - }, - invalidSession: { - error: true, - message: 'Session ID not found: invalid-session-123', - code: 'SESSION_NOT_FOUND' - }, - processingError: { - error: true, - message: 'Failed to process telemetry data: corrupt data format', - code: 'PROCESSING_ERROR' - } -}; - -export const mockApiResponses = { - sessions: { - success: { - status: 200, - data: mockSessions - }, - error: { - status: 500, - error: mockErrorStates.databaseError - }, - empty: { - status: 200, - data: [] - } - }, - telemetry: { - success: { - status: 200, - data: { - dataWithGPSCoordinates: mockTelemetryData, - trackBounds: mockTrackBounds, - processError: null - } - }, - error: { - status: 500, - data: { - dataWithGPSCoordinates: [], - trackBounds: null, - processError: mockErrorStates.processingError.message - } - } - }, - health: { - healthy: { - status: 200, - data: { - status: 'healthy', - database: 'connected', - timestamp: new Date().toISOString() - } - }, - unhealthy: { - status: 503, - data: { - status: 'unhealthy', - database: 'disconnected', - error: 'Database connection failed', - timestamp: new Date().toISOString() - } - } - } -}; - -/** - * Mock route handlers for testing - */ -export const mockRouteHandlers = { - // Mock successful sessions endpoint - mockSessionsSuccess: async (route: any) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(mockSessions) - }); - }, - - // Mock sessions endpoint error - mockSessionsError: async (route: any) => { - await route.fulfill({ - status: 500, - contentType: 'application/json', - body: JSON.stringify(mockErrorStates.databaseError) - }); - }, - - // Mock empty sessions response - mockSessionsEmpty: async (route: any) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify([]) - }); - }, - - // Mock telemetry data success - mockTelemetrySuccess: async (route: any) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - dataWithGPSCoordinates: mockTelemetryData, - trackBounds: mockTrackBounds, - processError: null - }) - }); - }, - - // Mock telemetry data error - mockTelemetryError: async (route: any) => { - await route.fulfill({ - status: 500, - contentType: 'application/json', - body: JSON.stringify({ - dataWithGPSCoordinates: [], - trackBounds: null, - processError: mockErrorStates.processingError.message - }) - }); - }, - - // Mock health check success - mockHealthSuccess: async (route: any) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(mockApiResponses.health.healthy.data) - }); - }, - - // Mock health check failure - mockHealthError: async (route: any) => { - await route.fulfill({ - status: 503, - contentType: 'application/json', - body: JSON.stringify(mockApiResponses.health.unhealthy.data) - }); - } -}; - -/** - * Test scenarios for different application states - */ -export const testScenarios = { - // Happy path: everything working - allSystemsOperational: { - sessions: mockRouteHandlers.mockSessionsSuccess, - health: mockRouteHandlers.mockHealthSuccess, - description: 'All systems operational with available sessions' - }, - - // Database connection issues - databaseConnectionError: { - sessions: mockRouteHandlers.mockSessionsError, - health: mockRouteHandlers.mockHealthError, - description: 'Database connection error scenario' - }, - - // Empty database - noSessionsAvailable: { - sessions: mockRouteHandlers.mockSessionsEmpty, - health: mockRouteHandlers.mockHealthSuccess, - description: 'No sessions available in database' - }, - - // Partial system failure - healthyButNoSessions: { - sessions: mockRouteHandlers.mockSessionsEmpty, - health: mockRouteHandlers.mockHealthSuccess, - description: 'System healthy but no telemetry sessions' - } -}; \ No newline at end of file diff --git a/dashboard/tests/global-setup.ts b/dashboard/tests/global-setup.ts deleted file mode 100644 index 7b704be..0000000 --- a/dashboard/tests/global-setup.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { chromium, FullConfig } from '@playwright/test'; - -async function globalSetup(config: FullConfig) { - console.log('🎭 Starting Playwright Global Setup...'); - - // Launch browser to verify setup - const browser = await chromium.launch(); - const context = await browser.newContext(); - const page = await context.newPage(); - - try { - // Verify the application is running - await page.goto(config.projects[0].use.baseURL || 'http://localhost:3000', { - waitUntil: 'networkidle', - timeout: 30000 - }); - - console.log('βœ… Application is accessible'); - - // Perform any pre-test setup here if needed - // e.g., database seeding, authentication, etc. - - } catch (error) { - console.error('❌ Global setup failed:', error); - throw error; - } finally { - await context.close(); - await browser.close(); - } - - console.log('🎭 Global Setup Complete'); -} - -export default globalSetup; \ No newline at end of file diff --git a/dashboard/tests/utils/test-helpers.ts b/dashboard/tests/utils/test-helpers.ts deleted file mode 100644 index a658216..0000000 --- a/dashboard/tests/utils/test-helpers.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { Page, Locator, expect } from '@playwright/test'; - -/** - * Test helper utilities for IRacing Telemetry Dashboard - */ - -export class DashboardHelpers { - constructor(private page: Page) {} - - /** - * Navigate to homepage and wait for it to load - */ - async navigateToHomepage() { - await this.page.goto('/'); - await this.page.waitForLoadState('networkidle'); - } - - /** - * Wait for dashboard to be ready (no loading states) - */ - async waitForDashboardReady() { - // Wait for main content to be visible - await expect(this.page.locator('main')).toBeVisible(); - - // Wait for any loading indicators to disappear - await this.page.waitForLoadState('networkidle'); - } - - /** - * Get status indicators from the header - */ - getConnectionStatus(): Locator { - return this.page.locator('[data-testid="connection-status"]'); - } - - /** - * Get system status cards - */ - getSystemStatusCards(): Locator { - return this.page.locator('[data-testid="system-status-card"]'); - } - - /** - * Get session selection area - */ - getSessionSelector(): Locator { - return this.page.locator('[data-testid="session-selector"]'); - } - - /** - * Get session cards - */ - getSessionCards(): Locator { - return this.page.locator('[data-testid="session-card"]'); - } - - /** - * Select a session by ID - */ - async selectSessionById(sessionId: string) { - const sessionCard = this.page.locator(`[data-testid="session-card-${sessionId}"]`); - await sessionCard.click(); - } - - /** - * Check if error message is displayed - */ - async hasErrorMessage(): Promise { - const errorElement = this.page.locator('[data-testid="error-message"]'); - return await errorElement.isVisible(); - } - - /** - * Get error message text - */ - async getErrorMessage(): Promise { - const errorElement = this.page.locator('[data-testid="error-message"]'); - return await errorElement.textContent() || ''; - } - - /** - * Wait for sessions to load - */ - async waitForSessionsToLoad(timeout: number = 10000) { - // Wait for either sessions to appear or error message - await this.page.waitForFunction( - () => { - const hasSessionCards = document.querySelectorAll('[data-testid="session-card"]').length > 0; - const hasError = document.querySelector('[data-testid="error-message"]') !== null; - const hasNoSessionsMessage = document.querySelector('[data-testid="no-sessions"]') !== null; - return hasSessionCards || hasError || hasNoSessionsMessage; - }, - { timeout } - ); - } - - /** - * Take screenshot with timestamp for debugging - */ - async takeTimestampedScreenshot(name: string) { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - await this.page.screenshot({ - path: `test-results/screenshots/${name}-${timestamp}.png`, - fullPage: true - }); - } - - /** - * Verify responsive design at different viewport sizes - */ - async testResponsiveBreakpoints() { - const breakpoints = [ - { name: 'mobile', width: 375, height: 667 }, - { name: 'tablet', width: 768, height: 1024 }, - { name: 'desktop', width: 1024, height: 768 }, - { name: 'large-desktop', width: 1440, height: 900 } - ]; - - const results = []; - - for (const breakpoint of breakpoints) { - await this.page.setViewportSize({ - width: breakpoint.width, - height: breakpoint.height - }); - - await this.page.waitForTimeout(500); // Allow layout to settle - - const isResponsive = await this.page.evaluate(() => { - // Check if layout doesn't have horizontal overflow - return document.documentElement.scrollWidth <= window.innerWidth; - }); - - results.push({ - ...breakpoint, - isResponsive, - actualWidth: await this.page.evaluate(() => document.documentElement.scrollWidth) - }); - } - - return results; - } -} - -/** - * Mock QuestDB responses for testing - */ -export const MockQuestDB = { - /** - * Mock successful sessions response - */ - mockSessionsSuccess: [ - { - session_id: 'test-session-1', - last_updated: new Date('2024-01-15T10:30:00Z'), - track_name: 'Monza' - }, - { - session_id: 'test-session-2', - last_updated: new Date('2024-01-14T15:45:00Z'), - track_name: 'Spa-Francorchamps' - }, - { - session_id: 'test-session-3', - last_updated: new Date('2024-01-13T09:15:00Z'), - track_name: 'Silverstone' - } - ], - - /** - * Mock database connection error - */ - mockConnectionError: { - error: 'Connection failed', - message: 'Unable to connect to QuestDB' - } -}; - -/** - * Performance testing utilities - */ -export class PerformanceHelpers { - constructor(private page: Page) {} - - /** - * Measure page load performance - */ - async measurePageLoad() { - const start = Date.now(); - - await this.page.goto('/', { waitUntil: 'networkidle' }); - - const loadTime = Date.now() - start; - - const metrics = await this.page.evaluate(() => ({ - // @ts-ignore - fcp: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0, - // @ts-ignore - lcp: performance.getEntriesByType('largest-contentful-paint')[0]?.startTime || 0, - // @ts-ignore - cls: performance.getEntriesByType('layout-shift').reduce((sum: number, entry: any) => sum + entry.value, 0) - })); - - return { - totalLoadTime: loadTime, - ...metrics - }; - } - - /** - * Check for console errors - */ - async getConsoleErrors(): Promise { - const errors: string[] = []; - - this.page.on('console', msg => { - if (msg.type() === 'error') { - errors.push(msg.text()); - } - }); - - return errors; - } -} \ No newline at end of file diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index 8859ebc..85bd5e8 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -1,27 +1,26 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", + /* Bundler mode */ "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - }, - "target": "ES2017" + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": ["src"] } diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts new file mode 100644 index 0000000..d5e2d15 --- /dev/null +++ b/dashboard/vite.config.ts @@ -0,0 +1,24 @@ +import tailwindcss from "@tailwindcss/vite"; +import { tanstackRouter } from "@tanstack/router-plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + base: process.env.VITE_BASE_PATH || "/", + plugins: [ + tailwindcss(), + tanstackRouter({ target: "react", autoCodeSplitting: true }), + react(), + ], + server: { + cors: false, + proxy: { + "/api": { + target: process.env.API_URL ?? "http://localhost:8010/", + changeOrigin: true, + secure: false, + }, + }, + }, +}); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 47544c6..560353d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,105 +1,230 @@ services: - rabbitMQ: - container_name: rabbitMQ - image: rabbitmq:4.0-management + traefik: + image: traefik:latest + restart: unless-stopped + security_opt: + - label:disable + user: "0" ports: - - "5672:5672" - - "15672:15672" + - "8080:80" + - "8443:443" + - "8081:8080" + networks: + - telemetry-network volumes: - - ./rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf - - rabbitmq-data:/var/lib/rabbitmq + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./traefik:/etc/traefik:ro + - traefik-logs:/var/log/traefik + environment: + - TRAEFIK_DASHBOARD_AUTH=${TRAEFIK_DASHBOARD_AUTH} + - LOCAL_DOMAIN=${LOCAL_DOMAIN:-localhost} healthcheck: - test: [ "CMD", "rabbitmq-diagnostics", "ping" ] - interval: 10s - timeout: 5s + test: ["CMD", "traefik", "healthcheck"] + interval: 30s + timeout: 10s retries: 3 - start_period: 30s + labels: + - "traefik.enable=true" + # Dashboard accessible via both domains with enhanced security + - "traefik.http.routers.dashboard-local.rule=Host(`${LOCAL_DOMAIN:-localhost}`) && PathPrefix(`/net-dash`)" + - "traefik.http.routers.dashboard-local.entrypoints=websecure" + - "traefik.http.routers.dashboard-local.service=api@internal" + - "traefik.http.routers.dashboard-local.tls=true" + - "traefik.http.routers.dashboard-local.middlewares=dashboard-auth,secure-dashboard@file" + - "traefik.http.middlewares.dashboard-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}" + + questdb: + image: questdb/questdb:latest + container_name: questdb + restart: unless-stopped + ports: + - "9000:9000" + - "8812:8812" + - "9009:9009" + - "9003:9003" + volumes: + - questdb-data:/var/lib/questdb + environment: + JAVA_OPTS: "-Xmx2g -Xms1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1ReservePercent=20" + QDB_SHARED_WORKER_COUNT: "5" + QDB_HTTP_ENABLED: "true" + QDB_PG_ENABLED: "false" + QDB_LINE_TCP_ENABLED: "true" + QDB_METRICS_ENABLED: "true" + QDB_CAIRO_WAL_ENABLED_DEFAULT: "true" + QDB_CAIRO_COMMIT_LAG: "120000" + QDB_CAIRO_MAX_UNCOMMITTED_ROWS: "50000" networks: - telemetry-network - restart: always - environment: - RABBITMQ_DEFAULT_USER: guest - RABBITMQ_DEFAULT_PASS: guest + labels: + - "traefik.enable=true" + # QuestDB API routes (no web UI, direct API access) + - "traefik.http.routers.questdb-local.rule=Host(`${LOCAL_DOMAIN}`) && PathPrefix(`/questdb`)" + - "traefik.http.routers.questdb-local.entrypoints=websecure" + - "traefik.http.routers.questdb-local.tls=true" + - "traefik.http.routers.questdb-local.middlewares=questdb-stripprefix" + - "traefik.http.middlewares.questdb-stripprefix.stripprefix.prefixes=/questdb" + - "traefik.http.services.questdb.loadbalancer.server.port=9000" - telemetry_service: - build: ./telemetryService/telemetryService - container_name: telemetry_service - restart: on-failure - depends_on: - rabbitMQ: - condition: service_healthy - ports: - - "5000:5000" + telemetry-service: + build: + context: ./telemetryService/golang + dockerfile: Dockerfile + container_name: telemetry-service + restart: unless-stopped networks: - telemetry-network environment: - QUESTDB_URL: ${QUESTDB_URL} - DOTNET_ENVIRONMENT: Development - ASPNETCORE_URLS: http://+:5000 - DOTNET_GCRetainVM: 1 - DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 - shm_size: 256m + QUESTDB_URL: questdb:8812;username=admin;password=quest + QUESTDB_HOST: questdb + QUESTDB_PORT: 9000 + GOMAXPROCS: "6" # Limit Go scheduler + GOGC: "200" # Less aggressive GC + ports: + - "9092:9092" # Prometheus metrics + - "8010:8010" # Prometheus metrics + depends_on: + questdb: + condition: service_started labels: - com.centurylinklabs.watchtower.enable: "true" + - "traefik.enable=true" + - "traefik.http.routers.telemetry-api-local.rule=Host(`${LOCAL_DOMAIN:-localhost}`) &&PathPrefix(`/api`)" + - "traefik.http.routers.telemetry-api-local.entrypoints=web" + - "traefik.http.routers.telemetry-api-local.middlewares=api-security@file" + - "traefik.http.services.telemetry-service.loadbalancer.server.port=8010" telemetry-dashboard: build: - context: dashboard/ + context: ./dashboard dockerfile: Dockerfile container_name: telemetry-dashboard restart: unless-stopped + networks: + - telemetry-network + deploy: + resources: + limits: + cpus: '1.0' + memory: 1g + labels: + - "traefik.enable=true" + # Dashboard main routes + - "traefik.http.routers.telemetry-dashboard-local.rule=Host(`${LOCAL_DOMAIN:-localhost}`) &&PathPrefix(`/dashboard`)" + - "traefik.http.routers.telemetry-dashboard-local.entrypoints=web" + # Service configuration + - "traefik.http.services.telemetry-dashboard.loadbalancer.server.port=8080" + - "com.centurylinklabs.watchtower.enable=true" + + grafana: + image: dhi.io/grafana:12.3 + container_name: grafana + restart: unless-stopped + volumes: + - grafana-data:/var/lib/grafana + - ./config/grafana:/etc/grafana/provisioning:ro ports: - - "3000:3000" + - "3002:3000" environment: - QUESTDB_HOST: questdb - QUESTDB_PORT: 8812 - QUESTDB_USER: admin - QUESTDB_PASSWORD: quest - QUESTDB_DATABASE: qdb - NODE_ENV: production - NEXT_PUBLIC_APP_URL: http://localhost:3000 + # Grafana settings optimized for high performance + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin123} + GF_USERS_ALLOW_SIGN_UP: "false" + GF_SERVER_ROOT_URL: http://localhost:3002 + GF_SERVER_SERVE_FROM_SUB_PATH: "false" + GF_INSTALL_PLUGINS: grafana-clock-panel + GF_PROVISIONING_AUTO_ASSIGN_ORG: "true" + GF_PROVISIONING_AUTO_ASSIGN_ORG_ROLE: Admin + + GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /etc/grafana/provisioning/dashboards/files/system-monitoring.json + GF_USERS_DEFAULT_THEME: dark + GF_ANALYTICS_REPORTING_ENABLED: "false" + GF_ANALYTICS_CHECK_FOR_UPDATES: "false" + networks: - telemetry-network - shm_size: 256m + deploy: + resources: + limits: + cpus: '0.75' + memory: 1.5g + reservations: + cpus: '0.25' + memory: 256m + labels: + - "traefik.enable=true" + - "traefik.http.routers.grafana-local.rule=Host(`${LOCAL_DOMAIN:-localhost}`) &&PathPrefix(`/grafana`)" + - "traefik.http.routers.grafana-local.entrypoints=websecure" + - "traefik.http.routers.grafana-local.tls=true" + - "traefik.http.routers.grafana-local.middlewares=secure-management@file" + - "traefik.http.services.grafana.loadbalancer.server.port=3000" - questdb: - image: questdb/questdb:latest - container_name: questdb + prometheus: + image: prom/prometheus:v2.47.2 + container_name: prometheus restart: unless-stopped - ports: - - "9000:9000" - - "8812:8812" volumes: - - questdb-data:/root/.questdb - extra_hosts: - - "host.docker.internal:host-gateway" - healthcheck: - test: ["CMD-SHELL", "timeout 5 bash -c ' - # bash -c " - # export RABBITMQ_URL=rabbitmq:5672 && - # /init-rabbitmq.sh && - # echo 'RabbitMQ initialization completed successfully'" + # QuestDB API routes (no web UI, direct API access) + - "traefik.http.routers.questdb-local.rule=(Host(`${LOCAL_DOMAIN}`) || Host(`localhost`)) && PathPrefix(`/questdb`)" + - "traefik.http.routers.questdb-local.entrypoints=web" + - "traefik.http.routers.questdb-local.middlewares=questdb-stripprefix@file" + - "traefik.http.routers.questdb-tailscale.rule=Host(`${TAILSCALE_DOMAIN}`) && PathPrefix(`/questdb`)" + - "traefik.http.routers.questdb-tailscale.entrypoints=websecure" + - "traefik.http.routers.questdb-tailscale.tls=true" + - "traefik.http.routers.questdb-tailscale.tls.certresolver=tailscale" + - "traefik.http.routers.questdb-tailscale.middlewares=questdb-stripprefix@file" + - "traefik.http.services.questdb.loadbalancer.server.port=9000" telemetry-service: image: ghcr.io/ojparkinson/iracing-telemetryservice:latest - container_name: telemetry-service restart: unless-stopped - depends_on: - rabbitmq: - condition: service_healthy - questdb: - condition: service_healthy + ports: + - "9092:9092" # Prometheus metrics networks: - telemetry-network environment: - QUESTDB_URL: ${QUESTDB_URL:-Host=questdb;Port=8812;Database=qdb;Username=admin;Password=quest} - DOTNET_ENVIRONMENT: Production - ASPNETCORE_URLS: http://+:5000 - RABBITMQ_HOST: rabbitmq - RABBITMQ_USER: ${RABBITMQ_USER:-admin} - RABBITMQ_PASS: ${RABBITMQ_PASS:-changeme} - RABBITMQ_URL: amqp://admin:changeme@rabbitmq:5672/ + QUESTDB_URL: questdb:8812;username=admin;password=quest + QUESTDB_HOST: ${QUESTDB_HOST:-questdb} + QUESTDB_PORT: ${QUESTDB_HTTP_PORT:-9000} + depends_on: + questdb: + condition: service_started deploy: resources: limits: - cpus: 1 + cpus: '1.25' + memory: 8g + reservations: + cpus: '0.25' memory: 1g labels: - - "com.centurylinklabs.watchtower.enable=true" + - "traefik.enable=true" + # API routes for local access + - "traefik.http.routers.telemetry-api-local.rule=(Host(`${LOCAL_DOMAIN:-localhost}`) || Host(`localhost`)) && PathPrefix(`/api`)" + - "traefik.http.routers.telemetry-api-local.entrypoints=web" + - "traefik.http.routers.telemetry-api-local.middlewares=api-security@file" + # API routes for Tailscale access + - "traefik.http.routers.telemetry-api-tailscale.rule=Host(`${TAILSCALE_DOMAIN}`) && PathPrefix(`/api`)" + - "traefik.http.routers.telemetry-api-tailscale.entrypoints=websecure" + - "traefik.http.routers.telemetry-api-tailscale.tls=true" + - "traefik.http.routers.telemetry-api-tailscale.tls.certresolver=tailscale" + - "traefik.http.routers.telemetry-api-tailscale.middlewares=api-security@file" + # Service configuration + - "traefik.http.services.telemetry-service.loadbalancer.server.port=8010" telemetry-dashboard: image: ghcr.io/ojparkinson/iracing-display:latest - container_name: telemetry-dashboard restart: unless-stopped - environment: - NODE_ENV: production - NEXT_PUBLIC_APP_URL: https://${LOCAL_DOMAIN:-pi.local}/dashboard - NEXT_PUBLIC_BACKEND_URL: https://${LOCAL_DOMAIN:-pi.local}/api networks: - telemetry-network deploy: resources: limits: - cpus: 1 + cpus: '1.0' memory: 1g labels: - "traefik.enable=true" - # Dashboard main routes - - "traefik.http.routers.telemetry-dashboard-local.rule=Host(`${LOCAL_DOMAIN:-pi.local}`) && PathPrefix(`/dashboard`)" - - "traefik.http.routers.telemetry-dashboard-local.entrypoints=websecure" - - "traefik.http.routers.telemetry-dashboard-local.tls=true" - - "traefik.http.routers.telemetry-dashboard-local.middlewares=dashboard-stripprefix,default-security@file" - - "traefik.http.routers.telemetry-dashboard-tailscale.rule=Host(`${TAILSCALE_DOMAIN:-your-tailscale-name}.ts.net`) && PathPrefix(`/dashboard`)" + # Dashboard routes + - "traefik.http.routers.telemetry-dashboard-local.rule=(Host(`${LOCAL_DOMAIN:-localhost}`) || Host(`localhost`)) && PathPrefix(`/dashboard`)" + - "traefik.http.routers.telemetry-dashboard-local.entrypoints=web" + - "traefik.http.routers.telemetry-dashboard-tailscale.rule=Host(`${TAILSCALE_DOMAIN}`) && PathPrefix(`/dashboard`)" - "traefik.http.routers.telemetry-dashboard-tailscale.entrypoints=websecure" - "traefik.http.routers.telemetry-dashboard-tailscale.tls=true" - - "traefik.http.routers.telemetry-dashboard-tailscale.middlewares=dashboard-stripprefix,default-security@file" - # Static assets routes (higher priority) - - "traefik.http.routers.dashboard-static-local.rule=Host(`${LOCAL_DOMAIN:-pi.local}`) && PathPrefix(`/_next/static`)" - - "traefik.http.routers.dashboard-static-local.entrypoints=websecure" - - "traefik.http.routers.dashboard-static-local.tls=true" - - "traefik.http.routers.dashboard-static-local.priority=200" - - "traefik.http.routers.dashboard-static-tailscale.rule=Host(`${TAILSCALE_DOMAIN:-your-tailscale-name}.ts.net`) && PathPrefix(`/_next/static`)" - - "traefik.http.routers.dashboard-static-tailscale.entrypoints=websecure" - - "traefik.http.routers.dashboard-static-tailscale.tls=true" - - "traefik.http.routers.dashboard-static-tailscale.priority=200" - # Middlewares and services - - "traefik.http.middlewares.dashboard-stripprefix.stripprefix.prefixes=/dashboard" - - "traefik.http.services.telemetry-dashboard.loadbalancer.server.port=3000" - - "com.centurylinklabs.watchtower.enable=true" + - "traefik.http.routers.telemetry-dashboard-tailscale.tls.certresolver=tailscale" + # Service configuration + - "traefik.http.services.telemetry-dashboard.loadbalancer.server.port=8080" - questdb: - image: questdb/questdb:latest - container_name: questdb + grafana: + image: dhi.io/grafana:12.3 + container_name: grafana restart: unless-stopped - ports: - - "9000:9000" # HTTP port - - "8812:8812" # Postgres wire protocol - - "9003:9003" # InfluxDB line protocol volumes: - - questdb-data:/root/.questdb + - grafana-data:/var/lib/grafana + - ./config/grafana:/etc/grafana/provisioning:ro environment: - QDB_METRICS_ENABLED: true - QDB_HTTP_ENABLED: true - QDB_HTTP_BIND_TO: 0.0.0.0:9000 + # Grafana settings optimized for high performance + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin123} + GF_USERS_ALLOW_SIGN_UP: "false" + GF_SERVER_ROOT_URL: http://${LOCAL_DOMAIN:-localhost}/grafana + GF_SERVER_SERVE_FROM_SUB_PATH: "true" + GF_INSTALL_PLUGINS: grafana-clock-panel + GF_PROVISIONING_AUTO_ASSIGN_ORG: "true" + GF_PROVISIONING_AUTO_ASSIGN_ORG_ROLE: Admin + + GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /etc/grafana/provisioning/dashboards/files/system-monitoring.json + GF_USERS_DEFAULT_THEME: dark + GF_ANALYTICS_REPORTING_ENABLED: "false" + GF_ANALYTICS_CHECK_FOR_UPDATES: "false" + networks: - telemetry-network deploy: resources: limits: - cpus: 1 - memory: 2g - healthcheck: - test: ["CMD-SHELL", "timeout 5 bash -c ' ../telemetryService/golang + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.40.0 + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/sys v0.38.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/e2e/go.sum b/e2e/go.sum new file mode 100644 index 0000000..ccb1854 --- /dev/null +++ b/e2e/go.sum @@ -0,0 +1,170 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM= +github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.40.0 h1:wGznWj8ZlEoqWfMN2L+EWjQBbjZ99vhoy/S61h+cED0= +github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.40.0/go.mod h1:Y+9/8YMZo3ElEZmHZOgFnjKrxE4+H2OFrjWdYzm/jtU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/e2e/pkg/config/config.go b/e2e/pkg/config/config.go new file mode 100644 index 0000000..fbd2236 --- /dev/null +++ b/e2e/pkg/config/config.go @@ -0,0 +1,26 @@ +package config + +import ( + "time" +) + +type E2eTestingConfig struct { + RecordCount int + BatchSize int + SessionID string + + publisherWorkers int + PublisherRate int + + NetworkLatency time.Duration + NetworkBandwidth int // Mbps + NetworkJitter time.Duration + PacketLoss float32 + + verificationTimeout time.Duration + IntegritySampleSize int + + RabbitMQMemory string + QuestDBMemory string + TelemetryServiceMemory string +} diff --git a/e2e/pkg/containers/network.go b/e2e/pkg/containers/network.go new file mode 100644 index 0000000..9dd5471 --- /dev/null +++ b/e2e/pkg/containers/network.go @@ -0,0 +1,13 @@ +package containers + +import ( + "context" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" +) + +func CreateNetwork(ctx context.Context) (*testcontainers.DockerNetwork, error) { + network, err := network.New(ctx) + return network, err +} diff --git a/e2e/pkg/containers/questdb.go b/e2e/pkg/containers/questdb.go new file mode 100644 index 0000000..418f34c --- /dev/null +++ b/e2e/pkg/containers/questdb.go @@ -0,0 +1,49 @@ +package containers + +import ( + "context" + "testing" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" +) + +func SpinUpQuestDB(t *testing.T, ctx context.Context, nw *testcontainers.DockerNetwork) *testcontainers.DockerContainer { + + container, err := testcontainers.Run( + ctx, + "questdb/questdb:latest", + testcontainers.WithName("e2e-questdb"), + testcontainers.WithExposedPorts("9000:9000", "8812:8812", "9009:9009", "9003:9003"), + testcontainers.WithEnv(map[string]string{ + "QDB_SHARED_WORKER_COUNT": "12", + "QDB_CAIRO_MAX_UNCOMMITTED_ROWS": "2000000", // Increased to 2M for larger batches + "QDB_CAIRO_COMMIT_LAG": "120000", // 2 min commit lag (increased from default 60s) + "QDB_LINE_TCP_ENABLED": "true", + "QDB_LINE_TCP_CONNECTION_POOL_CAPACITY": "64", // Match sender pool size + "QDB_LINE_TCP_NET_CONNECTION_LIMIT": "256", + "QDB_LINE_TCP_RECV_BUFFER_SIZE": "1048576", // 1MB TCP receive buffer + "QDB_HTTP_CONNECTION_POOL_INITIAL_CAPACITY": "64", + "QDB_PG_NET_CONNECTION_LIMIT": "128", + "QDB_CAIRO_COMMIT_MODE": "nosync", // Async commits for max throughput + "QDB_CAIRO_WAL_ENABLED_DEFAULT": "true", + "JAVA_OPTS": "-Xmx6g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:ParallelGCThreads=8", // Reduced GC pause target + }), + testcontainers.WithWaitStrategy( + wait.ForListeningPort("8812/tcp"), + ), + network.WithNetwork([]string{"questdb"}, nw), + ) + if err != nil { + t.Fatalf("Error running QuestDB server: %v", err) + } + + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Logf("Failed to terminate QuestDB container: %v", err) + } + }) + + return container +} diff --git a/e2e/pkg/containers/rabbitmq.go b/e2e/pkg/containers/rabbitmq.go new file mode 100644 index 0000000..4325e54 --- /dev/null +++ b/e2e/pkg/containers/rabbitmq.go @@ -0,0 +1,62 @@ +package containers + +import ( + "context" + "path/filepath" + "testing" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" +) + +func StartRabbitMQ(t *testing.T, ctx context.Context, nw *testcontainers.DockerNetwork) *testcontainers.DockerContainer { + // Get absolute path for Docker bind mount + definitionsPath, err := filepath.Abs(filepath.Join("..", "..", "config", "definitions.json")) + if err != nil { + t.Fatalf("Failed to resolve absolute path for definitions.json: %v", err) + } + + enabledPluginsPath, err := filepath.Abs(filepath.Join("..", "..", "config", "enabled_plugins")) + if err != nil { + t.Fatalf("Failed to resolve enabled_plugins path: %v", err) + } + + rabbitmqConfPath, err := filepath.Abs(filepath.Join("..", "..", "config", "rabbitmq.conf")) + + container, err := testcontainers.Run(ctx, "rabbitmq:4.1", + testcontainers.WithExposedPorts("5672/tcp", "15672/tcp", "15692/tcp"), + testcontainers.WithName("e2e-rabbitmq"), + testcontainers.WithMounts( + testcontainers.BindMount(definitionsPath, + testcontainers.ContainerMountTarget("/etc/rabbitmq/definitions.json")), + testcontainers.BindMount(enabledPluginsPath, + testcontainers.ContainerMountTarget("/etc/rabbitmq/enabled_plugins")), + testcontainers.BindMount(rabbitmqConfPath, + testcontainers.ContainerMountTarget("/etc/rabbitmq/rabbitmq.conf")), + ), + testcontainers.WithEnv(map[string]string{ + "RABBITMQ_MANAGEMENT_LOAD_DEFINITIONS": "/etc/rabbitmq/definitions.json", + "RABBITMQ_LOOPBACK_USERS": "none", + }), + testcontainers.WithWaitStrategy( + wait.ForAll( + wait.ForListeningPort("5672/tcp"), // AMQP + wait.ForListeningPort("15672/tcp"), // Management + ), + ), + network.WithNetwork([]string{"rabbitmq"}, nw), + ) + + if err != nil { + t.Fatalf("Error running RabbitMQ server: %v", err) + } + + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Logf("Failed to terminate rabbitmq container: %v", err) + } + }) + + return container +} diff --git a/e2e/pkg/containers/telemetry_service.go b/e2e/pkg/containers/telemetry_service.go new file mode 100644 index 0000000..6684495 --- /dev/null +++ b/e2e/pkg/containers/telemetry_service.go @@ -0,0 +1,52 @@ +package containers + +import ( + "context" + "path/filepath" + "testing" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" +) + +func StartTelemetryService(t *testing.T, ctx context.Context, nw *testcontainers.DockerNetwork) *testcontainers.DockerContainer { + df := testcontainers.FromDockerfile{ + Context: filepath.Join("..", "..", "telemetryService", "golang"), + Dockerfile: "Dockerfile", + Repo: "IRacingService", + Tag: "latest", + KeepImage: true, + BuildArgs: map[string]*string{}, + } + + container, err := testcontainers.Run( + ctx, + "", + testcontainers.WithDockerfile(df), + testcontainers.WithExposedPorts("9092/tcp", "6060/tcp"), + testcontainers.WithName("e2e-telemetryService"), + testcontainers.WithEnv(map[string]string{ + "QUESTDB_URL": "questdb:8812;username=admin;password=quest", + "QUESTDB_HOST": "questdb", + "QUESTDB_PORT": "9000", + "RABBITMQ_HOST": "rabbitmq", + "SENDER_POOL_SIZE": "60", + }), + testcontainers.WithWaitStrategy( + wait.ForLog("Starting to consume messages from RabbitMQ"), + ), + network.WithNetwork([]string{"telemetry-service"}, nw), + ) + if err != nil { + t.Fatalf("Error running telemetry service: %v", err) + } + + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Logf("Failed to terminate telemetry service container: %v", err) + } + }) + + return container +} diff --git a/e2e/pkg/containers/toxiproxy.go b/e2e/pkg/containers/toxiproxy.go new file mode 100644 index 0000000..d7d9a4e --- /dev/null +++ b/e2e/pkg/containers/toxiproxy.go @@ -0,0 +1,34 @@ +package containers + +import ( + "context" + "testing" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" +) + +func SpinUpToxiProxy(t *testing.T, ctx context.Context, nw *testcontainers.DockerNetwork) *testcontainers.DockerContainer { + container, err := testcontainers.Run( + ctx, + "ghcr.io/shopify/toxiproxy:latest", + testcontainers.WithExposedPorts("8474:8474"), + testcontainers.WithEnv(map[string]string{}), + testcontainers.WithWaitStrategy( + wait.ForListeningPort("8474"), + ), + network.WithNetwork([]string{"toxiproxy"}, nw), + ) + if err != nil { + t.Fatalf("Error running Toxiproxy: %v", err) + } + + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Logf("Failed to terminate Toxiproxy container: %v", err) + } + }) + + return container +} diff --git a/e2e/pkg/publisher/dataGenerator.go b/e2e/pkg/publisher/dataGenerator.go new file mode 100644 index 0000000..760336b --- /dev/null +++ b/e2e/pkg/publisher/dataGenerator.go @@ -0,0 +1,77 @@ +package publisher + +import ( + "fmt" + "math/rand" + "time" + + "google.golang.org/protobuf/types/known/timestamppb" +) + +func GenerateBatch(numBatches, recordsPerBatch int) []*TelemetryBatch { + items := make([]*TelemetryBatch, numBatches) + for i := 0; i < numBatches; i++ { + items[i] = &TelemetryBatch{ + SessionId: "test-session", + BatchId: "batch-" + string(rune(i)), + Records: GenerateRecords(recordsPerBatch), + } + } + return items +} + +func GenerateRecords(count int) []*Telemetry { + now := time.Now() + records := make([]*Telemetry, count) + for i := 0; i < count; i++ { + records[i] = &Telemetry{ + SessionId: fmt.Sprintf("session-%d", rand.Int()), + TrackName: "Spa-Francorchamps", + TrackId: "14", + LapId: "lap-1", + SessionNum: "0", + SessionType: "Race", + SessionName: "Feature Race", + CarId: "mercedes_amg_gt3", + Speed: 150.5, + LapDistPct: 0.45, + SessionTime: 123.45, + Lat: 50.4372, + Lon: 5.9714, + Gear: 4, + PlayerCarPosition: 3, + Throttle: 0.85, + Brake: 0.0, + SteeringWheelAngle: -0.15, + Rpm: 7500.0, + VelocityX: 25.5, + VelocityY: 0.5, + VelocityZ: 35.2, + FuelLevel: 45.5, + Alt: 123.4, + LatAccel: 1.2, + LongAccel: 0.8, + VertAccel: 0.1, + Pitch: 0.05, + Roll: -0.02, + Yaw: 1.57, + YawNorth: 3.14, + Voltage: 13.8, + WaterTemp: 85.5, + LapCurrentLapTime: 95.234, + LapLastLapTime: 94.567, + LapDeltaToBestLap: 0.667, + LFpressure: 28.5, + RFpressure: 28.6, + LRpressure: 27.8, + RRpressure: 27.9, + LFtempM: 85.2, + RFtempM: 86.1, + LRtempM: 84.5, + RRtempM: 85.0, + TickTime: timestamppb.New(now), + } + + } + return records +} diff --git a/e2e/pkg/publisher/publisher.go b/e2e/pkg/publisher/publisher.go new file mode 100644 index 0000000..748917e --- /dev/null +++ b/e2e/pkg/publisher/publisher.go @@ -0,0 +1,110 @@ +package publisher + +import ( + "context" + "fmt" + "runtime" + sync "sync" + "time" + + amqp "github.com/rabbitmq/amqp091-go" + "github.com/testcontainers/testcontainers-go" + "google.golang.org/protobuf/proto" +) + +type Publisher struct { + conn *amqp.Connection + channel *amqp.Channel +} + +func NewPublisher(rabbitmq *testcontainers.DockerContainer, ctx context.Context) (*Publisher, error) { + host, _ := rabbitmq.Host(ctx) + port, _ := rabbitmq.MappedPort(ctx, "5672") + + conn, err := amqp.Dial(fmt.Sprintf("amqp://admin:changeme@%s:%s", host, port.Port())) + if err != nil { + return nil, err + } + + fmt.Println("Connecting channel") + channel, err := conn.Channel() + if err != nil { + return nil, err + } + + return &Publisher{ + conn: conn, + channel: channel, + }, nil +} + +func (p *Publisher) PublishBatch(rabbitmq *testcontainers.DockerContainer, batches []*TelemetryBatch, ctx context.Context) { + numWorkers := runtime.NumCPU() // Use all CPUs + if numWorkers > len(batches)/10 { + numWorkers = len(batches) / 10 + } + if numWorkers < 1 { + numWorkers = 1 + } + + workChan := make(chan *TelemetryBatch, len(batches)) + errChan := make(chan error, len(batches)) + var wg sync.WaitGroup + + // Start workers + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + published := 0 + + for batch := range workChan { + data, err := proto.Marshal(batch) + if err != nil { + errChan <- fmt.Errorf("worker %d: marshal error: %w", workerID, err) + continue + } + + err = p.channel.PublishWithContext(ctx, "telemetry_topic", "telemetry.ticks", + false, false, + amqp.Publishing{ + ContentType: "application/x-protobuf", + Body: data, + DeliveryMode: amqp.Transient, + Timestamp: time.Now(), + MessageId: batch.BatchId, + }) + + if err != nil { + errChan <- fmt.Errorf("worker %d: publish error: %w", workerID, err) + } else { + published++ + } + } + + fmt.Printf("Worker %d published %d batches\n", workerID, published) + }(i) + } + + // Send all batches to workers + start := time.Now() + for _, batch := range batches { + workChan <- batch + } + close(workChan) + + // Wait for completion + wg.Wait() + close(errChan) + + // Report errors + errorCount := 0 + for err := range errChan { + fmt.Println(err) + errorCount++ + } + + elapsed := time.Since(start) + fmt.Printf("βœ… Published %d batches in %v (%.0f batches/sec, %d errors)\n", + len(batches), elapsed, float64(len(batches))/elapsed.Seconds(), errorCount) +} diff --git a/e2e/pkg/publisher/telemetry.pb.go b/e2e/pkg/publisher/telemetry.pb.go new file mode 100644 index 0000000..86fd6dd --- /dev/null +++ b/e2e/pkg/publisher/telemetry.pb.go @@ -0,0 +1,632 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.2 +// source: telemetry.proto + +package publisher + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Telemetry struct { + state protoimpl.MessageState `protogen:"open.v1"` + LapId string `protobuf:"bytes,1,opt,name=lap_id,json=lapId,proto3" json:"lap_id,omitempty"` + Speed float64 `protobuf:"fixed64,2,opt,name=speed,proto3" json:"speed,omitempty"` + LapDistPct float64 `protobuf:"fixed64,3,opt,name=lap_dist_pct,json=lapDistPct,proto3" json:"lap_dist_pct,omitempty"` + SessionId string `protobuf:"bytes,4,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + SessionNum string `protobuf:"bytes,5,opt,name=session_num,json=sessionNum,proto3" json:"session_num,omitempty"` + SessionType string `protobuf:"bytes,6,opt,name=session_type,json=sessionType,proto3" json:"session_type,omitempty"` + SessionName string `protobuf:"bytes,7,opt,name=session_name,json=sessionName,proto3" json:"session_name,omitempty"` + SessionTime float64 `protobuf:"fixed64,8,opt,name=session_time,json=sessionTime,proto3" json:"session_time,omitempty"` + CarId string `protobuf:"bytes,9,opt,name=car_id,json=carId,proto3" json:"car_id,omitempty"` + TrackName string `protobuf:"bytes,10,opt,name=track_name,json=trackName,proto3" json:"track_name,omitempty"` + TrackId string `protobuf:"bytes,11,opt,name=track_id,json=trackId,proto3" json:"track_id,omitempty"` + WorkerId uint32 `protobuf:"varint,12,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + SteeringWheelAngle float64 `protobuf:"fixed64,13,opt,name=steering_wheel_angle,json=steeringWheelAngle,proto3" json:"steering_wheel_angle,omitempty"` + PlayerCarPosition float64 `protobuf:"fixed64,14,opt,name=player_car_position,json=playerCarPosition,proto3" json:"player_car_position,omitempty"` + VelocityX float64 `protobuf:"fixed64,15,opt,name=velocity_x,json=velocityX,proto3" json:"velocity_x,omitempty"` + VelocityY float64 `protobuf:"fixed64,16,opt,name=velocity_y,json=velocityY,proto3" json:"velocity_y,omitempty"` + VelocityZ float64 `protobuf:"fixed64,17,opt,name=velocity_z,json=velocityZ,proto3" json:"velocity_z,omitempty"` + FuelLevel float64 `protobuf:"fixed64,18,opt,name=fuel_level,json=fuelLevel,proto3" json:"fuel_level,omitempty"` + Throttle float64 `protobuf:"fixed64,19,opt,name=throttle,proto3" json:"throttle,omitempty"` + Brake float64 `protobuf:"fixed64,20,opt,name=brake,proto3" json:"brake,omitempty"` + Rpm float64 `protobuf:"fixed64,21,opt,name=rpm,proto3" json:"rpm,omitempty"` + Lat float64 `protobuf:"fixed64,22,opt,name=lat,proto3" json:"lat,omitempty"` + Lon float64 `protobuf:"fixed64,23,opt,name=lon,proto3" json:"lon,omitempty"` + Gear uint32 `protobuf:"varint,24,opt,name=gear,proto3" json:"gear,omitempty"` + Alt float64 `protobuf:"fixed64,25,opt,name=alt,proto3" json:"alt,omitempty"` + LatAccel float64 `protobuf:"fixed64,26,opt,name=lat_accel,json=latAccel,proto3" json:"lat_accel,omitempty"` + LongAccel float64 `protobuf:"fixed64,27,opt,name=long_accel,json=longAccel,proto3" json:"long_accel,omitempty"` + VertAccel float64 `protobuf:"fixed64,28,opt,name=vert_accel,json=vertAccel,proto3" json:"vert_accel,omitempty"` + Pitch float64 `protobuf:"fixed64,29,opt,name=pitch,proto3" json:"pitch,omitempty"` + Roll float64 `protobuf:"fixed64,30,opt,name=roll,proto3" json:"roll,omitempty"` + Yaw float64 `protobuf:"fixed64,31,opt,name=yaw,proto3" json:"yaw,omitempty"` + YawNorth float64 `protobuf:"fixed64,32,opt,name=yaw_north,json=yawNorth,proto3" json:"yaw_north,omitempty"` + Voltage float64 `protobuf:"fixed64,33,opt,name=voltage,proto3" json:"voltage,omitempty"` + LapLastLapTime float64 `protobuf:"fixed64,34,opt,name=lap_last_lap_time,json=lapLastLapTime,proto3" json:"lap_last_lap_time,omitempty"` + WaterTemp float64 `protobuf:"fixed64,35,opt,name=water_temp,json=waterTemp,proto3" json:"water_temp,omitempty"` + LapDeltaToBestLap float64 `protobuf:"fixed64,36,opt,name=lap_delta_to_best_lap,json=lapDeltaToBestLap,proto3" json:"lap_delta_to_best_lap,omitempty"` + LapCurrentLapTime float64 `protobuf:"fixed64,37,opt,name=lap_current_lap_time,json=lapCurrentLapTime,proto3" json:"lap_current_lap_time,omitempty"` + LFpressure float64 `protobuf:"fixed64,38,opt,name=l_fpressure,json=lFpressure,proto3" json:"l_fpressure,omitempty"` + RFpressure float64 `protobuf:"fixed64,39,opt,name=r_fpressure,json=rFpressure,proto3" json:"r_fpressure,omitempty"` + LRpressure float64 `protobuf:"fixed64,40,opt,name=l_rpressure,json=lRpressure,proto3" json:"l_rpressure,omitempty"` + RRpressure float64 `protobuf:"fixed64,41,opt,name=r_rpressure,json=rRpressure,proto3" json:"r_rpressure,omitempty"` + LFtempM float64 `protobuf:"fixed64,42,opt,name=l_ftemp_m,json=lFtempM,proto3" json:"l_ftemp_m,omitempty"` + RFtempM float64 `protobuf:"fixed64,43,opt,name=r_ftemp_m,json=rFtempM,proto3" json:"r_ftemp_m,omitempty"` + LRtempM float64 `protobuf:"fixed64,44,opt,name=l_rtemp_m,json=lRtempM,proto3" json:"l_rtemp_m,omitempty"` + RRtempM float64 `protobuf:"fixed64,45,opt,name=r_rtemp_m,json=rRtempM,proto3" json:"r_rtemp_m,omitempty"` + TickTime *timestamppb.Timestamp `protobuf:"bytes,46,opt,name=tick_time,json=tickTime,proto3" json:"tick_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Telemetry) Reset() { + *x = Telemetry{} + mi := &file_telemetry_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Telemetry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Telemetry) ProtoMessage() {} + +func (x *Telemetry) ProtoReflect() protoreflect.Message { + mi := &file_telemetry_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Telemetry.ProtoReflect.Descriptor instead. +func (*Telemetry) Descriptor() ([]byte, []int) { + return file_telemetry_proto_rawDescGZIP(), []int{0} +} + +func (x *Telemetry) GetLapId() string { + if x != nil { + return x.LapId + } + return "" +} + +func (x *Telemetry) GetSpeed() float64 { + if x != nil { + return x.Speed + } + return 0 +} + +func (x *Telemetry) GetLapDistPct() float64 { + if x != nil { + return x.LapDistPct + } + return 0 +} + +func (x *Telemetry) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *Telemetry) GetSessionNum() string { + if x != nil { + return x.SessionNum + } + return "" +} + +func (x *Telemetry) GetSessionType() string { + if x != nil { + return x.SessionType + } + return "" +} + +func (x *Telemetry) GetSessionName() string { + if x != nil { + return x.SessionName + } + return "" +} + +func (x *Telemetry) GetSessionTime() float64 { + if x != nil { + return x.SessionTime + } + return 0 +} + +func (x *Telemetry) GetCarId() string { + if x != nil { + return x.CarId + } + return "" +} + +func (x *Telemetry) GetTrackName() string { + if x != nil { + return x.TrackName + } + return "" +} + +func (x *Telemetry) GetTrackId() string { + if x != nil { + return x.TrackId + } + return "" +} + +func (x *Telemetry) GetWorkerId() uint32 { + if x != nil { + return x.WorkerId + } + return 0 +} + +func (x *Telemetry) GetSteeringWheelAngle() float64 { + if x != nil { + return x.SteeringWheelAngle + } + return 0 +} + +func (x *Telemetry) GetPlayerCarPosition() float64 { + if x != nil { + return x.PlayerCarPosition + } + return 0 +} + +func (x *Telemetry) GetVelocityX() float64 { + if x != nil { + return x.VelocityX + } + return 0 +} + +func (x *Telemetry) GetVelocityY() float64 { + if x != nil { + return x.VelocityY + } + return 0 +} + +func (x *Telemetry) GetVelocityZ() float64 { + if x != nil { + return x.VelocityZ + } + return 0 +} + +func (x *Telemetry) GetFuelLevel() float64 { + if x != nil { + return x.FuelLevel + } + return 0 +} + +func (x *Telemetry) GetThrottle() float64 { + if x != nil { + return x.Throttle + } + return 0 +} + +func (x *Telemetry) GetBrake() float64 { + if x != nil { + return x.Brake + } + return 0 +} + +func (x *Telemetry) GetRpm() float64 { + if x != nil { + return x.Rpm + } + return 0 +} + +func (x *Telemetry) GetLat() float64 { + if x != nil { + return x.Lat + } + return 0 +} + +func (x *Telemetry) GetLon() float64 { + if x != nil { + return x.Lon + } + return 0 +} + +func (x *Telemetry) GetGear() uint32 { + if x != nil { + return x.Gear + } + return 0 +} + +func (x *Telemetry) GetAlt() float64 { + if x != nil { + return x.Alt + } + return 0 +} + +func (x *Telemetry) GetLatAccel() float64 { + if x != nil { + return x.LatAccel + } + return 0 +} + +func (x *Telemetry) GetLongAccel() float64 { + if x != nil { + return x.LongAccel + } + return 0 +} + +func (x *Telemetry) GetVertAccel() float64 { + if x != nil { + return x.VertAccel + } + return 0 +} + +func (x *Telemetry) GetPitch() float64 { + if x != nil { + return x.Pitch + } + return 0 +} + +func (x *Telemetry) GetRoll() float64 { + if x != nil { + return x.Roll + } + return 0 +} + +func (x *Telemetry) GetYaw() float64 { + if x != nil { + return x.Yaw + } + return 0 +} + +func (x *Telemetry) GetYawNorth() float64 { + if x != nil { + return x.YawNorth + } + return 0 +} + +func (x *Telemetry) GetVoltage() float64 { + if x != nil { + return x.Voltage + } + return 0 +} + +func (x *Telemetry) GetLapLastLapTime() float64 { + if x != nil { + return x.LapLastLapTime + } + return 0 +} + +func (x *Telemetry) GetWaterTemp() float64 { + if x != nil { + return x.WaterTemp + } + return 0 +} + +func (x *Telemetry) GetLapDeltaToBestLap() float64 { + if x != nil { + return x.LapDeltaToBestLap + } + return 0 +} + +func (x *Telemetry) GetLapCurrentLapTime() float64 { + if x != nil { + return x.LapCurrentLapTime + } + return 0 +} + +func (x *Telemetry) GetLFpressure() float64 { + if x != nil { + return x.LFpressure + } + return 0 +} + +func (x *Telemetry) GetRFpressure() float64 { + if x != nil { + return x.RFpressure + } + return 0 +} + +func (x *Telemetry) GetLRpressure() float64 { + if x != nil { + return x.LRpressure + } + return 0 +} + +func (x *Telemetry) GetRRpressure() float64 { + if x != nil { + return x.RRpressure + } + return 0 +} + +func (x *Telemetry) GetLFtempM() float64 { + if x != nil { + return x.LFtempM + } + return 0 +} + +func (x *Telemetry) GetRFtempM() float64 { + if x != nil { + return x.RFtempM + } + return 0 +} + +func (x *Telemetry) GetLRtempM() float64 { + if x != nil { + return x.LRtempM + } + return 0 +} + +func (x *Telemetry) GetRRtempM() float64 { + if x != nil { + return x.RRtempM + } + return 0 +} + +func (x *Telemetry) GetTickTime() *timestamppb.Timestamp { + if x != nil { + return x.TickTime + } + return nil +} + +type TelemetryBatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Records []*Telemetry `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + BatchId string `protobuf:"bytes,2,opt,name=batch_id,json=batchId,proto3" json:"batch_id,omitempty"` + SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + WorkerId uint32 `protobuf:"varint,4,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TelemetryBatch) Reset() { + *x = TelemetryBatch{} + mi := &file_telemetry_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TelemetryBatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TelemetryBatch) ProtoMessage() {} + +func (x *TelemetryBatch) ProtoReflect() protoreflect.Message { + mi := &file_telemetry_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TelemetryBatch.ProtoReflect.Descriptor instead. +func (*TelemetryBatch) Descriptor() ([]byte, []int) { + return file_telemetry_proto_rawDescGZIP(), []int{1} +} + +func (x *TelemetryBatch) GetRecords() []*Telemetry { + if x != nil { + return x.Records + } + return nil +} + +func (x *TelemetryBatch) GetBatchId() string { + if x != nil { + return x.BatchId + } + return "" +} + +func (x *TelemetryBatch) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *TelemetryBatch) GetWorkerId() uint32 { + if x != nil { + return x.WorkerId + } + return 0 +} + +func (x *TelemetryBatch) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +var File_telemetry_proto protoreflect.FileDescriptor + +const file_telemetry_proto_rawDesc = "" + + "\n" + + "\x0ftelemetry.proto\x12\x06pubSub\x1a\x1fgoogle/protobuf/timestamp.proto\"\x85\v\n" + + "\tTelemetry\x12\x15\n" + + "\x06lap_id\x18\x01 \x01(\tR\x05lapId\x12\x14\n" + + "\x05speed\x18\x02 \x01(\x01R\x05speed\x12 \n" + + "\flap_dist_pct\x18\x03 \x01(\x01R\n" + + "lapDistPct\x12\x1d\n" + + "\n" + + "session_id\x18\x04 \x01(\tR\tsessionId\x12\x1f\n" + + "\vsession_num\x18\x05 \x01(\tR\n" + + "sessionNum\x12!\n" + + "\fsession_type\x18\x06 \x01(\tR\vsessionType\x12!\n" + + "\fsession_name\x18\a \x01(\tR\vsessionName\x12!\n" + + "\fsession_time\x18\b \x01(\x01R\vsessionTime\x12\x15\n" + + "\x06car_id\x18\t \x01(\tR\x05carId\x12\x1d\n" + + "\n" + + "track_name\x18\n" + + " \x01(\tR\ttrackName\x12\x19\n" + + "\btrack_id\x18\v \x01(\tR\atrackId\x12\x1b\n" + + "\tworker_id\x18\f \x01(\rR\bworkerId\x120\n" + + "\x14steering_wheel_angle\x18\r \x01(\x01R\x12steeringWheelAngle\x12.\n" + + "\x13player_car_position\x18\x0e \x01(\x01R\x11playerCarPosition\x12\x1d\n" + + "\n" + + "velocity_x\x18\x0f \x01(\x01R\tvelocityX\x12\x1d\n" + + "\n" + + "velocity_y\x18\x10 \x01(\x01R\tvelocityY\x12\x1d\n" + + "\n" + + "velocity_z\x18\x11 \x01(\x01R\tvelocityZ\x12\x1d\n" + + "\n" + + "fuel_level\x18\x12 \x01(\x01R\tfuelLevel\x12\x1a\n" + + "\bthrottle\x18\x13 \x01(\x01R\bthrottle\x12\x14\n" + + "\x05brake\x18\x14 \x01(\x01R\x05brake\x12\x10\n" + + "\x03rpm\x18\x15 \x01(\x01R\x03rpm\x12\x10\n" + + "\x03lat\x18\x16 \x01(\x01R\x03lat\x12\x10\n" + + "\x03lon\x18\x17 \x01(\x01R\x03lon\x12\x12\n" + + "\x04gear\x18\x18 \x01(\rR\x04gear\x12\x10\n" + + "\x03alt\x18\x19 \x01(\x01R\x03alt\x12\x1b\n" + + "\tlat_accel\x18\x1a \x01(\x01R\blatAccel\x12\x1d\n" + + "\n" + + "long_accel\x18\x1b \x01(\x01R\tlongAccel\x12\x1d\n" + + "\n" + + "vert_accel\x18\x1c \x01(\x01R\tvertAccel\x12\x14\n" + + "\x05pitch\x18\x1d \x01(\x01R\x05pitch\x12\x12\n" + + "\x04roll\x18\x1e \x01(\x01R\x04roll\x12\x10\n" + + "\x03yaw\x18\x1f \x01(\x01R\x03yaw\x12\x1b\n" + + "\tyaw_north\x18 \x01(\x01R\byawNorth\x12\x18\n" + + "\avoltage\x18! \x01(\x01R\avoltage\x12)\n" + + "\x11lap_last_lap_time\x18\" \x01(\x01R\x0elapLastLapTime\x12\x1d\n" + + "\n" + + "water_temp\x18# \x01(\x01R\twaterTemp\x120\n" + + "\x15lap_delta_to_best_lap\x18$ \x01(\x01R\x11lapDeltaToBestLap\x12/\n" + + "\x14lap_current_lap_time\x18% \x01(\x01R\x11lapCurrentLapTime\x12\x1f\n" + + "\vl_fpressure\x18& \x01(\x01R\n" + + "lFpressure\x12\x1f\n" + + "\vr_fpressure\x18' \x01(\x01R\n" + + "rFpressure\x12\x1f\n" + + "\vl_rpressure\x18( \x01(\x01R\n" + + "lRpressure\x12\x1f\n" + + "\vr_rpressure\x18) \x01(\x01R\n" + + "rRpressure\x12\x1a\n" + + "\tl_ftemp_m\x18* \x01(\x01R\alFtempM\x12\x1a\n" + + "\tr_ftemp_m\x18+ \x01(\x01R\arFtempM\x12\x1a\n" + + "\tl_rtemp_m\x18, \x01(\x01R\alRtempM\x12\x1a\n" + + "\tr_rtemp_m\x18- \x01(\x01R\arRtempM\x127\n" + + "\ttick_time\x18. \x01(\v2\x1a.google.protobuf.TimestampR\btickTime\"\xce\x01\n" + + "\x0eTelemetryBatch\x12+\n" + + "\arecords\x18\x01 \x03(\v2\x11.pubSub.TelemetryR\arecords\x12\x19\n" + + "\bbatch_id\x18\x02 \x01(\tR\abatchId\x12\x1d\n" + + "\n" + + "session_id\x18\x03 \x01(\tR\tsessionId\x12\x1b\n" + + "\tworker_id\x18\x04 \x01(\rR\bworkerId\x128\n" + + "\ttimestamp\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestampB:Z8github.com/OJPARKINSON/IRacing-Display/e2e/pkg/publisherb\x06proto3" + +var ( + file_telemetry_proto_rawDescOnce sync.Once + file_telemetry_proto_rawDescData []byte +) + +func file_telemetry_proto_rawDescGZIP() []byte { + file_telemetry_proto_rawDescOnce.Do(func() { + file_telemetry_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_telemetry_proto_rawDesc), len(file_telemetry_proto_rawDesc))) + }) + return file_telemetry_proto_rawDescData +} + +var file_telemetry_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_telemetry_proto_goTypes = []any{ + (*Telemetry)(nil), // 0: pubSub.Telemetry + (*TelemetryBatch)(nil), // 1: pubSub.TelemetryBatch + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp +} +var file_telemetry_proto_depIdxs = []int32{ + 2, // 0: pubSub.Telemetry.tick_time:type_name -> google.protobuf.Timestamp + 0, // 1: pubSub.TelemetryBatch.records:type_name -> pubSub.Telemetry + 2, // 2: pubSub.TelemetryBatch.timestamp:type_name -> google.protobuf.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_telemetry_proto_init() } +func file_telemetry_proto_init() { + if File_telemetry_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_telemetry_proto_rawDesc), len(file_telemetry_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_telemetry_proto_goTypes, + DependencyIndexes: file_telemetry_proto_depIdxs, + MessageInfos: file_telemetry_proto_msgTypes, + }.Build() + File_telemetry_proto = out.File + file_telemetry_proto_goTypes = nil + file_telemetry_proto_depIdxs = nil +} diff --git a/e2e/pkg/publisher/telemetry.proto b/e2e/pkg/publisher/telemetry.proto new file mode 100644 index 0000000..3e4802f --- /dev/null +++ b/e2e/pkg/publisher/telemetry.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; +package pubSub; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/OJPARKINSON/IRacing-Display/e2e/pkg/publisher"; + +message Telemetry { + string lap_id = 1; + double speed = 2; + double lap_dist_pct = 3; + string session_id = 4; + string session_num = 5; + string session_type = 6; + string session_name = 7; + double session_time = 8; + string car_id = 9; + string track_name = 10; + string track_id = 11; + uint32 worker_id = 12; + double steering_wheel_angle = 13; + double player_car_position = 14; + double velocity_x = 15; + double velocity_y = 16; + double velocity_z = 17; + double fuel_level = 18; + double throttle = 19; + double brake = 20; + double rpm = 21; + double lat = 22; + double lon = 23; + uint32 gear = 24; + double alt = 25; + double lat_accel = 26; + double long_accel = 27; + double vert_accel = 28; + double pitch = 29; + double roll = 30; + double yaw = 31; + double yaw_north = 32; + double voltage = 33; + double lap_last_lap_time = 34; + double water_temp = 35; + double lap_delta_to_best_lap = 36; + double lap_current_lap_time = 37; + double l_fpressure = 38; + double r_fpressure = 39; + double l_rpressure = 40; + double r_rpressure = 41; + double l_ftemp_m = 42; + double r_ftemp_m = 43; + double l_rtemp_m = 44; + double r_rtemp_m = 45; + google.protobuf.Timestamp tick_time = 46; +} + +message TelemetryBatch { + repeated Telemetry records = 1; + string batch_id = 2; + string session_id = 3; + uint32 worker_id = 4; + google.protobuf.Timestamp timestamp = 5; +} \ No newline at end of file diff --git a/e2e/pkg/verification/mertrics_collector.go b/e2e/pkg/verification/mertrics_collector.go new file mode 100644 index 0000000..dfc4bbc --- /dev/null +++ b/e2e/pkg/verification/mertrics_collector.go @@ -0,0 +1 @@ +package verification diff --git a/e2e/pkg/verification/questdb_verifier.go b/e2e/pkg/verification/questdb_verifier.go new file mode 100644 index 0000000..b2c0f9d --- /dev/null +++ b/e2e/pkg/verification/questdb_verifier.go @@ -0,0 +1,230 @@ +package verification + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "sort" + "time" +) + +type recordsCountResponse struct { + Query string `json:"query"` + Columns []column `json:"columns"` // Should be array of column objects + Timestamp int64 `json:"timestamp"` + Dataset [][]interface{} `json:"dataset"` // Mixed types in QuestDB responses + Count int `json:"count"` +} +type truncateResponse struct { + DDL string `json:"ddl"` +} + +type column struct { + Name string `json:"name"` + Type string `json:"type"` +} + +func GetRecordCount() (int, error) { + u, err := url.Parse("http://localhost:9000") + if err != nil { + return -1, fmt.Errorf("error parsing url, ", err) + } + + u.Path += "exec" + params := url.Values{} + params.Add("query", ` + SELECT count(timestamp) FROM TelemetryTicks + `) + u.RawQuery = params.Encode() + url := fmt.Sprintf("%v", u) + + res, err := http.Get(url) + if err != nil { + return -1, fmt.Errorf("error to get stored records, ", err) + } + + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + fmt.Println("error: failed ro read body, ", err) + } + + response := recordsCountResponse{} + + if err := json.Unmarshal(body, &response); err != nil { + return -1, fmt.Errorf("failed to unmarshal: %w, body: %s", err, string(body)) + } + + if len(response.Dataset) == 0 || len(response.Dataset[0]) == 0 { + log.Printf("empty dataset in response") + } + + actualCount := int(response.Dataset[0][0].(float64)) // JSON numbers are float64 + + log.Printf("βœ“ Count validation passed: %d records", actualCount) + + return actualCount, nil +} + +func TunicateTable() error { + u, err := url.Parse("http://localhost:9000") + if err != nil { + return fmt.Errorf("error parsing url, ", err) + } + + u.Path += "exec" + params := url.Values{} + params.Add("query", ` + TRUNCATE TABLE TelemetryTicks + `) + u.RawQuery = params.Encode() + url := fmt.Sprintf("%v", u) + + res, err := http.Get(url) + if err != nil { + return fmt.Errorf("error making request to truncate table, ", err) + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println("error: failed ro read body, ", err) + } + + response := truncateResponse{} + + if err := json.Unmarshal(body, &response); err != nil { + return fmt.Errorf("failed to unmarshal: %w, body: %s", err, string(body)) + } + + if response.DDL != "OK" { + log.Printf("empty dataset in response") + } + + log.Printf("Table successfully truncated") + + num, err := GetRecordCount() + + fmt.Println("number, ", num) + + return nil + +} + +func WaitForRecordCountWithMetrics(expectedCount int, timeout time.Duration) (*ThroughputMetrics, error) { + metrics := &ThroughputMetrics{ + StartTime: time.Now(), + Samples: []Sample{}, + } + + deadline := time.Now().Add(timeout) + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + lastCount := 0 + lastTime := time.Now() + firstMeasurement := true // Add this flag + + for { + now := time.Now() + count, err := GetRecordCount() + if err != nil { + return nil, err + } + + if firstMeasurement && count < 50000 { // Skip until meaningful data + firstMeasurement = false + continue + } + + if !firstMeasurement { + deltaRecords := count - lastCount + deltaTime := now.Sub(lastTime).Seconds() + + var instantThroughput float64 + if deltaTime > 0.1 { + instantThroughput = float64(deltaRecords) / deltaTime + } + + metrics.Samples = append(metrics.Samples, Sample{ + Timestamp: now, + Count: count, + Throughput: instantThroughput, + }) + + progress := float64(count) / float64(expectedCount) * 100 + log.Printf("Progress: %d/%d (%.1f%%) | Throughput: %.0f rec/sec", + count, expectedCount, progress, instantThroughput) + } else { + firstMeasurement = false + log.Printf("Starting monitoring - initial count: %d", count) + } + + if count >= expectedCount { + metrics.EndTime = now + metrics.TotalRecords = count + return metrics, nil + } + + if now.After(deadline) { + return metrics, fmt.Errorf("timeout: %d/%d records after %v", + count, expectedCount, timeout) + } + + lastCount = count + lastTime = now + + <-ticker.C + } +} + +type ThroughputMetrics struct { + StartTime time.Time + EndTime time.Time + TotalRecords int + Samples []Sample +} + +type Sample struct { + Timestamp time.Time + Count int + Throughput float64 +} + +func (m *ThroughputMetrics) AvgThroughput() float64 { + if m.EndTime.IsZero() { + return 0 + } + return float64(m.TotalRecords) / m.EndTime.Sub(m.StartTime).Seconds() +} + +func (m *ThroughputMetrics) PeakThroughput() float64 { + peak := 0.0 + for _, s := range m.Samples { + if s.Throughput > peak { + peak = s.Throughput + } + } + return peak +} + +func (m *ThroughputMetrics) P95Throughput() float64 { + if len(m.Samples) == 0 { + return 0 + } + + throughputs := make([]float64, len(m.Samples)) + for i, s := range m.Samples { + throughputs[i] = s.Throughput + } + sort.Float64s(throughputs) + + idx := int(float64(len(throughputs)) * 0.95) + return throughputs[idx] +} diff --git a/e2e/tests/e2e_test.go b/e2e/tests/e2e_test.go new file mode 100644 index 0000000..1eec3a9 --- /dev/null +++ b/e2e/tests/e2e_test.go @@ -0,0 +1,251 @@ +package tests + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/ojparkinson/IRacing-Display/e2e/pkg/containers" + "github.com/ojparkinson/IRacing-Display/e2e/pkg/publisher" + "github.com/ojparkinson/IRacing-Display/e2e/pkg/verification" +) + +func TestAllTicksAreStored(t *testing.T) { + testCases := []struct { + name string + numBatches int + recordsPerBatch int + maxWaitTime time.Duration + short bool + }{ + { + name: "500k_Records", + numBatches: 20, + recordsPerBatch: 25000, + maxWaitTime: 1 * time.Minute, + short: true, + }, + { + name: "1M_Records", + numBatches: 40, + recordsPerBatch: 25000, + maxWaitTime: 2 * time.Minute, + short: false, // swap back to true + }, + { + name: "5M_Records", + numBatches: 200, + recordsPerBatch: 25000, + maxWaitTime: 10 * time.Minute, + short: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if testing.Short() && tc.short { + t.Skip() + } + + ctx := context.Background() + network, _ := containers.CreateNetwork(ctx) + + containers.SpinUpQuestDB(t, ctx, network) + rabbitmqC := containers.StartRabbitMQ(t, ctx, network) + containers.StartTelemetryService(t, ctx, network) + + batches := publisher.GenerateBatch(tc.numBatches, tc.recordsPerBatch) + + pub, err := publisher.NewPublisher(rabbitmqC, ctx) + if err != nil { + t.Fatalf("Failed to create publisher: %v", err) + } + + publishStart := time.Now() + pub.PublishBatch(rabbitmqC, batches, ctx) + + publishDuration := time.Since(publishStart) + expectedCount := tc.numBatches * tc.recordsPerBatch + publishThroughput := float64(expectedCount) / publishDuration.Seconds() + + metrics, err := verification.WaitForRecordCountWithMetrics(expectedCount, 5*time.Minute) + if err != nil { + t.Fatalf("Processing failed: %v", err) + } + + // Report + t.Logf("πŸ“Š Throughput Metrics:") + t.Logf(" Publisher: %.0f rec/sec", publishThroughput) + t.Logf(" E2E Avg: %.0f rec/sec", metrics.AvgThroughput()) + t.Logf(" E2E Peak: %.0f rec/sec", metrics.PeakThroughput()) + t.Logf(" E2E P95: %.0f rec/sec", metrics.P95Throughput()) + + // Assert minimum performance + if metrics.P95Throughput() < 50000 { + t.Errorf("Throughput below target: %.0f < 50000 rec/sec", + metrics.P95Throughput()) + } + + err2 := verification.TunicateTable() + if err2 != nil { + log.Fatal("error TunicateTable, ", err) + } + }) + } +} +func TestFixedFilesProcessedSpeed(t *testing.T) { + ctx := context.Background() + network, _ := containers.CreateNetwork(ctx) + + // Discover .ibt files + ibtPath, err := filepath.Abs("../../ingest/go/ibt") + if err != nil { + t.Fatalf("Failed to get absolute path: %v", err) + } + + dirEntries, err := os.ReadDir(ibtPath) + if err != nil { + t.Fatalf("Failed to read ibt directory: %v", err) + } + + // Count .ibt files + ibtFileCount := 0 + for _, entry := range dirEntries { + if !entry.IsDir() && filepath.Ext(entry.Name()) == ".ibt" { + ibtFileCount++ + } + } + + if ibtFileCount == 0 { + t.Fatal("No .ibt files found to process") + } + + t.Logf("Found %d .ibt files to process", ibtFileCount) + + // Start containers + containers.SpinUpQuestDB(t, ctx, network) + rabbitmqC := containers.StartRabbitMQ(t, ctx, network) + containers.StartTelemetryService(t, ctx, network) + + // Get RabbitMQ connection details for ingest app + host, _ := rabbitmqC.Host(ctx) + port, _ := rabbitmqC.MappedPort(ctx, "5672") + + // Set environment variables for ingest app + os.Setenv("RABBITMQ_URL", fmt.Sprintf("amqp://admin:changeme@%s:%s", host, port.Port())) + os.Setenv("IBT_DATA_DIR", ibtPath) + defer os.Unsetenv("RABBITMQ_URL") + defer os.Unsetenv("IBT_DATA_DIR") + + // Get initial count from QuestDB + initialCount, err := verification.GetRecordCount() + if err != nil { + t.Fatalf("Failed to get initial record count: %v", err) + } + t.Logf("Initial DB record count: %d", initialCount) + + // Record start time + ingestStart := time.Now() + + // Run the ingest app + t.Logf("πŸš€ Starting ingest process...") + ingestCmd := fmt.Sprintf("cd ../../ingest/go && go run cmd/ingest-app/main.go --quiet \"%s\"", ibtPath) + + // Run ingest in background and capture output + ingestResult := make(chan error, 1) + go func() { + // Use bash -c to run the cd && go run command + cmd := fmt.Sprintf("bash -c '%s'", ingestCmd) + err := runCommand(cmd, 10*time.Minute) + ingestResult <- err + }() + + // Poll QuestDB to detect when ingestion completes + t.Logf("πŸ“Š Monitoring QuestDB for record count changes...") + var finalCount int + stableCount := 0 + lastCount := initialCount + pollInterval := 2 * time.Second + maxStablePolls := 5 // Consider complete if count stable for 5 polls (10 seconds) + + pollTicker := time.NewTicker(pollInterval) + defer pollTicker.Stop() + + ingestComplete := false + for !ingestComplete { + select { + case err := <-ingestResult: + if err != nil { + t.Fatalf("Ingest process failed: %v", err) + } + t.Logf("βœ… Ingest process completed") + // Continue polling until count stabilizes + case <-pollTicker.C: + currentCount, err := verification.GetRecordCount() + if err != nil { + t.Logf("Warning: failed to get record count: %v", err) + continue + } + + if currentCount > lastCount { + t.Logf(" Records in DB: %d (+%d)", currentCount, currentCount-lastCount) + lastCount = currentCount + stableCount = 0 + } else if currentCount == lastCount { + stableCount++ + if stableCount >= maxStablePolls { + finalCount = currentCount + ingestComplete = true + t.Logf(" Record count stable at %d", finalCount) + } + } + case <-time.After(15 * time.Minute): + t.Fatalf("Timeout waiting for ingestion to complete") + } + } + + // Calculate end-to-end time + e2eDuration := time.Since(ingestStart) + totalRecords := finalCount - initialCount + + t.Logf("") + t.Logf("πŸ“Š End-to-End Performance Metrics:") + t.Logf(" Total records processed: %d", totalRecords) + t.Logf(" Total E2E time: %v", e2eDuration) + t.Logf(" Average throughput: %.0f rec/sec", float64(totalRecords)/e2eDuration.Seconds()) + + // Cleanup + err2 := verification.TunicateTable() + if err2 != nil { + log.Fatal("error TunicateTable, ", err) + } +} + +// runCommand runs a shell command with a timeout +func runCommand(cmdString string, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Extract the actual command from the bash -c wrapper + actualCmd := cmdString[len("bash -c '") : len(cmdString)-1] + + log.Printf("Executing: %s", actualCmd) + + // Create the command with bash -c + cmd := exec.CommandContext(ctx, "bash", "-c", actualCmd) + + // Capture output + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Command failed: %v\nOutput: %s", err, string(output)) + return err + } + + log.Printf("Command completed. Output:\n%s", string(output)) + return nil +} diff --git a/generate-traefik-certs.sh b/generate-traefik-certs.sh deleted file mode 100755 index 6e39797..0000000 --- a/generate-traefik-certs.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -# Load environment variables if .env exists -if [ -f .env ]; then - export $(cat .env | xargs) -fi - -# Use HOST_IP from environment or default -HOST_IP=${HOST_IP:-192.168.1.202} - -# Create certs directory -mkdir -p certs - -# Set secure permissions on certs directory -chmod 700 certs - -echo "πŸ” Generating self-signed SSL certificates for host: $HOST_IP" - -# Generate self-signed certificate for local testing -openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout certs/localhost.key \ - -out certs/localhost.crt \ - -subj "/C=US/ST=State/L=City/O=LocalDev/CN=$HOST_IP" \ - -addext "subjectAltName=DNS:localhost,DNS:$HOST_IP,IP:$HOST_IP,IP:127.0.0.1" - -# Set secure permissions on certificate files -chmod 600 certs/localhost.key -chmod 644 certs/localhost.crt - -# Copy TLS configuration template -cp dynamic/tls.yaml.example dynamic/tls.yaml - -echo "βœ… Self-signed certificates generated in certs/ directory" -echo " - Certificate: certs/localhost.crt" -echo " - Private Key: certs/localhost.key" -echo " - TLS Config: dynamic/tls.yaml" -echo "" -echo "πŸ”’ Secure file permissions set:" -echo " - Private key (600): owner read/write only" -echo " - Certificate (644): world readable" -echo " - Certs directory (700): owner access only" -echo "" -echo "⚠️ Note: Browsers will show security warnings for self-signed certificates" -echo " You can safely proceed through the warning for local development." -echo "" -echo "πŸš€ You can now start Traefik with: docker-compose up traefik -d" \ No newline at end of file diff --git a/ingest/dotnet/Program.cs b/ingest/dotnet/Program.cs deleted file mode 100644 index 460e876..0000000 --- a/ingest/dotnet/Program.cs +++ /dev/null @@ -1,178 +0,0 @@ -ο»Ώusing Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; -using ingest.Models; -using System; -using System.IO; -using SVappsLAB.iRacingTelemetrySDK.Models; -using ingest.PubSub; - -namespace ingest -{ - [RequiredTelemetryVars(["Lap", "LapDistPct", "Speed", "Throttle", "Brake", "Gear", "RPM", - "SteeringWheelAngle", "VelocityX", "VelocityY", "VelocityZ", "Lat", "Lon", "SessionTime", - "PlayerCarPosition", "FuelLevel", "PlayerCarIdx", "SessionNum", "alt", "LatAccel", "LongAccel", - "VertAccel", "pitch", "roll", "yaw", "YawNorth", "Voltage", "LapLastLapTime", "WaterTemp", - "LapDeltaToBestLap", "LapCurrentLapTime", "LFpressure", "RFpressure", "LRpressure", "RRpressure", "LFtempM", - "RFtempM", "LRtempM", "RRtempM"])] - internal class Program - { - private static string _trackName = ""; - private static string _trackId = ""; - private static int _sessionId = 1; - private static bool _shutdownRequested = false; - private static readonly object _shutdownLock = new object(); - - private static async Task Main(string[] args) - { - var logger = LoggerFactory - .Create(builder => builder.AddConsole().AddSimpleConsole(o => o.SingleLine = true)) - .CreateLogger("logger"); - - var ibtOptions = - new IBTOptions(@"./ibt_files/mclaren720sgt3_monza full 2025-02-09 12-58-11.ibt", int.MaxValue); - - var ps = new BufferedPubSub( - maxBatchSize: 1000, - maxBatchBytes: 250000, - flushInterval: TimeSpan.FromMilliseconds(50) - ); - - ITelemetryClient? telemetryClient = null; - - using var cts = new CancellationTokenSource(); - - Console.CancelKeyPress += (sender, e) => - { - lock (_shutdownLock) - { - if (_shutdownRequested) - { - logger.LogWarning("Force shutdown requested!"); - Environment.Exit(1); - return; - } - - _shutdownRequested = true; - e.Cancel = true; - - logger.LogInformation("Shutdown requested. Gracefully shutting down..."); - cts.Cancel(); - } - }; - - try - { - telemetryClient = TelemetryClient.Create(logger: logger, ibtOptions: ibtOptions); - - telemetryClient.OnSessionInfoUpdate += OnSessionInfoUpdate; - telemetryClient.OnTelemetryUpdate += (sender, e) => OnTelemetryUpdate(sender, e, ps, logger); - - logger.LogInformation("Starting telemetry monitoring..."); - - await telemetryClient.Monitor(cts.Token); - } - catch (OperationCanceledException) - { - logger.LogInformation("Telemetry monitoring was cancelled"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error during telemetry monitoring"); - } - finally - { - logger.LogInformation("Performing final cleanup..."); - - try - { - using var flushCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - await ps.ForceFlush(_sessionId); - logger.LogInformation("Final flush completed"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error during final flush"); - } - - try - { - ps.Dispose(); - logger.LogInformation("PubSub disposed"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error disposing PubSub"); - } - - try - { - telemetryClient?.Dispose(); - logger.LogInformation("Telemetry client disposed"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error disposing telemetry client"); - } - - logger.LogInformation("Application shutdown complete"); - } - - void OnTelemetryUpdate(object? sender, TelemetryData e, BufferedPubSub pubSub, ILogger log) - { - if (_shutdownRequested) - { - log.LogInformation("Ignoring telemetry update due to shutdown request"); - return; - } - - try - { - pubSub.Publish(log, e, _trackName, _trackId, _sessionId); - } - catch (Exception ex) - { - log.LogError(ex, "Error publishing telemetry data"); - } - } - - void OnSessionInfoUpdate(object? sender, TelemetrySessionInfo e) - { - if (_shutdownRequested) return; - - try - { - var weekendInfo = e.WeekendInfo; - if (weekendInfo != null) - { - _trackName = weekendInfo.TrackDisplayShortName ?? ""; - _trackName = _trackName.Replace(" ", "-"); - _trackId = weekendInfo.TrackID.ToString() ?? ""; - _sessionId = weekendInfo.SubSessionID; - - logger.LogInformation($"Track Name: {_trackName}, Session ID: {_sessionId}"); - } - - if (e.SessionInfo?.Sessions != null && e.SessionInfo.Sessions.Count > 0) - { - int sessionIndex = 0; - if (e.SessionInfo.Sessions.Count > 2) - { - sessionIndex = 2; - } - - var selectedSession = e.SessionInfo.Sessions[sessionIndex]; - logger.LogInformation($"SessionIndex: {sessionIndex}"); - } - else - { - logger.LogInformation($"No sessions found"); - } - } - catch (Exception ex) - { - logger.LogError(ex, "Error in session info update"); - } - } - } - } -} \ No newline at end of file diff --git a/ingest/dotnet/ingest.csproj b/ingest/dotnet/ingest.csproj deleted file mode 100644 index 14f1550..0000000 --- a/ingest/dotnet/ingest.csproj +++ /dev/null @@ -1,16 +0,0 @@ -ο»Ώ - - - Exe - net8.0 - enable - enable - - - - - - - - - diff --git a/ingest/dotnet/src/DirWatcher.cs b/ingest/dotnet/src/DirWatcher.cs deleted file mode 100644 index df9690e..0000000 --- a/ingest/dotnet/src/DirWatcher.cs +++ /dev/null @@ -1,61 +0,0 @@ - -namespace ingest; - -class DirWatcher -{ - public void Watch(string directoryName) - { - if (!Directory.Exists(directoryName)) - { - throw new DirectoryNotFoundException(directoryName); - } - - Console.WriteLine($"Directory: {directoryName}"); - - using var watcher = new FileSystemWatcher(directoryName); - - watcher.NotifyFilter = NotifyFilters.Attributes - | NotifyFilters.CreationTime - | NotifyFilters.DirectoryName - | NotifyFilters.FileName - | NotifyFilters.LastAccess - | NotifyFilters.LastWrite - | NotifyFilters.Security - | NotifyFilters.Size; - - watcher.Changed += OnChanged; - watcher.Created += OnCreated; - watcher.Error += OnError; - - watcher.Filter = "*.ibt"; - watcher.IncludeSubdirectories = true; - watcher.EnableRaisingEvents = true; - } - - private static void OnCreated(object sender, FileSystemEventArgs e) - { - if (e.ChangeType == WatcherChangeTypes.Created) - { - Console.WriteLine($"Created: {e.FullPath}"); - Console.WriteLine($"sender: {sender}"); - } - } - - private static void OnChanged(object sender, FileSystemEventArgs e) - { - Console.WriteLine($"Changed: {e.FullPath}"); - Console.WriteLine($"sender: {sender}"); - } - - private static void OnError(object sender, ErrorEventArgs e) - { - var ex = e.GetException(); - if (ex != null) - { - Console.WriteLine($"Message: {ex.Message}"); - Console.WriteLine("Stacktrace:"); - Console.WriteLine(ex.StackTrace); - Console.WriteLine(); - } - } -} \ No newline at end of file diff --git a/ingest/dotnet/src/FilesProcessor/FilesProcessor.cs b/ingest/dotnet/src/FilesProcessor/FilesProcessor.cs deleted file mode 100644 index 658cda4..0000000 --- a/ingest/dotnet/src/FilesProcessor/FilesProcessor.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Threading.Channels; -using ingest.PubSub; -using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; - -namespace ingest.FilesProcessor; - -public class FilesProcessor -{ - private readonly SemaphoreSlim _workerSemaphore; - private readonly Channel _fileQueue; - - public FilesProcessor(List fileQueue) - { - foreach (var file in fileQueue) - { - _fileQueue.Writer.WriteAsync(file); - } - } - - async public void Process(ILogger logger) - { - var ps = new BufferedPubSub( - maxBatchSize: 1000, - maxBatchBytes: 250000, - flushInterval: TimeSpan.FromMilliseconds(50) - ); - - using var cts = new CancellationTokenSource(); - - ITelemetryClient? telemetryClient = null; - - - await foreach (var file in _fileQueue.Reader.ReadAllAsync()) - { - var process = new IBTFileProcessor(file, telemetryClient, logger, ps); - } - } -} \ No newline at end of file diff --git a/ingest/dotnet/src/IBTFileProcessor/IBTFileProcessor.cs b/ingest/dotnet/src/IBTFileProcessor/IBTFileProcessor.cs deleted file mode 100644 index 0f9c623..0000000 --- a/ingest/dotnet/src/IBTFileProcessor/IBTFileProcessor.cs +++ /dev/null @@ -1,174 +0,0 @@ -using ingest.PubSub; -using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; -using SVappsLAB.iRacingTelemetrySDK.Models; - -namespace ingest.IBTFileProcessor; - -public class IBTFileProcessor -{ - private static string _trackName = ""; - private static string _trackId = ""; - private static int _sessionId = 1; - - - - async public void ProcessFile(string filePath, ITelemetryClient telemetryClient, ILogger logger, - ingest.PubSub.BufferedPubSub ps) - { - var ibtOptions = new IBTOptions(filePath); - try - { - telemetryClient = TelemetryClient.Create(logger: logger, ibtOptions: ibtOptions); - - telemetryClient.OnSessionInfoUpdate += OnSessionInfoUpdate; - telemetryClient.OnTelemetryUpdate += (sender, e) => OnTelemetryUpdate(sender, e, ps, logger); - - logger.LogInformation("Starting telemetry monitoring..."); - - } - catch (OperationCanceledException) - { - logger.LogInformation("Telemetry monitoring was cancelled"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error during telemetry monitoring"); - } - finally - { - logger.LogInformation("Performing final cleanup..."); - - try - { - using var flushCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - await ps.ForceFlush(_sessionId); - logger.LogInformation("Final flush completed"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error during final flush"); - } - - try - { - ps.Dispose(); - logger.LogInformation("PubSub disposed"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error disposing PubSub"); - } - - try - { - telemetryClient?.Dispose(); - logger.LogInformation("Telemetry client disposed"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error disposing telemetry client"); - } - - logger.LogInformation("Application shutdown complete"); - } - - void OnTelemetryUpdate(object? sender, TelemetryData e, BufferedPubSub pubSub, ILogger log) - { - - try - { - pubSub.Publish(log, e, _trackName, _trackId, _sessionId); - } - catch (Exception ex) - { - log.LogError(ex, "Error publishing telemetry data"); - } - } - - void OnSessionInfoUpdate(object? sender, TelemetrySessionInfo e) - { - try - { - var weekendInfo = e.WeekendInfo; - if (weekendInfo != null) - { - _trackName = weekendInfo.TrackDisplayShortName ?? ""; - _trackName = _trackName.Replace(" ", "-"); - _trackId = weekendInfo.TrackID.ToString() ?? ""; - _sessionId = weekendInfo.SubSessionID; - - logger.LogInformation($"Track Name: {_trackName}, Session ID: {_sessionId}"); - } - - if (e.SessionInfo?.Sessions != null && e.SessionInfo.Sessions.Count > 0) - { - int sessionIndex = 0; - if (e.SessionInfo.Sessions.Count > 2) - { - sessionIndex = 2; - } - - var selectedSession = e.SessionInfo.Sessions[sessionIndex]; - logger.LogInformation($"SessionIndex: {sessionIndex}"); - } - else - { - logger.LogInformation($"No sessions found"); - } - } - catch (Exception ex) - { - logger.LogError(ex, "Error in session info update"); - } - } - } - void OnTelemetryUpdate(object? sender, TelemetryData e, BufferedPubSub pubSub, ILogger log) - { - try - { - pubSub.Publish(log, e, _trackName, _trackId, _sessionId); - } - catch (Exception ex) - { - log.LogError(ex, "Error publishing telemetry data"); - } - } - - void OnSessionInfoUpdate(object? sender, TelemetrySessionInfo e, ILogger logger) - { - try - { - var weekendInfo = e.WeekendInfo; - if (weekendInfo != null) - { - _trackName = weekendInfo.TrackDisplayShortName ?? ""; - _trackName = _trackName.Replace(" ", "-"); - _trackId = weekendInfo.TrackID.ToString() ?? ""; - _sessionId = weekendInfo.SubSessionID; - - logger.LogInformation($"Track Name: {_trackName}, Session ID: {_sessionId}"); - } - - if (e.SessionInfo?.Sessions != null && e.SessionInfo.Sessions.Count > 0) - { - int sessionIndex = 0; - if (e.SessionInfo.Sessions.Count > 2) - { - sessionIndex = 2; - } - - var selectedSession = e.SessionInfo.Sessions[sessionIndex]; - logger.LogInformation($"SessionIndex: {sessionIndex}"); - } - else - { - logger.LogInformation($"No sessions found"); - } - } - catch (Exception ex) - { - logger.LogError(ex, "Error in session info update"); - } - } -} diff --git a/ingest/dotnet/src/Models/Telemetry.cs b/ingest/dotnet/src/Models/Telemetry.cs deleted file mode 100644 index 9e8fd88..0000000 --- a/ingest/dotnet/src/Models/Telemetry.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace ingest.Models; - -public struct TelemetryData2 -{ - public float? Gear { get; set; } - public float IsOnTrackCar { get; set; } - public float Rpm { get; set; } - public float Speed { get; set; } - public float BrakeRaw { get; set; } - public int Lap { get; set; } - public float LapDistPct { get; set; } - public float SteeringWheelAngle { get; set; } - public float VelocityY { get; set; } - public double VelocityX { get; set; } - public float Lat { get; set; } - public float Lon { get; set; } - public float SessionTime { get; set; } - public float LapCurrentLapTime { get; set; } - public float PlayerCarPosition { get; set; } - public float FuelLevel { get; set; } -} \ No newline at end of file diff --git a/ingest/dotnet/src/PubSub/PubSub.cs b/ingest/dotnet/src/PubSub/PubSub.cs deleted file mode 100644 index 74cc265..0000000 --- a/ingest/dotnet/src/PubSub/PubSub.cs +++ /dev/null @@ -1,371 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; -using RabbitMQ.Client; - -namespace ingest.PubSub; - -class BufferedPubSub : IDisposable -{ - private IConnection? _connection; - private IChannel? _channel; - private readonly object _lockObject = new object(); - private bool _isConnected = false; - private bool _disposed = false; - - private readonly int _maxBatchSize; - private readonly int _maxBatchBytes; - private readonly TimeSpan _flushInterval; - - private readonly List _buffer = new(); - private readonly Timer _flushTimer; - private int _currentBufferBytes; - - private long _totalPointsBuffered; - private long _totalBatchesSent; - private DateTime _lastFlush = DateTime.UtcNow; - - private readonly CancellationTokenSource _cancellationTokenSource = new(); - - public BufferedPubSub( - int maxBatchSize = 1000, - int maxBatchBytes = 250000, - TimeSpan? flushInterval = null - ) - { - _maxBatchSize = maxBatchSize; - _maxBatchBytes = maxBatchBytes; - _flushInterval = flushInterval ?? TimeSpan.FromMilliseconds(50); - - _flushTimer = new Timer(FlushTimerCallback, null, _flushInterval, _flushInterval); - - Console.WriteLine( - $"BufferedPubSub initialized: maxBatch={_maxBatchSize}, maxBytes={_maxBatchBytes}, flushInterval={_flushInterval.TotalMilliseconds}ms"); - } - - public async Task InitializeAsync() - { - if (_isConnected || _disposed) return; - - lock (_lockObject) - { - if (_isConnected || _disposed) return; - - try - { - var factory = new ConnectionFactory(); - factory.Uri = new Uri("amqp://guest:guest@localhost:5672/"); - - _connection = factory.CreateConnectionAsync().Result; - _channel = _connection.CreateChannelAsync(null).Result; - - _isConnected = true; - Console.WriteLine("RabbitMQ connection established and reused for all messages"); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to initialize RabbitMQ connection: {ex.Message}"); - _isConnected = false; - throw; - } - } - } - - public async void Publish(ILogger logger, TelemetryData data, string trackName = "", string trackId = "", - int sessionId = 0) - { - if (_disposed || _cancellationTokenSource.Token.IsCancellationRequested) - { - logger.LogWarning("Publish called after disposal or cancellation"); - return; - } - - try - { - await InitializeAsync(); - - if (_channel == null) - { - Console.WriteLine("RabbitMQ channel not available"); - return; - } - - var tick = new - { - lap_id = data.Lap.ToString(), - speed = data.Speed, - lap_dist_pct = data.LapDistPct, - session_id = sessionId, - session_num = data.SessionNum, - session_time = data.SessionTime, - car_id = data.PlayerCarIdx, - track_name = trackName, - track_id = trackId, - worker_id = 1, - steering_wheel_angle = data.SteeringWheelAngle, - player_car_position = data.PlayerCarPosition, - velocity_x = data.VelocityX, - velocity_y = data.VelocityY, - velocity_z = data.VelocityZ, - fuel_level = data.FuelLevel, - throttle = data.Throttle, - brake = data.Brake, - rpm = data.RPM, - lat = data.Lat, - lon = data.Lon, - gear = data.Gear, - alt = data.Alt, - lat_accel = data.LatAccel, - long_accel = data.LongAccel, - vert_accel = data.VertAccel, - pitch = data.Pitch, - roll = data.Roll, - yaw = data.Yaw, - yaw_north = data.YawNorth, - voltage = data.Voltage, - lapLastLapTime = data.LapLastLapTime, - waterTemp = data.WaterTemp, - lapDeltaToBestLap = data.LapDeltaToBestLap, - lapCurrentLapTime = data.LapCurrentLapTime, - lFpressure = data.LFpressure, - rFpressure = data.RFpressure, - lRpressure = data.LRpressure, - rRpressure = data.RRpressure, - lFtempM = data.LFtempM, - rFtempM = data.RFtempM, - lRtempM = data.LRtempM, - rRtempM = data.RRtempM, - tick_time = DateTime.UtcNow - }; - - await BufferTick(tick, sessionId); - } - catch (Exception ex) - { - Console.WriteLine($"Error in buffered publish: {ex}"); - _isConnected = false; - } - } - - private async Task BufferTick(object tick, int sessionId) - { - if (_disposed || _cancellationTokenSource.Token.IsCancellationRequested) return; - - bool shouldFlush = false; - - lock (_lockObject) - { - if (_disposed) return; - - _buffer.Add(tick); - _currentBufferBytes += EstimateTickSize(tick); - _totalPointsBuffered++; - - shouldFlush = _buffer.Count >= _maxBatchSize || _currentBufferBytes >= _maxBatchBytes; - - if (_buffer.Count % 500 == 0) - { - Console.WriteLine( - $"Buffer status: {_buffer.Count}/{_maxBatchSize} points, {_currentBufferBytes}/{_maxBatchBytes} bytes"); - } - } - - if (shouldFlush) - { - Console.WriteLine($"Triggering immediate flush: {_buffer.Count} points, {_currentBufferBytes} bytes"); - await FlushBuffer(sessionId); - } - } - - private void FlushTimerCallback(object? state) - { - if (_disposed || _cancellationTokenSource.Token.IsCancellationRequested) return; - - _ = Task.Run(async () => - { - try - { - await FlushBuffer(1); - } - catch (Exception ex) - { - Console.WriteLine($"Error in timer-triggered flush: {ex.Message}"); - } - }, _cancellationTokenSource.Token); - } - - private async Task FlushBuffer(int sessionId) - { - if (_disposed || _cancellationTokenSource.Token.IsCancellationRequested) return; - - List dataToFlush; - - lock (_lockObject) - { - if (_disposed || _buffer.Count == 0) - return; - - dataToFlush = new List(_buffer); - _buffer.Clear(); - _currentBufferBytes = 0; - } - - try - { - await SendBatchToRabbitMQ(dataToFlush, sessionId); - - lock (_lockObject) - { - _totalBatchesSent++; - _lastFlush = DateTime.UtcNow; - } - - if (_totalBatchesSent % 100 == 0) - { - var elapsed = DateTime.UtcNow - _lastFlush; - if (elapsed.TotalSeconds > 0) - { - var rate = _totalPointsBuffered / elapsed.TotalSeconds; - Console.WriteLine( - $"Flush metrics: {_totalPointsBuffered} points in {_totalBatchesSent} batches ({rate:F0} points/sec)"); - } - } - } - catch (Exception ex) - { - Console.WriteLine($"ERROR flushing buffer to RabbitMQ: {ex.Message}"); - } - } - - private async Task SendBatchToRabbitMQ(List batch, int sessionId) - { - if (batch.Count == 0 || _channel == null || _disposed) return; - - try - { - Dictionary headers = new Dictionary(); - headers.Add("worker_id", 1); - headers.Add("batch_size", batch.Count); - headers.Add("session_id", sessionId); - - string jsonString = JsonSerializer.Serialize(batch, new JsonSerializerOptions - { - WriteIndented = false - }); - - byte[] messageBodyBytes = System.Text.Encoding.UTF8.GetBytes(jsonString); - - var props = new BasicProperties(); - props.ContentType = "application/json"; - props.DeliveryMode = DeliveryModes.Transient; - props.Timestamp = new AmqpTimestamp(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - props.Headers = headers; - - await _channel.BasicPublishAsync("telemetry_topic", "telemetry.ticks", false, props, messageBodyBytes, - _cancellationTokenSource.Token); - - Console.WriteLine($"Sent batch of {batch.Count} points ({messageBodyBytes.Length} bytes) to RabbitMQ"); - } - catch (OperationCanceledException) - { - Console.WriteLine("Batch sending was cancelled"); - } - catch (Exception ex) - { - Console.WriteLine($"Error sending batch to RabbitMQ: {ex}"); - throw; - } - } - - private static int EstimateTickSize(object tick) - { - return 600; - } - - public async Task ForceFlush(int sessionId = 1) - { - if (_disposed) return; - - Console.WriteLine("Force flushing remaining data..."); - await FlushBuffer(sessionId); - Console.WriteLine("Force flush completed"); - } - - public void Dispose() - { - if (_disposed) return; - - _disposed = true; - - Console.WriteLine("Starting BufferedPubSub disposal..."); - - _cancellationTokenSource.Cancel(); - - try - { - _flushTimer?.Dispose(); - Console.WriteLine("Timer disposed"); - } - catch (Exception ex) - { - Console.WriteLine($"Error disposing timer: {ex.Message}"); - } - - try - { - var flushTask = ForceFlush(); - if (flushTask.Wait(TimeSpan.FromSeconds(3))) - { - Console.WriteLine("Final flush completed successfully"); - } - else - { - Console.WriteLine("Final flush timed out"); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error during final flush: {ex.Message}"); - } - - lock (_lockObject) - { - try - { - _channel?.CloseAsync().Wait(TimeSpan.FromSeconds(2)); - _channel?.Dispose(); - Console.WriteLine("Channel closed and disposed"); - } - catch (Exception ex) - { - Console.WriteLine($"Error disposing channel: {ex.Message}"); - } - - try - { - _connection?.CloseAsync().Wait(TimeSpan.FromSeconds(2)); - _connection?.Dispose(); - Console.WriteLine("Connection closed and disposed"); - } - catch (Exception ex) - { - Console.WriteLine($"Error disposing connection: {ex.Message}"); - } - - _isConnected = false; - } - - try - { - _cancellationTokenSource.Dispose(); - } - catch (Exception ex) - { - Console.WriteLine($"Error disposing cancellation token source: {ex.Message}"); - } - - Console.WriteLine( - $"BufferedPubSub disposed. Final stats: {_totalPointsBuffered} points, {_totalBatchesSent} batches"); - } -} \ No newline at end of file diff --git a/ingest/go/.env.template b/ingest/go/.env.template new file mode 100644 index 0000000..f703a44 --- /dev/null +++ b/ingest/go/.env.template @@ -0,0 +1,20 @@ +FILE_QUEUE_SIZE=5000 +WORKER_TIMEOUT=45m + +BATCH_SIZE_BYTES=16777216 +BATCH_SIZE_RECORDS=8000 +BATCH_TIMEOUT=5s + +GOGC=200 + +FILE_PROCESS_TIMEOUT=10m +RETRY_DELAY=500ms +MAX_RETRIES=3 + + +ENABLE_PPROF=false +PPROF_PORT=6060 +MEMORY_TUNING=false + +# File Processing +FILE_AGE_THRESHOLD=30s \ No newline at end of file diff --git a/ingest/go/LICENSE b/ingest/go/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/ingest/go/Makefile b/ingest/go/Makefile index 25b3fa9..c457193 100644 --- a/ingest/go/Makefile +++ b/ingest/go/Makefile @@ -1,4 +1,34 @@ -.PHONY: restart logs +.PHONY: help restart logs test test-v test-short coverage coverage-html bench bench-mmap bench-parser bench-struct bench-tick bench-all bench-save clean-test + +# Default target +.DEFAULT_GOAL := help + +help: + @echo "πŸ“š Available Make Targets:" + @echo "" + @echo " Docker Commands:" + @echo " make restart - Restart Docker services (clean rebuild)" + @echo " make logs - Follow Docker logs" + @echo "" + @echo " Testing Commands:" + @echo " make test - Run all tests with race detector" + @echo " make test-v - Run tests (verbose)" + @echo " make test-short - Run short tests only" + @echo " make coverage - Generate test coverage report" + @echo " make coverage-html - Generate and open HTML coverage report" + @echo "" + @echo " Benchmark Commands:" + @echo " make bench - Run all benchmarks (3s each)" + @echo " make bench-mmap - Run mmap benchmarks only" + @echo " make bench-parser - Run parser benchmarks only" + @echo " make bench-struct - Run struct parser benchmarks" + @echo " make bench-tick - Run tick benchmarks" + @echo " make bench-all - Run comprehensive benchmarks (5s each)" + @echo " make bench-save - Run benchmarks and save results with timestamp" + @echo "" + @echo " Cleanup Commands:" + @echo " make clean-test - Remove test artifacts and profiles" + @echo "" restart: @echo "πŸš€ Restarting Docker services..." @@ -8,4 +38,72 @@ restart: @echo "βœ… Done! Check logs with: make logs" logs: - @docker-compose logs -f go_app \ No newline at end of file + @docker-compose logs -f go_app + +# Testing targets +test: + @echo "πŸ§ͺ Running all tests..." + @cd ibt && go test -v -race -timeout 30s ./... + +test-v: + @echo "πŸ§ͺ Running all tests (verbose)..." + @cd ibt && go test -v -race -timeout 30s ./... + +test-short: + @echo "πŸ§ͺ Running short tests..." + @cd ibt && go test -short -race -timeout 10s ./... + +# Coverage targets +coverage: + @echo "πŸ“Š Generating coverage report..." + @cd ibt && go test -coverprofile=coverage.out ./... + @cd ibt && go tool cover -func=coverage.out + @echo "" + @echo "πŸ“„ Coverage profile saved to ibt/coverage.out" + @echo "πŸ’‘ Run 'make coverage-html' to view in browser" + +coverage-html: + @echo "πŸ“Š Generating HTML coverage report..." + @cd ibt && go test -coverprofile=coverage.out ./... + @cd ibt && go tool cover -html=coverage.out + @echo "βœ… Coverage report opened in browser" + +# Benchmark targets +bench: + @echo "⚑ Running all benchmarks..." + @cd ibt && go test -bench=. -run=^$$ -benchmem -benchtime=3s + +bench-mmap: + @echo "⚑ Running mmap benchmarks..." + @cd ibt && go test -bench=BenchmarkMmap -run=^$$ -benchmem -benchtime=3s + +bench-parser: + @echo "⚑ Running parser benchmarks..." + @cd ibt && go test -bench=BenchmarkParser -run=^$$ -benchmem -benchtime=3s + +bench-struct: + @echo "⚑ Running struct benchmarks..." + @cd ibt && go test -bench=BenchmarkDirectStruct -run=^$$ -benchmem -benchtime=3s + +bench-tick: + @echo "⚑ Running tick benchmarks..." + @cd ibt && go test -bench=BenchmarkTick -run=^$$ -benchmem -benchtime=3s + +bench-all: + @echo "⚑ Running comprehensive benchmarks (5s each)..." + @cd ibt && go test -bench=. -run=^$$ -benchmem -benchtime=5s -timeout 30m + +# Benchmark comparison (save results) +bench-save: + @echo "πŸ’Ύ Running benchmarks and saving results..." + @mkdir -p benchmarks + @cd ibt && go test -bench=. -run=^$$ -benchmem -benchtime=3s | tee ../benchmarks/bench_$(shell date +%Y%m%d_%H%M%S).txt + @echo "βœ… Results saved to benchmarks/" + +# Clean test artifacts +clean-test: + @echo "🧹 Cleaning test artifacts..." + @rm -rf ibt/*.test + @rm -rf ibt/*.prof + @rm -rf ibt/coverage.out + @echo "βœ… Clean complete" \ No newline at end of file diff --git a/ingest/go/cmd/ingest-app/main.go b/ingest/go/cmd/ingest-app/main.go deleted file mode 100644 index ecb0077..0000000 --- a/ingest/go/cmd/ingest-app/main.go +++ /dev/null @@ -1,171 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "log" - "net/http" - "os" - "os/signal" - "path/filepath" - "runtime" - "runtime/pprof" - "strings" - "syscall" - "time" - - "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/config" - "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/processing" - "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/worker" -) - -var progress *worker.ProgressDisplay - -func main() { - startTime := time.Now() - - cfg := config.LoadConfig() - - // Enable profiling if ENABLE_PPROF environment variable is set - if os.Getenv("ENABLE_PPROF") == "true" { - go func() { - log.Println("Starting pprof server on :6060") - log.Println(http.ListenAndServe(":6060", nil)) - }() - } - - // Enable CPU profiling if CPU_PROFILE environment variable is set - if cpuProfile := os.Getenv("CPU_PROFILE"); cpuProfile != "" { - f, err := os.Create(cpuProfile) - if err != nil { - log.Fatal("Could not create CPU profile: ", err) - } - defer f.Close() - - if err := pprof.StartCPUProfile(f); err != nil { - log.Fatal("Could not start CPU profile: ", err) - } - defer pprof.StopCPUProfile() - log.Printf("CPU profiling enabled, writing to %s", cpuProfile) - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - signalCh := make(chan os.Signal, 1) - signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) - go func() { - <-signalCh - if progress != nil { - progress.AddLog("Received shutdown signal...") - } - cancel() - }() - - if len(os.Args) < 2 { - log.Fatal("Usage: ./telemetry-app [--test-mode]") - } - - telemetryFolder := os.Args[1] - - if !strings.HasSuffix(telemetryFolder, string(filepath.Separator)) { - telemetryFolder += string(filepath.Separator) - } - - pool := worker.NewWorkerPool(cfg) - - expectedFiles, err := discoverAndQueueFiles(ctx, pool, telemetryFolder, cfg) - if err != nil { - log.Printf("Error during file discovery: %v", err) - return - } - - progress = worker.NewProgressDisplay(cfg.WorkerCount, expectedFiles) - pool.SetProgressDisplay(progress) - - // Temporarily disable log discarding to see error messages - log.SetOutput(io.Discard) - - progress.Start() - defer progress.Stop() - - progress.AddLog(fmt.Sprintf("Starting %d workers for %d files", cfg.WorkerCount, expectedFiles)) - - pool.Start() - defer pool.Stop() - - waitForCompletion(ctx, pool, startTime, expectedFiles) - - progress.AddLog(fmt.Sprintf("Completed in %v", time.Since(startTime))) - - // Write memory profile if MEM_PROFILE environment variable is set - if memProfile := os.Getenv("MEM_PROFILE"); memProfile != "" { - f, err := os.Create(memProfile) - if err != nil { - log.Printf("Could not create memory profile: %v", err) - } else { - defer f.Close() - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.Printf("Could not write memory profile: %v", err) - } else { - log.Printf("Memory profile written to %s", memProfile) - } - } - } - - time.Sleep(2 * time.Second) -} - -func discoverAndQueueFiles(ctx context.Context, pool *worker.WorkerPool, telemetryFolder string, cfg *config.Config) (int, error) { - directory := processing.NewDir(telemetryFolder, cfg) - files := directory.WatchDir() - - filesQueued := 0 - for _, file := range files { - select { - case <-ctx.Done(): - return filesQueued, ctx.Err() - default: - } - - fileName := file.Name() - - if !strings.Contains(fileName, ".ibt") { - continue - } - - workItem := worker.WorkItem{ - FilePath: filepath.Join(telemetryFolder, fileName), - FileInfo: file, - RetryCount: 0, - } - - if err := pool.SubmitFile(workItem); err != nil { - return filesQueued, err - } - - filesQueued++ - } - - return filesQueued, nil -} - -func waitForCompletion(ctx context.Context, pool *worker.WorkerPool, startTime time.Time, expectedFiles int) { - for { - select { - case <-ctx.Done(): - progress.AddLog("Shutdown requested...") - return - default: - time.Sleep(20 * time.Millisecond) - metrics := pool.GetMetrics() - - if metrics.QueueDepth == 0 && metrics.TotalFilesProcessed >= expectedFiles { - progress.AddLog("All files processed!") - return - } - } - } -} diff --git a/ingest/go/cmd/ingest/cmd/process.go b/ingest/go/cmd/ingest/cmd/process.go new file mode 100644 index 0000000..f003d85 --- /dev/null +++ b/ingest/go/cmd/ingest/cmd/process.go @@ -0,0 +1,221 @@ +/* +Copyright Β© 2026 NAME HERE +*/ +package cmd + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + "runtime" + "runtime/pprof" + "strings" + "syscall" + "time" + + "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/config" + "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/processing" + "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/worker" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +var ( + fresh bool + logger *zap.Logger +) + +var processCmd = &cobra.Command{ + Use: "ingest", + Short: "Process the telemetry data in the background", + Long: `Watch the telemetry directory and in the background process new telemetry files + + To clean the cache of sent file run with --fresh to upload all data in the dir again`, + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Inside rootCmd Run with args: %v\n", args) + Process(args[0]) + }, +} + +func init() { + processCmd.Flags().BoolVarP(&display, "display", "d", true, "terminal display of the ingest process") + processCmd.Flags().StringVarP(&telemetryPath, "telemetryPath", "p", "", "path to IRacing telemetry folder") + + processCmd.Flags().BoolVarP(&fresh, "fresh", "f", false, "will clean the local store of files that have been processed and start from fresh") +} + +func Process(telemetryFolder string) { + + startTime := time.Now() + + // Initialize Zap logger + var err error + // Verbose mode: full development logging + logger, err = zap.NewDevelopment() + + if err != nil { + log.Fatalf("Failed to initialize logger: %v", err) + } + defer logger.Sync() + + // Load configuration + cfg := config.LoadConfig() + + // Apply GOMAXPROCS if explicitly configured (0 means use Go's default) + if cfg.GoMaxProcs > 0 { + runtime.GOMAXPROCS(cfg.GoMaxProcs) + } + + if os.Getenv("ENABLE_PPROF") == "true" { + go func() { + if err := http.ListenAndServe(":6060", nil); err != nil { + logger.Error("pprof server failed", + zap.Error(err), + zap.String("action", "Check port 6060 is not in use")) + } + }() + } + + if cpuProfile := os.Getenv("CPU_PROFILE"); cpuProfile != "" { + f, err := os.Create(cpuProfile) + if err != nil { + logger.Fatal("Could not create CPU profile", + zap.Error(err), + zap.String("path", cpuProfile), + zap.String("action", "Check directory exists and has write permissions")) + } + defer f.Close() + + if err := pprof.StartCPUProfile(f); err != nil { + logger.Fatal("Could not start CPU profile", + zap.Error(err), + zap.String("action", "Check file can be written")) + } + defer pprof.StopCPUProfile() + } + + // Setup context and signal handling + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + go func() { + <-signalCh + cancel() + }() + + if !strings.HasSuffix(telemetryFolder, string(filepath.Separator)) { + telemetryFolder += string(filepath.Separator) + } + + // Verify telemetry folder exists + if _, err := os.Stat(telemetryFolder); os.IsNotExist(err) { + logger.Fatal("Telemetry directory does not exist", + zap.String("path", telemetryFolder), + zap.String("action", "Create directory or set IBT_DATA_DIR environment variable")) + } + + // Create worker pool + pool := worker.NewWorkerPool(cfg, logger) + + expectedFiles, err := discoverAndQueueFiles(ctx, pool, telemetryFolder, cfg, logger) + if err != nil { + logger.Error("File discovery failed", + zap.Error(err), + zap.String("path", telemetryFolder), + zap.String("action", "Check directory permissions and IBT files exist")) + return + } + log.Printf("STARTUP: Found %d IBT files to process", expectedFiles) + + // Start worker pool + if err := pool.Start(); err != nil { + logger.Fatal("Failed to start worker pool", + zap.Error(err), + zap.String("action", "Check system resources and configuration")) + } + defer func() { + if err := pool.Stop(); err != nil { + logger.Error("Error stopping worker pool", + zap.Error(err)) + } + }() + + // Wait for completion + waitForCompletion(ctx, pool, startTime, expectedFiles) + + // Write memory profile if MEM_PROFILE environment variable is set + if memProfile := os.Getenv("MEM_PROFILE"); memProfile != "" { + f, err := os.Create(memProfile) + if err != nil { + logger.Error("Could not create memory profile", + zap.Error(err), + zap.String("path", memProfile), + zap.String("action", "Check directory exists and has write permissions")) + } else { + defer f.Close() + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + logger.Error("Could not write memory profile", + zap.Error(err), + zap.String("action", "Check disk space and file permissions")) + } + } + } +} + +func discoverAndQueueFiles(ctx context.Context, pool *worker.WorkerPool, telemetryFolder string, cfg *config.Config, logger *zap.Logger) (int, error) { + directory := processing.NewDir(telemetryFolder, cfg, logger) + files := directory.WatchDir() + + filesQueued := 0 + for _, file := range files { + select { + case <-ctx.Done(): + return filesQueued, ctx.Err() + default: + } + + fileName := file.Name() + + if !strings.Contains(fileName, ".ibt") { + continue + } + + workItem := worker.WorkItem{ + FilePath: filepath.Join(telemetryFolder, fileName), + FileInfo: file, + RetryCount: 0, + } + + if err := pool.SubmitFile(workItem); err != nil { + return filesQueued, err + } + + filesQueued++ + } + + return filesQueued, nil +} + +func waitForCompletion(ctx context.Context, pool *worker.WorkerPool, startTime time.Time, expectedFiles int) { + for { + select { + case <-ctx.Done(): + // Shutdown requested - no log needed, handled by pool + return + default: + time.Sleep(20 * time.Millisecond) + metrics := pool.GetMetrics() + + if metrics.QueueDepth == 0 && metrics.TotalFilesProcessed >= expectedFiles { + // Completion - metrics available via Prometheus, no log needed + return + } + } + } +} diff --git a/ingest/go/cmd/ingest/cmd/root.go b/ingest/go/cmd/ingest/cmd/root.go new file mode 100644 index 0000000..4c2ad05 --- /dev/null +++ b/ingest/go/cmd/ingest/cmd/root.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "log" + "os" + + "github.com/spf13/cobra" +) + +var ( + telemetryPath string + display bool +) + +var rootCmd = &cobra.Command{ + Use: "ingest", + Short: "IRacting telemetry ingest", + Long: `The telemetry ingest allows us to take data from our racing sim and IRacing session and visualise that. + In traditional motorsports that would give better insights to the race engineer who can build off the data to improve the driver and car. + + The ingest service uploads all the sessions that are stored on your local machine to the IRacing dashboard service. It can be run in the background or as a one off.`, + Args: cobra.ArbitraryArgs, + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Inside rootCmd Run with args: %v \n", cmd) + + telemetryPath := cmd.Flag("telemetryPath").Value.String() + + if telemetryPath == "" { + log.Printf("no telemetry path found") + } else { + Process(telemetryPath) + } + + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.Flags().BoolVarP(&display, "display", "d", false, "terminal display of the ingest process") + rootCmd.Flags().StringVarP(&telemetryPath, "telemetryPath", "p", "", "path to IRacing telemetry folder") +} diff --git a/ingest/go/cmd/ingest/main.go b/ingest/go/cmd/ingest/main.go new file mode 100644 index 0000000..549fae9 --- /dev/null +++ b/ingest/go/cmd/ingest/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/OJPARKINSON/IRacing-Display/ingest/go/cmd/ingest/cmd" + +func main() { + cmd.Execute() +} diff --git a/ingest/go/go.mod b/ingest/go/go.mod index 28d2a60..2c527ef 100644 --- a/ingest/go/go.mod +++ b/ingest/go/go.mod @@ -1,30 +1,42 @@ module github.com/OJPARKINSON/IRacing-Display/ingest/go -go 1.24 +go 1.25.5 require ( - github.com/OJPARKINSON/ibt v0.0.0-20250726143902-84776e9ce68d - github.com/fatih/color v1.18.0 + github.com/OJPARKINSON/ibt v0.1.4 + github.com/jedib0t/go-pretty/v6 v6.7.8 + github.com/prometheus/client_golang v1.23.2 github.com/rabbitmq/amqp091-go v1.10.0 + go.uber.org/zap v1.27.1 + golang.org/x/sync v0.19.0 + google.golang.org/protobuf v1.36.11 ) -// Use local fork instead of remote dependency -replace github.com/OJPARKINSON/ibt => ./ibt - require ( - github.com/kr/pretty v0.3.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect ) +// // Use local fork instead of remote dependency +replace github.com/OJPARKINSON/ibt => ./ibt + require ( - github.com/golang/protobuf v1.5.4 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/teamjorge/ibt v0.0.0-20240923192211-5f50fa19d38d // indirect - golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.24.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 // indirect + github.com/vbauerster/mpb/v8 v8.11.3 + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/ingest/go/go.sum b/ingest/go/go.sum index 85fd8e2..733320e 100644 --- a/ingest/go/go.sum +++ b/ingest/go/go.sum @@ -1,44 +1,75 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= +github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/teamjorge/ibt v0.0.0-20240923192211-5f50fa19d38d h1:uP7sOhcOd+QPAiIYOp4WG2HM1pd7ZW+VI9sgHvDEaCw= -github.com/teamjorge/ibt v0.0.0-20240923192211-5f50fa19d38d/go.mod h1:KsjKzqrbkWfPbXRPMi78uBrFBcGhYbCY6ESKQYX00bY= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vbauerster/mpb/v8 v8.11.3 h1:iniBmO4ySXCl4gVdmJpgrtormH5uvjpxcx/dMyVU9Jw= +github.com/vbauerster/mpb/v8 v8.11.3/go.mod h1:n9M7WbP0NFjpgKS5XdEC3tMRgZTNM/xtC8zWGkiMuy0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ingest/go/ibt b/ingest/go/ibt index bbb5577..f518f9b 160000 --- a/ingest/go/ibt +++ b/ingest/go/ibt @@ -1 +1 @@ -Subproject commit bbb55771b45ad1d578b6b2a0bff0962083cafc26 +Subproject commit f518f9bbe37657864aaae5a3fa3657125abb8668 diff --git a/ingest/go/internal/config/config.go b/ingest/go/internal/config/config.go index e636dae..c45451c 100644 --- a/ingest/go/internal/config/config.go +++ b/ingest/go/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "os" + "runtime" "strconv" "time" ) @@ -23,59 +24,81 @@ type Config struct { FileProcessTimeout time.Duration GoMaxProcs int - GOGC int EnablePprof bool PprofPort string MemoryTuning bool - RabbitMQPoolSize int - RabbitMQPrefetchCount int - RabbitMQBatchSize int - RabbitMQBatchTimeout time.Duration - RabbitMQConfirms bool - RabbitMQPersistent bool - RabbitMQHeartbeat time.Duration - RabbitMQChannelMax int - RabbitMQFrameSize int + IngestUrl string BatchSizeRecords int + + UseStructPipeline bool + + // Data directory configuration + DataDirectory string + + CFAccountID string + CFD1DatabaseID string + CFApiToken string + R2AccountID string + R2AccessKeyID string + R2SecretAccess string + R2BucketNme string } func LoadConfig() *Config { + cpuCount := runtime.NumCPU() + + defaultWorkerCount := cpuCount + (cpuCount / 4) + if defaultWorkerCount < 4 { + defaultWorkerCount = 4 + } + + defaultGoMaxProcs := 0 + + workerCount := getEnvAsInt("WORKER_COUNT", defaultWorkerCount) + return &Config{ - WorkerCount: getEnvAsInt("WORKER_COUNT", 24), - FileQueueSize: getEnvAsInt("FILE_QUEUE_SIZE", 500), + WorkerCount: workerCount, + FileQueueSize: getEnvAsInt("FILE_QUEUE_SIZE", 1000), WorkerTimeout: getEnvAsDuration("WORKER_TIMEOUT", 30*time.Minute), - BatchSizeBytes: getEnvAsInt("BATCH_SIZE_BYTES", 4194304), - BatchTimeout: getEnvAsDuration("BATCH_TIMEOUT", 500*time.Millisecond), + BatchSizeBytes: getEnvAsInt("BATCH_SIZE_BYTES", 33554432), + BatchTimeout: getEnvAsDuration("BATCH_TIMEOUT", 50*time.Millisecond), MaxRetries: getEnvAsInt("MAX_RETRIES", 3), RetryDelay: getEnvAsDuration("RETRY_DELAY", 250*time.Millisecond), - RabbitMQURL: getEnv("RABBITMQ_URL", "amqp://admin:changeme@rabbitmq:5672/"), + DisableRabbitMQ: getEnvAsBool("DISABLE_RABBITMQ", false), + RabbitMQURL: getEnv("RABBITMQ_URL", "amqp://admin:changeme@localhost:5672"), FileAgeThreshold: getEnvAsDuration("FILE_AGE_THRESHOLD", 30*time.Second), FileProcessTimeout: getEnvAsDuration("FILE_PROCESS_TIMEOUT", 10*time.Minute), - GoMaxProcs: getEnvAsInt("GOMAXPROCS", 0), - GOGC: getEnvAsInt("GOGC", 50), + GoMaxProcs: getEnvAsInt("GOMAXPROCS", defaultGoMaxProcs), - EnablePprof: getEnvAsBool("ENABLE_PPROF", true), + // Development & Monitoring + EnablePprof: getEnvAsBool("ENABLE_PPROF", false), PprofPort: getEnv("PPROF_PORT", "6060"), MemoryTuning: getEnvAsBool("MEMORY_TUNING", true), - RabbitMQPoolSize: getEnvAsInt("RABBITMQ_POOL_SIZE", 24), - RabbitMQPrefetchCount: getEnvAsInt("RABBITMQ_PREFETCH_COUNT", 10000), - RabbitMQBatchSize: getEnvAsInt("RABBITMQ_BATCH_SIZE", 1000), - RabbitMQBatchTimeout: getEnvAsDuration("RABBITMQ_BATCH_TIMEOUT", 5*time.Millisecond), - RabbitMQConfirms: getEnvAsBool("RABBITMQ_CONFIRMS", false), - RabbitMQPersistent: getEnvAsBool("RABBITMQ_PERSISTENT", false), - RabbitMQHeartbeat: getEnvAsDuration("RABBITMQ_HEARTBEAT", 30*time.Second), - RabbitMQChannelMax: getEnvAsInt("RABBITMQ_CHANNEL_MAX", 4096), - RabbitMQFrameSize: getEnvAsInt("RABBITMQ_FRAME_SIZE", 1048576), - - BatchSizeRecords: getEnvAsInt("BATCH_SIZE_RECORDS", 4000), + IngestUrl: getEnv("INGEST_URL", "http://localhost:8010/api/ingest"), + + UseStructPipeline: getEnvAsBool("USE_STRUCT_PIPELINE", true), + + // Record Processing + BatchSizeRecords: getEnvAsInt("BATCH_SIZE_RECORDS", 16000), + + CFAccountID: getEnv("CF_ACCOUNT_ID", ""), + CFD1DatabaseID: getEnv("CF_D1_DATABASE_ID", ""), + CFApiToken: getEnv("CF_API_TOKEN", ""), + R2AccountID: getEnv("R2_ACCOUNT_ID", ""), + R2AccessKeyID: getEnv("R2_ACCESS_KEY_ID", ""), + R2SecretAccess: getEnv("R2_SECRET_ACCESS_KEY", ""), + R2BucketNme: getEnv("R2_BUCKET_NAME", ""), + + // Data Directory - defaults to ./ibt_files/ for backward compatibility + // DataDirectory: getEnv("IBT_DATA_DIR", "./ibt_files/"), } } diff --git a/ingest/go/internal/messaging/batchPool.go b/ingest/go/internal/messaging/batchPool.go new file mode 100644 index 0000000..8255294 --- /dev/null +++ b/ingest/go/internal/messaging/batchPool.go @@ -0,0 +1,36 @@ +package messaging + +import ( + "sync" + + "google.golang.org/protobuf/types/known/timestamppb" +) + +type BatchPool struct { + pool sync.Pool +} + +func NewBatchPool(batchSize int) *BatchPool { + return &BatchPool{ + pool: sync.Pool{ + New: func() interface{} { + return &TelemetryBatch{ + Records: make([]*Telemetry, 0, batchSize), + } + }, + }, + } +} + +func (bp *BatchPool) Get() *TelemetryBatch { + return bp.pool.Get().(*TelemetryBatch) +} + +func (bp *BatchPool) Put(batch *TelemetryBatch) { + batch.Records = batch.Records[:0] + batch.BatchId = "" + batch.Timestamp = ×tamppb.Timestamp{} + batch.WorkerId = 0 + batch.SessionId = "" + bp.pool.Put(batch) +} diff --git a/ingest/go/internal/messaging/pubSub.go b/ingest/go/internal/messaging/pubSub.go index f26a329..3afe3be 100644 --- a/ingest/go/internal/messaging/pubSub.go +++ b/ingest/go/internal/messaging/pubSub.go @@ -5,99 +5,43 @@ import ( "context" "fmt" "log" + "net/http" "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/config" - amqp "github.com/rabbitmq/amqp091-go" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) -var jsonBufferPool = sync.Pool{ - New: func() interface{} { - return &bytes.Buffer{} - }, -} - type ConnectionPool struct { - connections []*amqp.Connection - channels []*amqp.Channel - mu sync.RWMutex - url string - poolSize int - current int + url string + poolSize int + current atomic.Uint32 // Lock-free round-robin counter + closing atomic.Bool } +var ( + activePublishers sync.WaitGroup + publisherShutdown atomic.Bool +) + func NewConnectionPool(url string, poolSize int) (*ConnectionPool, error) { pool := &ConnectionPool{ - connections: make([]*amqp.Connection, poolSize), - channels: make([]*amqp.Channel, poolSize), - url: url, - poolSize: poolSize, - } - - for i := 0; i < poolSize; i++ { - conn, err := amqp.Dial(url) - if err != nil { - pool.Close() - return nil, fmt.Errorf("failed to create connection %d: %w", i, err) - } - - ch, err := conn.Channel() - if err != nil { - conn.Close() - pool.Close() - - return nil, fmt.Errorf("failed to create channel %d: %w", i, err) - } - - err = ch.Qos(1000, 0, false) - if err != nil { - ch.Close() - conn.Close() - pool.Close() - return nil, fmt.Errorf("failed to set Qos for channel %d: %w", i, err) - } - - pool.connections[i] = conn - pool.channels[i] = ch + url: url, + poolSize: poolSize, } return pool, nil } -func (p *ConnectionPool) GetChannel() *amqp.Channel { - p.mu.RLock() - defer p.mu.RUnlock() - - if p.channels == nil || len(p.channels) == 0 { - return nil - } - - ch := p.channels[p.current] - p.current = (p.current + 1) % p.poolSize - - // Check if channel is still open - if ch == nil || ch.IsClosed() { - log.Printf("Channel %d is closed, attempting to recreate", p.current-1) - return nil - } - - return ch -} - func (p *ConnectionPool) Close() { - p.mu.Lock() - defer p.mu.Unlock() + time.Sleep(500 * time.Millisecond) - for i := 0; i < len(p.channels); i++ { - if p.channels[i] != nil { - p.channels[i].Close() - } - } + p.closing.Store(true) } type PubSub struct { @@ -118,32 +62,78 @@ type PubSub struct { batchSizeBytes int batchSizeRecords int + // Data persistence for RabbitMQ failures + failedBatchCount atomic.Int32 + persistedBatches int + maxPersistentBytes int64 + + // Publish failures fallback + consecutiveFailures atomic.Int32 + lastFailureTime time.Time + maxConsecutiveFailures int + + // Async publishing + publishQueue chan *publishRequest + publishWg sync.WaitGroup + publishDone chan struct{} + isShuttingDown atomic.Bool + mu sync.Mutex + + client http.Client +} + +type publishRequest struct { + batch *TelemetryBatch + data []byte + errCh chan error } type PublishMetrics struct { - TotalBatches int - TotalRecords int - TotalBytes int64 - CurrentBatchSize int - LastFlush time.Time + TotalBatches int + TotalRecords int + TotalBytes int64 + CurrentBatchSize int + LastFlush time.Time + FailedBatches atomic.Int32 + PersistedBatches int + CircuitBreakerOpen bool + ConsecutiveFailures atomic.Int32 } -func NewPubSub(sessionId string, sessionTime time.Time, cfg *config.Config, pool *ConnectionPool) *PubSub { +func NewPubSub(sessionId string, sessionTime time.Time, cfg *config.Config, pool *ConnectionPool, workerId int) *PubSub { + client := http.Client{Timeout: 10 * time.Second} + ps := &PubSub{ - pool: pool, - sessionID: sessionId, - sessionTime: sessionTime, - config: cfg, - ctx: context.Background(), - batchPool: NewBatchPool(cfg.RabbitMQBatchSize), - batchSizeBytes: cfg.BatchSizeBytes, - batchSizeRecords: cfg.BatchSizeRecords, - lastFlush: time.Now(), + pool: pool, + sessionID: sessionId, + sessionTime: sessionTime, + config: cfg, + ctx: context.Background(), + batchPool: NewBatchPool(cfg.WorkerCount), + batchSizeBytes: cfg.BatchSizeBytes, + batchSizeRecords: cfg.BatchSizeRecords, + lastFlush: time.Now(), + maxPersistentBytes: 500 * 1024 * 1024, // 500MB max persistent storage per worker + + consecutiveFailures: atomic.Int32{}, + maxConsecutiveFailures: 3, // Open circuit after 3 consecutive failures + + // Async publishing - buffer up to 20 batches to prevent blocking + publishQueue: make(chan *publishRequest, 20), + publishDone: make(chan struct{}), + + workerID: workerId, + client: client, } ps.recordBatch = make([]*Telemetry, 0, cfg.BatchSizeRecords) + // Start async publisher goroutine + activePublishers.Add(1) + ps.publishWg.Add(1) + go ps.publishWorker() + return ps } @@ -192,14 +182,6 @@ func (ps *PubSub) Exec(data []map[string]interface{}) error { return nil } - if len(data) > 0 { - if wid, ok := data[0]["workerID"]; ok { - if widInt, ok := wid.(int); ok { - ps.workerID = widInt - } - } - } - for _, record := range data { if err := ps.AddRecord(record); err != nil { return fmt.Errorf("failed to add record to batch: %w", err) @@ -208,6 +190,15 @@ func (ps *PubSub) Exec(data []map[string]interface{}) error { return nil } +func (ps *PubSub) recordFailure() { + ps.consecutiveFailures.Add(1) + ps.lastFailureTime = time.Now() +} + +func (ps *PubSub) recordSuccess() { + ps.consecutiveFailures.Store(0) +} + func (ps *PubSub) AddRecord(record map[string]interface{}) error { ps.mu.Lock() defer ps.mu.Unlock() @@ -237,17 +228,23 @@ func (ps *PubSub) transformRecord(record map[string]interface{}) *Telemetry { sessionNum := "" if val, ok := record["SessionNum"]; ok { - sessionNum = fmt.Sprintf("%v", val) + if v, ok := val.(int); ok { + sessionNum = strconv.Itoa(v) + } } sessionType := "" if val, ok := record["sessionType"]; ok { - sessionType = fmt.Sprintf("%v", val) + if v, ok := val.(int); ok { + sessionType = strconv.Itoa(v) + } } sessionName := "" if val, ok := record["sessionName"]; ok { - sessionName = fmt.Sprintf("%v", val) + if v, ok := val.(int); ok { + sessionName = strconv.Itoa(v) + } } trackName := "" @@ -258,12 +255,16 @@ func (ps *PubSub) transformRecord(record map[string]interface{}) *Telemetry { trackID := "" if val, ok := record["trackID"]; ok { - trackID = fmt.Sprintf("%v", val) + if v, ok := val.(int); ok { + trackID = strconv.Itoa(v) + } } carID := "" if val, ok := record["PlayerCarIdx"]; ok { - carID = fmt.Sprintf("%v", val) + if v, ok := val.(int); ok { + carID = strconv.Itoa(v) + } } tickTime := ps.sessionTime.Add(time.Duration(sessionTime * float64(time.Second))) @@ -318,6 +319,70 @@ func (ps *PubSub) transformRecord(record map[string]interface{}) *Telemetry { } } +// publishWorker runs in background goroutine to handle async publishing +func (ps *PubSub) publishWorker() { + defer ps.publishWg.Done() + defer activePublishers.Done() + log.Printf("Worker %d: publishWorker goroutine started for session %s", ps.workerID, ps.sessionID) + + for { + select { + case req := <-ps.publishQueue: + log.Printf("Worker %d: Processing batch %s from async queue", ps.workerID, req.batch.BatchId) + err := ps.doPublish(req.batch, req.data) + if err != nil { + log.Printf("Worker %d: ERROR publishing batch %s asynchronously: %v", + ps.workerID, req.batch.BatchId, err) + } else { + log.Printf("Worker %d: Successfully published batch %s", ps.workerID, req.batch.BatchId) + } + req.errCh <- err + case <-ps.publishDone: + log.Printf("Worker %d: Draining %d remaining batches from queue", ps.workerID, len(ps.publishQueue)) + for len(ps.publishQueue) > 0 { + req := <-ps.publishQueue + err := ps.doPublish(req.batch, req.data) + if err != nil { + log.Printf("Worker %d: ERROR publishing batch %s during shutdown: %v", + ps.workerID, req.batch.BatchId, err) + } + req.errCh <- err + } + return + } + } +} + +// doPublish performs the actual HTTP publish operation +func (ps *PubSub) doPublish(batch *TelemetryBatch, data []byte) error { + startTime := time.Now() + dataReader := bytes.NewReader(data) + + res, err := ps.client.Post(ps.config.IngestUrl, "application/x-protobuf", dataReader) + if err != nil { + log.Printf("Worker %d: Failed to publish batch %s: %v", ps.workerID, batch.BatchId, err) + ps.recordFailure() + + ps.failedBatchCount.Add(1) + + return nil + } + defer res.Body.Close() + + if res.StatusCode >= 200 && res.StatusCode < 300 { + ps.recordSuccess() + return nil + } + + log.Printf("Worker %d: Batch %s publish returned status %d", ps.workerID, batch.BatchId, res.StatusCode) + ps.recordFailure() + + ps.failedBatchCount.Add(1) + + log.Println("do publish took: ", time.Since(startTime)) + return nil +} + func (ps *PubSub) flushBatchInternal() error { if len(ps.recordBatch) == 0 { return nil @@ -333,62 +398,52 @@ func (ps *PubSub) flushBatchInternal() error { data, err := proto.Marshal(batch) if err != nil { - return fmt.Errorf("failed to marshal protobuf batch: %w", err) + return fmt.Errorf("failed to marshal protobuf batch: %w\nAction: This is an internal error - check telemetry data validity", err) } - maxRetries := 3 - for retry := 0; retry < maxRetries; retry++ { - ch := ps.pool.GetChannel() - if ch == nil { - if retry < maxRetries-1 { - time.Sleep(time.Duration(retry+1) * 100 * time.Millisecond) - continue - } - return fmt.Errorf("failed to get channel after %d retries", maxRetries) - } + // During shutdown, publish synchronously to avoid queuing delays + if ps.isShuttingDown.Load() { + err := ps.doPublish(batch, data) + ps.recordBatch = ps.recordBatch[:0] + ps.totalBytes = 0 + ps.totalBatches++ + ps.lastFlush = time.Now() + return err + } - ctx, cancel := context.WithTimeout(ps.ctx, 10*time.Second) - - err := ch.PublishWithContext(ctx, "telemetry_topic", "telemetry.ticks", false, false, - amqp.Publishing{ - ContentType: "application/x-protobuf", - Body: data, - DeliveryMode: amqp.Transient, - Timestamp: time.Now(), - MessageId: batch.BatchId, - Headers: amqp.Table{ - "worker_id": ps.workerID, - "record_count": len(ps.recordBatch), - "batch_size": len(data), - "format": "protobuf", - }, - }) - - cancel() - - if err == nil { - ps.recordBatch = ps.recordBatch[:0] - ps.totalBytes = 0 - ps.totalBatches++ - ps.lastFlush = time.Now() - - if ps.totalBatches%50 == 0 { - log.Printf("Worker %d: Published batch %d (%d records, %d bytes)", - ps.workerID, ps.totalBatches, len(batch.Records), len(batch.Records)) - } + // Try async publishing first (non-blocking if queue has space) + req := &publishRequest{ + batch: batch, + data: data, + errCh: make(chan error, 1), + } - return nil - } + select { + case ps.publishQueue <- req: + // Successfully queued for async publishing + // Clear batch immediately so parser can continue + ps.recordBatch = ps.recordBatch[:0] + ps.totalBytes = 0 + ps.totalBatches++ + ps.lastFlush = time.Now() + + // Don't wait for result - let it publish async + // Errors are logged by the async worker + return nil - log.Printf("Worker %d: Failed to publish batch (attempt %d/%d): %v", - ps.workerID, retry+1, maxRetries, err) + case <-time.After(100 * time.Millisecond): + // Queue is full/slow - do sync publish to avoid blocking parser too long + log.Printf("Worker %d: Publish queue full, falling back to sync publish", ps.workerID) + err := ps.doPublish(batch, data) - if retry < maxRetries-1 { - time.Sleep(time.Duration(retry+1) * 250 * time.Millisecond) - } - } + // Clear batch regardless of error (error is handled via persistence) + ps.recordBatch = ps.recordBatch[:0] + ps.totalBytes = 0 + ps.totalBatches++ + ps.lastFlush = time.Now() - return fmt.Errorf("failed to publish batch after %d retries", maxRetries) + return err + } } func (ps *PubSub) FlushBatch() error { @@ -398,13 +453,33 @@ func (ps *PubSub) FlushBatch() error { } func (ps *PubSub) Close() error { + // Mark as shutting down to skip retries/delays + ps.isShuttingDown.Store(true) + + // Flush any remaining batches if err := ps.FlushBatch(); err != nil { log.Printf("Worker %d: Error flushing final batch: %v", ps.workerID, err) - return err } - log.Printf("Worker %d: Published %d batches with %d total records", - ps.workerID, ps.totalBatches, ps.totalRecords) + // Signal async publisher to shut down + close(ps.publishDone) + + // Wait for async publisher to finish with timeout + done := make(chan struct{}) + go func() { + ps.publishWg.Wait() + close(done) + }() + + select { + case <-done: + // Normal shutdown completed + case <-time.After(4 * time.Second): + // Timeout - queue taking too long, abandon remaining messages + // Silently continue - messages may be lost + } + + // Close completes silently - stats available via GetMetrics() return nil } @@ -413,10 +488,32 @@ func (ps *PubSub) GetMetrics() PublishMetrics { defer ps.mu.Unlock() return PublishMetrics{ - TotalBatches: ps.totalBatches, - TotalRecords: ps.totalRecords, - TotalBytes: ps.totalBytes, - CurrentBatchSize: len(ps.recordBatch), - LastFlush: ps.lastFlush, + TotalBatches: ps.totalBatches, + TotalRecords: ps.totalRecords, + TotalBytes: ps.totalBytes, + CurrentBatchSize: len(ps.recordBatch), + LastFlush: ps.lastFlush, + FailedBatches: ps.failedBatchCount, + PersistedBatches: ps.persistedBatches, + ConsecutiveFailures: ps.consecutiveFailures, } } + +func (ps *PubSub) GetDisplayMetrics() map[string]interface{} { + ps.mu.Lock() + defer ps.mu.Unlock() + + return map[string]interface{}{ + "batches_sent": ps.totalBatches, + "records_send": ps.totalRecords, + "queue_size": len(ps.publishQueue), + "failed_batches": ps.failedBatchCount, + } +} + +func WaitForAllPublishers() { + publisherShutdown.Store(true) + log.Println("Waiting for all publishers to finish draining...") + activePublishers.Wait() // Wait indefinitely until all done + log.Println("All publishers finished draining") +} diff --git a/ingest/go/internal/messaging/transform_struct.go b/ingest/go/internal/messaging/transform_struct.go new file mode 100644 index 0000000..5a79b41 --- /dev/null +++ b/ingest/go/internal/messaging/transform_struct.go @@ -0,0 +1,114 @@ +package messaging + +import ( + "fmt" + "strconv" + "time" + + "github.com/OJPARKINSON/ibt" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TransformStructBatch(ticks []*ibt.TelemetryTick) ([]*Telemetry, error) { + result := make([]*Telemetry, len(ticks)) + + for i, tick := range ticks { + result[i] = &Telemetry{ + LapId: strconv.Itoa(int(tick.LapID)), + Speed: tick.Speed, + LapDistPct: tick.LapDistPct, + Throttle: tick.Throttle, + Brake: tick.Brake, + Gear: tick.Gear, + Rpm: tick.RPM, + SteeringWheelAngle: tick.SteeringWheelAngle, + VelocityX: tick.VelocityX, + VelocityY: tick.VelocityY, + VelocityZ: tick.VelocityZ, + Lat: tick.Lat, + Lon: tick.Lon, + SessionTime: tick.SessionTime, + PlayerCarPosition: tick.PlayerCarPosition, + FuelLevel: tick.FuelLevel, + CarId: strconv.Itoa(int(tick.PlayerCarIdx)), + SessionNum: strconv.Itoa(int(tick.SessionNum)), + Alt: tick.Alt, + LatAccel: tick.LatAccel, + LongAccel: tick.LongAccel, + VertAccel: tick.VertAccel, + Pitch: tick.Pitch, + Roll: tick.Roll, + Yaw: tick.Yaw, + YawNorth: tick.YawNorth, + Voltage: tick.Voltage, + LapLastLapTime: tick.LapLastLapTime, + WaterTemp: tick.WaterTemp, + LapDeltaToBestLap: tick.LapDeltaToBestLap, + LapCurrentLapTime: tick.LapCurrentLapTime, + LFpressure: tick.LFpressure, + RFpressure: tick.RFpressure, + LRpressure: tick.LRpressure, + RRpressure: tick.RRpressure, + LFtempM: tick.LFtempM, + RFtempM: tick.RFtempM, + LRtempM: tick.LRtempM, + RRtempM: tick.RRtempM, + + // Session metadata + SessionId: tick.SessionID, + SessionType: tick.SessionType, + SessionName: tick.SessionName, + TrackName: tick.TrackName, + TrackId: fmt.Sprintf("%d", tick.TrackID), + WorkerId: uint32(tick.WorkerID), + + TickTime: timestamppb.New(tick.TickTime), + } + } + + return result, nil +} + +func (ps *PubSub) ExecStructs(ticks []*ibt.TelemetryTick) error { + if len(ticks) == 0 { + return nil + } + + // Extract workerID from first tick if available + if len(ticks) > 0 { + ps.workerID = ticks[0].WorkerID + } + + // Transform batch (no lock needed - pure transformation) + protoTicks, err := TransformStructBatch(ticks) + if err != nil { + return fmt.Errorf("failed to transform struct batch: %w", err) + } + + // Pre-calculate batch size ONCE instead of per-tick + // Use fixed estimate to avoid expensive proto.Size() calls + const estimatedTickSize = 512 + batchSize := int64(len(protoTicks) * estimatedTickSize) + + // Lock ONCE for the entire batch operation + ps.mu.Lock() + defer ps.mu.Unlock() + + // Check if adding this batch would exceed limits - flush first if needed + wouldExceedRecords := len(ps.recordBatch)+len(protoTicks) >= ps.batchSizeRecords + wouldExceedBytes := ps.totalBytes+batchSize >= int64(ps.batchSizeBytes) + shouldFlushTime := time.Since(ps.lastFlush) > time.Duration(ps.config.BatchTimeout) + + if (wouldExceedRecords || wouldExceedBytes || shouldFlushTime) && len(ps.recordBatch) > 0 { + if err := ps.flushBatchInternal(); err != nil { + return err + } + } + + // Append entire batch at once (fast slice append) + ps.recordBatch = append(ps.recordBatch, protoTicks...) + ps.totalRecords += len(protoTicks) + ps.totalBytes += batchSize + + return nil +} diff --git a/ingest/go/internal/messaging/types.go b/ingest/go/internal/messaging/types.go deleted file mode 100644 index 82a9765..0000000 --- a/ingest/go/internal/messaging/types.go +++ /dev/null @@ -1,114 +0,0 @@ -package messaging - -import ( - "sync" - - "google.golang.org/protobuf/types/known/timestamppb" -) - -type TelemetryRecord struct { - CarID string `json:"car_id"` - Brake float32 `json:"brake"` - FuelLevel float32 `json:"fuel_level"` - Gear int16 `json:"gear"` - TrackName string `json:"track_name"` - TrackID int32 `json:"track_id"` - LapCurrentLapTime float32 `json:"lap_current_lap_time"` - LapDistPct float32 `json:"lap_dist_pct"` - LapID string `json:"lap_id"` - Lat float32 `json:"lat"` - Lon float32 `json:"lon"` - PlayerCarPosition int16 `json:"player_car_position"` - RPM float32 `json:"rpm"` - SessionID string `json:"session_id"` - SessionNum string `json:"session_num"` - SessionType string `json:"session_type"` - SessionName string `json:"session_name"` - SessionTime float32 `json:"session_time"` - Speed float32 `json:"speed"` - SteeringWheelAngle float32 `json:"steering_wheel_angle"` - Throttle float32 `json:"throttle"` - TickTime string `json:"tick_time"` - VelocityX float32 `json:"velocity_x"` - VelocityY float32 `json:"velocity_y"` - VelocityZ float32 `json:"velocity_z"` - Alt float32 `json:"alt"` - LatAccel float32 `json:"lat_accel"` - LongAccel float32 `json:"long_accel"` - VertAccel float32 `json:"vert_accel"` - Pitch float32 `json:"pitch"` - Roll float32 `json:"roll"` - Yaw float32 `json:"yaw"` - YawNorth float32 `json:"yaw_north"` - Voltage float32 `json:"voltage"` - LapLastLapTime float32 `json:"lapLastLapTime"` - WaterTemp float32 `json:"waterTemp"` - LapDeltaToBestLap float32 `json:"lapDeltaToBestLap"` - LFPressure float32 `json:"lFpressure"` - RFPressure float32 `json:"rFpressure"` - LRPressure float32 `json:"lRpressure"` - RRPressure float32 `json:"rRpressure"` - LFTempM float32 `json:"lFtempM"` - RFTempM float32 `json:"rFtempM"` - LRTempM float32 `json:"lRtempM"` - RRTempM float32 `json:"rRtempM"` - - GroupNum int `json:"-"` - WorkerID int `json:"-"` -} - -func (tr *TelemetryRecord) Reset() { - *tr = TelemetryRecord{} -} - -type RecordPool struct { - pool sync.Pool -} - -func NewRecordPool() *RecordPool { - return &RecordPool{ - pool: sync.Pool{ - New: func() interface{} { - return &TelemetryRecord{} - }, - }, - } -} - -func (rp *RecordPool) Get() *TelemetryRecord { - return rp.pool.Get().(*TelemetryRecord) -} - -func (rp *RecordPool) Put(record *TelemetryRecord) { - record.Reset() - rp.pool.Put(record) -} - -type BatchPool struct { - pool sync.Pool -} - -func NewBatchPool(batchSize int) *BatchPool { - return &BatchPool{ - pool: sync.Pool{ - New: func() interface{} { - return &TelemetryBatch{ - Records: make([]*Telemetry, 0, batchSize), - } - }, - }, - } -} - -func (bp *BatchPool) Get() *TelemetryBatch { - return bp.pool.Get().(*TelemetryBatch) -} - -func (bp *BatchPool) Put(batch *TelemetryBatch) { - batch.Records = batch.Records[:0] - batch.BatchId = "" - batch.Timestamp = ×tamppb.Timestamp{} - batch.WorkerId = 0 - batch.SessionId = "" - bp.pool.Put(batch) -} diff --git a/ingest/go/internal/metrics/metrics.go b/ingest/go/internal/metrics/metrics.go new file mode 100644 index 0000000..254b0d0 --- /dev/null +++ b/ingest/go/internal/metrics/metrics.go @@ -0,0 +1,49 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // File processing metrics + FilesProcessedTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "ingest_files_processed_total", + Help: "Total number of IBT files processed", + }) + + FileProcessingDuration = promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "ingest_file_processing_duration_seconds", + Help: "Time taken to process a single IBT file", + Buckets: prometheus.ExponentialBuckets(0.1, 2, 10), // 0.1s to ~102s + }) + + // Record throughput metrics + RecordsProcessedTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "ingest_records_processed_total", + Help: "Total number of telemetry records processed", + }) + + // Batch metrics + BatchesSentTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "ingest_batches_sent_total", + Help: "Total number of batches sent to RabbitMQ", + }) + + BatchSizeBytes = promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "ingest_batch_size_bytes", + Help: "Size of batches sent to RabbitMQ in bytes", + Buckets: prometheus.ExponentialBuckets(1024*1024, 2, 8), // 1MB to 128MB + }) + + // Worker pool metrics + ActiveWorkers = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "ingest_active_workers", + Help: "Current number of active workers", + }) + + QueueDepth = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "ingest_queue_depth", + Help: "Current depth of the file processing queue", + }) +) diff --git a/ingest/go/internal/processing/directory.go b/ingest/go/internal/processing/directory.go index 2eebebf..74f66fb 100644 --- a/ingest/go/internal/processing/directory.go +++ b/ingest/go/internal/processing/directory.go @@ -1,23 +1,25 @@ package processing import ( - "log" "os" "time" "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/config" + "go.uber.org/zap" ) type Directory struct { path string lastScan time.Time fileAgeThreshold time.Duration + logger *zap.Logger } -func NewDir(path string, cfg *config.Config) *Directory { +func NewDir(path string, cfg *config.Config, logger *zap.Logger) *Directory { return &Directory{ path: path, fileAgeThreshold: cfg.FileAgeThreshold, + logger: logger, } } @@ -25,7 +27,7 @@ func (d *Directory) WatchDir() []os.DirEntry { d.lastScan = time.Now() files, err := os.ReadDir(d.path) if err != nil { - log.Fatalf("Could not read the given input directory: %v", err) + d.logger.Fatal("Could not read the given input directory", zap.Error(err)) } filesToProcess := make([]os.DirEntry, 0) @@ -33,18 +35,22 @@ func (d *Directory) WatchDir() []os.DirEntry { for _, file := range files { info, err := file.Info() if err != nil { - log.Printf("Could not get file info for %s: %v", file.Name(), err) + d.logger.Warn("Could not get file info", zap.String("file", file.Name()), zap.Error(err)) continue } if info.ModTime().Before(time.Now().Add(-d.fileAgeThreshold)) { filesToProcess = append(filesToProcess, file) } else { - log.Printf("Skipping recent file (still being written? Age threshold: %v): %s", d.fileAgeThreshold, file.Name()) + d.logger.Debug("Skipping recent file (still being written?)", + zap.Duration("age_threshold", d.fileAgeThreshold), + zap.String("file", file.Name())) } } d.lastScan = time.Now() - log.Printf("Found %d files ready for processing out of %d total files", len(filesToProcess), len(files)) + d.logger.Info("Files found for processing", + zap.Int("ready_files", len(filesToProcess)), + zap.Int("total_files", len(files))) return filesToProcess } diff --git a/ingest/go/internal/processing/file.go b/ingest/go/internal/processing/file.go index eabb8d4..7fa596e 100644 --- a/ingest/go/internal/processing/file.go +++ b/ingest/go/internal/processing/file.go @@ -3,6 +3,7 @@ package processing import ( "context" "fmt" + "log" "os" "path/filepath" "regexp" @@ -16,109 +17,177 @@ import ( ) type FileProcessor struct { - config *config.Config - workerID int - pubSub *messaging.PubSub - pool *messaging.ConnectionPool + config *config.Config + workerID int + pool *messaging.ConnectionPool + progressCallback ProgressCallback } type ProcessResult struct { - RecordCount int - BatchCount int - SessionID string - TrackName string + RecordCount int + BatchCount int + SessionID string + TrackName string + MessagingMetrics *messaging.PublishMetrics } func NewFileProcessor(cfg *config.Config, workerID int, pool *messaging.ConnectionPool) (*FileProcessor, error) { return &FileProcessor{ - config: cfg, - workerID: workerID, - pool: pool, + config: cfg, + workerID: workerID, + pool: pool, + progressCallback: &NoOpProgressCallback{}, }, nil } +func (fp *FileProcessor) SetProgressCallback(callback ProgressCallback) { + if callback != nil { + fp.progressCallback = callback + } +} + func (fp *FileProcessor) ProcessFile(ctx context.Context, telemetryFolder string, fileEntry os.DirEntry) (*ProcessResult, error) { fileName := fileEntry.Name() if !strings.Contains(fileName, ".ibt") { - return nil, fmt.Errorf("not an IBT file: %s", fileName) + return nil, fmt.Errorf("not an IBT file: %s\nAction: Ensure file has .ibt extension", fileName) } sessionTime, err := fp.parseFileName(fileName) if err != nil { - return nil, fmt.Errorf("failed to parse time from filename: %w", err) - } - - fullPath := filepath.Join(telemetryFolder, fileName) - - files, err := filepath.Glob(fullPath) - if err != nil { - return nil, fmt.Errorf("could not glob file %s: %w", fullPath, err) + return nil, fmt.Errorf("failed to parse time from filename %s: %w\nAction: Ensure filename contains date pattern YYYY-MM-DD HH-MM-SS", fileName, err) } - if len(files) == 0 { - return nil, fmt.Errorf("no files found matching pattern: %s", fullPath) - } + file := filepath.Join(telemetryFolder, fileName) - // Reduced logging for performance - stubs, err := ibt.ParseStubs(files...) + stubs, err := ibt.ParseStubs(file) if err != nil { - return nil, fmt.Errorf("failed to parse stubs for %v: %w", files, err) + return nil, fmt.Errorf("failed to parse stubs for %v: %w\nAction: File may be corrupted or incomplete - verify file integrity", file, err) } if len(stubs) == 0 { - return nil, fmt.Errorf("no telemetry data found in IBT file: %s", fileName) + return nil, fmt.Errorf("no telemetry data found in IBT file: %s\nAction: File is empty or contains no valid telemetry data", fileName) } - headers := stubs[0].Headers() - weekendInfo := headers.SessionInfo.WeekendInfo + groups := stubs.Group() - fp.pubSub = messaging.NewPubSub( - strconv.Itoa(weekendInfo.SubSessionID), - sessionTime, - fp.config, - fp.pool, - ) + // Pre-count total records for progress tracking + totalExpectedRecords := 0 + for _, group := range groups { + totalExpectedRecords += len(group) + } - groups := stubs.Group() + // Notify progress callback that file processing is starting + fp.progressCallback.OnFileStart(fileName, totalExpectedRecords) totalRecords := 0 totalBatches := 0 + processors := make([]ibt.Processor, 0, len(groups)) + + // Track metrics across all groups + var allMessagingMetrics *messaging.PublishMetrics + var firstSessionID string + var firstTrackName string + for groupNumber, group := range groups { select { case <-ctx.Done(): + // Graceful shutdown: flush all active processors + log.Printf("Context cancelled, flushing %d active processors", len(processors)) + for i, proc := range processors { + if flushErr := proc.FlushPendingData(); flushErr != nil { + log.Printf("Failed to flush processor %d: %v", i, flushErr) + } + } return nil, ctx.Err() default: } - processor := NewLoaderProcessor(fp.pubSub, groupNumber, fp.config, fp.workerID) + + // Extract SubSessionID from this group + if len(group) == 0 { + continue + } + + groupHeaders := group[0].Headers() + groupWeekendInfo := groupHeaders.SessionInfo.WeekendInfo + groupSessionID := strconv.Itoa(groupWeekendInfo.SubSessionID) + + // Track first session's info for result + if groupNumber == 0 { + firstSessionID = groupSessionID + firstTrackName = groupWeekendInfo.TrackDisplayName + } + + // Create PubSub for this specific group + pubSub := messaging.NewPubSub( + groupSessionID, + sessionTime, + fp.config, + fp.pool, + fp.workerID, + ) + + // Create telemetry processor with the correct SubSessionID + processor := NewProcessor(pubSub, groupNumber, fp.config, fp.workerID, groupSessionID) + processor.SetProgressCallback(fp.progressCallback, fileName) + processors = append(processors, processor) + if err := ibt.Process(ctx, group, processor); err != nil { + // Try to flush this processor before returning error + if flushErr := processor.FlushPendingData(); flushErr != nil { + log.Printf("Failed to flush processor on error: %v", flushErr) + } + pubSub.Close() return nil, err } if err := processor.Close(); err != nil { - return nil, fmt.Errorf("error closing processor for group %d: %w", groupNumber, err) + pubSub.Close() + return nil, fmt.Errorf("error closing processor for group %d: %w\nAction: Check RabbitMQ connectivity and disk space", groupNumber, err) + } + + // Collect metrics from this group's PubSub + metrics := pubSub.GetMetrics() + totalRecords += metrics.TotalRecords + totalBatches += metrics.TotalBatches + if allMessagingMetrics == nil { + allMessagingMetrics = &metrics + } else { + allMessagingMetrics.TotalBatches += metrics.TotalBatches + allMessagingMetrics.TotalRecords += metrics.TotalRecords + allMessagingMetrics.TotalBytes += metrics.TotalBytes + allMessagingMetrics.FailedBatches.Add(metrics.FailedBatches.Load()) + allMessagingMetrics.PersistedBatches += metrics.PersistedBatches } - metrics := processor.GetMetrics() - totalRecords += metrics.totalProcessed - totalBatches += metrics.totalBatches + // Close PubSub for this group + if err := pubSub.Close(); err != nil { + log.Printf("Failed to close PubSub for group %d: %v", groupNumber, err) + } } ibt.CloseAllStubs(groups) + // Notify progress callback that file processing is complete + fp.progressCallback.OnFileComplete(fileName) + return &ProcessResult{ - RecordCount: totalRecords, - BatchCount: totalBatches, - SessionID: strconv.Itoa(weekendInfo.SubSessionID), - TrackName: weekendInfo.TrackDisplayName, + RecordCount: totalRecords, + BatchCount: totalBatches, + SessionID: firstSessionID, + TrackName: firstTrackName, + MessagingMetrics: allMessagingMetrics, }, nil } +func (fp *FileProcessor) FlushPendingData() error { + // No-op: each processor handles its own flushing + return nil +} + func (fp *FileProcessor) Close() error { - if fp.pubSub != nil { - return fp.pubSub.Close() - } + // No-op: PubSub instances are closed per-group return nil } @@ -127,12 +196,12 @@ func (fp *FileProcessor) parseFileName(fileName string) (time.Time, error) { match := regex.FindString(fileName) if match == "" { - return time.Time{}, fmt.Errorf("no date pattern found in filename: %s", fileName) + return time.Time{}, fmt.Errorf("no date pattern found in filename: %s\nAction: Filename must contain YYYY-MM-DD HH-MM-SS pattern", fileName) } parts := strings.Split(match, " ") if len(parts) != 2 { - return time.Time{}, fmt.Errorf("invalid date format: %s", match) + return time.Time{}, fmt.Errorf("invalid date format: %s\nAction: Date pattern must be YYYY-MM-DD HH-MM-SS", match) } timeStr := strings.ReplaceAll(parts[1], "-", ":") @@ -140,7 +209,7 @@ func (fp *FileProcessor) parseFileName(fileName string) (time.Time, error) { parsedTime, err := time.Parse(time.RFC3339, rfc3339Str) if err != nil { - return time.Time{}, fmt.Errorf("failed to parse time %s: %w", rfc3339Str, err) + return time.Time{}, fmt.Errorf("failed to parse time %s: %w\nAction: Verify date format is valid YYYY-MM-DD HH-MM-SS", rfc3339Str, err) } return parsedTime, nil diff --git a/ingest/go/internal/processing/processor.go b/ingest/go/internal/processing/processor.go new file mode 100644 index 0000000..ef679af --- /dev/null +++ b/ingest/go/internal/processing/processor.go @@ -0,0 +1,258 @@ +package processing + +import ( + "fmt" + "log" + "sync" + "time" + + "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/config" + "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/messaging" + "github.com/OJPARKINSON/ibt" + "github.com/OJPARKINSON/ibt/headers" +) + +// loaderProcessor processes telemetry data and sends it to RabbitMQ. +// It uses struct-based processing for optimal performance. +type loaderProcessor struct { + pubSub *messaging.PubSub + cache []*ibt.TelemetryTick + groupNumber int + thresholdBytes int + workerID int + mu sync.Mutex + config *config.Config + + session *headers.Session + + subSessionID string // The unique SubSessionID for this group + sessionMap map[int]sessionInfo + trackName string + trackID int + sessionInfoSet bool + + tickPool *sync.Pool + + // Metrics tracking + totalProcessed int + totalBatches int + + // Progress tracking + progressCallback ProgressCallback + currentFile string +} + +// sessionInfo holds session metadata +type sessionInfo struct { + sessionNum int + sessionType string + sessionName string +} + +// ProcessorMetrics contains telemetry processing metrics +type ProcessorMetrics struct { + TotalProcessed int + TotalBatches int + ProcessingTime time.Duration + MaxBatchSize int + ProcessingStarted time.Time + MemoryPressureEvents int + AdaptiveBatchSize int +} + +// NewProcessor creates a new telemetry processor +func NewProcessor(pubSub *messaging.PubSub, groupNumber int, config *config.Config, workerID int, subSessionID string) *loaderProcessor { + return &loaderProcessor{ + pubSub: pubSub, + cache: make([]*ibt.TelemetryTick, 0, config.BatchSizeRecords), + groupNumber: groupNumber, + config: config, + thresholdBytes: config.BatchSizeBytes, + workerID: workerID, + subSessionID: subSessionID, + sessionMap: make(map[int]sessionInfo), + progressCallback: &NoOpProgressCallback{}, + tickPool: &sync.Pool{ + New: func() any { + return &ibt.TelemetryTick{} + }, + }, + } +} + +func (l *loaderProcessor) SetProgressCallback(callback ProgressCallback, filename string) { + if callback != nil { + l.progressCallback = callback + l.currentFile = filename + } +} + +func (l *loaderProcessor) Init(session *headers.Session) error { + l.session = session + return nil +} + +func (l *loaderProcessor) ProcessStruct(tick *ibt.TelemetryTick, hasNext bool) error { + if !l.sessionInfoSet && l.session != nil && len(l.session.SessionInfo.Sessions) > 0 { + for _, sess := range l.session.SessionInfo.Sessions { + l.sessionMap[sess.SessionNum] = sessionInfo{ + sessionNum: sess.SessionNum, + sessionType: sess.SessionType, + sessionName: sess.SessionName, + } + } + l.trackName = l.session.WeekendInfo.TrackDisplayShortName + l.trackID = l.session.WeekendInfo.TrackID + l.sessionInfoSet = true + } + + tick.GroupNum = l.groupNumber + tick.WorkerID = l.workerID + tick.TrackName = l.trackName + tick.TrackID = l.trackID + + // Use the SubSessionID from the header instead of SessionNum + tick.SessionID = l.subSessionID + + if sessionInfo, exists := l.sessionMap[int(tick.SessionNum)]; exists { + tick.SessionType = sessionInfo.sessionType + tick.SessionName = sessionInfo.sessionName + } + + l.mu.Lock() + defer l.mu.Unlock() + + estimatedSize := 512 + + shouldFlush := len(l.cache) >= l.config.BatchSizeRecords || len(l.cache)*estimatedSize > l.thresholdBytes + + if shouldFlush && len(l.cache) > 0 { + if err := l.loadBatch(); err != nil { + return fmt.Errorf("failed to load batch: %w", err) + } + } + + tickCopy := l.tickPool.Get().(*ibt.TelemetryTick) + *tickCopy = *tick + l.cache = append(l.cache, tickCopy) + l.totalProcessed++ + + return nil +} + +func (l *loaderProcessor) loadBatch() error { + if len(l.cache) == 0 { + return nil + } + + batchSize := len(l.cache) + + if !l.config.DisableRabbitMQ { + err := l.pubSub.ExecStructs(l.cache) + if err != nil { + return err + } + } + + for _, tick := range l.cache { + l.tickPool.Put(tick) + } + + l.cache = l.cache[:0] + l.totalBatches++ + + // Report progress after batch is sent + l.progressCallback.OnBatchSent(l.currentFile, l.totalProcessed, l.totalBatches) + + _ = batchSize // Keep for potential future use + + return nil +} + +func (l *loaderProcessor) Close() error { + l.mu.Lock() + defer l.mu.Unlock() + + if len(l.cache) > 0 { + return l.loadBatch() + } + + return nil +} + +// Fields defines the telemetry fields this processor needs. +// Whitelist is automatically extracted from ibt tags. +func (l *loaderProcessor) Fields() any { + return struct { + // Lap & Position + LapID int32 `ibt:"Lap"` + LapDistPct float64 `ibt:"LapDistPct"` + Speed float64 `ibt:"Speed"` + PlayerCarPosition float64 `ibt:"PlayerCarPosition"` + PlayerCarIdx int32 `ibt:"PlayerCarIdx"` + + // Pedals & Gear + Throttle float64 `ibt:"Throttle"` + Brake float64 `ibt:"Brake"` + Gear int32 `ibt:"Gear"` + RPM float64 `ibt:"RPM"` + + // Steering & Velocity + SteeringWheelAngle float64 `ibt:"SteeringWheelAngle"` + VelocityX float64 `ibt:"VelocityX"` + VelocityY float64 `ibt:"VelocityY"` + VelocityZ float64 `ibt:"VelocityZ"` + + // GPS & Orientation + Lat float64 `ibt:"Lat"` + Lon float64 `ibt:"Lon"` + Alt float64 `ibt:"alt"` + Pitch float64 `ibt:"pitch"` + Roll float64 `ibt:"roll"` + Yaw float64 `ibt:"yaw"` + YawNorth float64 `ibt:"YawNorth"` + + // Acceleration + LatAccel float64 `ibt:"LatAccel"` + LongAccel float64 `ibt:"LongAccel"` + VertAccel float64 `ibt:"VertAccel"` + + // Session & Timing + SessionTime float64 `ibt:"SessionTime"` + SessionNum int32 `ibt:"SessionNum"` + FuelLevel float64 `ibt:"FuelLevel"` + Voltage float64 `ibt:"Voltage"` + WaterTemp float64 `ibt:"WaterTemp"` + LapLastLapTime float64 `ibt:"LapLastLapTime"` + LapDeltaToBestLap float64 `ibt:"LapDeltaToBestLap"` + LapCurrentLapTime float64 `ibt:"LapCurrentLapTime"` + + // Tire Pressures + LFpressure float64 `ibt:"LFpressure"` + RFpressure float64 `ibt:"RFpressure"` + LRpressure float64 `ibt:"LRpressure"` + RRpressure float64 `ibt:"RRpressure"` + + // Tire Temps + LFtempM float64 `ibt:"LFtempM"` + RFtempM float64 `ibt:"RFtempM"` + LRtempM float64 `ibt:"LRtempM"` + RRtempM float64 `ibt:"RRtempM"` + }{} +} + +func (l *loaderProcessor) FlushPendingData() error { + l.mu.Lock() + defer l.mu.Unlock() + + if len(l.cache) > 0 { + log.Printf("Worker %d: Flushing %d pending struct records", + l.workerID, len(l.cache)) + return l.loadBatch() + } + return nil +} + +func (l *loaderProcessor) GetMetrics() any { + return nil // Return nil for now - can be extended later if needed +} diff --git a/ingest/go/internal/processing/processors.go b/ingest/go/internal/processing/processors.go deleted file mode 100644 index f8648d4..0000000 --- a/ingest/go/internal/processing/processors.go +++ /dev/null @@ -1,336 +0,0 @@ -package processing - -import ( - "fmt" - "log" - "sync" - "time" - - "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/config" - "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/messaging" - "github.com/OJPARKINSON/ibt" - "github.com/teamjorge/ibt/headers" -) - -type loaderProcessor struct { - pubSub *messaging.PubSub - cache []map[string]interface{} - groupNumber int - thresholdBytes int - workerID int - mu sync.Mutex - lastFlush time.Time - flushTimer *time.Timer - metrics processorMetrics - currentBytes int - batchBuffer []map[string]interface{} - bufferPool *sync.Pool - config *config.Config - - sessionMap map[int]sessionInfo - trackName string - trackID int - sessionInfoSet bool -} - -type sessionInfo struct { - sessionNum int - sessionType string - sessionName string -} - -type processorMetrics struct { - totalProcessed int - totalBatches int - processingTime time.Duration - maxBatchSize int - processingStarted time.Time -} - -func NewLoaderProcessor(pubSub *messaging.PubSub, groupNumber int, config *config.Config, workerID int) *loaderProcessor { - lp := &loaderProcessor{ - pubSub: pubSub, - cache: make([]map[string]interface{}, 0, 1000), // Initial capacity - groupNumber: groupNumber, - config: config, - thresholdBytes: config.BatchSizeBytes, - workerID: workerID, - lastFlush: time.Now(), - currentBytes: 0, - sessionMap: make(map[int]sessionInfo), - metrics: processorMetrics{ - processingStarted: time.Now(), - }, - batchBuffer: make([]map[string]interface{}, 0, 1000), - bufferPool: &sync.Pool{ - New: func() interface{} { - return make(map[string]interface{}, 64) - }, - }, - } - - return lp -} - -func (l *loaderProcessor) flushTimerCallback() { - l.mu.Lock() - hasData := len(l.cache) > 0 - if hasData { - if err := l.loadBatch(); err != nil { - log.Printf("Worker %d: Error during auto-flush: %v", l.workerID, err) - } - } - l.mu.Unlock() - - l.flushTimer.Reset(l.config.BatchTimeout) -} - -func (l *loaderProcessor) Whitelist() []string { - return []string{ - "Lap", "LapDistPct", "Speed", "Throttle", "Brake", "Gear", "RPM", - "SteeringWheelAngle", "VelocityX", "VelocityY", "VelocityZ", "Lat", "Lon", "SessionTime", - "PlayerCarPosition", "FuelLevel", "PlayerCarIdx", "SessionNum", "alt", "LatAccel", "LongAccel", - "VertAccel", "pitch", "roll", "yaw", "YawNorth", "Voltage", "LapLastLapTime", "WaterTemp", - "LapDeltaToBestLap", "LapCurrentLapTime", "LFpressure", "RFpressure", "LRpressure", "RRpressure", "LFtempM", - "RFtempM", "LRtempM", "RRtempM", - } -} - -func (l *loaderProcessor) Process(input ibt.Tick, hasNext bool, session *headers.Session) error { - // Build session map once on first call instead of every tick - if !l.sessionInfoSet && session != nil && len(session.SessionInfo.Sessions) > 0 { - // Map all available sessions by their SessionNum - for _, sess := range session.SessionInfo.Sessions { - l.sessionMap[sess.SessionNum] = sessionInfo{ - sessionNum: sess.SessionNum, - sessionType: sess.SessionType, - sessionName: sess.SessionName, - } - } - l.trackName = session.WeekendInfo.TrackDisplayShortName - l.trackID = session.WeekendInfo.TrackID - l.sessionInfoSet = true - - log.Printf("Worker %d: Mapped %d sessions for track %s", - l.workerID, len(l.sessionMap), l.trackName) - } - - // Removed frequent logging for performance - - enrichedInput := l.bufferPool.Get().(map[string]interface{}) - - for k, v := range input { - enrichedInput[k] = v - } - - enrichedInput["groupNum"] = l.groupNumber - enrichedInput["workerID"] = l.workerID - - if l.sessionInfoSet { - enrichedInput["trackDisplayShortName"] = l.trackName - enrichedInput["trackID"] = l.trackID - - // Use the actual SessionNum from telemetry data to get session info - if sessionNumVal, ok := input["SessionNum"]; ok { - if sessionNum, ok := sessionNumVal.(int); ok { - if sessionInfo, exists := l.sessionMap[sessionNum]; exists { - enrichedInput["sessionID"] = sessionInfo.sessionNum - enrichedInput["sessionType"] = sessionInfo.sessionType - enrichedInput["sessionName"] = sessionInfo.sessionName - } else { - // Fallback if session not found in map - enrichedInput["sessionID"] = sessionNum - enrichedInput["sessionType"] = "Unknown" - enrichedInput["sessionName"] = "Unknown" - } - } else { - // Fallback if conversion fails - enrichedInput["sessionID"] = 0 - enrichedInput["sessionType"] = "Unknown" - enrichedInput["sessionName"] = "Unknown" - } - } else { - // Fallback if SessionNum not present - enrichedInput["sessionID"] = 0 - enrichedInput["sessionType"] = "Unknown" - enrichedInput["sessionName"] = "Unknown" - } - } else { - enrichedInput["sessionID"] = 0 - enrichedInput["sessionType"] = "Unknown" - enrichedInput["sessionName"] = "Unknown" - enrichedInput["trackDisplayShortName"] = "" - enrichedInput["trackID"] = 0 - } - - estimatedSize := len(input)*20 + 100 // Rough estimate - - l.mu.Lock() - - // PERFORMANCE OPTIMIZATION: Dynamic batch size based on memory pressure - maxBatchSize := 2000 - if l.currentBytes > l.thresholdBytes/2 { - maxBatchSize = 1000 // Reduce batch size when memory pressure is high - } - - shouldFlush := len(l.cache) >= maxBatchSize || l.currentBytes+estimatedSize > l.thresholdBytes - if shouldFlush && len(l.cache) > 0 { - if err := l.loadBatch(); err != nil { - l.mu.Unlock() - return fmt.Errorf("failed to load batch: %w", err) - } - } - - // PERFORMANCE OPTIMIZATION: Pre-allocate cache capacity to reduce slice growth - if cap(l.cache) == 0 { - l.cache = make([]map[string]interface{}, 0, 2000) - } - l.cache = append(l.cache, enrichedInput) - l.currentBytes += estimatedSize - - l.metrics.totalProcessed++ - if len(l.cache) > l.metrics.maxBatchSize { - l.metrics.maxBatchSize = len(l.cache) - } - - l.mu.Unlock() - - return nil -} - -func (l *loaderProcessor) estimateJSONSize(record map[string]interface{}) int { - - size := 2 - - for k, v := range record { - - size += len(k) + 3 - - switch val := v.(type) { - case string: - size += len(val) + 2 - case int: - if val == 0 { - size += 1 - } else if val < 0 { - size += 1 + intDigits(-val) - } else { - size += intDigits(val) - } - case int32, int64: - size += 10 - case float32, float64: - size += 15 - case bool: - if val { - size += 4 - } else { - size += 5 - } - default: - size += 20 - } - - size += 1 - } - - return size -} - -func intDigits(n int) int { - if n < 10 { - return 1 - } - if n < 100 { - return 2 - } - if n < 1000 { - return 3 - } - if n < 10000 { - return 4 - } - if n < 100000 { - return 5 - } - if n < 1000000 { - return 6 - } - return 7 -} - -func (l *loaderProcessor) loadBatch() error { - if len(l.cache) == 0 { - return nil - } - - if cap(l.batchBuffer) < len(l.cache) { - l.batchBuffer = make([]map[string]interface{}, len(l.cache)) - } else { - l.batchBuffer = l.batchBuffer[:len(l.cache)] - } - - copy(l.batchBuffer, l.cache) - - var err error - if !l.config.DisableRabbitMQ { - err = l.pubSub.Exec(l.batchBuffer) - } - - for _, m := range l.cache { - for k := range m { - delete(m, k) - } - l.bufferPool.Put(m) - } - - l.cache = l.cache[:0] - l.currentBytes = 0 - l.lastFlush = time.Now() - - l.metrics.totalBatches++ - - return err -} - -func (l *loaderProcessor) Close() error { - if l.flushTimer != nil { - l.flushTimer.Stop() - } - - l.mu.Lock() - defer l.mu.Unlock() - - if len(l.cache) > 0 { - log.Printf("Worker %d: Flushing remaining %d records (%d bytes) on close", - l.workerID, len(l.cache), l.currentBytes) - if err := l.loadBatch(); err != nil { - return fmt.Errorf("failed to flush data on close: %w", err) - } - } - - l.logMetrics() - - return nil -} - -func (l *loaderProcessor) logMetrics() { - totalTime := time.Since(l.metrics.processingStarted).Milliseconds() - var pointsPerSecond int64 - if totalTime > 0 { - pointsPerSecond = int64(l.metrics.totalProcessed) * 1000 / totalTime - } - - log.Printf("Worker %d processing metrics for group %d:", l.workerID, l.groupNumber) - log.Printf(" Total points processed: %d", l.metrics.totalProcessed) - log.Printf(" Total batches sent: %d", l.metrics.totalBatches) - log.Printf(" Maximum batch size: %d", l.metrics.maxBatchSize) - log.Printf(" Processing time: %d milliseconds", totalTime) - log.Printf(" Points per second: %d", pointsPerSecond) -} - -func (l *loaderProcessor) GetMetrics() processorMetrics { - l.mu.Lock() - defer l.mu.Unlock() - return l.metrics -} diff --git a/ingest/go/internal/processing/progress.go b/ingest/go/internal/processing/progress.go new file mode 100644 index 0000000..5ea97c7 --- /dev/null +++ b/ingest/go/internal/processing/progress.go @@ -0,0 +1,20 @@ +package processing + +// ProgressCallback provides real-time progress updates during file processing +type ProgressCallback interface { + // OnFileStart is called when a file begins processing + OnFileStart(filename string, totalRecords int) + + // OnBatchSent is called after each batch is sent to RabbitMQ + OnBatchSent(filename string, recordsSent int, batchNum int) + + // OnFileComplete is called when file processing finishes + OnFileComplete(filename string) +} + +// NoOpProgressCallback is a default implementation that does nothing +type NoOpProgressCallback struct{} + +func (n *NoOpProgressCallback) OnFileStart(filename string, totalRecords int) {} +func (n *NoOpProgressCallback) OnBatchSent(filename string, recordsSent int, batchNum int) {} +func (n *NoOpProgressCallback) OnFileComplete(filename string) {} diff --git a/ingest/go/internal/worker/pool.go b/ingest/go/internal/worker/pool.go index 6275b3c..98d0726 100644 --- a/ingest/go/internal/worker/pool.go +++ b/ingest/go/internal/worker/pool.go @@ -2,16 +2,18 @@ package worker import ( "context" - "log" "os" "sync" + "sync/atomic" "time" "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/config" "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/messaging" + "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/metrics" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" ) -// Helper to convert os.FileInfo to os.DirEntry for retry logic type dirEntryFromFileInfo struct { os.FileInfo } @@ -31,14 +33,20 @@ type WorkerPool struct { errorsChan chan WorkError ctx context.Context cancel context.CancelFunc - wg sync.WaitGroup + eg *errgroup.Group metrics PoolMetrics mu sync.Mutex rabbitPool *messaging.ConnectionPool + logger *zap.Logger - workerMetrics []WorkerMetrics - progressDisplay *ProgressDisplay + workerMetrics []WorkerMetrics + + // Data loss monitoring + totalPublishFailures atomic.Int32 + totalPersistedBatches int + totalCircuitBreakerEvents int + totalMemoryPressureEvents int } type PoolMetrics struct { @@ -50,17 +58,30 @@ type PoolMetrics struct { ActiveWorkers int QueueDepth int WorkerMetrics []WorkerMetrics + + // Data loss tracking + totalPublishFailures atomic.Int32 + PersistedBatches int + CircuitBreakerEvents int + MemoryPressureEvents int + DataLossRate float64 } -func NewWorkerPool(cfg *config.Config) *WorkerPool { +func NewWorkerPool(cfg *config.Config, logger *zap.Logger) *WorkerPool { ctx, cancel := context.WithCancel(context.Background()) - rabbitPool, err := messaging.NewConnectionPool(cfg.RabbitMQURL, cfg.RabbitMQPoolSize) - if err != nil { - log.Fatalf("Failed to create RabbitMQ connection pool: %v", err) - } + var rabbitPool *messaging.ConnectionPool + var err error - log.Printf("Created RabbitMQ connection pool with %d connections", cfg.RabbitMQPoolSize) + if !cfg.DisableRabbitMQ { + rabbitPool, err = messaging.NewConnectionPool(cfg.RabbitMQURL, cfg.WorkerCount) + if err != nil { + logger.Fatal("Failed to create connection pool", + zap.Error(err), + zap.String("url", cfg.RabbitMQURL), + ) + } + } workerMetrics := make([]WorkerMetrics, cfg.WorkerCount) for i := range workerMetrics { @@ -79,6 +100,7 @@ func NewWorkerPool(cfg *config.Config) *WorkerPool { ctx: ctx, cancel: cancel, rabbitPool: rabbitPool, + logger: logger, workerMetrics: workerMetrics, metrics: PoolMetrics{ StartTime: time.Now(), @@ -87,39 +109,40 @@ func NewWorkerPool(cfg *config.Config) *WorkerPool { } } -func (wp *WorkerPool) SetProgressDisplay(pd *ProgressDisplay) { - wp.progressDisplay = pd -} - -func (wp *WorkerPool) Start() { - log.Printf("Starting worker pool with %d workers", wp.config.WorkerCount) +func (wp *WorkerPool) Start() error { + eg, ctx := errgroup.WithContext(wp.ctx) + wp.eg = eg + wp.ctx = ctx - wp.wg.Add(1) - go func() { - defer wp.wg.Done() + // Start result collector + wp.eg.Go(func() error { wp.resultCollector() - }() + return nil + }) - wp.wg.Add(1) - go func() { - defer wp.wg.Done() + // Start error collector + wp.eg.Go(func() error { wp.errorCollector() - }() + return nil + }) + // Start workers for i := 0; i < wp.config.WorkerCount; i++ { - wp.wg.Add(1) workerID := i - go func() { - defer wp.wg.Done() + wp.eg.Go(func() error { wp.startWorker(workerID) - }() + return nil + }) } wp.mu.Lock() wp.metrics.ActiveWorkers = wp.config.WorkerCount wp.mu.Unlock() - log.Printf("Worker pool started successfully") + // Update Prometheus metrics + metrics.ActiveWorkers.Set(float64(wp.config.WorkerCount)) + + return nil } func (wp *WorkerPool) SubmitFile(item WorkItem) error { @@ -127,6 +150,7 @@ func (wp *WorkerPool) SubmitFile(item WorkItem) error { case wp.fileQueue <- item: wp.mu.Lock() wp.metrics.QueueDepth++ + metrics.QueueDepth.Set(float64(wp.metrics.QueueDepth)) wp.mu.Unlock() return nil case <-wp.ctx.Done(): @@ -134,25 +158,35 @@ func (wp *WorkerPool) SubmitFile(item WorkItem) error { } } -func (wp *WorkerPool) Stop() { - log.Println("Stopping worker pool...") +func (wp *WorkerPool) Stop() error { + wp.logger.Info("Starting graceful shutdown") close(wp.fileQueue) + wp.logger.Info("File queue closed, waiting for workers to finish current files") + time.Sleep(6 * time.Second) + + // Now cancel the context for any remaining operations wp.cancel() + err := wp.eg.Wait() + + wp.logger.Info("All workers stopped, waiting for async publishers to drain queues") - wp.wg.Wait() + // Wait indefinitely for all publishers - ensures no data loss + messaging.WaitForAllPublishers() + wp.logger.Info("All publishers finished draining") if wp.rabbitPool != nil { + wp.logger.Info("Closing connection pool") wp.rabbitPool.Close() - log.Println("Closed RabbitMQ connection pool") } close(wp.resultsChan) close(wp.errorsChan) wp.logFinalMetrics() - log.Println("Worker pool stopped") + + return err } func (wp *WorkerPool) GetMetrics() PoolMetrics { @@ -165,6 +199,20 @@ func (wp *WorkerPool) GetMetrics() PoolMetrics { metrics.WorkerMetrics = make([]WorkerMetrics, len(wp.workerMetrics)) copy(metrics.WorkerMetrics, wp.workerMetrics) + // Copy data loss tracking metrics + metrics.totalPublishFailures = wp.totalPublishFailures + metrics.PersistedBatches = wp.totalPersistedBatches + metrics.CircuitBreakerEvents = wp.totalCircuitBreakerEvents + metrics.MemoryPressureEvents = wp.totalMemoryPressureEvents + + // Calculate data loss rate + totalBatches := metrics.TotalBatchesProcessed + wp.totalPersistedBatches + if totalBatches > 0 { + // Data loss rate = (persisted batches / total batches) * 100 + // This represents the percentage of data that had to be persisted due to RabbitMQ failures + metrics.DataLossRate = (float64(wp.totalPersistedBatches) / float64(totalBatches)) * 100 + } + return metrics } @@ -177,29 +225,6 @@ func (wp *WorkerPool) UpdateWorkerStatus(workerID int, currentFile, status strin wp.workerMetrics[workerID].Status = status wp.workerMetrics[workerID].LastActivity = time.Now() - if wp.progressDisplay != nil { - var displayStatus WorkerStatus - switch status { - case "PROCESSING": - displayStatus = StatusProcessing - case "ERROR": - displayStatus = StatusError - case "COMPLETED": - displayStatus = StatusCompleted - default: - displayStatus = StatusIdle - } - - wm := wp.workerMetrics[workerID] - wp.progressDisplay.UpdateWorker( - workerID, - wm.FilesProcessed, - wm.TotalRecords, - wm.TotalBatches, - wm.CurrentFile, - displayStatus, - ) - } } } @@ -240,6 +265,22 @@ func (wp *WorkerPool) handleResult(result WorkResult) { wp.metrics.TotalBatchesProcessed += result.BatchCount wp.metrics.QueueDepth-- + // Update Prometheus metrics + metrics.FilesProcessedTotal.Inc() + metrics.RecordsProcessedTotal.Add(float64(result.ProcessedCount)) + metrics.BatchesSentTotal.Add(float64(result.BatchCount)) + metrics.FileProcessingDuration.Observe(result.Duration.Seconds()) + metrics.QueueDepth.Set(float64(wp.metrics.QueueDepth)) + + // Aggregate messaging metrics from result if available + if result.MessagingMetrics != nil { + wp.totalPublishFailures.Add(result.MessagingMetrics.FailedBatches.Load()) + wp.totalPersistedBatches += result.MessagingMetrics.PersistedBatches + if result.MessagingMetrics.CircuitBreakerOpen { + wp.totalCircuitBreakerEvents++ + } + } + if result.WorkerID >= 0 && result.WorkerID < len(wp.workerMetrics) { wm := &wp.workerMetrics[result.WorkerID] wm.FilesProcessed++ @@ -258,44 +299,22 @@ func (wp *WorkerPool) handleResult(result WorkResult) { wm.ProcessingRate = float64(result.ProcessedCount) / result.Duration.Seconds() } } - - if wp.progressDisplay != nil { - status := StatusCompleted - if result.WorkerID >= 0 && result.WorkerID < len(wp.workerMetrics) { - wm := wp.workerMetrics[result.WorkerID] - wp.progressDisplay.UpdateWorkerWithTiming( - result.WorkerID, - wm.FilesProcessed, - wm.TotalRecords, - wm.TotalBatches, - wm.CurrentFile, - status, - wm.AvgTimePerFile, - wm.TotalFileTime, - ) - } - } - - log.Printf("Worker %d completed file %s: %d records in %d batches (took %v)", - result.WorkerID, result.FilePath, result.ProcessedCount, - result.BatchCount, result.Duration) } func (wp *WorkerPool) handleError(workError WorkError) { - log.Printf("WORKER POOL ERROR: Worker %d failed on file %s: %v", workError.WorkerID, workError.FilePath, workError.Error) wp.mu.Lock() wp.metrics.TotalErrors++ wp.metrics.QueueDepth-- wp.mu.Unlock() - log.Printf("Worker %d error processing %s: %v", - workError.WorkerID, workError.FilePath, workError.Error) - - if workError.Retry && workError.WorkerID < wp.config.MaxRetries { + if workError.Retry && workError.RetryCount < wp.config.MaxRetries { // Try to get FileInfo for retry fileInfo, err := os.Stat(workError.FilePath) if err != nil { - log.Printf("Cannot retry %s: file stat error: %v", workError.FilePath, err) + wp.logger.Error("Cannot retry file", + zap.String("file_path", workError.FilePath), + zap.Error(err), + zap.String("action", "Check file exists and has read permissions")) return } @@ -305,18 +324,22 @@ func (wp *WorkerPool) handleError(workError WorkError) { retryItem := WorkItem{ FilePath: workError.FilePath, FileInfo: dirEntry, - RetryCount: workError.WorkerID + 1, + RetryCount: workError.RetryCount + 1, } time.AfterFunc(wp.config.RetryDelay, func() { select { case wp.fileQueue <- retryItem: - log.Printf("Retrying file %s (attempt %d)", workError.FilePath, retryItem.RetryCount) + // Retry scheduled - no log needed case <-wp.ctx.Done(): } }) } else { - log.Printf("File %s failed permanently after retries", workError.FilePath) + wp.logger.Error("File processing failed", + zap.String("file_path", workError.FilePath), + zap.Int("attempts", workError.RetryCount+1), + zap.Error(workError.Error), + zap.String("action", "Check file format is valid IBT or investigate error above")) } } @@ -324,25 +347,24 @@ func (wp *WorkerPool) logFinalMetrics() { wp.mu.Lock() defer wp.mu.Unlock() - duration := time.Since(wp.metrics.StartTime) - - log.Printf("=== Final Worker Pool Metrics ===") - log.Printf("Total processing time: %v", duration) - log.Printf("Files processed: %d", wp.metrics.TotalFilesProcessed) - log.Printf("Records processed: %d", wp.metrics.TotalRecordsProcessed) - log.Printf("Batches sent: %d", wp.metrics.TotalBatchesProcessed) - log.Printf("Errors encountered: %d", wp.metrics.TotalErrors) - - if wp.metrics.TotalFilesProcessed > 0 { - avgPerFile := duration / time.Duration(wp.metrics.TotalFilesProcessed) - log.Printf("Average time per file: %v", avgPerFile) + // Only log actionable warnings/errors - all metrics available via Prometheus + totalBatches := wp.metrics.TotalBatchesProcessed + wp.totalPersistedBatches + if totalBatches > 0 { + dataLossRate := (float64(wp.totalPersistedBatches) / float64(totalBatches)) * 100 + + if dataLossRate > 5.0 { + wp.logger.Warn("High data persistence rate detected", + zap.Float64("rate_percent", dataLossRate), + zap.Int("persisted_batches", wp.totalPersistedBatches), + zap.Int("total_batches", totalBatches), + zap.String("action", "Check RabbitMQ connectivity and service health at "+wp.config.RabbitMQURL)) + } } - if duration.Seconds() > 0 { - filesPerSec := float64(wp.metrics.TotalFilesProcessed) / duration.Seconds() - recordsPerSec := float64(wp.metrics.TotalRecordsProcessed) / duration.Seconds() - log.Println("filesPerSec: ", duration.Milliseconds()) - log.Println("recordsPerSec: ", duration.Milliseconds()) - log.Printf("Throughput: %d files/ms, %d records/ms", filesPerSec, recordsPerSec) + if wp.metrics.TotalErrors > 0 { + wp.logger.Error("Processing completed with errors", + zap.Int("total_errors", wp.metrics.TotalErrors), + zap.Int("files_processed", wp.metrics.TotalFilesProcessed), + zap.String("action", "Review error logs above for failed files")) } } diff --git a/ingest/go/internal/worker/progress.go b/ingest/go/internal/worker/progress.go deleted file mode 100644 index e1aca0f..0000000 --- a/ingest/go/internal/worker/progress.go +++ /dev/null @@ -1,464 +0,0 @@ -package worker - -import ( - "fmt" - "strings" - "sync" - "time" - - "github.com/fatih/color" -) - -type ProgressDisplay struct { - startTime time.Time - totalFiles int - workers []*WorkerProgress - mu sync.RWMutex - isRunning bool - stopChan chan bool - refreshRate time.Duration - logBuffer []string - maxLogs int -} - -type WorkerProgress struct { - ID int - FilesProcessed int - RecordsProcessed int64 - BatchesProcessed int64 - CurrentFile string - Status WorkerStatus - LastActivity time.Time - ProcessingRate float64 - AvgTimePerFile time.Duration - TotalFileTime time.Duration -} - -type WorkerStatus int - -const ( - StatusIdle WorkerStatus = iota - StatusProcessing - StatusError - StatusCompleted -) - -func (s WorkerStatus) String() string { - switch s { - case StatusIdle: - return "IDLE" - case StatusProcessing: - return "PROC" - case StatusError: - return "ERR" - case StatusCompleted: - return "DONE" - default: - return "UNK" - } -} - -func NewProgressDisplay(workerCount, totalFiles int) *ProgressDisplay { - workers := make([]*WorkerProgress, workerCount) - for i := range workers { - workers[i] = &WorkerProgress{ - ID: i, - Status: StatusIdle, - LastActivity: time.Now(), - } - } - - return &ProgressDisplay{ - startTime: time.Now(), - totalFiles: totalFiles, - workers: workers, - refreshRate: 200 * time.Millisecond, - stopChan: make(chan bool), - logBuffer: make([]string, 0), - maxLogs: 3, - } -} - -func (pd *ProgressDisplay) Start() { - pd.mu.Lock() - pd.isRunning = true - pd.mu.Unlock() - - fmt.Print("\033[?25l\033[2J\033[H") - - go pd.displayLoop() -} - -func (pd *ProgressDisplay) Stop() { - pd.mu.Lock() - pd.isRunning = false - pd.mu.Unlock() - - pd.stopChan <- true - - fmt.Print("\033[?25h") -} - -func (pd *ProgressDisplay) UpdateWorker(workerID int, filesProcessed int, recordsProcessed int64, batchesProcessed int64, currentFile string, status WorkerStatus) { - pd.mu.Lock() - defer pd.mu.Unlock() - - if workerID >= 0 && workerID < len(pd.workers) { - worker := pd.workers[workerID] - - elapsed := time.Since(worker.LastActivity).Seconds() - if elapsed > 0 { - recordsDelta := recordsProcessed - worker.RecordsProcessed - worker.ProcessingRate = float64(recordsDelta) / elapsed - } - - worker.FilesProcessed = filesProcessed - worker.RecordsProcessed = recordsProcessed - worker.BatchesProcessed = batchesProcessed - worker.CurrentFile = currentFile - worker.Status = status - worker.LastActivity = time.Now() - } -} - -func (pd *ProgressDisplay) UpdateWorkerWithTiming(workerID int, filesProcessed int, recordsProcessed int64, batchesProcessed int64, currentFile string, status WorkerStatus, avgTimePerFile time.Duration, totalFileTime time.Duration) { - pd.mu.Lock() - defer pd.mu.Unlock() - - if workerID >= 0 && workerID < len(pd.workers) { - worker := pd.workers[workerID] - - elapsed := time.Since(worker.LastActivity).Seconds() - if elapsed > 0 { - recordsDelta := recordsProcessed - worker.RecordsProcessed - worker.ProcessingRate = float64(recordsDelta) / elapsed - } - - worker.FilesProcessed = filesProcessed - worker.RecordsProcessed = recordsProcessed - worker.BatchesProcessed = batchesProcessed - worker.CurrentFile = currentFile - worker.Status = status - worker.LastActivity = time.Now() - worker.AvgTimePerFile = avgTimePerFile - worker.TotalFileTime = totalFileTime - } -} - -func (pd *ProgressDisplay) AddLog(message string) { - pd.mu.Lock() - defer pd.mu.Unlock() - - timestamp := time.Now().Format("15:04:05") - logMsg := fmt.Sprintf("[%s] %s", timestamp, message) - - pd.logBuffer = append(pd.logBuffer, logMsg) - if len(pd.logBuffer) > pd.maxLogs { - pd.logBuffer = pd.logBuffer[1:] - } -} - -func (pd *ProgressDisplay) displayLoop() { - ticker := time.NewTicker(pd.refreshRate) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - pd.render() - case <-pd.stopChan: - return - } - } -} - -func (pd *ProgressDisplay) render() { - pd.mu.RLock() - defer pd.mu.RUnlock() - - fmt.Print("\033[H") - - accent := color.New(color.FgCyan, color.Bold) - - muted := color.New(color.FgHiBlack) - - elapsed := time.Since(pd.startTime) - - totalFiles := pd.getTotalFilesProcessed() - totalRecords := pd.getTotalRecordsProcessed() - totalBatches := pd.getTotalBatchesProcessed() - - overallProgress := float64(totalFiles) / float64(pd.totalFiles) * 100 - if pd.totalFiles == 0 { - overallProgress = 0 - } - - width := 95 - - fmt.Print("β”Œ") - fmt.Print(strings.Repeat("─", width-2)) - fmt.Println("┐") - - fmt.Print("β”‚ ") - accent.Print("⚑ IRacing Telemetry Ingest") - fmt.Print(" - Performance Monitor") - remaining := width - 52 - fmt.Print(strings.Repeat(" ", remaining)) - fmt.Println("β”‚") - - fmt.Print("β”œ") - fmt.Print(strings.Repeat("─", width-2)) - fmt.Println("─") - - fmt.Print("β”‚ Overall: ") - pd.renderProgressBar(overallProgress, 35) - progressSuffix := fmt.Sprintf(" %.1f%% (%d/%d files)", overallProgress, totalFiles, pd.totalFiles) - fmt.Print(progressSuffix) - remaining = width - 10 - 37 - len(progressSuffix) - 1 - if remaining > 0 { - fmt.Print(strings.Repeat(" ", remaining)) - } - fmt.Println("β”‚") - - avgTimePerFile := pd.getAverageTimePerFile() - - var perfIcon string - if avgTimePerFile > 0 { - ms := avgTimePerFile.Milliseconds() - if ms < 50 { - perfIcon = "🟒" // Green - excellent - } else if ms < 100 { - perfIcon = "🟑" // Yellow - good - } else { - perfIcon = "πŸ”΄" // Red - needs optimization - } - } else { - perfIcon = "βšͺ" // White - no data - } - - statsText := fmt.Sprintf("⏱️ Time: %v πŸ“Š Records: %s πŸ“¦ Batches: %s %s Avg/File: %s", - elapsed.Round(time.Second), - formatNumber(totalRecords), - formatNumber(totalBatches), - perfIcon, - formatDuration(avgTimePerFile)) - - if elapsed.Seconds() > 0 { - rate := float64(totalRecords) / elapsed.Seconds() - statsText += fmt.Sprintf(" πŸš€ Rate: %s/s", formatNumber(int64(rate))) - } - - fmt.Print("β”‚ ") - fmt.Print(statsText) - - displayWidth := getDisplayWidth(statsText) - remaining = width - displayWidth - 3 - if remaining > 0 { - fmt.Print(strings.Repeat(" ", remaining)) - } - fmt.Println("β”‚") - - fmt.Print("β”œ") - fmt.Print(strings.Repeat("─", width-2)) - fmt.Println("─") - - fmt.Print("β”‚ ") - accent.Print("Worker Status Files Records Batches Rate/s Ms/File Current File") - remaining = width - 73 - fmt.Print(strings.Repeat(" ", remaining)) - fmt.Println("β”‚") - - fmt.Print("β”œ") - fmt.Print(strings.Repeat("─", width-2)) - fmt.Println("─") - - for _, worker := range pd.workers { - msPerFile := formatDuration(worker.AvgTimePerFile) - if worker.FilesProcessed == 0 || worker.AvgTimePerFile == 0 { - msPerFile = "-" - } - - currentFile := worker.CurrentFile - if len(currentFile) > 25 { - currentFile = "..." + currentFile[len(currentFile)-22:] - } - - lineContent := fmt.Sprintf("W%-2d %-4s %-5d %-10s %-7s %-8s %-7s %-25s", - worker.ID, - worker.Status.String(), - worker.FilesProcessed, - formatNumber(worker.RecordsProcessed), - formatNumber(worker.BatchesProcessed), - formatRate(worker.ProcessingRate), - msPerFile, - currentFile) - - contentWidth := len([]rune(lineContent)) - padding := width - contentWidth - 4 - if padding < 0 { - padding = 0 - } - - fmt.Printf("β”‚ %s%s β”‚\n", lineContent, strings.Repeat(" ", padding)) - } - - if len(pd.logBuffer) > 0 { - fmt.Print("β”œ") - fmt.Print(strings.Repeat("─", width-2)) - fmt.Println("─") - - for _, logMsg := range pd.logBuffer { - if len(logMsg) > width-4 { - logMsg = logMsg[:width-7] + "..." - } - fmt.Printf("β”‚ %s", logMsg) - remaining := width - len(logMsg) - 3 - fmt.Print(strings.Repeat(" ", remaining)) - fmt.Println("β”‚") - } - } - - fmt.Print("β””") - fmt.Print(strings.Repeat("─", width-2)) - fmt.Println("β”˜") - - muted.Println("Press Ctrl+C to stop gracefully") - - fmt.Print("\033[J") -} - -func (pd *ProgressDisplay) renderProgressBar(percentage float64, width int) { - filled := int(percentage / 100 * float64(width)) - if filled > width { - filled = width - } - - accent := color.New(color.FgCyan, color.Bold) - muted := color.New(color.FgHiBlack) - - fmt.Print("β–ˆ") - - if filled > 0 { - accent.Print(strings.Repeat("β–ˆ", filled)) - } - - remaining := width - filled - if remaining > 0 { - muted.Print(strings.Repeat("β–‘", remaining)) - } - - fmt.Print("β–ˆ") -} - -func (pd *ProgressDisplay) getTotalFilesProcessed() int { - total := 0 - for _, worker := range pd.workers { - total += worker.FilesProcessed - } - return total -} - -func (pd *ProgressDisplay) getTotalRecordsProcessed() int64 { - var total int64 - for _, worker := range pd.workers { - total += worker.RecordsProcessed - } - return total -} - -func (pd *ProgressDisplay) getTotalBatchesProcessed() int64 { - var total int64 - for _, worker := range pd.workers { - total += worker.BatchesProcessed - } - return total -} - -func formatNumber(n int64) string { - if n < 1000 { - return fmt.Sprintf("%d", n) - } else if n < 1000000 { - return fmt.Sprintf("%.1fK", float64(n)/1000) - } else if n < 1000000000 { - return fmt.Sprintf("%.1fM", float64(n)/1000000) - } - return fmt.Sprintf("%.1fB", float64(n)/1000000000) -} - -func formatRate(rate float64) string { - if rate < 1000 { - return fmt.Sprintf("%.0f", rate) - } else if rate < 1000000 { - return fmt.Sprintf("%.1fK", rate/1000) - } - return fmt.Sprintf("%.1fM", rate/1000000) -} - -func formatDuration(d time.Duration) string { - if d == 0 { - return "-" - } - ms := d.Milliseconds() - if ms < 1000 { - return fmt.Sprintf("%dms", ms) - } else if ms < 10000 { - return fmt.Sprintf("%dms", ms) - } else if ms < 60000 { - return fmt.Sprintf("%.1fs", float64(ms)/1000) - } - return fmt.Sprintf("%.1fm", float64(ms)/60000) -} - -func (pd *ProgressDisplay) getAverageTimePerFile() time.Duration { - var totalTime time.Duration - var totalFiles int - - for _, worker := range pd.workers { - if worker.FilesProcessed > 0 { - totalTime += worker.TotalFileTime - totalFiles += worker.FilesProcessed - } - } - - if totalFiles == 0 { - return 0 - } - return totalTime / time.Duration(totalFiles) -} - -func getDisplayWidth(s string) int { - width := 0 - runes := []rune(s) - for _, r := range runes { - if r > 0x1F600 && r < 0x1F6FF { - width += 2 - } else { - width += 1 - } - } - return width -} - -func (pd *ProgressDisplay) UpdateFromPoolMetrics(metrics PoolMetrics, workerMetrics []WorkerMetrics) { - pd.mu.Lock() - defer pd.mu.Unlock() - - for i, wm := range workerMetrics { - if i < len(pd.workers) { - status := StatusProcessing - if wm.ErrorCount > 0 { - status = StatusError - } else if wm.FilesProcessed == 0 { - status = StatusIdle - } - - pd.workers[i].FilesProcessed = wm.FilesProcessed - pd.workers[i].RecordsProcessed = int64(wm.TotalRecords) - pd.workers[i].BatchesProcessed = int64(wm.TotalBatches) - pd.workers[i].Status = status - pd.workers[i].LastActivity = wm.LastActivity - pd.workers[i].AvgTimePerFile = wm.AvgTimePerFile - pd.workers[i].TotalFileTime = wm.TotalFileTime - } - } -} diff --git a/ingest/go/internal/worker/types.go b/ingest/go/internal/worker/types.go index 3a3b494..88e85ad 100644 --- a/ingest/go/internal/worker/types.go +++ b/ingest/go/internal/worker/types.go @@ -3,6 +3,8 @@ package worker import ( "os" "time" + + "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/messaging" ) type WorkItem struct { @@ -12,21 +14,23 @@ type WorkItem struct { } type WorkResult struct { - FilePath string - ProcessedCount int - BatchCount int - Duration time.Duration - SessionID string - TrackName string - WorkerID int + FilePath string + ProcessedCount int + BatchCount int + Duration time.Duration + SessionID string + TrackName string + WorkerID int + MessagingMetrics *messaging.PublishMetrics } type WorkError struct { - FilePath string - Error error - Retry bool - WorkerID int - Timestamp time.Time + FilePath string + Error error + Retry bool + WorkerID int + RetryCount int + Timestamp time.Time } type WorkerMetrics struct { diff --git a/ingest/go/internal/worker/worker.go b/ingest/go/internal/worker/worker.go index 8ccfaa6..f84cba7 100644 --- a/ingest/go/internal/worker/worker.go +++ b/ingest/go/internal/worker/worker.go @@ -3,16 +3,14 @@ package worker import ( "context" "fmt" - "log" "path/filepath" "time" "github.com/OJPARKINSON/IRacing-Display/ingest/go/internal/processing" + "go.uber.org/zap" ) func (wp *WorkerPool) startWorker(workerID int) { - log.Printf("Worker %d starting", workerID) - workerCtx, cancel := context.WithTimeout(wp.ctx, wp.config.WorkerTimeout) defer cancel() @@ -20,14 +18,14 @@ func (wp *WorkerPool) startWorker(workerID int) { select { case workItem, ok := <-wp.fileQueue: if !ok { - log.Printf("Worker %d shutting down - queue closed", workerID) + // Worker shutting down - queue closed (normal shutdown) return } wp.processWorkItem(workerCtx, workerID, workItem) case <-workerCtx.Done(): - log.Printf("Worker %d shutting down - context done: %v", workerID, workerCtx.Err()) + // Worker shutting down due to context cancellation (normal shutdown) return } } @@ -37,13 +35,14 @@ func (wp *WorkerPool) processWorkItem(ctx context.Context, workerID int, item Wo startTime := time.Now() if item.FileInfo == nil { - log.Printf("Worker %d: FileInfo is nil for path: %s", workerID, item.FilePath) + wp.logger.Error("Worker FileInfo is nil", zap.Int("worker_id", workerID), zap.String("file_path", item.FilePath)) wp.errorsChan <- WorkError{ - FilePath: item.FilePath, - Error: fmt.Errorf("FileInfo is nil"), - Retry: false, - WorkerID: workerID, - Timestamp: time.Now(), + FilePath: item.FilePath, + Error: fmt.Errorf("FileInfo is nil"), + Retry: false, + WorkerID: workerID, + RetryCount: item.RetryCount, + Timestamp: time.Now(), } return } @@ -51,19 +50,21 @@ func (wp *WorkerPool) processWorkItem(ctx context.Context, workerID int, item Wo filename := item.FileInfo.Name() wp.UpdateWorkerStatus(workerID, filename, "PROCESSING") - log.Printf("Worker %d processing file: %s (retry %d)", workerID, item.FilePath, item.RetryCount) - processor, err := processing.NewFileProcessor(wp.config, workerID, wp.rabbitPool) if err != nil { - log.Printf("Worker %d ERROR creating file processor for %s: %v", workerID, filename, err) + wp.logger.Error("Failed to create file processor", + zap.String("file", item.FilePath), + zap.Error(err), + zap.String("action", "Check system resources and RabbitMQ connectivity")) wp.UpdateWorkerStatus(workerID, filename, "ERROR") wp.errorsChan <- WorkError{ - FilePath: item.FilePath, - Error: err, - Retry: true, - WorkerID: workerID, - Timestamp: time.Now(), + FilePath: item.FilePath, + Error: err, + Retry: true, + WorkerID: workerID, + RetryCount: item.RetryCount, + Timestamp: time.Now(), } return } @@ -93,31 +94,46 @@ func (wp *WorkerPool) processWorkItem(ctx context.Context, workerID int, item Wo result = res.result processErr = res.err case <-processCtx.Done(): - processErr = fmt.Errorf("file processing timeout after %v", wp.config.FileProcessTimeout) - log.Printf("Worker %d: File processing timeout for %s", workerID, filename) + if processCtx.Err() == context.DeadlineExceeded { + processErr = fmt.Errorf("file processing timeout after %v", wp.config.FileProcessTimeout) + wp.logger.Error("File processing timeout", + zap.String("file", item.FilePath), + zap.Duration("timeout", wp.config.FileProcessTimeout), + zap.String("action", "File may be corrupted or unusually large - consider increasing FILE_PROCESS_TIMEOUT")) + } else { + // Graceful shutdown - attempt data flush + if flushErr := processor.FlushPendingData(); flushErr != nil { + wp.logger.Error("Failed to flush pending data during shutdown", + zap.String("file", item.FilePath), + zap.Error(flushErr), + zap.String("action", "Some data may be lost - check disk persistence directory")) + } + processErr = fmt.Errorf("processing cancelled due to graceful shutdown") + } } if processErr != nil { - log.Printf("Worker %d ERROR processing file %s: %v", workerID, filename, processErr) wp.UpdateWorkerStatus(workerID, filename, "ERROR") wp.errorsChan <- WorkError{ - FilePath: item.FilePath, - Error: processErr, - Retry: shouldRetry(processErr, item.RetryCount), - WorkerID: workerID, - Timestamp: time.Now(), + FilePath: item.FilePath, + Error: processErr, + Retry: shouldRetry(processErr, item.RetryCount), + WorkerID: workerID, + RetryCount: item.RetryCount, + Timestamp: time.Now(), } return } wp.resultsChan <- WorkResult{ - FilePath: item.FilePath, - ProcessedCount: result.RecordCount, - BatchCount: result.BatchCount, - Duration: time.Since(startTime), - SessionID: result.SessionID, - TrackName: result.TrackName, - WorkerID: workerID, + FilePath: item.FilePath, + ProcessedCount: result.RecordCount, + BatchCount: result.BatchCount, + Duration: time.Since(startTime), + SessionID: result.SessionID, + TrackName: result.TrackName, + WorkerID: workerID, + MessagingMetrics: result.MessagingMetrics, } } diff --git a/install-mkcert-rpi.sh b/install-mkcert-rpi.sh deleted file mode 100755 index 1a97f49..0000000 --- a/install-mkcert-rpi.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/bash - -echo "πŸ“ Installing mkcert for Raspberry Pi..." - -# Detect architecture -ARCH=$(uname -m) -echo "πŸ“‹ Detected architecture: $ARCH" - -# Determine download URL based on architecture -case $ARCH in - aarch64|arm64) - echo "πŸ“¦ Downloading mkcert for ARM64..." - DOWNLOAD_URL="https://dl.filippo.io/mkcert/latest?for=linux/arm64" - BINARY_PATTERN="mkcert-v*-linux-arm64" - ;; - armv7l|armv6l|arm) - echo "πŸ“¦ Downloading mkcert for ARM..." - DOWNLOAD_URL="https://dl.filippo.io/mkcert/latest?for=linux/arm" - BINARY_PATTERN="mkcert-v*-linux-arm" - ;; - x86_64) - echo "πŸ“¦ Downloading mkcert for x64..." - DOWNLOAD_URL="https://dl.filippo.io/mkcert/latest?for=linux/amd64" - BINARY_PATTERN="mkcert-v*-linux-amd64" - ;; - *) - echo "❌ Unsupported architecture: $ARCH" - exit 1 - ;; -esac - -# Download mkcert -echo "⬇️ Downloading mkcert..." -curl -JLO "$DOWNLOAD_URL" - -if [ $? -ne 0 ]; then - echo "❌ Failed to download mkcert" - exit 1 -fi - -# Find the downloaded binary -BINARY_FILE=$(ls $BINARY_PATTERN 2>/dev/null | head -1) - -if [ -z "$BINARY_FILE" ]; then - echo "❌ Downloaded binary not found. Expected pattern: $BINARY_PATTERN" - ls -la mkcert* - exit 1 -fi - -echo "βœ… Downloaded: $BINARY_FILE" - -# Make it executable -chmod +x "$BINARY_FILE" - -# Move to system path -echo "πŸ”§ Installing to /usr/local/bin/mkcert..." -sudo mv "$BINARY_FILE" /usr/local/bin/mkcert - -if [ $? -ne 0 ]; then - echo "❌ Failed to install mkcert to /usr/local/bin/" - exit 1 -fi - -# Verify installation -echo "πŸ” Verifying installation..." -mkcert_version=$(mkcert -version 2>/dev/null) - -if [ $? -eq 0 ]; then - echo "βœ… mkcert installed successfully: $mkcert_version" - echo "πŸ“ Location: $(which mkcert)" -else - echo "❌ mkcert installation verification failed" - exit 1 -fi - -# Install root CA -echo "πŸ” Installing mkcert root CA..." -mkcert -install - -if [ $? -eq 0 ]; then - echo "βœ… mkcert root CA installed successfully" - echo "πŸ“‚ CA Root: $(mkcert -CAROOT)" -else - echo "❌ Failed to install mkcert root CA" - exit 1 -fi - -echo "" -echo "πŸŽ‰ mkcert setup complete!" -echo "" -echo "You can now run:" -echo " ./setup-nginx.sh # Set up nginx with trusted certificates" -echo " or" -echo " ./generate-ssl-certs.sh # Generate certificates only" -echo "" -echo "πŸ“ Note: The root CA is installed system-wide, so browsers on this" -echo " Raspberry Pi will trust the certificates without warnings." -echo "" -echo "🌐 For other devices to trust the certificates, you'll need to:" -echo " 1. Copy $(mkcert -CAROOT)/rootCA.pem to other devices" -echo " 2. Install it as a trusted root certificate" \ No newline at end of file diff --git a/telemetryService/README.md b/telemetryService/README.md new file mode 100644 index 0000000..06b9717 --- /dev/null +++ b/telemetryService/README.md @@ -0,0 +1,368 @@ +# TelemetryService + +High-performance C# telemetry processing service for IRacing data. Consumes Protocol Buffer messages from RabbitMQ and persists to QuestDB with automatic schema optimization and memory-aware processing. + +## Quick Start + +### Prerequisites +- .NET 8.0 SDK +- Docker and Docker Compose +- RabbitMQ (configured via docker-compose.yml) +- QuestDB (configured via docker-compose.yml) + +### Running with Docker Compose (Recommended) +```bash +# Start entire system including telemetry service +docker compose up -d + +# View telemetry service logs +docker compose logs -f telemetry-service + +# Restart just the telemetry service +docker compose restart telemetry-service +``` + +### Running Locally for Development +```bash +cd telemetryService/telemetryService + +# Build the solution +dotnet build + +# Run the API version (includes web endpoints) +dotnet run --project src/TelemetryService.API + +# OR run the Worker version (console only, lower overhead) +dotnet run --project src/TelemetryService.Worker +``` + +## Service Variants + +### TelemetryService.API +- **Web API** with Swagger documentation +- **Health check endpoints** at `/api/health` +- **Prometheus metrics** at `/metrics` +- **Schema optimization** via `POST /api/health/optimize-schema` +- Background RabbitMQ processing via hosted service +- **Best for**: Development, monitoring, and integrated deployments + +### TelemetryService.Worker +- **Console application** with no HTTP overhead +- Pure RabbitMQ β†’ QuestDB processing +- Optimized for memory and CPU efficiency +- **Best for**: Production deployments and resource-constrained environments + +## Configuration + +### Environment Variables +```bash +# Required +QUESTDB_URL=tcp://questdb:9009 + +# Optional (defaults to embedded connection string) +RABBITMQ_URL=amqp://admin:changeme@rabbitmq:5672/ + +# ASP.NET Core (API only) +ASPNETCORE_ENVIRONMENT=Production +ASPNETCORE_URLS=http://+:80 +``` + +### Docker Environment +Place a `.env` file in the working directory: +```bash +QUESTDB_URL=tcp://questdb:9009 +``` + +## Architecture Overview + +``` +Go Ingest Service (7600X) + ↓ 32MB Protocol Buffer batches +RabbitMQ (Pi5) + ↓ Pull-based consumption (50 concurrent workers) +TelemetryService (Pi5) + ↓ Memory-aware processing with auto-pause +QuestDB (Pi5) + ↓ PostgreSQL wire protocol queries +Next.js Dashboard (Pi5) +``` + +### Key Components +- **Subscriber**: RabbitMQ consumer with memory monitoring and backpressure +- **QuestDbService**: High-throughput TCP writer with connection resilience +- **QuestDbSchemaManager**: Automatic schema optimization and migration +- **TelemetryBackgroundService**: ASP.NET Core hosted service integration + +## Performance Characteristics + +### Memory Management +- **Working Set Limit**: 5GB (auto-pause processing) +- **Recovery Threshold**: 4GB (resume processing) +- **Monitoring Interval**: 5 seconds +- **Concurrent Workers**: 50 (with semaphore throttling) + +### Throughput +- **Sustained**: 3-6 GB/hour telemetry processing +- **Peak**: 8-10 GB/hour burst capacity +- **Write Batching**: 10K rows or 1-second auto-flush +- **Memory Usage**: 2-3GB typical, 5GB limit + +### Resource Usage (Pi5 Optimized) +- **CPU**: 1-2 cores during active processing +- **Memory**: 2-3GB working set, 6GB container limit +- **Network**: 200-400 Mbps sustained throughput +- **Storage**: Optimized for USB 3.0 SSD patterns + +## Data Flow + +### Message Processing Pipeline +1. **Go Ingest Service** processes .ibt files into 32MB Protocol Buffer batches +2. **RabbitMQ** queues batches on exchange `telemetry_topic` with routing key `telemetry.ticks` +3. **TelemetryService** pulls messages in batches of 10 with 50 concurrent workers +4. **QuestDbService** writes telemetry via TCP ingress (port 9009) with auto-flush +5. **Dashboard** queries QuestDB via PostgreSQL wire protocol (port 8812) + +### Protocol Buffer Schema +```protobuf +message Telemetry { + string session_id = 1; + double speed = 2; + double lap_dist_pct = 3; + string track_name = 10; + // ... 46 total fields including tire data, accelerations, etc. + google.protobuf.Timestamp tick_time = 46; +} + +message TelemetryBatch { + repeated Telemetry records = 1; + string batch_id = 2; + uint32 worker_id = 4; +} +``` + +## Database Schema + +### TelemetryTicks Table (Optimized) +```sql +CREATE TABLE TelemetryTicks ( + -- Indexed SYMBOL columns for categorical data + session_id SYMBOL CAPACITY 50000 INDEX, + track_name SYMBOL CAPACITY 100 INDEX, + track_id SYMBOL CAPACITY 100 INDEX, + lap_id SYMBOL CAPACITY 500, + session_num SYMBOL CAPACITY 20, + session_type SYMBOL CAPACITY 10, + session_name SYMBOL CAPACITY 50, + + -- Core telemetry data + car_id VARCHAR, + gear INT, + player_car_position INT, + speed DOUBLE, + lap_dist_pct DOUBLE, + session_time DOUBLE, + lat DOUBLE, + lon DOUBLE, + + -- Vehicle dynamics (FLOAT for efficiency) + throttle FLOAT, + brake FLOAT, + steering_wheel_angle FLOAT, + rpm FLOAT, + velocity_x FLOAT, + velocity_y FLOAT, + velocity_z FLOAT, + + -- Tire pressures and temperatures + lFpressure FLOAT, + rFpressure FLOAT, + lRpressure FLOAT, + rRpressure FLOAT, + lFtempM FLOAT, + rFtempM FLOAT, + lRtempM FLOAT, + rRtempM FLOAT, + + timestamp TIMESTAMP +) TIMESTAMP(timestamp) PARTITION BY HOUR WITH maxUncommittedRows=1000000; + +-- Performance indexes +ALTER TABLE TelemetryTicks ADD INDEX session_lap_idx (session_id, lap_id); +ALTER TABLE TelemetryTicks ADD INDEX track_session_idx (track_name, session_id); +ALTER TABLE TelemetryTicks ADD INDEX session_time_idx (session_id, session_time); +``` + +### Schema Management +- **Automatic Creation**: Creates optimized table if none exists +- **Migration Support**: Upgrades legacy schemas with backup/rollback +- **Orphan Cleanup**: Removes old session-based tables automatically +- **Index Optimization**: Adds composite indexes for common query patterns + +## API Endpoints + +### Health and Monitoring +```http +GET /api/health +# Returns service health status + +POST /api/health/optimize-schema +# Triggers manual schema optimization + +GET /metrics +# Prometheus metrics endpoint +``` + +### Example Response +```json +{ + "message": "Test endpoint working!", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +## Error Handling + +### Memory Protection +- **OutOfMemoryException**: Automatic processing pause with recovery +- **Memory Monitoring**: 5-second intervals with detailed logging +- **Garbage Collection**: Forced cleanup during memory pressure +- **Graceful Degradation**: Temporary pause instead of crash + +### Connection Resilience +- **RabbitMQ**: 10 retry attempts with exponential backoff +- **QuestDB**: Automatic sender reset on connection errors +- **Message Safety**: Acknowledgment only after successful write +- **Health Checks**: Dependency verification during startup + +### Data Integrity +- **Validation**: Required field checks and data sanitization +- **Transaction Safety**: Atomic batch writes to QuestDB +- **Error Classification**: Connection vs. data format errors +- **Recovery Logic**: Automatic retry for transient failures + +## Monitoring + +### Key Metrics +- **Processing Rate**: Messages per second with 10-second reporting +- **Memory Usage**: Working set and GC memory with thresholds +- **Queue Health**: Processing semaphore availability +- **Write Performance**: QuestDB batch sizes and latency +- **Connection Status**: RabbitMQ and QuestDB connectivity + +### Logging Output +``` +πŸš€ Telemetry Service Starting... +πŸ“Š Initial Memory Usage: 0.15GB +πŸ”— Initializing QuestDB connections: + TCP Ingress: tcp::addr=questdb:9009 + HTTP Schema: http://questdb:9000 +πŸ”§ Checking QuestDB schema optimization... +βœ… TelemetryTicks table is already optimized +πŸ”Œ Starting telemetry service subscriber... +πŸ“₯ Ready to pull messages from queue... +πŸ“Š Pull Stats: 1250 msgs processed, 125.0 msgs/sec, 45/50 threads available +πŸ”„ Starting batch write to QuestDB: 500 records +βœ… Successfully wrote 500/500 telemetry points in 45.2ms +``` + +## Development + +### Project Structure +``` +telemetryService/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ TelemetryService.Domain/ # Protocol Buffers & Models +β”‚ β”œβ”€β”€ TelemetryService.Application/ # Business Logic & DTOs +β”‚ β”œβ”€β”€ TelemetryService.Infrastructure/ # Database & Messaging +β”‚ β”œβ”€β”€ TelemetryService.API/ # HTTP Web API +β”‚ └── TelemetryService.Worker/ # Console Application +β”œβ”€β”€ tests/ # Unit & Integration Tests +β”œβ”€β”€ Dockerfile # Multi-stage container build +β”œβ”€β”€ Dockerfile.Worker # Optimized worker container +└── telemetryService.sln # Solution file +``` + +### Building and Testing +```bash +# Restore dependencies +dotnet restore + +# Build all projects +dotnet build + +# Run tests +dotnet test + +# Publish for deployment +dotnet publish src/TelemetryService.API -c Release -o publish/api +dotnet publish src/TelemetryService.Worker -c Release -o publish/worker +``` + +### Docker Build +```bash +# API version +docker build -t telemetry-service:api . + +# Worker version +docker build -f Dockerfile.Worker -t telemetry-service:worker . +``` + +## Troubleshooting + +### Common Issues + +#### High Memory Usage +```bash +# Check container memory limit +docker stats telemetry-service + +# View memory monitoring logs +docker logs telemetry-service | grep "Memory Status" + +# Trigger garbage collection via schema optimization +curl -X POST http://localhost/api/health/optimize-schema +``` + +#### RabbitMQ Connection Issues +```bash +# Verify RabbitMQ is accessible +docker exec telemetry-service ping rabbitmq + +# Check RabbitMQ management interface +open http://localhost:15672 # admin/changeme + +# View connection logs +docker logs telemetry-service | grep "RabbitMQ" +``` + +#### QuestDB Write Errors +```bash +# Check QuestDB HTTP interface +curl http://localhost:9000/ + +# Verify TCP ingress port +netstat -an | grep 9009 + +# Check table structure +curl "http://localhost:9000/exec?query=SHOW%20COLUMNS%20FROM%20TelemetryTicks" +``` + +### Performance Tuning + +#### Memory Optimization +- Adjust `MaxConcurrentProcessing` in Subscriber.cs for available RAM +- Tune `auto_flush_rows` in QuestDbService for batch sizes +- Modify memory thresholds in `CheckMemoryPressure()` + +#### Throughput Optimization +- Increase `BatchSize` for higher message pull rates +- Tune `prefetch` settings in RabbitMQ channel configuration + +#### Database Performance +- Monitor QuestDB partition sizes for query performance +- Add custom indexes for specific query patterns +- Adjust `maxUncommittedRows` for write performance vs. memory + +For detailed architecture information, see [ARCHITECTURE.md](ARCHITECTURE.md). + +protoc --go_out=. --go_opt=paths=source_relative telemetry.proto \ No newline at end of file diff --git a/telemetryService/golang/Dockerfile b/telemetryService/golang/Dockerfile new file mode 100644 index 0000000..c4b4ecc --- /dev/null +++ b/telemetryService/golang/Dockerfile @@ -0,0 +1,25 @@ + FROM golang:1.26-alpine AS builder + + WORKDIR /build + + COPY go.mod go.sum ./ + RUN go mod download + + COPY . . + + RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \ + -o telemetry-service \ + -trimpath \ + -ldflags="-w -s" \ + ./cmd/telemetry-service + + FROM gcr.io/distroless/static:nonroot + + # Copy binary to root of distroless image + COPY --from=builder /build/telemetry-service /telemetry-service + + USER nonroot:nonroot + + EXPOSE 8010 + + ENTRYPOINT ["/telemetry-service"] \ No newline at end of file diff --git a/telemetryService/golang/Makefile b/telemetryService/golang/Makefile new file mode 100644 index 0000000..4722013 --- /dev/null +++ b/telemetryService/golang/Makefile @@ -0,0 +1,199 @@ +.PHONY: help build run test bench bench-mem bench-cpu bench-compare clean profile-mem profile-cpu view-trace restart logs + +# Directories +BENCH_DIR := benchmark_results + +# Default target +.DEFAULT_GOAL := help + +help: + @echo "πŸ“š Available Make Targets:" + @echo "" + @echo " Docker Commands:" + @echo " make restart - Restart Docker services (clean rebuild)" + @echo " make logs - Follow Docker logs for telemetry service" + @echo "" + @echo " Build & Run:" + @echo " make build - Build the telemetry service" + @echo " make run - Run the telemetry service locally" + @echo "" + @echo " Testing Commands:" + @echo " make test - Run all tests" + @echo "" + @echo " Benchmark Commands:" + @echo " make bench - Run all benchmarks with summary" + @echo " make bench-mem - Run benchmarks with memory profiling" + @echo " make bench-cpu - Run benchmarks with CPU profiling" + @echo " make bench-trace - Run benchmarks with execution trace" + @echo " make bench-compare - Compare current benchmarks with baseline" + @echo " make bench-save - Save benchmark results with timestamp" + @echo "" + @echo " Analysis Tools:" + @echo " make profile-mem - View memory profile interactively" + @echo " make profile-cpu - View CPU profile interactively" + @echo " make view-trace - View execution trace" + @echo "" + @echo " Cleanup Commands:" + @echo " make clean - Clean benchmark results and profiles" + @echo "" + +# Docker commands +restart: + @echo "πŸš€ Restarting telemetry service..." + @cd ../.. && docker compose down telemetry-service + @cd ../.. && docker compose build --no-cache telemetry-service + @cd ../.. && docker compose up -d telemetry-service + @echo "βœ… Done! Check logs with: make logs" + +logs: + @cd ../.. && docker compose logs -f telemetry-service + +# Build the service +build: + @echo "πŸ”¨ Building telemetry service..." + @go build -o bin/telemetry-service ./main.go + @echo "βœ… Build complete!" + +# Run the service +run: + @echo "πŸš€ Running telemetry service..." + @go run ./main.go + +# Run tests +test: + @echo "πŸ§ͺ Running all tests..." + @go test -v -race -timeout 30s ./... + +# Run all benchmarks with detailed output +bench: + @echo "⚑ Running all benchmarks..." + @mkdir -p $(BENCH_DIR) + @echo "" + @echo "--- Subscriber Benchmarks ---" + @go test -bench=. -benchmem -benchtime=3s ./internal/queue | tee $(BENCH_DIR)/subscriber_bench.txt + @echo "" + @echo "--- Persistance Benchmarks ---" + @go test -bench=. -benchmem -benchtime=3s ./internal/persistance | tee $(BENCH_DIR)/persistance_bench.txt + @echo "" + @echo "βœ… Results saved to $(BENCH_DIR)/" + +# Run benchmarks with memory profiling +bench-mem: + @echo "πŸ“Š Running Benchmarks with Memory Profiling..." + @mkdir -p $(BENCH_DIR) + @echo "Profiling subscriber package..." + @go test -bench=. -benchmem -memprofile=$(BENCH_DIR)/subscriber_mem.prof -benchtime=3s ./internal/queue > $(BENCH_DIR)/subscriber_bench.txt + @echo "Profiling persistance package..." + @go test -bench=. -benchmem -memprofile=$(BENCH_DIR)/persistance_mem.prof -benchtime=3s ./internal/persistance > $(BENCH_DIR)/persistance_bench.txt + @echo "" + @echo "=== Memory Profile Summary ===" + @echo "" + @echo "--- Top Memory Allocations (Subscriber) ---" + @go tool pprof -top -alloc_space $(BENCH_DIR)/subscriber_mem.prof 2>/dev/null | head -n 15 + @echo "" + @echo "--- Top Memory Allocations (Persistance) ---" + @go tool pprof -top -alloc_space $(BENCH_DIR)/persistance_mem.prof 2>/dev/null | head -n 15 + @echo "" + @echo "βœ… Profiles saved to $(BENCH_DIR)/" + @echo "πŸ’‘ View interactively: make profile-mem" + +# Run benchmarks with CPU profiling +bench-cpu: + @echo "⚑ Running Benchmarks with CPU Profiling..." + @mkdir -p $(BENCH_DIR) + @echo "Profiling subscriber package..." + @go test -bench=. -cpuprofile=$(BENCH_DIR)/subscriber_cpu.prof -benchtime=3s ./internal/queue > /dev/null + @echo "Profiling persistance package..." + @go test -bench=. -cpuprofile=$(BENCH_DIR)/persistance_cpu.prof -benchtime=3s ./internal/persistance > /dev/null + @echo "" + @echo "=== CPU Profile Summary ===" + @echo "" + @echo "--- Top CPU Usage (Subscriber) ---" + @go tool pprof -top $(BENCH_DIR)/subscriber_cpu.prof 2>/dev/null | head -n 15 + @echo "" + @echo "--- Top CPU Usage (Persistance) ---" + @go tool pprof -top $(BENCH_DIR)/persistance_cpu.prof 2>/dev/null | head -n 15 + @echo "" + @echo "βœ… Profiles saved to $(BENCH_DIR)/" + @echo "πŸ’‘ View interactively: make profile-cpu" + +# Run benchmarks with execution trace +bench-trace: + @echo "πŸ” Running Benchmarks with Execution Trace..." + @mkdir -p $(BENCH_DIR) + @echo "Tracing subscriber package..." + @go test -bench=BenchmarkFlushBatches/Large -trace=$(BENCH_DIR)/subscriber_trace.out -benchtime=1s ./internal/queue > /dev/null + @echo "Tracing persistance package..." + @go test -bench=BenchmarkBatchSerialization/5000 -trace=$(BENCH_DIR)/persistance_trace.out -benchtime=1s ./internal/persistance > /dev/null + @echo "" + @echo "βœ… Traces saved to $(BENCH_DIR)/" + @echo "πŸ’‘ View with: make view-trace" + +# Save benchmark results with timestamp +bench-save: + @echo "πŸ’Ύ Running benchmarks and saving results..." + @mkdir -p benchmarks + @go test -bench=. -benchmem -benchtime=3s ./internal/queue ./internal/persistance | tee benchmarks/bench_$(shell date +%Y%m%d_%H%M%S).txt + @echo "βœ… Results saved to benchmarks/" + +# Compare benchmarks with baseline (requires benchstat) +bench-compare: + @if [ ! -f $(BENCH_DIR)/baseline_subscriber.txt ]; then \ + echo "πŸ“Š No baseline found. Creating baseline from current results..."; \ + $(MAKE) bench; \ + cp $(BENCH_DIR)/subscriber_bench.txt $(BENCH_DIR)/baseline_subscriber.txt; \ + cp $(BENCH_DIR)/persistance_bench.txt $(BENCH_DIR)/baseline_persistance.txt; \ + echo "βœ… Baseline created. Make code changes and run 'make bench-compare' again."; \ + else \ + echo "πŸ“Š Comparing with Baseline..."; \ + $(MAKE) bench; \ + echo ""; \ + echo "--- Subscriber Comparison ---"; \ + benchstat $(BENCH_DIR)/baseline_subscriber.txt $(BENCH_DIR)/subscriber_bench.txt || echo "⚠️ Install benchstat: make install-benchstat"; \ + echo ""; \ + echo "--- Persistance Comparison ---"; \ + benchstat $(BENCH_DIR)/baseline_persistance.txt $(BENCH_DIR)/persistance_bench.txt || echo "⚠️ Install benchstat: make install-benchstat"; \ + fi + +# View memory profile interactively +profile-mem: + @if [ ! -f $(BENCH_DIR)/subscriber_mem.prof ]; then \ + echo "⚠️ No memory profile found. Run 'make bench-mem' first."; \ + exit 1; \ + fi + @echo "πŸ” Opening memory profile (subscriber)..." + @echo "πŸ’‘ Commands: top, list , web, pdf, help" + @go tool pprof $(BENCH_DIR)/subscriber_mem.prof + +# View CPU profile interactively +profile-cpu: + @if [ ! -f $(BENCH_DIR)/subscriber_cpu.prof ]; then \ + echo "⚠️ No CPU profile found. Run 'make bench-cpu' first."; \ + exit 1; \ + fi + @echo "πŸ” Opening CPU profile (subscriber)..." + @echo "πŸ’‘ Commands: top, list , web, pdf, help" + @go tool pprof $(BENCH_DIR)/subscriber_cpu.prof + +# View execution trace +view-trace: + @if [ ! -f $(BENCH_DIR)/subscriber_trace.out ]; then \ + echo "⚠️ No trace found. Run 'make bench-trace' first."; \ + exit 1; \ + fi + @echo "πŸ” Opening trace viewer in browser..." + @go tool trace $(BENCH_DIR)/subscriber_trace.out + +# Clean benchmark results +clean: + @echo "🧹 Cleaning benchmark results..." + @rm -rf $(BENCH_DIR) + @rm -rf benchmarks + @rm -rf bin + @echo "βœ… Clean complete" + +# Install benchstat tool (for comparison) +install-benchstat: + @echo "πŸ“¦ Installing benchstat..." + @go install golang.org/x/perf/cmd/benchstat@latest + @echo "βœ… Done. You can now use 'make bench-compare'" diff --git a/telemetryService/golang/cmd/telemetry-service/main.go b/telemetryService/golang/cmd/telemetry-service/main.go new file mode 100644 index 0000000..57e1947 --- /dev/null +++ b/telemetryService/golang/cmd/telemetry-service/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "log" + _ "net/http/pprof" + "os" + "os/signal" + "syscall" + + "github.com/ojparkinson/telemetryService/internal/api" + "github.com/ojparkinson/telemetryService/internal/config" + "github.com/ojparkinson/telemetryService/internal/metrics" + "github.com/ojparkinson/telemetryService/internal/persistance" +) + +func main() { + log.Println("Starting telemetry service") + + config := config.NewConfig() + + // Create database schema + schema := persistance.NewSchema(config) + if err := schema.CreateTableHTTP(); err != nil { + log.Printf("Failed to create table: %v", err) + log.Println("Exiting due to database initialization failure") + os.Exit(1) + } + log.Println("Database schema initialized successfully") + + // Create sender pool + senderPool, err := persistance.NewSenderPool(config) + if err != nil { + log.Printf("Failed to create sender pool: %v", err) + log.Println("Exiting due to sender pool initialization failure") + os.Exit(1) + } + log.Println("Sender pool created successfully") + + apiServer := api.NewServer(":8010", config, senderPool) + + log.Println("creating server") + go func() { + if err := apiServer.Start(); err != nil { + log.Printf("API server error: %v", err) + } + }() + + // Start Prometheus metrics server + go metrics.MetricsHandler() + log.Println("Starting to consume messages from RabbitMQ") + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + <-sigChan + log.Println("Shutting down...") +} diff --git a/telemetryService/golang/go.mod b/telemetryService/golang/go.mod new file mode 100644 index 0000000..8cc0a01 --- /dev/null +++ b/telemetryService/golang/go.mod @@ -0,0 +1,28 @@ +module github.com/ojparkinson/telemetryService + +go 1.26 + +require ( + github.com/andybalholm/brotli v1.2.0 + github.com/go-chi/chi/v5 v5.2.5 + github.com/prometheus/client_golang v1.23.2 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/tools v0.41.0 // indirect +) + +require ( + github.com/questdb/go-questdb-client/v4 v4.1.0 + google.golang.org/protobuf v1.36.11 +) diff --git a/telemetryService/golang/go.sum b/telemetryService/golang/go.sum new file mode 100644 index 0000000..87bcb77 --- /dev/null +++ b/telemetryService/golang/go.sum @@ -0,0 +1,133 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= +github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= +github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= +github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/questdb/go-questdb-client/v4 v4.1.0 h1:pZ30OgdR3bBDAf3cWK9/PugdqgC8V6MWh6i9jmtrpcQ= +github.com/questdb/go-questdb-client/v4 v4.1.0/go.mod h1:Q749HQ2rJg6pZGCeMLEczL3+E90P47lybx5vI6Si8kA= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.26.0 h1:uqcYdoOHBy1ca7gKODfBd9uTHVK3a7UL848z09MVZ0c= +github.com/testcontainers/testcontainers-go v0.26.0/go.mod h1:ICriE9bLX5CLxL9OFQ2N+2N+f+803LNJ1utJb1+Inx0= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/exp v0.0.0-20231005195138-3e424a577f31 h1:9k5exFQKQglLo+RoP+4zMjOFE14P6+vyR0baDAi0Rcs= +golang.org/x/exp v0.0.0-20231005195138-3e424a577f31/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/telemetryService/golang/internal/api/handlers.go b/telemetryService/golang/internal/api/handlers.go new file mode 100644 index 0000000..4dc76bf --- /dev/null +++ b/telemetryService/golang/internal/api/handlers.go @@ -0,0 +1,151 @@ +package api + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "slices" + "strconv" + + "github.com/go-chi/chi/v5" + "github.com/ojparkinson/telemetryService/internal/geojson" + "github.com/ojparkinson/telemetryService/internal/messaging" + "github.com/ojparkinson/telemetryService/internal/persistance" + "github.com/ojparkinson/telemetryService/internal/sync" + "google.golang.org/protobuf/proto" +) + +// /api/sessions +func (s *Server) handleGetSessions(w http.ResponseWriter, r *http.Request) { + sessions, err := s.queryExecutor.QuerySessions() + if err != nil { + log.Println(err) + respondError(w, http.StatusInternalServerError, "Failed to fetch sessions") + return + } + + respondJSON(w, 200, sessions) +} + +// /api/sessions/123456/laps +func (s *Server) handleGetLaps(w http.ResponseWriter, r *http.Request) { + sessionID := chi.URLParam(r, "sessionId") + if sessionID == "" { + respondError(w, http.StatusInternalServerError, "Invalid session ID") + return + } + + rows, err := s.queryExecutor.QueryLaps(r.Context(), sessionID) + if err != nil { + log.Println(err) + respondError(w, http.StatusInternalServerError, "Failed to fetch laps") + return + } + + laps := make([]int, len(rows)) + for i, row := range rows { + laps[i], _ = strconv.Atoi(row["lap_id"].(string)) + } + + slices.Sort(laps) + + respondJSON(w, 200, laps) + +} + +// /api/sessions/123456/laps/1 +func (s *Server) handleGetTelemetry(w http.ResponseWriter, r *http.Request) { + sessionID := chi.URLParam(r, "sessionId") + lapID := chi.URLParam(r, "lapId") + if sessionID == "" || lapID == "" { + respondError(w, http.StatusBadRequest, "Invalid session ID") + return + } + + lapData, err := s.queryExecutor.QueryLap(r.Context(), sessionID, lapID) + if err != nil { + log.Println(err) + respondError(w, http.StatusInternalServerError, "Failed to fetch lap data") + return + } + + respondJSON(w, 200, lapData) + +} + +// /api/sessions/123456/laps/1/geojson +func (s *Server) handleGetTelemetryGeoJson(w http.ResponseWriter, r *http.Request) { + sessionID := chi.URLParam(r, "sessionId") + lapID := chi.URLParam(r, "lapId") + + options := geojson.ConversionOptions{} + + lapData, err := s.queryExecutor.QueryLap(r.Context(), sessionID, lapID) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to fetch lap data") + return + } + + geoJSON, err := geojson.ConvertToGeoJSON(lapData, options) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to convert to GeoJSON") + return + } + + respondJSON(w, http.StatusOK, geoJSON) + +} + +// /api/sync/lap/{sessionId}/{lapId} +func (s *Server) handleSyncLap(w http.ResponseWriter, r *http.Request) { + sessionID := chi.URLParam(r, "sessionId") + lapID := chi.URLParam(r, "lapId") + + sessionData, err := s.queryExecutor.QueryGeneralLap(r.Context(), sessionID, lapID) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to fetch lap data") + return + } + + data, _ := json.Marshal(sessionData) + + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + gz.Write(data) + gz.Close() + + sync.SyncLap(sessionData) + + w.WriteHeader(http.StatusOK) + +} + +// /api/ingest +func (s *Server) handleIngest(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Content-Type") == "application/x-protobuf" { + body, err := io.ReadAll(r.Body) + if err != nil { + respondError(w, 500, fmt.Sprintf("failed to read body: %w", err)) + return + } + defer r.Body.Close() + + batch := &messaging.TelemetryBatch{} + if err := proto.Unmarshal(body, batch); err != nil { + respondError(w, http.StatusBadRequest, "Failed to fetch lap data") + return + } + sender := s.senderPool.Get() + defer s.senderPool.Return(sender) + + persistance.WriteBatch(sender, batch.Records) + + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusInternalServerError) + } +} diff --git a/telemetryService/golang/internal/api/server.go b/telemetryService/golang/internal/api/server.go new file mode 100644 index 0000000..faf1f79 --- /dev/null +++ b/telemetryService/golang/internal/api/server.go @@ -0,0 +1,69 @@ +package api + +import ( + "io" + "log" + "net/http" + "os" + + "github.com/andybalholm/brotli" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/ojparkinson/telemetryService/internal/config" + "github.com/ojparkinson/telemetryService/internal/persistance" +) + +type Server struct { + config *config.Config + logger *log.Logger + queryExecutor *persistance.QueryExecutor + senderPool *persistance.SenderPool + addr string + + app *chi.Mux +} + +func NewServer(addr string, config *config.Config, senderPool *persistance.SenderPool) *Server { + r := chi.NewRouter() + + compressor := middleware.NewCompressor(5) + compressor.SetEncoder("br", func(w io.Writer, level int) io.Writer { + return brotli.NewWriterLevel(w, level) + }) + r.Use(middleware.Logger) + + server := &Server{ + queryExecutor: &persistance.QueryExecutor{Config: config}, + logger: log.New(os.Stdout, "[API] ", log.LstdFlags), + config: config, + senderPool: senderPool, + app: r, + addr: addr, + } + + server.setupRoutes() + + return server +} + +func (s *Server) Start() error { + s.logger.Printf("Starting api server on: %s", s.addr) + if err := http.ListenAndServe(s.addr, s.app); err != nil { + log.Fatal(err) + } + return nil +} + +// func (s *Server) Shutdown(ctx context.Context) error { +// s.logger.Println("Shutting down admin server...") +// return s.app() +// } + +func (s *Server) setupRoutes() { + s.app.Post("/api/ingest", s.handleIngest) + + s.app.Get("/api/sessions", s.handleGetSessions) + s.app.Get("/api/sessions/{sessionId}/laps", s.handleGetLaps) + s.app.Get("/api/sessions/{sessionId}/laps/{lapId}", s.handleGetTelemetry) + s.app.Get("/api/sessions/{sessionId}/laps/{lapId}/geojson", s.handleGetTelemetryGeoJson) +} diff --git a/telemetryService/golang/internal/api/types.go b/telemetryService/golang/internal/api/types.go new file mode 100644 index 0000000..57215f2 --- /dev/null +++ b/telemetryService/golang/internal/api/types.go @@ -0,0 +1,111 @@ +package api + +import ( + "time" + + "github.com/ojparkinson/telemetryService/internal/messaging" +) + +type Session struct { + SessionID string `json:"session_id"` + TrackName string `json:"track_name"` + SessionName string `json:"session_name"` + MaxLapID int `json:"max_lap_id"` + LastUpdated time.Time `json:"last_updated"` +} + +type Lap struct { + LapID string `json:"lap_id"` +} + +type TelemetryDataPoint struct { + Index int `json:"index"` + SessionTime float64 `json:"sessionTime"` + + // Converted values (display-ready) + Speed float64 `json:"Speed"` // km/h + RPM float64 `json:"RPM"` + Throttle float64 `json:"Throttle"` // 0-100% + Brake float64 `json:"Brake"` // 0-100% + Gear uint32 `json:"Gear"` + LapDistPct float64 `json:"LapDistPct"` // 0-100% + SteeringWheelAngle float64 `json:"SteeringWheelAngle"` // degrees + + // Position + Lat float64 `json:"Lat"` + Lon float64 `json:"Lon"` + Alt float64 `json:"Alt"` + + // Motion + VelocityX float64 `json:"VelocityX"` + VelocityY float64 `json:"VelocityY"` + VelocityZ float64 `json:"VelocityZ"` + + // Forces + LatAccel float64 `json:"LatAccel"` + LongAccel float64 `json:"LongAccel"` + VertAccel float64 `json:"VertAccel"` + + // Orientation + Pitch float64 `json:"Pitch"` + Roll float64 `json:"Roll"` + Yaw float64 `json:"Yaw"` + YawNorth float64 `json:"YawNorth"` + + // Other + FuelLevel float64 `json:"FuelLevel"` + LapCurrentLapTime float64 `json:"LapCurrentLapTime"` + PlayerCarPosition uint32 `json:"PlayerCarPosition"` + TrackName string `json:"TrackName"` + SessionNum string `json:"SessionNum"` +} + +func ConvertToDisplayFormat(raw []messaging.Telemetry) []TelemetryDataPoint { + result := make([]TelemetryDataPoint, len(raw)) + + for i, d := range raw { + result[i] = TelemetryDataPoint{ + Index: i, + SessionTime: d.SessionTime, + + // Unit conversions matching frontend expectations + Speed: d.Speed * 3.6, // m/s β†’ km/h + RPM: d.Rpm, + Throttle: d.Throttle * 100, // 0-1 β†’ 0-100% + Brake: d.Brake * 100, // 0-1 β†’ 0-100% + Gear: d.Gear, + LapDistPct: d.LapDistPct * 100, // 0-1 β†’ 0-100% + SteeringWheelAngle: d.SteeringWheelAngle, + + // Position (no conversion) + Lat: d.Lat, + Lon: d.Lon, + Alt: d.Alt, + + // Motion (no conversion) + VelocityX: d.VelocityX, + VelocityY: d.VelocityY, + VelocityZ: d.VelocityZ, + + // Forces (no conversion) + LatAccel: d.LatAccel, + LongAccel: d.LongAccel, + VertAccel: d.VertAccel, + + // Orientation (no conversion) + Pitch: d.Pitch, + Roll: d.Roll, + Yaw: d.Yaw, + YawNorth: d.YawNorth, + + // Other + FuelLevel: d.FuelLevel, + LapCurrentLapTime: d.LapCurrentLapTime, + PlayerCarPosition: d.PlayerCarPosition, + TrackName: d.TrackName, + SessionNum: d.SessionNum, + } + } + + return result +} diff --git a/telemetryService/golang/internal/api/utils.go b/telemetryService/golang/internal/api/utils.go new file mode 100644 index 0000000..2a76993 --- /dev/null +++ b/telemetryService/golang/internal/api/utils.go @@ -0,0 +1,18 @@ +package api + +import ( + "encoding/json" + "net/http" +) + +func respondJSON(w http.ResponseWriter, status int, data interface{}) { + w.WriteHeader(status) + w.Header().Set("Content-Type", "application/json") + jsonData, _ := json.Marshal(data) + w.Write([]byte(jsonData)) +} + +func respondError(w http.ResponseWriter, status int, message string) { + w.WriteHeader(status) + w.Write([]byte(message)) +} diff --git a/telemetryService/golang/internal/config/config.go b/telemetryService/golang/internal/config/config.go new file mode 100644 index 0000000..f4b77dc --- /dev/null +++ b/telemetryService/golang/internal/config/config.go @@ -0,0 +1,53 @@ +package config + +import ( + "os" + "runtime" + "strconv" +) + +type Config struct { + QuestDbHost string + QuestDBPort int + QuestPoolSize int + RabbitMQHost string +} + +func NewConfig() *Config { + questdbHost := getEnv("QUESTDB_HOST", "localhost") + questdbPort := getEnvInt("QUESTDB_PORT", 9000) + rabbitMqHost := getEnv("RABBITMQ_HOST", "localhost") + + return &Config{ + QuestDbHost: questdbHost, + QuestDBPort: questdbPort, + QuestPoolSize: getSenderPool(), + RabbitMQHost: rabbitMqHost, + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func getEnvInt(key string, defaultValue int) int { + if value := os.Getenv(key); value != "" { + if intVal, err := strconv.Atoi(value); err == nil { + return intVal + } + } + return defaultValue +} + +func getSenderPool() int { + if envValue := os.Getenv("SENDER_POOL_SIZE"); envValue != "" { + intEnv, _ := strconv.Atoi(envValue) + return intEnv + } + + cpuCount := runtime.NumCPU() + return min(cpuCount*2+cpuCount/2, 50) +} diff --git a/telemetryService/golang/internal/geojson/geojson.go b/telemetryService/golang/internal/geojson/geojson.go new file mode 100644 index 0000000..03a5689 --- /dev/null +++ b/telemetryService/golang/internal/geojson/geojson.go @@ -0,0 +1,85 @@ +package geojson + +import ( + "math" + + "github.com/ojparkinson/telemetryService/internal/messaging" +) + +func ConvertToGeoJSON(lapData []messaging.Telemetry, options ConversionOptions) (*FeatureCollection, error) { + minSpeed := math.MaxFloat32 + maxSpeed := 0.0 + for _, lap := range lapData { + speed := lap.Speed + if speed < minSpeed { + minSpeed = speed + } + + if speed > maxSpeed { + maxSpeed = speed + } + } + + type ColourPosition struct { + Positions [][]float64 + colour string + } + + coords := make([]ColourPosition, 0) + for _, lap := range lapData { + + normalisedSpeed := (lap.Speed - minSpeed) / (maxSpeed - minSpeed) + + var colour string + switch true { + case normalisedSpeed < 0.3: + colour = "#ef4444" + case normalisedSpeed < 0.5: + colour = "#f46839" + case normalisedSpeed < 0.6: + colour = "#ef9744" + case normalisedSpeed < 0.7: + colour = "#f9b916" + case normalisedSpeed < 0.8: + colour = "#d0ea08" + case normalisedSpeed < 0.8: + colour = "#9fea08" + default: + colour = "#22c55e" + } + + if len(coords) > 0 && colour == coords[len(coords)-1].colour { + coords[len(coords)-1].Positions = append(coords[len(coords)-1].Positions, []float64{lap.Lon, lap.Lat}) + } else { + coords = append(coords, ColourPosition{ + Positions: [][]float64{{lap.Lon, lap.Lat}}, + colour: colour, + }) + } + } + + features := make([]Feature, 0, len(coords)) + + for _, coord := range coords { + if len(coord.Positions) > 0 { + features = append(features, Feature{ + Type: "Feature", + Geometry: Geometry{ + Type: "LineString", + Coordinates: coord.Positions, + }, + Properties: map[string]interface{}{ + "color": coord.colour, + }, + }) + } + } + + featureCollection := &FeatureCollection{ + Type: "FeatureCollection", + Features: features, + Metadata: map[string]interface{}{}, + } + + return featureCollection, nil +} diff --git a/telemetryService/golang/internal/geojson/types.go b/telemetryService/golang/internal/geojson/types.go new file mode 100644 index 0000000..0cf0bfc --- /dev/null +++ b/telemetryService/golang/internal/geojson/types.go @@ -0,0 +1,67 @@ +package geojson + +// GeoJSON Standard Types (RFC 7946 compliant) +type FeatureCollection struct { + Type string `json:"type"` + Features []Feature `json:"features"` + Metadata map[string]interface{} `json:"metadata"` +} + +type Feature struct { + Type string `json:"type"` + Geometry Geometry `json:"geometry"` + Properties map[string]interface{} `json:"properties"` +} + +type Geometry struct { + Type string `json:"type"` + Coordinates [][]float64 `json:"coordinates"` +} + +type Position []float64 + +// Conversion settings +type ConversionOptions struct { + tolerance float64 + SimplifyEnabled bool + MinPoints int + MaxPoints int + + StyleMetric StyleMetric + SegmentMode SegmentMode + ColourScheme ColourScheme + ColourSteps int + + IncludeRawData bool + PropertiesToInclude []string +} + +type StyleMetric string + +const ( + StyleMetricSpeed StyleMetric = "speed" + StyleMetricThrottle StyleMetric = "throttle" + StyleMetricBrake StyleMetric = "brake" + StyleMetricLatAccel StyleMetric = "lat_accel" + StyleMetricLongAccel StyleMetric = "long_accel" + StyleMetricCombined StyleMetric = "combined" +) + +type SegmentMode string + +const ( + SegmentModeGradient SegmentMode = "gradient" + SegmentModeThreshold SegmentMode = "threshold" + SegmentModeQuantile SegmentMode = "quantile" +) + +type ColourScheme string + +const ( + ColorSchemeSpeed ColourScheme = "speed" // Blueβ†’Yellowβ†’Red + ColorSchemeThrottle ColourScheme = "throttle" // Redβ†’Green + ColorSchemeBrake ColourScheme = "brake" // Greenβ†’Red + ColorSchemeGForce ColourScheme = "gforce" // Blueβ†’Purple + ColorSchemeViridis ColourScheme = "viridis" // Perceptually uniform + ColorSchemeTurbo ColourScheme = "turbo" // High contrast +) diff --git a/telemetryService/golang/internal/messaging/telemetryTick.pb.go b/telemetryService/golang/internal/messaging/telemetryTick.pb.go new file mode 100644 index 0000000..6ff932a --- /dev/null +++ b/telemetryService/golang/internal/messaging/telemetryTick.pb.go @@ -0,0 +1,632 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v5.29.3 +// source: telemetryTick.proto + +package messaging + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Telemetry struct { + state protoimpl.MessageState `protogen:"open.v1"` + LapId string `protobuf:"bytes,1,opt,name=lap_id,json=lapId,proto3" json:"lap_id,omitempty"` + Speed float64 `protobuf:"fixed64,2,opt,name=speed,proto3" json:"speed,omitempty"` + LapDistPct float64 `protobuf:"fixed64,3,opt,name=lap_dist_pct,json=lapDistPct,proto3" json:"lap_dist_pct,omitempty"` + SessionId string `protobuf:"bytes,4,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + SessionNum string `protobuf:"bytes,5,opt,name=session_num,json=sessionNum,proto3" json:"session_num,omitempty"` + SessionType string `protobuf:"bytes,6,opt,name=session_type,json=sessionType,proto3" json:"session_type,omitempty"` + SessionName string `protobuf:"bytes,7,opt,name=session_name,json=sessionName,proto3" json:"session_name,omitempty"` + SessionTime float64 `protobuf:"fixed64,8,opt,name=session_time,json=sessionTime,proto3" json:"session_time,omitempty"` + CarId string `protobuf:"bytes,9,opt,name=car_id,json=carId,proto3" json:"car_id,omitempty"` + TrackName string `protobuf:"bytes,10,opt,name=track_name,json=trackName,proto3" json:"track_name,omitempty"` + TrackId string `protobuf:"bytes,11,opt,name=track_id,json=trackId,proto3" json:"track_id,omitempty"` + WorkerId uint32 `protobuf:"varint,12,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + SteeringWheelAngle float64 `protobuf:"fixed64,13,opt,name=steering_wheel_angle,json=steeringWheelAngle,proto3" json:"steering_wheel_angle,omitempty"` + PlayerCarPosition uint32 `protobuf:"varint,14,opt,name=player_car_position,json=playerCarPosition,proto3" json:"player_car_position,omitempty"` + VelocityX float64 `protobuf:"fixed64,15,opt,name=velocity_x,json=velocityX,proto3" json:"velocity_x,omitempty"` + VelocityY float64 `protobuf:"fixed64,16,opt,name=velocity_y,json=velocityY,proto3" json:"velocity_y,omitempty"` + VelocityZ float64 `protobuf:"fixed64,17,opt,name=velocity_z,json=velocityZ,proto3" json:"velocity_z,omitempty"` + FuelLevel float64 `protobuf:"fixed64,18,opt,name=fuel_level,json=fuelLevel,proto3" json:"fuel_level,omitempty"` + Throttle float64 `protobuf:"fixed64,19,opt,name=throttle,proto3" json:"throttle,omitempty"` + Brake float64 `protobuf:"fixed64,20,opt,name=brake,proto3" json:"brake,omitempty"` + Rpm float64 `protobuf:"fixed64,21,opt,name=rpm,proto3" json:"rpm,omitempty"` + Lat float64 `protobuf:"fixed64,22,opt,name=lat,proto3" json:"lat,omitempty"` + Lon float64 `protobuf:"fixed64,23,opt,name=lon,proto3" json:"lon,omitempty"` + Gear uint32 `protobuf:"varint,24,opt,name=gear,proto3" json:"gear,omitempty"` + Alt float64 `protobuf:"fixed64,25,opt,name=alt,proto3" json:"alt,omitempty"` + LatAccel float64 `protobuf:"fixed64,26,opt,name=lat_accel,json=latAccel,proto3" json:"lat_accel,omitempty"` + LongAccel float64 `protobuf:"fixed64,27,opt,name=long_accel,json=longAccel,proto3" json:"long_accel,omitempty"` + VertAccel float64 `protobuf:"fixed64,28,opt,name=vert_accel,json=vertAccel,proto3" json:"vert_accel,omitempty"` + Pitch float64 `protobuf:"fixed64,29,opt,name=pitch,proto3" json:"pitch,omitempty"` + Roll float64 `protobuf:"fixed64,30,opt,name=roll,proto3" json:"roll,omitempty"` + Yaw float64 `protobuf:"fixed64,31,opt,name=yaw,proto3" json:"yaw,omitempty"` + YawNorth float64 `protobuf:"fixed64,32,opt,name=yaw_north,json=yawNorth,proto3" json:"yaw_north,omitempty"` + Voltage float64 `protobuf:"fixed64,33,opt,name=voltage,proto3" json:"voltage,omitempty"` + LapLastLapTime float64 `protobuf:"fixed64,34,opt,name=lap_last_lap_time,json=lapLastLapTime,proto3" json:"lap_last_lap_time,omitempty"` + WaterTemp float64 `protobuf:"fixed64,35,opt,name=water_temp,json=waterTemp,proto3" json:"water_temp,omitempty"` + LapDeltaToBestLap float64 `protobuf:"fixed64,36,opt,name=lap_delta_to_best_lap,json=lapDeltaToBestLap,proto3" json:"lap_delta_to_best_lap,omitempty"` + LapCurrentLapTime float64 `protobuf:"fixed64,37,opt,name=lap_current_lap_time,json=lapCurrentLapTime,proto3" json:"lap_current_lap_time,omitempty"` + LFpressure float64 `protobuf:"fixed64,38,opt,name=l_fpressure,json=lFpressure,proto3" json:"l_fpressure,omitempty"` + RFpressure float64 `protobuf:"fixed64,39,opt,name=r_fpressure,json=rFpressure,proto3" json:"r_fpressure,omitempty"` + LRpressure float64 `protobuf:"fixed64,40,opt,name=l_rpressure,json=lRpressure,proto3" json:"l_rpressure,omitempty"` + RRpressure float64 `protobuf:"fixed64,41,opt,name=r_rpressure,json=rRpressure,proto3" json:"r_rpressure,omitempty"` + LFtempM float64 `protobuf:"fixed64,42,opt,name=l_ftemp_m,json=lFtempM,proto3" json:"l_ftemp_m,omitempty"` + RFtempM float64 `protobuf:"fixed64,43,opt,name=r_ftemp_m,json=rFtempM,proto3" json:"r_ftemp_m,omitempty"` + LRtempM float64 `protobuf:"fixed64,44,opt,name=l_rtemp_m,json=lRtempM,proto3" json:"l_rtemp_m,omitempty"` + RRtempM float64 `protobuf:"fixed64,45,opt,name=r_rtemp_m,json=rRtempM,proto3" json:"r_rtemp_m,omitempty"` + TickTime *timestamppb.Timestamp `protobuf:"bytes,46,opt,name=tick_time,json=tickTime,proto3" json:"tick_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Telemetry) Reset() { + *x = Telemetry{} + mi := &file_telemetryTick_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Telemetry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Telemetry) ProtoMessage() {} + +func (x *Telemetry) ProtoReflect() protoreflect.Message { + mi := &file_telemetryTick_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Telemetry.ProtoReflect.Descriptor instead. +func (*Telemetry) Descriptor() ([]byte, []int) { + return file_telemetryTick_proto_rawDescGZIP(), []int{0} +} + +func (x *Telemetry) GetLapId() string { + if x != nil { + return x.LapId + } + return "" +} + +func (x *Telemetry) GetSpeed() float64 { + if x != nil { + return x.Speed + } + return 0 +} + +func (x *Telemetry) GetLapDistPct() float64 { + if x != nil { + return x.LapDistPct + } + return 0 +} + +func (x *Telemetry) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *Telemetry) GetSessionNum() string { + if x != nil { + return x.SessionNum + } + return "" +} + +func (x *Telemetry) GetSessionType() string { + if x != nil { + return x.SessionType + } + return "" +} + +func (x *Telemetry) GetSessionName() string { + if x != nil { + return x.SessionName + } + return "" +} + +func (x *Telemetry) GetSessionTime() float64 { + if x != nil { + return x.SessionTime + } + return 0 +} + +func (x *Telemetry) GetCarId() string { + if x != nil { + return x.CarId + } + return "" +} + +func (x *Telemetry) GetTrackName() string { + if x != nil { + return x.TrackName + } + return "" +} + +func (x *Telemetry) GetTrackId() string { + if x != nil { + return x.TrackId + } + return "" +} + +func (x *Telemetry) GetWorkerId() uint32 { + if x != nil { + return x.WorkerId + } + return 0 +} + +func (x *Telemetry) GetSteeringWheelAngle() float64 { + if x != nil { + return x.SteeringWheelAngle + } + return 0 +} + +func (x *Telemetry) GetPlayerCarPosition() uint32 { + if x != nil { + return x.PlayerCarPosition + } + return 0 +} + +func (x *Telemetry) GetVelocityX() float64 { + if x != nil { + return x.VelocityX + } + return 0 +} + +func (x *Telemetry) GetVelocityY() float64 { + if x != nil { + return x.VelocityY + } + return 0 +} + +func (x *Telemetry) GetVelocityZ() float64 { + if x != nil { + return x.VelocityZ + } + return 0 +} + +func (x *Telemetry) GetFuelLevel() float64 { + if x != nil { + return x.FuelLevel + } + return 0 +} + +func (x *Telemetry) GetThrottle() float64 { + if x != nil { + return x.Throttle + } + return 0 +} + +func (x *Telemetry) GetBrake() float64 { + if x != nil { + return x.Brake + } + return 0 +} + +func (x *Telemetry) GetRpm() float64 { + if x != nil { + return x.Rpm + } + return 0 +} + +func (x *Telemetry) GetLat() float64 { + if x != nil { + return x.Lat + } + return 0 +} + +func (x *Telemetry) GetLon() float64 { + if x != nil { + return x.Lon + } + return 0 +} + +func (x *Telemetry) GetGear() uint32 { + if x != nil { + return x.Gear + } + return 0 +} + +func (x *Telemetry) GetAlt() float64 { + if x != nil { + return x.Alt + } + return 0 +} + +func (x *Telemetry) GetLatAccel() float64 { + if x != nil { + return x.LatAccel + } + return 0 +} + +func (x *Telemetry) GetLongAccel() float64 { + if x != nil { + return x.LongAccel + } + return 0 +} + +func (x *Telemetry) GetVertAccel() float64 { + if x != nil { + return x.VertAccel + } + return 0 +} + +func (x *Telemetry) GetPitch() float64 { + if x != nil { + return x.Pitch + } + return 0 +} + +func (x *Telemetry) GetRoll() float64 { + if x != nil { + return x.Roll + } + return 0 +} + +func (x *Telemetry) GetYaw() float64 { + if x != nil { + return x.Yaw + } + return 0 +} + +func (x *Telemetry) GetYawNorth() float64 { + if x != nil { + return x.YawNorth + } + return 0 +} + +func (x *Telemetry) GetVoltage() float64 { + if x != nil { + return x.Voltage + } + return 0 +} + +func (x *Telemetry) GetLapLastLapTime() float64 { + if x != nil { + return x.LapLastLapTime + } + return 0 +} + +func (x *Telemetry) GetWaterTemp() float64 { + if x != nil { + return x.WaterTemp + } + return 0 +} + +func (x *Telemetry) GetLapDeltaToBestLap() float64 { + if x != nil { + return x.LapDeltaToBestLap + } + return 0 +} + +func (x *Telemetry) GetLapCurrentLapTime() float64 { + if x != nil { + return x.LapCurrentLapTime + } + return 0 +} + +func (x *Telemetry) GetLFpressure() float64 { + if x != nil { + return x.LFpressure + } + return 0 +} + +func (x *Telemetry) GetRFpressure() float64 { + if x != nil { + return x.RFpressure + } + return 0 +} + +func (x *Telemetry) GetLRpressure() float64 { + if x != nil { + return x.LRpressure + } + return 0 +} + +func (x *Telemetry) GetRRpressure() float64 { + if x != nil { + return x.RRpressure + } + return 0 +} + +func (x *Telemetry) GetLFtempM() float64 { + if x != nil { + return x.LFtempM + } + return 0 +} + +func (x *Telemetry) GetRFtempM() float64 { + if x != nil { + return x.RFtempM + } + return 0 +} + +func (x *Telemetry) GetLRtempM() float64 { + if x != nil { + return x.LRtempM + } + return 0 +} + +func (x *Telemetry) GetRRtempM() float64 { + if x != nil { + return x.RRtempM + } + return 0 +} + +func (x *Telemetry) GetTickTime() *timestamppb.Timestamp { + if x != nil { + return x.TickTime + } + return nil +} + +type TelemetryBatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Records []*Telemetry `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + BatchId string `protobuf:"bytes,2,opt,name=batch_id,json=batchId,proto3" json:"batch_id,omitempty"` + SessionId string `protobuf:"bytes,3,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + WorkerId uint32 `protobuf:"varint,4,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TelemetryBatch) Reset() { + *x = TelemetryBatch{} + mi := &file_telemetryTick_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TelemetryBatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TelemetryBatch) ProtoMessage() {} + +func (x *TelemetryBatch) ProtoReflect() protoreflect.Message { + mi := &file_telemetryTick_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TelemetryBatch.ProtoReflect.Descriptor instead. +func (*TelemetryBatch) Descriptor() ([]byte, []int) { + return file_telemetryTick_proto_rawDescGZIP(), []int{1} +} + +func (x *TelemetryBatch) GetRecords() []*Telemetry { + if x != nil { + return x.Records + } + return nil +} + +func (x *TelemetryBatch) GetBatchId() string { + if x != nil { + return x.BatchId + } + return "" +} + +func (x *TelemetryBatch) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *TelemetryBatch) GetWorkerId() uint32 { + if x != nil { + return x.WorkerId + } + return 0 +} + +func (x *TelemetryBatch) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +var File_telemetryTick_proto protoreflect.FileDescriptor + +const file_telemetryTick_proto_rawDesc = "" + + "\n" + + "\x13telemetryTick.proto\x12\x06pubSub\x1a\x1fgoogle/protobuf/timestamp.proto\"\x85\v\n" + + "\tTelemetry\x12\x15\n" + + "\x06lap_id\x18\x01 \x01(\tR\x05lapId\x12\x14\n" + + "\x05speed\x18\x02 \x01(\x01R\x05speed\x12 \n" + + "\flap_dist_pct\x18\x03 \x01(\x01R\n" + + "lapDistPct\x12\x1d\n" + + "\n" + + "session_id\x18\x04 \x01(\tR\tsessionId\x12\x1f\n" + + "\vsession_num\x18\x05 \x01(\tR\n" + + "sessionNum\x12!\n" + + "\fsession_type\x18\x06 \x01(\tR\vsessionType\x12!\n" + + "\fsession_name\x18\a \x01(\tR\vsessionName\x12!\n" + + "\fsession_time\x18\b \x01(\x01R\vsessionTime\x12\x15\n" + + "\x06car_id\x18\t \x01(\tR\x05carId\x12\x1d\n" + + "\n" + + "track_name\x18\n" + + " \x01(\tR\ttrackName\x12\x19\n" + + "\btrack_id\x18\v \x01(\tR\atrackId\x12\x1b\n" + + "\tworker_id\x18\f \x01(\rR\bworkerId\x120\n" + + "\x14steering_wheel_angle\x18\r \x01(\x01R\x12steeringWheelAngle\x12.\n" + + "\x13player_car_position\x18\x0e \x01(\rR\x11playerCarPosition\x12\x1d\n" + + "\n" + + "velocity_x\x18\x0f \x01(\x01R\tvelocityX\x12\x1d\n" + + "\n" + + "velocity_y\x18\x10 \x01(\x01R\tvelocityY\x12\x1d\n" + + "\n" + + "velocity_z\x18\x11 \x01(\x01R\tvelocityZ\x12\x1d\n" + + "\n" + + "fuel_level\x18\x12 \x01(\x01R\tfuelLevel\x12\x1a\n" + + "\bthrottle\x18\x13 \x01(\x01R\bthrottle\x12\x14\n" + + "\x05brake\x18\x14 \x01(\x01R\x05brake\x12\x10\n" + + "\x03rpm\x18\x15 \x01(\x01R\x03rpm\x12\x10\n" + + "\x03lat\x18\x16 \x01(\x01R\x03lat\x12\x10\n" + + "\x03lon\x18\x17 \x01(\x01R\x03lon\x12\x12\n" + + "\x04gear\x18\x18 \x01(\rR\x04gear\x12\x10\n" + + "\x03alt\x18\x19 \x01(\x01R\x03alt\x12\x1b\n" + + "\tlat_accel\x18\x1a \x01(\x01R\blatAccel\x12\x1d\n" + + "\n" + + "long_accel\x18\x1b \x01(\x01R\tlongAccel\x12\x1d\n" + + "\n" + + "vert_accel\x18\x1c \x01(\x01R\tvertAccel\x12\x14\n" + + "\x05pitch\x18\x1d \x01(\x01R\x05pitch\x12\x12\n" + + "\x04roll\x18\x1e \x01(\x01R\x04roll\x12\x10\n" + + "\x03yaw\x18\x1f \x01(\x01R\x03yaw\x12\x1b\n" + + "\tyaw_north\x18 \x01(\x01R\byawNorth\x12\x18\n" + + "\avoltage\x18! \x01(\x01R\avoltage\x12)\n" + + "\x11lap_last_lap_time\x18\" \x01(\x01R\x0elapLastLapTime\x12\x1d\n" + + "\n" + + "water_temp\x18# \x01(\x01R\twaterTemp\x120\n" + + "\x15lap_delta_to_best_lap\x18$ \x01(\x01R\x11lapDeltaToBestLap\x12/\n" + + "\x14lap_current_lap_time\x18% \x01(\x01R\x11lapCurrentLapTime\x12\x1f\n" + + "\vl_fpressure\x18& \x01(\x01R\n" + + "lFpressure\x12\x1f\n" + + "\vr_fpressure\x18' \x01(\x01R\n" + + "rFpressure\x12\x1f\n" + + "\vl_rpressure\x18( \x01(\x01R\n" + + "lRpressure\x12\x1f\n" + + "\vr_rpressure\x18) \x01(\x01R\n" + + "rRpressure\x12\x1a\n" + + "\tl_ftemp_m\x18* \x01(\x01R\alFtempM\x12\x1a\n" + + "\tr_ftemp_m\x18+ \x01(\x01R\arFtempM\x12\x1a\n" + + "\tl_rtemp_m\x18, \x01(\x01R\alRtempM\x12\x1a\n" + + "\tr_rtemp_m\x18- \x01(\x01R\arRtempM\x127\n" + + "\ttick_time\x18. \x01(\v2\x1a.google.protobuf.TimestampR\btickTime\"\xce\x01\n" + + "\x0eTelemetryBatch\x12+\n" + + "\arecords\x18\x01 \x03(\v2\x11.pubSub.TelemetryR\arecords\x12\x19\n" + + "\bbatch_id\x18\x02 \x01(\tR\abatchId\x12\x1d\n" + + "\n" + + "session_id\x18\x03 \x01(\tR\tsessionId\x12\x1b\n" + + "\tworker_id\x18\x04 \x01(\rR\bworkerId\x128\n" + + "\ttimestamp\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestampBSZQgithub.com/OJPARKINSON/IRacing-Display/telemetryService/golang/internal/messagingb\x06proto3" + +var ( + file_telemetryTick_proto_rawDescOnce sync.Once + file_telemetryTick_proto_rawDescData []byte +) + +func file_telemetryTick_proto_rawDescGZIP() []byte { + file_telemetryTick_proto_rawDescOnce.Do(func() { + file_telemetryTick_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_telemetryTick_proto_rawDesc), len(file_telemetryTick_proto_rawDesc))) + }) + return file_telemetryTick_proto_rawDescData +} + +var file_telemetryTick_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_telemetryTick_proto_goTypes = []any{ + (*Telemetry)(nil), // 0: pubSub.Telemetry + (*TelemetryBatch)(nil), // 1: pubSub.TelemetryBatch + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp +} +var file_telemetryTick_proto_depIdxs = []int32{ + 2, // 0: pubSub.Telemetry.tick_time:type_name -> google.protobuf.Timestamp + 0, // 1: pubSub.TelemetryBatch.records:type_name -> pubSub.Telemetry + 2, // 2: pubSub.TelemetryBatch.timestamp:type_name -> google.protobuf.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_telemetryTick_proto_init() } +func file_telemetryTick_proto_init() { + if File_telemetryTick_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_telemetryTick_proto_rawDesc), len(file_telemetryTick_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_telemetryTick_proto_goTypes, + DependencyIndexes: file_telemetryTick_proto_depIdxs, + MessageInfos: file_telemetryTick_proto_msgTypes, + }.Build() + File_telemetryTick_proto = out.File + file_telemetryTick_proto_goTypes = nil + file_telemetryTick_proto_depIdxs = nil +} diff --git a/telemetryService/golang/internal/messaging/telemetryTick.proto b/telemetryService/golang/internal/messaging/telemetryTick.proto new file mode 100644 index 0000000..cfa0c53 --- /dev/null +++ b/telemetryService/golang/internal/messaging/telemetryTick.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; +package pubSub; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/OJPARKINSON/IRacing-Display/telemetryService/golang/internal/messaging"; + +message Telemetry { + string lap_id = 1; + double speed = 2; + double lap_dist_pct = 3; + string session_id = 4; + string session_num = 5; + string session_type = 6; + string session_name = 7; + double session_time = 8; + string car_id = 9; + string track_name = 10; + string track_id = 11; + uint32 worker_id = 12; + double steering_wheel_angle = 13; + uint32 player_car_position = 14; + double velocity_x = 15; + double velocity_y = 16; + double velocity_z = 17; + double fuel_level = 18; + double throttle = 19; + double brake = 20; + double rpm = 21; + double lat = 22; + double lon = 23; + uint32 gear = 24; + double alt = 25; + double lat_accel = 26; + double long_accel = 27; + double vert_accel = 28; + double pitch = 29; + double roll = 30; + double yaw = 31; + double yaw_north = 32; + double voltage = 33; + double lap_last_lap_time = 34; + double water_temp = 35; + double lap_delta_to_best_lap = 36; + double lap_current_lap_time = 37; + double l_fpressure = 38; + double r_fpressure = 39; + double l_rpressure = 40; + double r_rpressure = 41; + double l_ftemp_m = 42; + double r_ftemp_m = 43; + double l_rtemp_m = 44; + double r_rtemp_m = 45; + google.protobuf.Timestamp tick_time = 46; +} + +message TelemetryBatch { + repeated Telemetry records = 1; + string batch_id = 2; + string session_id = 3; + uint32 worker_id = 4; + google.protobuf.Timestamp timestamp = 5; +} \ No newline at end of file diff --git a/telemetryService/golang/internal/metrics/handler.go b/telemetryService/golang/internal/metrics/handler.go new file mode 100644 index 0000000..8fdcfee --- /dev/null +++ b/telemetryService/golang/internal/metrics/handler.go @@ -0,0 +1,16 @@ +package metrics + +import ( + "log" + "net/http" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func MetricsHandler() { + http.Handle("/metrics", promhttp.Handler()) + log.Println("Starting Prometheus metrics server on :9092") + if err := http.ListenAndServe(":9092", nil); err != nil { + log.Printf("metrics server failed: %v", err) + } +} diff --git a/telemetryService/golang/internal/metrics/metrics.go b/telemetryService/golang/internal/metrics/metrics.go new file mode 100644 index 0000000..5d3d65c --- /dev/null +++ b/telemetryService/golang/internal/metrics/metrics.go @@ -0,0 +1,40 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // Records received from RabbitMQ + RecordsReceivedTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "telemetry_records_received_total", + Help: "Total number of telemetry records received from RabbitMQ", + }) + + // Records written to QuestDB + RecordsWrittenTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "telemetry_records_written_total", + Help: "Total number of telemetry records successfully written to QuestDB", + }) + + // Database write latency + DBWriteDuration = promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "telemetry_db_write_duration_seconds", + Help: "Time taken to write a batch to QuestDB", + Buckets: prometheus.ExponentialBuckets(0.01, 2, 10), // 10ms to ~10s + }) + + // Write errors + DBWriteErrors = promauto.NewCounter(prometheus.CounterOpts{ + Name: "telemetry_db_write_errors_total", + Help: "Total number of failed QuestDB write operations", + }) + + // Batch size metrics + BatchSizeRecords = promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "telemetry_batch_size_records", + Help: "Number of records per batch written to QuestDB", + Buckets: prometheus.ExponentialBuckets(100, 2, 10), // 100 to ~102k records + }) +) diff --git a/telemetryService/golang/internal/persistance/persistance.go b/telemetryService/golang/internal/persistance/persistance.go new file mode 100644 index 0000000..253adb5 --- /dev/null +++ b/telemetryService/golang/internal/persistance/persistance.go @@ -0,0 +1,260 @@ +package persistance + +import ( + "context" + "fmt" + "log" + "math" + "strings" + "time" + + "github.com/ojparkinson/telemetryService/internal/messaging" + qdb "github.com/questdb/go-questdb-client/v4" +) + +func WriteBatch(sender qdb.LineSender, records []*messaging.Telemetry) error { + ctx := context.Background() + const flushInterval = 100000 + + for i, record := range records { + sender.Table("TelemetryTicks"). + Symbol("session_id", sanitise(record.SessionId)). + Symbol("track_name", sanitise(record.TrackName)). + Symbol("track_id", sanitise(record.TrackId)). + Symbol("lap_id", sanitise(record.LapId)). + Symbol("session_num", sanitise(record.SessionNum)). + Symbol("session_type", sanitise(record.SessionType)). + Symbol("session_name", sanitise(record.SessionName)). + Symbol("car_id", sanitise(record.CarId)). + Int64Column("gear", validateInt(record.Gear)). + Int64Column("player_car_position", validateInt(record.PlayerCarPosition)). + Float64Column("speed", validateDouble(record.Speed)). + Float64Column("lap_dist_pct", validateDouble(record.LapDistPct)). + Float64Column("session_time", validateDouble(record.SessionTime)). + Float64Column("lat", validateDouble(record.Lat)). + Float64Column("lon", validateDouble(record.Lon)). + Float64Column("lap_current_lap_time", validateDouble(record.LapCurrentLapTime)). + Float64Column("lapLastLapTime", validateDouble(record.LapLastLapTime)). + Float64Column("lapDeltaToBestLap", validateDouble(record.LapDeltaToBestLap)). + Float64Column("throttle", validateDouble(record.Throttle)). + Float64Column("brake", validateDouble(record.Brake)). + Float64Column("steering_wheel_angle", validateDouble(record.SteeringWheelAngle)). + Float64Column("rpm", validateDouble(record.Rpm)). + Float64Column("velocity_x", validateDouble(record.VelocityX)). + Float64Column("velocity_y", validateDouble(record.VelocityY)). + Float64Column("velocity_z", validateDouble(record.VelocityZ)). + Float64Column("fuel_level", validateDouble(record.FuelLevel)). + Float64Column("alt", validateDouble(record.Alt)). + Float64Column("lat_accel", validateDouble(record.LatAccel)). + Float64Column("long_accel", validateDouble(record.LongAccel)). + Float64Column("vert_accel", validateDouble(record.VertAccel)). + Float64Column("pitch", validateDouble(record.Pitch)). + Float64Column("roll", validateDouble(record.Roll)). + Float64Column("yaw", validateDouble(record.Yaw)). + Float64Column("yaw_north", validateDouble(record.YawNorth)). + Float64Column("voltage", validateDouble(record.Voltage)). + Float64Column("waterTemp", validateDouble(record.WaterTemp)). + Float64Column("lFpressure", validateDouble(record.LFpressure)). + Float64Column("rFpressure", validateDouble(record.RFpressure)). + Float64Column("lRpressure", validateDouble(record.LRpressure)). + Float64Column("rRpressure", validateDouble(record.RRpressure)). + Float64Column("lFtempM", validateDouble(record.LFtempM)). + Float64Column("rFtempM", validateDouble(record.RFtempM)). + Float64Column("lRtempM", validateDouble(record.LRtempM)). + Float64Column("rRtempM", validateDouble(record.RRtempM)). + At(ctx, tickTime(record)) + + // Flush every 10K records to keep memory and network packets reasonable + if (i+1)%flushInterval == 0 { + if err := sender.Flush(ctx); err != nil { + return fmt.Errorf("flush failed at record %d: %w", i, err) + } + } + } + + // Final flush for any remaining records + err := sender.Flush(ctx) + if err != nil { + return fmt.Errorf("final flush failed: %w", err) + } + + // fmt.Printf("wrote %d records to QuestDb\n", len(records)) + return nil +} + +func tickTime(record *messaging.Telemetry) time.Time { + if record.TickTime != nil { + return record.TickTime.AsTime() + } + log.Printf("WARNING: record has nil TickTime, using time.Now() as fallback") + return time.Now() +} + +func sanitise(value string) string { + if value == "" { + return "unknown" + } + + // Fast path: check if sanitization needed + needsSanitization := false + for i := 0; i < len(value); i++ { + c := value[i] + if c == ',' || c == ' ' || c == '=' || c == '\n' || + c == '\r' || c == '"' || c == '\'' || c == '\\' { + needsSanitization = true + break + } + } + + if !needsSanitization { + trimmed := strings.TrimSpace(value) + if trimmed == value { + return value // Zero allocations + } + return trimmed + } + + // Single-pass with pre-allocated builder + var builder strings.Builder + builder.Grow(len(value)) + + for i := 0; i < len(value); i++ { + c := value[i] + switch c { + case ',', ' ', '=', '\n', '\r', '"', '\'', '\\': + builder.WriteByte('_') + default: + builder.WriteByte(c) + } + } + + return strings.TrimSpace(builder.String()) +} + +func validateDouble(value float64) float64 { + if math.IsNaN(value) || math.IsInf(value, 0) { + return 0.0 + } + if value == math.MaxFloat64 || value == -math.MaxFloat64 { + return 0.0 + } + return value +} + +func validateInt(value uint32) int64 { + // Handle iRacing's invalid sentinel value (4294967295 = uint.MaxValue) + if value == 0xFFFFFFFF { + return 0 + } + return int64(value) +} + +func mapToTelemetry(m map[string]interface{}) messaging.Telemetry { + return messaging.Telemetry{ + LapId: getString(m, "lap_id"), + SessionId: getString(m, "session_id"), + SessionNum: getString(m, "session_num"), + SessionType: getString(m, "session_type"), + SessionName: getString(m, "session_name"), + CarId: getString(m, "car_id"), + TrackName: getString(m, "track_name"), + TrackId: getString(m, "track_id"), + + Lat: getFloat64(m, "lat"), + Lon: getFloat64(m, "lon"), + Alt: getFloat64(m, "alt"), + LapDistPct: getFloat64(m, "lap_dist_pct"), + + Speed: getFloat64(m, "speed"), + VelocityX: getFloat64(m, "velocity_x"), + VelocityY: getFloat64(m, "velocity_y"), + VelocityZ: getFloat64(m, "velocity_z"), + + // Driver Inputs + Throttle: getFloat64(m, "throttle"), + Brake: getFloat64(m, "brake"), + SteeringWheelAngle: getFloat64(m, "steering_wheel_angle"), + Gear: uint32(getInt(m, "gear")), + + // Engine + Rpm: getFloat64(m, "rpm"), + FuelLevel: getFloat64(m, "fuel_level"), + + // Forces + LatAccel: getFloat64(m, "lat_accel"), + LongAccel: getFloat64(m, "long_accel"), + VertAccel: getFloat64(m, "vert_accel"), + + // Orientation + Pitch: getFloat64(m, "pitch"), + Roll: getFloat64(m, "roll"), + Yaw: getFloat64(m, "yaw"), + YawNorth: getFloat64(m, "yaw_north"), + + // Telemetry + Voltage: getFloat64(m, "voltage"), + WaterTemp: getFloat64(m, "water_temp"), + + // Tire Pressures + LFpressure: getFloat64(m, "lFpressure"), + RFpressure: getFloat64(m, "rFpressure"), + LRpressure: getFloat64(m, "lRpressure"), + RRpressure: getFloat64(m, "rRpressure"), + + // Tire Temps + LFtempM: getFloat64(m, "lFtempM"), + RFtempM: getFloat64(m, "rFtempM"), + LRtempM: getFloat64(m, "lRtempM"), + RRtempM: getFloat64(m, "rRtempM"), + + // Timing + SessionTime: getFloat64(m, "session_time"), + LapCurrentLapTime: getFloat64(m, "lap_current_lap_time"), + LapLastLapTime: getFloat64(m, "lapLastLapTime"), + LapDeltaToBestLap: getFloat64(m, "lapDeltaToBestLap"), + PlayerCarPosition: uint32(getInt(m, "player_car_position")), + + // WorkerId not in DB - will be 0 + // TickTime not mapped - use raw timestamp if needed + } +} + +// Keep helper functions from before +func getString(m map[string]interface{}, key string) string { + if v, ok := m[key]; ok && v != nil { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +func getFloat64(m map[string]interface{}, key string) float64 { + if v, ok := m[key]; ok && v != nil { + switch val := v.(type) { + case float64: + return val + case float32: + return float64(val) + case int: + return float64(val) + case int64: + return float64(val) + } + } + return 0.0 +} + +func getInt(m map[string]interface{}, key string) int { + if v, ok := m[key]; ok && v != nil { + switch val := v.(type) { + case int: + return val + case int64: + return int(val) + case float64: + return int(val) + } + } + return 0 +} diff --git a/telemetryService/golang/internal/persistance/persistance_bench_test.go b/telemetryService/golang/internal/persistance/persistance_bench_test.go new file mode 100644 index 0000000..1f226b8 --- /dev/null +++ b/telemetryService/golang/internal/persistance/persistance_bench_test.go @@ -0,0 +1,279 @@ +package persistance + +import ( + "context" + "testing" + "time" + + "github.com/ojparkinson/telemetryService/internal/messaging" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// BenchmarkSanitise benchmarks the string sanitization function +func BenchmarkSanitise(b *testing.B) { + testCases := []struct { + name string + input string + }{ + {"Empty", ""}, + {"Clean", "clean_string_no_special_chars"}, + {"WithSpaces", "track name with spaces"}, + {"WithCommas", "value,with,commas"}, + {"WithQuotes", `value"with"quotes`}, + {"AllSpecialChars", `track,name=value with "quotes" and\backslash`}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = sanitise(tc.input) + } + }) + } +} + +// BenchmarkValidateDouble benchmarks float64 validation +func BenchmarkValidateDouble(b *testing.B) { + testCases := []struct { + name string + value float64 + }{ + {"Normal", 123.456}, + {"Zero", 0.0}, + {"Negative", -123.456}, + {"Large", 1e10}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = validateDouble(tc.value) + } + }) + } +} + +// BenchmarkValidateInt benchmarks uint32 validation +func BenchmarkValidateInt(b *testing.B) { + testCases := []struct { + name string + value uint32 + }{ + {"Normal", 42}, + {"Zero", 0}, + {"Max", 0xFFFFFFFF}, + {"Large", 1000000}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = validateInt(tc.value) + } + }) + } +} + +// MockLineSender implements a no-op version of qdb.LineSender for benchmarking +type MockLineSender struct { + rowCount int +} + +func (m *MockLineSender) Table(name string) *MockLineSender { + return m +} + +func (m *MockLineSender) Symbol(name, value string) *MockLineSender { + return m +} + +func (m *MockLineSender) StringColumn(name, value string) *MockLineSender { + return m +} + +func (m *MockLineSender) Int64Column(name string, value int64) *MockLineSender { + return m +} + +func (m *MockLineSender) Float64Column(name string, value float64) *MockLineSender { + return m +} + +func (m *MockLineSender) At(ctx context.Context, t time.Time) *MockLineSender { + m.rowCount++ + return m +} + +func (m *MockLineSender) Flush(ctx context.Context) error { + m.rowCount = 0 + return nil +} + +func (m *MockLineSender) Close(ctx context.Context) error { + return nil +} + +// BenchmarkRecordSerialization benchmarks the conversion of a single telemetry record +func BenchmarkRecordSerialization(b *testing.B) { + record := generateTelemetryRecord("session-123", "Spa-Francorchamps") + sender := &MockLineSender{} + ctx := context.Background() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + sender.Table("TelemetryTicks"). + Symbol("session_id", sanitise(record.SessionId)). + Symbol("track_name", sanitise(record.TrackName)). + Symbol("track_id", sanitise(record.TrackId)). + Symbol("lap_id", sanitise(record.LapId)). + Symbol("session_num", sanitise(record.SessionNum)). + Symbol("session_type", sanitise(record.SessionType)). + Symbol("session_name", sanitise(record.SessionName)). + StringColumn("car_id", sanitise(record.CarId)). + Int64Column("gear", validateInt(record.Gear)). + Int64Column("player_car_position", validateInt(record.PlayerCarPosition)). + Float64Column("speed", validateDouble(record.Speed)). + Float64Column("lap_dist_pct", validateDouble(record.LapDistPct)). + Float64Column("session_time", validateDouble(record.SessionTime)). + Float64Column("lat", validateDouble(record.Lat)). + Float64Column("lon", validateDouble(record.Lon)). + Float64Column("lap_current_lap_time", validateDouble(record.LapCurrentLapTime)). + Float64Column("lapLastLapTime", validateDouble(record.LapLastLapTime)). + Float64Column("lapDeltaToBestLap", validateDouble(record.LapDeltaToBestLap)). + Float64Column("throttle", validateDouble(record.Throttle)). + Float64Column("brake", validateDouble(record.Brake)). + Float64Column("steering_wheel_angle", validateDouble(record.SteeringWheelAngle)). + Float64Column("rpm", validateDouble(record.Rpm)). + Float64Column("velocity_x", validateDouble(record.VelocityX)). + Float64Column("velocity_y", validateDouble(record.VelocityY)). + Float64Column("velocity_z", validateDouble(record.VelocityZ)). + Float64Column("fuel_level", validateDouble(record.FuelLevel)). + Float64Column("alt", validateDouble(record.Alt)). + Float64Column("lat_accel", validateDouble(record.LatAccel)). + Float64Column("long_accel", validateDouble(record.LongAccel)). + Float64Column("vert_accel", validateDouble(record.VertAccel)). + Float64Column("pitch", validateDouble(record.Pitch)). + Float64Column("roll", validateDouble(record.Roll)). + Float64Column("yaw", validateDouble(record.Yaw)). + Float64Column("yaw_north", validateDouble(record.YawNorth)). + Float64Column("voltage", validateDouble(record.Voltage)). + Float64Column("waterTemp", validateDouble(record.WaterTemp)). + Float64Column("lFpressure", validateDouble(record.LFpressure)). + Float64Column("rFpressure", validateDouble(record.RFpressure)). + Float64Column("lRpressure", validateDouble(record.LRpressure)). + Float64Column("rRpressure", validateDouble(record.RRpressure)). + Float64Column("lFtempM", validateDouble(record.LFtempM)). + Float64Column("rFtempM", validateDouble(record.RFtempM)). + Float64Column("lRtempM", validateDouble(record.LRtempM)). + Float64Column("rRtempM", validateDouble(record.RRtempM)). + At(ctx, record.TickTime.AsTime()) + } +} + +// BenchmarkBatchSerialization benchmarks processing different batch sizes +func BenchmarkBatchSerialization(b *testing.B) { + benchmarks := []struct { + name string + count int + }{ + {"100records", 100}, + {"1000records", 1000}, + {"5000records", 5000}, + {"10000records", 10000}, + {"25000records", 25000}, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + records := generateTelemetryRecords(bm.count) + sender := &MockLineSender{} + ctx := context.Background() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + for _, record := range records { + sender.Table("TelemetryTicks"). + Symbol("session_id", sanitise(record.SessionId)). + Symbol("track_name", sanitise(record.TrackName)). + Symbol("track_id", sanitise(record.TrackId)). + Symbol("lap_id", sanitise(record.LapId)). + Symbol("session_num", sanitise(record.SessionNum)). + Symbol("session_type", sanitise(record.SessionType)). + Symbol("session_name", sanitise(record.SessionName)). + StringColumn("car_id", sanitise(record.CarId)). + Int64Column("gear", validateInt(record.Gear)). + Int64Column("player_car_position", validateInt(record.PlayerCarPosition)). + Float64Column("speed", validateDouble(record.Speed)). + Float64Column("lap_dist_pct", validateDouble(record.LapDistPct)). + Float64Column("session_time", validateDouble(record.SessionTime)). + At(ctx, record.TickTime.AsTime()) + } + sender.Flush(ctx) + } + }) + } +} + +// Helper functions +func generateTelemetryRecords(count int) []*messaging.Telemetry { + records := make([]*messaging.Telemetry, count) + for i := 0; i < count; i++ { + records[i] = generateTelemetryRecord("session-123", "Spa-Francorchamps") + } + return records +} + +func generateTelemetryRecord(sessionID, trackName string) *messaging.Telemetry { + now := time.Now() + return &messaging.Telemetry{ + SessionId: sessionID, + TrackName: trackName, + TrackId: "14", + LapId: "lap-1", + SessionNum: "0", + SessionType: "Race", + SessionName: "Feature Race", + CarId: "mercedes_amg_gt3", + Speed: 150.5, + LapDistPct: 0.45, + SessionTime: 123.45, + Lat: 50.4372, + Lon: 5.9714, + Gear: 4, + PlayerCarPosition: 3, + Throttle: 0.85, + Brake: 0.0, + SteeringWheelAngle: -0.15, + Rpm: 7500.0, + VelocityX: 25.5, + VelocityY: 0.5, + VelocityZ: 35.2, + FuelLevel: 45.5, + Alt: 123.4, + LatAccel: 1.2, + LongAccel: 0.8, + VertAccel: 0.1, + Pitch: 0.05, + Roll: -0.02, + Yaw: 1.57, + YawNorth: 3.14, + Voltage: 13.8, + WaterTemp: 85.5, + LapCurrentLapTime: 95.234, + LapLastLapTime: 94.567, + LapDeltaToBestLap: 0.667, + LFpressure: 28.5, + RFpressure: 28.6, + LRpressure: 27.8, + RRpressure: 27.9, + LFtempM: 85.2, + RFtempM: 86.1, + LRtempM: 84.5, + RRtempM: 85.0, + TickTime: timestamppb.New(now), + } +} diff --git a/telemetryService/golang/internal/persistance/queries.go b/telemetryService/golang/internal/persistance/queries.go new file mode 100644 index 0000000..6ff889c --- /dev/null +++ b/telemetryService/golang/internal/persistance/queries.go @@ -0,0 +1,91 @@ +package persistance + +import ( + "context" + "fmt" + + "github.com/ojparkinson/telemetryService/internal/config" + "github.com/ojparkinson/telemetryService/internal/messaging" +) + +type QueryExecutor struct { + Config *config.Config +} + +func (s *QueryExecutor) QuerySession(ctx context.Context, sessionID string) ([]map[string]interface{}, error) { + query := ` + SELECT DISTINCT session_id, track_name, session_name, + MAX(lap_id) as max_lap_id, + MAX(timestamp) as last_updated + FROM TelemetryTicks + WHERE session_name = 'RACE' AND lap_id > 0 + GROUP BY session_id, track_name, session_name + ORDER BY last_updated DESC + ` + return ExecuteSelectQuery(query, s.Config) +} + +func (s *QueryExecutor) QuerySessions() ([]map[string]interface{}, error) { + query := ` + SELECT DISTINCT session_id, track_name, session_name, + MAX(lap_id) as max_lap_id, + MAX(timestamp) as last_updated + FROM TelemetryTicks + WHERE session_name = 'RACE' AND lap_id > 0 + GROUP BY session_id, track_name, session_name + ORDER BY last_updated DESC + ` + return ExecuteSelectQuery(query, s.Config) +} + +func (s *QueryExecutor) QueryLaps(ctx context.Context, sessionID string) ([]map[string]interface{}, error) { + query := fmt.Sprintf(` + SELECT DISTINCT lap_id + FROM TelemetryTicks + WHERE session_id = %s + ORDER BY lap_id ASC + `, sessionID) + + return ExecuteSelectQuery(query, s.Config) +} + +func (s *QueryExecutor) QueryLap(ctx context.Context, sessionID string, lapID string) ([]messaging.Telemetry, error) { + query := fmt.Sprintf(` + SELECT * FROM TelemetryTicks + WHERE session_name = 'RACE' AND session_id = '%s' AND lap_id = '%s' + ORDER BY timestamp ASC + `, sessionID, lapID) + + rows, err := ExecuteSelectQuery(query, s.Config) + if err != nil { + return nil, err + } + + points := make([]messaging.Telemetry, len(rows)) + for i, row := range rows { + points[i] = mapToTelemetry(row) + } + + return points, nil +} + +func (s *QueryExecutor) QueryGeneralLap(ctx context.Context, sessionID string, lapID string) ([]messaging.Telemetry, error) { + query := fmt.Sprintf(` + SELECT * FROM TelemetryTicks + WHERE session_id = %s + WHERE lap_id = %s + ORDER BY timestamp ASC + `, sessionID, lapID) + + rows, err := ExecuteSelectQuery(query, s.Config) + if err != nil { + return nil, err + } + + points := make([]messaging.Telemetry, len(rows)) + for i, row := range rows { + points[i] = mapToTelemetry(row) + } + + return points, nil +} diff --git a/telemetryService/golang/internal/persistance/schema.go b/telemetryService/golang/internal/persistance/schema.go new file mode 100644 index 0000000..d4e7563 --- /dev/null +++ b/telemetryService/golang/internal/persistance/schema.go @@ -0,0 +1,155 @@ +package persistance + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/ojparkinson/telemetryService/internal/config" +) + +type Schema struct { + config *config.Config +} + +func NewSchema(config *config.Config) *Schema { + return &Schema{ + config: config, + } +} + +func (s *Schema) CreateTableHTTP() error { + sql := ` + CREATE TABLE IF NOT EXISTS TelemetryTicks ( + session_id SYMBOL CAPACITY 50000 INDEX, + track_name SYMBOL CAPACITY 100 INDEX, + track_id SYMBOL CAPACITY 100 INDEX, + lap_id SYMBOL CAPACITY 500, + session_num SYMBOL CAPACITY 20, + session_type SYMBOL CAPACITY 10 INDEX, + session_name SYMBOL CAPACITY 50 INDEX, + car_id SYMBOL CAPACITY 1000 INDEX, + gear INT, + player_car_position INT, + speed DOUBLE, + lap_dist_pct DOUBLE, + session_time DOUBLE, + lat DOUBLE, + lon DOUBLE, + lap_current_lap_time DOUBLE, + lapLastLapTime DOUBLE, + lapDeltaToBestLap DOUBLE, + throttle DOUBLE, + brake DOUBLE, + steering_wheel_angle DOUBLE, + rpm DOUBLE, + velocity_x DOUBLE, + velocity_y DOUBLE, + velocity_z DOUBLE, + fuel_level DOUBLE, + alt DOUBLE, + lat_accel DOUBLE, + long_accel DOUBLE, + vert_accel DOUBLE, + pitch DOUBLE, + roll DOUBLE, + yaw DOUBLE, + yaw_north DOUBLE, + voltage DOUBLE, + waterTemp DOUBLE, + lFpressure DOUBLE, + rFpressure DOUBLE, + lRpressure DOUBLE, + rRpressure DOUBLE, + lFtempM DOUBLE, + rFtempM DOUBLE, + lRtempM DOUBLE, + rRtempM DOUBLE, + timestamp TIMESTAMP + ) TIMESTAMP(timestamp) PARTITION BY DAY + WAL + WITH maxUncommittedRows=1000000 + DEDUP UPSERT KEYS(timestamp, session_id); + ` + _, err := ExecuteSelectQuery(sql, s.config) + return err +} + +func (s *Schema) AddIndexes() error { + indexes := []string{ + "ALTER TABLE TelemetryTicks ADD INDEX session_lap_idx (session_id, lap_id);", + "ALTER TABLE TelemetryTicks ADD INDEX track_session_idx (track_name, session_id);", + "ALTER TABLE TelemetryTicks ADD INDEX session_time_idx (session_id, session_time);", + } + + for _, idx := range indexes { + if _, err := ExecuteSelectQuery(idx, s.config); err != nil { + return fmt.Errorf("failed to create index: %w", err) + } + } + + return nil +} + +func ExecuteSelectQuery(query string, config *config.Config) ([]map[string]interface{}, error) { + maxRetries := 3 + baseDelay := 500 * time.Millisecond + + for attempt := 0; attempt < maxRetries; attempt++ { + resp, err := http.Get( + fmt.Sprintf("http://%s:%d/exec?query=%s", + config.QuestDbHost, + config.QuestDBPort, + url.QueryEscape(query)), + ) + + if err != nil { + if attempt < maxRetries-1 { + delay := baseDelay * time.Duration(1< OptimizeSchema() - { - try - { - var result = await _questDbService.TriggerSchemaOptimization(); - - if (result) - { - return Ok(new - { - message = "Schema optimization completed successfully", - timestamp = DateTime.UtcNow, - optimized = true - }); - } - else - { - return BadRequest(new - { - message = "Schema optimization failed or was not needed", - timestamp = DateTime.UtcNow, - optimized = false - }); - } - } - catch (Exception ex) - { - return StatusCode(500, new - { - message = "Schema optimization error", - error = ex.Message, - timestamp = DateTime.UtcNow - }); - } - } } diff --git a/telemetryService/telemetryService/src/TelemetryService.API/Program.cs b/telemetryService/telemetryService/src/TelemetryService.API/Program.cs index 3193a50..2cc5d78 100644 --- a/telemetryService/telemetryService/src/TelemetryService.API/Program.cs +++ b/telemetryService/telemetryService/src/TelemetryService.API/Program.cs @@ -27,7 +27,6 @@ }); // Register telemetry services -builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); diff --git a/telemetryService/telemetryService/src/TelemetryService.Domain/telemetryTick.proto b/telemetryService/telemetryService/src/TelemetryService.Domain/telemetryTick.proto index aab4ffd..47abb13 100644 --- a/telemetryService/telemetryService/src/TelemetryService.Domain/telemetryTick.proto +++ b/telemetryService/telemetryService/src/TelemetryService.Domain/telemetryTick.proto @@ -19,7 +19,7 @@ message Telemetry { string track_id = 11; uint32 worker_id = 12; double steering_wheel_angle = 13; - double player_car_position = 14; + int player_car_position = 14; double velocity_x = 15; double velocity_y = 16; double velocity_z = 17; diff --git a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Configuration/DotEnv.cs b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Configuration/DotEnv.cs index 664dca1..d023a34 100644 --- a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Configuration/DotEnv.cs +++ b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Configuration/DotEnv.cs @@ -9,9 +9,7 @@ public static void Load(string filePath) foreach (var line in File.ReadAllLines(filePath)) { - var parts = line.Split( - '=', - StringSplitOptions.RemoveEmptyEntries); + var parts = line.Split('=', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) continue; diff --git a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Messaging/Subscriber.cs b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Messaging/Subscriber.cs index be360b4..16377d0 100644 --- a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Messaging/Subscriber.cs +++ b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Messaging/Subscriber.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Buffers; +using System.Threading.Channels; using RabbitMQ.Client; using RabbitMQ.Client.Events; using TelemetryService.Domain.Models; @@ -11,11 +12,13 @@ public class Subscriber { private const int MaxRetryAttempts = 10; private const int RetryDelayMs = 5000; - private readonly QuestDbService _questDbService; - public Subscriber(QuestDbService questDbService) + private volatile bool _stopRequested = false; + + private readonly Channel<(TelemetryBatch batch, ulong deliveryTag, IChannel channel)> _messageChannel = Channel.CreateUnbounded<(TelemetryBatch, ulong, IChannel)>(); + + public Subscriber() { - _questDbService = questDbService; } public async Task SubscribeAsync() @@ -44,17 +47,11 @@ public async Task SubscribeAsync() private async Task ConnectAndSubscribeAsync() { - var factory = new ConnectionFactory - { - // Potentially need to use localhost when running locally - HostName = "rabbitmq", - Port = 5672, - UserName = "guest", - Password = "guest", - RequestedHeartbeat = TimeSpan.FromSeconds(30), - AutomaticRecoveryEnabled = true, - NetworkRecoveryInterval = TimeSpan.FromSeconds(10) - }; + var factory = new ConnectionFactory(); + factory.Uri = new Uri("amqp://admin:changeme@rabbitmq:5672/"); + factory.RequestedHeartbeat = TimeSpan.FromSeconds(60); + factory.AutomaticRecoveryEnabled = true; + factory.NetworkRecoveryInterval = TimeSpan.FromSeconds(10); Console.WriteLine($"Connecting to RabbitMQ at {factory.HostName}:{factory.Port} with user {factory.UserName}"); @@ -62,56 +59,210 @@ private async Task ConnectAndSubscribeAsync() Console.WriteLine("Successfully connected to RabbitMQ!"); using var channel = await connection.CreateChannelAsync(); - await channel.BasicQosAsync(0, 1000, false); - Console.WriteLine("Channel created successfully"); - - await channel.ExchangeDeclareAsync( - "telemetry_topic", - ExchangeType.Topic, - true, - false); - Console.WriteLine("Exchange declared successfully"); - await channel.QueueDeclareAsync( - "telemetry_queue", - true, - false, - false); - Console.WriteLine("Queue declared successfully"); + await channel.BasicQosAsync(0, 5000, false); await channel.QueueBindAsync( "telemetry_queue", "telemetry_topic", "telemetry.ticks"); + Console.WriteLine("Queue bound to exchange with routing key telemetry.ticks"); - Console.WriteLine("Waiting for messages..."); + Console.WriteLine("πŸ”„ Starting push-based message consumption with batching..."); + + await StartPushBasedConsumption(channel); + } + + private async Task StartPushBasedConsumption(IChannel channel) + { var consumer = new AsyncEventingBasicConsumer(channel); - consumer.ReceivedAsync += async (_, ea) => + var messagesProcessed = 0; + var lastStatsTime = DateTime.UtcNow; + + Console.WriteLine("πŸ“₯ Ready to consume messages from queue..."); + + consumer.ReceivedAsync += async (sender, ea) => { + if (_stopRequested) return; + + + var bodyLength = ea.Body.Length; + var buffer = ArrayPool.Shared.Rent(bodyLength); try { - var body = ea.Body.ToArray(); - var message = TelemetryBatch.Parser.ParseFrom(body); - - var questTask = _questDbService.WriteBatch(message); - - await Task.WhenAll(questTask); + ea.Body.CopyTo(buffer); + var message = TelemetryBatch.Parser.ParseFrom(buffer, 0, bodyLength); + + await _messageChannel.Writer.WriteAsync((message, ea.DeliveryTag, channel)); + + Interlocked.Increment(ref messagesProcessed); } - catch (Exception ex) + finally { - Console.WriteLine($"Error processing message: {ex.Message}"); + ArrayPool.Shared.Return(buffer); } }; - await channel.BasicConsumeAsync( - "telemetry_queue", - true, - consumer); + await channel.BasicConsumeAsync(queue: "telemetry_queue", autoAck: false, consumer: consumer); + + _ = Task.Run(() => ProcessBatchMessages()); + while (!_stopRequested) + { + await Task.Delay(5000); // Log every 5 seconds + messagesProcessed = 0; + lastStatsTime = DateTime.UtcNow; + } + } - Console.WriteLine("Consumer registered. Waiting for messages..."); + private async Task ProcessBatchMessages() + { + const int targetBatchSize = 20; + const int maxRecordsPerBatch = 25000; + const int batchTimeoutMs = 5000; - await Task.Delay(Timeout.Infinite); + var batchBuffer = new List<(TelemetryBatch batch, ulong deliveryTag, IChannel channel)>(targetBatchSize); + (TelemetryBatch batch, ulong deliveryTag, IChannel channel)? pendingItem = null; + + Console.WriteLine($"πŸ“¦ Batch processor started (target: {targetBatchSize} msgs, max: {maxRecordsPerBatch} records, timeout: {batchTimeoutMs}ms)"); + + try + { + while (!_stopRequested) + { + var deadline = DateTime.UtcNow.AddMilliseconds(batchTimeoutMs); + + // Add pending item from previous batch if exists + if (pendingItem.HasValue) + { + batchBuffer.Add(pendingItem.Value); + pendingItem = null; + } + + while (batchBuffer.Count < targetBatchSize && DateTime.UtcNow < deadline) + { + var remaining = (deadline - DateTime.UtcNow).TotalMilliseconds; + if (remaining <= 0) break; + + var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(remaining)); + try + { + if (await _messageChannel.Reader.WaitToReadAsync(cts.Token)) + { + while (_messageChannel.Reader.TryRead(out var item)) + { + // Check if adding this item would exceed record limit + var currentRecordCount = batchBuffer.Sum(b => b.batch.Records.Count); + var potentialRecordCount = currentRecordCount + item.batch.Records.Count; + + if (potentialRecordCount > maxRecordsPerBatch && batchBuffer.Count > 0) + { + // Would exceed limit - save for next batch + Console.WriteLine($" Record limit would be exceeded: {currentRecordCount} + {item.batch.Records.Count} > {maxRecordsPerBatch}"); + pendingItem = item; + break; + } + + batchBuffer.Add(item); + + if (batchBuffer.Count >= targetBatchSize) + { + break; + } + } + } + } + catch (OperationCanceledException) + { + break; + } + + // Break if we have a pending item (batch is full) + if (pendingItem.HasValue) + { + break; + } + } + + + if (batchBuffer.Count > 0) + { + try + { + var allRecords = batchBuffer + .SelectMany(b => b.batch.Records) + .Where(QuestDbService.IsValidRecord) + .ToList(); + + if (allRecords.Any()) + { + try + { + await QuestDbService.WriteBatch(allRecords); + + Console.WriteLine($"πŸ“¦ Flushed {batchBuffer.Count} messages ({allRecords.Count} records)"); + + // Try to ACK with timeout protection + try + { + using var ackCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + foreach (var item in batchBuffer) + { + await item.channel.BasicAckAsync(item.deliveryTag, false, ackCts.Token); + } + } + catch (OperationCanceledException) + { + Console.WriteLine($"⚠️ ACK timeout - channel may be slow"); + } + catch (Exception ackEx) + { + Console.WriteLine($"⚠️ Could not ACK messages (channel closed): {ackEx.Message}"); + Console.WriteLine($" Messages will be re-delivered by RabbitMQ"); + } + } + catch (Exception ex) + { + Console.WriteLine($"❌ Batch write failed: {ex.Message}"); + + // Try to NACK with timeout protection + try + { + using var nackCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + foreach (var item in batchBuffer) + { + await item.channel.BasicNackAsync(item.deliveryTag, false, true, nackCts.Token); + } + } + catch (OperationCanceledException) + { + Console.WriteLine($"⚠️ NACK timeout - channel may be slow"); + } + catch (Exception nackEx) + { + Console.WriteLine($"⚠️ Could not NACK messages (channel closed): {nackEx.Message}"); + Console.WriteLine($" Messages will be re-delivered by RabbitMQ"); + } + } + } + + } + finally + { + batchBuffer.Clear(); + } + } + } + } + finally + { + Console.WriteLine("πŸ“¦ Batch processor stopped"); + } + } + + public void Dispose() + { + _stopRequested = true; } } \ No newline at end of file diff --git a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbPool.cs b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbPool.cs new file mode 100644 index 0000000..d8b138e --- /dev/null +++ b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbPool.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.ObjectPool; +using QuestDB; +using QuestDB.Senders; + +namespace TelemetryService.Infrastructure.Persistence; + +public class QuestDbSenderPool +{ + // Pool of 8 HTTP senders (allows 4 parallel + 4 spare for rotation) + private static readonly ObjectPool _pool = new DefaultObjectPool(new QuestDbSenderPooledObjectPolicy(), maximumRetained: 8); + + public static ISender Get() => _pool.Get(); + public static void Return(ISender sender) => _pool.Return(sender); + +} + +public class QuestDbSenderPooledObjectPolicy : IPooledObjectPolicy +{ + public ISender Create() + { + var host = Environment.GetEnvironmentVariable("QUESTDB_HTTP_HOST") ?? "questdb"; + var port = int.TryParse(Environment.GetEnvironmentVariable("QUESTDB_HTTP_PORT"), out var p) ? p : 9000; + + return Sender.New($"http::addr={host}:{port};auto_flush_rows=10000;request_timeout=60000;"); + } + + public bool Return(ISender obj) => true; +} diff --git a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbSchemaManager.cs b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbSchemaManager.cs index eddda10..d94726d 100644 --- a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbSchemaManager.cs +++ b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbSchemaManager.cs @@ -4,15 +4,19 @@ namespace TelemetryService.Infrastructure.Persistence; -public class QuestDbSchemaManager +public class QuestDbSchemaManager : IDisposable { private readonly string _questDbUrl; private readonly HttpClient _httpClient; + private volatile bool _disposed = false; public QuestDbSchemaManager(string questDbUrl) { _questDbUrl = questDbUrl.Replace("http://", "").Replace("https://", ""); - _httpClient = new HttpClient(); + _httpClient = new HttpClient() + { + Timeout = TimeSpan.FromSeconds(60) + }; } public async Task EnsureOptimizedSchemaExists() @@ -20,9 +24,15 @@ public async Task EnsureOptimizedSchemaExists() try { Console.WriteLine("πŸ”§ Checking QuestDB schema optimization..."); - + + // Wait for QuestDB to be ready + await WaitForQuestDbReady(); + + // Clean up any orphaned tables first + await CleanupOrphanedTables(); + var tableInfo = await GetTableInfo("TelemetryTicks"); - + if (tableInfo == null) { Console.WriteLine("πŸ“‹ No TelemetryTicks table found, creating optimized version..."); @@ -31,30 +41,8 @@ public async Task EnsureOptimizedSchemaExists() Console.WriteLine("βœ… Optimized TelemetryTicks table created successfully"); return true; } - - var needsOptimization = await NeedsOptimization(tableInfo); - var needsIndexes = await NeedsCriticalIndexes(); - - if (needsOptimization) - { - Console.WriteLine("πŸ”„ Existing table detected, applying optimizations..."); - await OptimizeExistingTable(); - Console.WriteLine("βœ… Table optimization completed"); - return true; - } - else if (needsIndexes) - { - Console.WriteLine("πŸ“Š Adding missing performance indexes..."); - await AddEssentialIndexes(); - Console.WriteLine("βœ… Performance indexes added"); - return true; - } - else - { - Console.WriteLine("βœ… TelemetryTicks table is already optimized"); - await LogCurrentTableStats(); - return true; - } + + return true; } catch (Exception ex) { @@ -74,45 +62,18 @@ public async Task EnsureOptimizedSchemaExists() } catch { - // Table doesn't exist return null; } } - private async Task NeedsOptimization(JsonElement? tableInfo) - { - if (!tableInfo.HasValue || !tableInfo.Value.TryGetProperty("dataset", out var dataset)) - return false; - - foreach (var row in dataset.EnumerateArray()) - { - var rowArray = row.EnumerateArray().ToArray(); - if (rowArray.Length >= 2) - { - var columnName = rowArray[0].GetString(); - var columnType = rowArray[1].GetString(); - - if (columnName == "gear" && columnType == "SYMBOL") - { - return true; - } - } - } - - var hasSessionIndex = await HasIndex("session_id"); - var hasTrackIndex = await HasIndex("track_name"); - - return !hasSessionIndex || !hasTrackIndex; - } - private async Task HasIndex(string columnName) { try { var query = $"SELECT indexed FROM table_columns('TelemetryTicks') WHERE column = '{columnName}'"; var response = await ExecuteQuery(query); - - if (response?.TryGetProperty("dataset", out var dataset) == true && + + if (response?.TryGetProperty("dataset", out var dataset) == true && dataset.EnumerateArray().Any()) { var firstRow = dataset.EnumerateArray().First(); @@ -124,89 +85,24 @@ private async Task HasIndex(string columnName) { // Assume no index if query fails } - - return false; - } - - private async Task NeedsCriticalIndexes() - { - try - { - // Check if essential indexes exist - var hasSessionIndex = await HasIndex("session_id"); - var hasTrackIndex = await HasIndex("track_name"); - - return !hasSessionIndex || !hasTrackIndex; - } - catch - { - return true; - } - } - - private async Task AddEssentialIndexes() - { - try - { - var hasSessionIndex = await HasIndex("session_id"); - var hasTrackIndex = await HasIndex("track_name"); - var hasTrackIdIndex = await HasIndex("track_id"); - if (!hasSessionIndex) - { - Console.WriteLine(" Adding session_id index..."); - await ExecuteQuery("ALTER TABLE TelemetryTicks ALTER COLUMN session_id ADD INDEX"); - } - - if (!hasTrackIndex) - { - Console.WriteLine(" Adding track_name index..."); - await ExecuteQuery("ALTER TABLE TelemetryTicks ALTER COLUMN track_name ADD INDEX"); - } - - if (!hasTrackIdIndex) - { - Console.WriteLine(" Adding track_id index..."); - await ExecuteQuery("ALTER TABLE TelemetryTicks ALTER COLUMN track_id ADD INDEX"); - } - } - catch (Exception ex) - { - Console.WriteLine($" Warning: Could not add some indexes: {ex.Message}"); - } - } - - private async Task LogCurrentTableStats() - { - try - { - var stats = await GetTableStats(); - Console.WriteLine("πŸ“Š TelemetryTicks table stats:"); - foreach (var stat in stats) - { - Console.WriteLine($" {stat.Key}: {stat.Value}"); - } - } - catch (Exception ex) - { - Console.WriteLine($" Could not retrieve table stats: {ex.Message}"); - } + return false; } private async Task CreateOptimizedTable() { var createTableSql = @" - CREATE TABLE TelemetryTicks ( + CREATE TABLE IF NOT EXISTS TelemetryTicks ( session_id SYMBOL CAPACITY 50000 INDEX, track_name SYMBOL CAPACITY 100 INDEX, track_id SYMBOL CAPACITY 100 INDEX, lap_id SYMBOL CAPACITY 500, session_num SYMBOL CAPACITY 20, - session_type SYMBOL CAPACITY 10, - session_name SYMBOL CAPACITY 50, - car_id VARCHAR, + session_type SYMBOL CAPACITY 10 INDEX, + session_name SYMBOL CAPACITY 50 INDEX, + car_id SYMBOL CAPACITY 1000 INDEX, gear INT, - player_car_position LONG, + player_car_position INT, speed DOUBLE, lap_dist_pct DOUBLE, session_time DOUBLE, @@ -242,136 +138,25 @@ CREATE TABLE TelemetryTicks ( lRtempM FLOAT, rRtempM FLOAT, timestamp TIMESTAMP - ) TIMESTAMP(timestamp) PARTITION BY HOUR WITH maxUncommittedRows=1000000; + ) TIMESTAMP(timestamp) PARTITION BY DAY + WAL + WITH maxUncommittedRows=1000000 + DEDUP UPSERT KEYS(timestamp, session_id); "; await ExecuteQuery(createTableSql); - } - - private async Task OptimizeExistingTable() - { - var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - Console.WriteLine($"πŸ”„ Starting table optimization process..."); - - try - { - var currentStats = await GetTableStats(); - if (currentStats.TryGetValue("row_count", out var rowCountObj)) - { - Console.WriteLine($" Current data: {rowCountObj} records"); - } - - Console.WriteLine(" Creating backup of existing table..."); - await ExecuteQuery($"RENAME TABLE TelemetryTicks TO TelemetryTicks_backup_{timestamp}"); - - Console.WriteLine(" Creating optimized table structure..."); - await CreateOptimizedTable(); - - var migrationSql = $@" - INSERT INTO TelemetryTicks - SELECT - session_id, - track_name, - track_id, - lap_id, - session_num, - COALESCE(session_type, 'Unknown') as session_type, - COALESCE(session_name, 'Unknown') as session_name, - car_id, - CASE - WHEN gear = '1' THEN 1 - WHEN gear = '2' THEN 2 - WHEN gear = '3' THEN 3 - WHEN gear = '4' THEN 4 - WHEN gear = '5' THEN 5 - WHEN gear = '6' THEN 6 - WHEN gear = '7' THEN 7 - WHEN gear = '8' THEN 8 - WHEN gear = 'R' THEN -1 - WHEN gear = 'N' THEN 0 - ELSE 0 - END as gear, - player_car_position, - speed, - lap_dist_pct, - session_time, - lat, - lon, - lap_current_lap_time, - lapLastLapTime, - lapDeltaToBestLap, - cast(throttle as FLOAT) as throttle, - cast(brake as FLOAT) as brake, - cast(steering_wheel_angle as FLOAT) as steering_wheel_angle, - cast(rpm as FLOAT) as rpm, - cast(velocity_x as FLOAT) as velocity_x, - cast(velocity_y as FLOAT) as velocity_y, - cast(velocity_z as FLOAT) as velocity_z, - cast(fuel_level as FLOAT) as fuel_level, - cast(alt as FLOAT) as alt, - cast(lat_accel as FLOAT) as lat_accel, - cast(long_accel as FLOAT) as long_accel, - cast(vert_accel as FLOAT) as vert_accel, - cast(pitch as FLOAT) as pitch, - cast(roll as FLOAT) as roll, - cast(yaw as FLOAT) as yaw, - cast(yaw_north as FLOAT) as yaw_north, - cast(voltage as FLOAT) as voltage, - cast(waterTemp as FLOAT) as waterTemp, - cast(lFpressure as FLOAT) as lFpressure, - cast(rFpressure as FLOAT) as rFpressure, - cast(lRpressure as FLOAT) as lRpressure, - cast(rRpressure as FLOAT) as rRpressure, - cast(lFtempM as FLOAT) as lFtempM, - cast(rFtempM as FLOAT) as rFtempM, - cast(lRtempM as FLOAT) as lRtempM, - cast(rRtempM as FLOAT) as rRtempM, - timestamp - FROM TelemetryTicks_backup_{timestamp}; - "; - - await ExecuteQuery(migrationSql); - - await AddOptimizedIndexes(); - - var newStats = await GetTableStats(); - if (newStats.TryGetValue("row_count", out var newRowCountObj)) - { - Console.WriteLine($" Migration verified: {newRowCountObj} records in optimized table"); - } - - } - catch (Exception ex) - { - Console.WriteLine($"❌ Migration failed: {ex.Message}"); - - // Attempt rollback - try - { - Console.WriteLine("πŸ”„ Attempting rollback..."); - await ExecuteQuery("DROP TABLE TelemetryTicks"); - await ExecuteQuery($"RENAME TABLE TelemetryTicks_backup_{timestamp} TO TelemetryTicks"); - Console.WriteLine("βœ… Rollback successful, original table restored"); - } - catch (Exception rollbackEx) - { - Console.WriteLine($"❌ Rollback failed: {rollbackEx.Message}"); - Console.WriteLine($"⚠️ Manual intervention required. Backup table: TelemetryTicks_backup_{timestamp}"); - } - - throw; - } } - private async Task AddOptimizedIndexes() { + Console.WriteLine("βœ… Composite indexes added successfully"); + try { - await ExecuteQuery("ALTER TABLE TelemetryTicks ADD INDEX session_lap_idx (session_id, lap_id)"); - await ExecuteQuery("ALTER TABLE TelemetryTicks ADD INDEX track_session_idx (track_name, session_id)"); - await ExecuteQuery("ALTER TABLE TelemetryTicks ADD INDEX session_time_idx (session_id, session_time)"); - + await ExecuteQuery("ALTER TABLE TelemetryTicks ADD INDEX session_lap_idx (session_id, lap_id);"); + await ExecuteQuery("ALTER TABLE TelemetryTicks ADD INDEX track_session_idx (track_name, session_id);"); + await ExecuteQuery("ALTER TABLE TelemetryTicks ADD INDEX session_time_idx (session_id, session_time);"); + Console.WriteLine("βœ… Composite indexes added successfully"); } catch (Exception ex) @@ -382,20 +167,28 @@ private async Task AddOptimizedIndexes() private async Task ExecuteQuery(string query) { + if (_disposed) + { + throw new ObjectDisposedException(nameof(QuestDbSchemaManager)); + } + try { var encodedQuery = Uri.EscapeDataString(query); var url = $"http://{_questDbUrl}/exec?query={encodedQuery}"; - + + Console.WriteLine($"πŸ” DEBUG: _questDbUrl = '{_questDbUrl}'"); + Console.WriteLine($"πŸ” DEBUG: Full URL = '{url}'"); + var response = await _httpClient.GetStringAsync(url); var jsonDoc = JsonDocument.Parse(response); - + // Check for errors if (jsonDoc.RootElement.TryGetProperty("error", out var error)) { throw new Exception($"QuestDB Error: {error.GetString()}"); } - + return jsonDoc.RootElement; } catch (Exception ex) @@ -406,60 +199,124 @@ private async Task AddOptimizedIndexes() } } - public async Task> GetTableStats() + public void Dispose() + { + if (_disposed) return; + + _httpClient?.Dispose(); + _disposed = true; + + GC.SuppressFinalize(this); + } + + private async Task CleanupOrphanedTables() { try { - var stats = new Dictionary(); - - // Get basic table info from system tables (much faster than scanning data) - var tableInfoQuery = "SELECT table_name FROM tables WHERE table_name = 'TelemetryTicks'"; - var tableResponse = await ExecuteQuery(tableInfoQuery); - if (tableResponse?.TryGetProperty("dataset", out var tableDataset) == true && - tableDataset.EnumerateArray().Any()) + Console.WriteLine("🧹 Checking for orphaned tables..."); + + // Get list of all tables + var tablesQuery = "SHOW TABLES"; + var response = await ExecuteQuery(tablesQuery); + + if (response?.TryGetProperty("dataset", out var dataset) == true) { - stats["table_exists"] = true; - - // Get approximate row count from table metadata (instant) - try + var tablesToDelete = new List(); + + foreach (var row in dataset.EnumerateArray()) + { + var rowArray = row.EnumerateArray().ToArray(); + if (rowArray.Length > 0) + { + var tableName = rowArray[0].GetString(); + + // Check if this is an orphaned table (numeric names that look like session IDs) + if (!string.IsNullOrEmpty(tableName) && + tableName != "TelemetryTicks" && + IsOrphanedTable(tableName)) + { + tablesToDelete.Add(tableName); + } + } + } + + // Delete orphaned tables + foreach (var tableName in tablesToDelete) { - var quickStatsQuery = "SELECT last(timestamp) as max_time FROM TelemetryTicks LIMIT 1"; - var quickResponse = await ExecuteQuery(quickStatsQuery); - if (quickResponse?.TryGetProperty("dataset", out var quickDataset) == true && - quickDataset.EnumerateArray().Any()) + try { - var maxTime = quickDataset.EnumerateArray().First().EnumerateArray().First().GetString(); - stats["max_time"] = maxTime ?? "unknown"; - stats["status"] = "optimized"; + Console.WriteLine($" πŸ—‘οΈ Dropping orphaned table: {tableName}"); + await ExecuteQuery($"DROP TABLE {tableName}"); } - else + catch (Exception ex) { - stats["status"] = "empty"; + Console.WriteLine($" ⚠️ Could not drop table {tableName}: {ex.Message}"); } } - catch + + if (tablesToDelete.Count > 0) + { + Console.WriteLine($"βœ… Cleaned up {tablesToDelete.Count} orphaned tables"); + } + else { - // If quick stats fail, just mark as available - stats["status"] = "available"; + Console.WriteLine("βœ… No orphaned tables found"); } } - else - { - stats["table_exists"] = false; - stats["status"] = "missing"; - } - - return stats; } catch (Exception ex) { - Console.WriteLine($"Error getting table stats: {ex.Message}"); - return new Dictionary { ["error"] = ex.Message }; + Console.WriteLine($"⚠️ Could not cleanup orphaned tables: {ex.Message}"); } } - public void Dispose() + private async Task WaitForQuestDbReady() { - _httpClient?.Dispose(); + for (int i = 0; i < 10; i++) + { + try + { + var healthCheck = await ExecuteQuery("SELECT 1 as health_check"); + if (healthCheck != null) + { + Console.WriteLine("βœ… QuestDB connection verified"); + return; + } + } + catch (Exception ex) + { + Console.WriteLine($"⏳ Waiting for QuestDB to be ready (attempt {i + 1}/10): {ex.Message}"); + await Task.Delay(2000); + } + } + + throw new Exception("QuestDB is not responding after 10 attempts"); + } + + private static bool IsOrphanedTable(string tableName) + { + // Check if table name looks like a session ID (numeric) or backup table + if (string.IsNullOrEmpty(tableName)) + return false; + + // Remove known good table names + if (tableName == "TelemetryTicks") + return false; + + // Check for numeric-only names (likely session IDs) + if (long.TryParse(tableName, out _)) + { + Console.WriteLine($" Found numeric table name (likely session ID): {tableName}"); + return true; + } + + // Check for backup tables from failed migrations + if (tableName.StartsWith("TelemetryTicks_backup_")) + { + Console.WriteLine($" Found backup table: {tableName}"); + return true; + } + + return false; } } \ No newline at end of file diff --git a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbService.cs b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbService.cs new file mode 100644 index 0000000..9a23c83 --- /dev/null +++ b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestDbService.cs @@ -0,0 +1,228 @@ +using QuestDB.Senders; +using TelemetryService.Domain.Models; + +namespace TelemetryService.Infrastructure.Persistence; + +public static class QuestDbService +{ + private const int PartitionCount = 4; + private const int MaxRetries = 3; + + public static async Task WriteBatch(List records, string tableName = "TelemetryTicks") + { + if (records == null || !records.Any()) + { + Console.WriteLine("⚠️ Empty batch received, skipping"); + return; + } + + var retryCount = 0; + + while (retryCount <= MaxRetries) + { + try + { + await WritePartitionedBatch(records, tableName); + return; + } + catch (Exception ex) when (IsRetryableError(ex) && retryCount < MaxRetries) + { + retryCount++; + var delay = Math.Min(1000 * retryCount, 5000); + Console.WriteLine($"⚠️ Attempt {retryCount} failed, retrying in {delay}ms: {ex.Message}"); + await Task.Delay(delay); + } + } + + Console.WriteLine($"❌ Failed to write batch after {MaxRetries + 1} attempts"); + throw new InvalidOperationException($"Failed to write batch after {MaxRetries + 1} attempts"); + } + + private static async Task WritePartitionedBatch(List records, string tableName) + { + + return; + // Partition records by session_id hash for consistent routing (dedup safety) + var partitions = records + .GroupBy(r => Math.Abs((r.SessionId ?? "unknown").GetHashCode()) % PartitionCount) + .ToList(); + + Console.WriteLine($"πŸ“Š Partitioned {records.Count} records into {partitions.Count} buckets"); + + var senders = new List(PartitionCount); + try + { + for (int i = 0; i < PartitionCount; i++) + { + senders.Add(QuestDbSenderPool.Get()); + } + + var writeTasks = partitions.Select(async (partition, index) => + { + var partitionRecords = partition.ToList(); + var sender = senders[index % senders.Count]; + + try + { + await WritePartition(sender, partitionRecords, tableName, index); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Partition {index} failed: {ex.Message}"); + throw; + } + }); + + await Task.WhenAll(writeTasks); + + Console.WriteLine($"βœ… Successfully wrote {records.Count} records across {partitions.Count} partitions"); + } + finally + { + // Return all senders to pool + foreach (var sender in senders) + { + QuestDbSenderPool.Return(sender); + } + } + } + + private static async Task WritePartition(ISender sender, List records, string tableName, int partitionIndex) + { + var validRecords = records.Where(IsValidRecord).ToList(); + + if (!validRecords.Any()) + { + return; + } + + foreach (var record in validRecords) + { + sender.Table(tableName) + .Symbol("session_id", Sanitize(record.SessionId)) + .Symbol("track_name", Sanitize(record.TrackName)) + .Symbol("track_id", Sanitize(record.TrackId)) + .Symbol("lap_id", Sanitize(record.LapId)) + .Symbol("session_num", Sanitize(record.SessionNum)) + .Symbol("session_type", Sanitize(record.SessionType)) + .Symbol("session_name", Sanitize(record.SessionName)) + + .Column("car_id", Sanitize(record.CarId)) + .Column("gear", ValidateInt(record.Gear)) + .Column("player_car_position", Math.Max(0, (long)record.PlayerCarPosition)) + + .Column("speed", ValidateDouble(record.Speed)) + .Column("lap_dist_pct", ValidateDouble(record.LapDistPct)) + .Column("session_time", ValidateDouble(record.SessionTime)) + .Column("lat", ValidateDouble(record.Lat)) + .Column("lon", ValidateDouble(record.Lon)) + .Column("lap_current_lap_time", ValidateDouble(record.LapCurrentLapTime)) + .Column("lapLastLapTime", ValidateDouble(record.LapLastLapTime)) + .Column("lapDeltaToBestLap", ValidateDouble(record.LapDeltaToBestLap)) + + .Column("throttle", (float)ValidateDouble(record.Throttle)) + .Column("brake", (float)ValidateDouble(record.Brake)) + .Column("steering_wheel_angle", (float)ValidateDouble(record.SteeringWheelAngle)) + .Column("rpm", (float)ValidateDouble(record.Rpm)) + .Column("velocity_x", (float)ValidateDouble(record.VelocityX)) + .Column("velocity_y", (float)ValidateDouble(record.VelocityY)) + .Column("velocity_z", (float)ValidateDouble(record.VelocityZ)) + .Column("fuel_level", (float)ValidateDouble(record.FuelLevel)) + .Column("alt", (float)ValidateDouble(record.Alt)) + .Column("lat_accel", (float)ValidateDouble(record.LatAccel)) + .Column("long_accel", (float)ValidateDouble(record.LongAccel)) + .Column("vert_accel", (float)ValidateDouble(record.VertAccel)) + .Column("pitch", (float)ValidateDouble(record.Pitch)) + .Column("roll", (float)ValidateDouble(record.Roll)) + .Column("yaw", (float)ValidateDouble(record.Yaw)) + .Column("yaw_north", (float)ValidateDouble(record.YawNorth)) + .Column("voltage", (float)ValidateDouble(record.Voltage)) + .Column("waterTemp", (float)ValidateDouble(record.WaterTemp)) + .Column("lFpressure", (float)ValidateDouble(record.LFpressure)) + .Column("rFpressure", (float)ValidateDouble(record.RFpressure)) + .Column("lRpressure", (float)ValidateDouble(record.LRpressure)) + .Column("rRpressure", (float)ValidateDouble(record.RRpressure)) + .Column("lFtempM", (float)ValidateDouble(record.LFtempM)) + .Column("rFtempM", (float)ValidateDouble(record.RFtempM)) + .Column("lRtempM", (float)ValidateDouble(record.LRtempM)) + .Column("rRtempM", (float)ValidateDouble(record.RRtempM)) + .At(record.TickTime.ToDateTime()); + } + + await sender.SendAsync(); + + Console.WriteLine($" πŸ“¦ Partition {partitionIndex}: {validRecords.Count} records"); + } + + #region Validation Helpers + + private static bool IsRetryableError(Exception ex) + { + var message = ex.Message?.ToLower() ?? ""; + var innerMessage = ex.InnerException?.Message?.ToLower() ?? ""; + + return ex is IOException || + message.Contains("socket") || + message.Contains("connection reset") || + message.Contains("could not write data") || + message.Contains("transport connection") || + innerMessage.Contains("connection reset") || + innerMessage.Contains("transport connection"); + } + + private static string Sanitize(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "unknown"; + + var length = value.Length; + Span buffer = length <= 256 ? stackalloc char[length] : new char[length]; + + value.AsSpan().CopyTo(buffer); + + for (int i = 0; i < buffer.Length; i++) + { + switch (buffer[i]) + { + case ',': + case ' ': + case '=': + case '\n': + case '\r': + case '"': + case '\'': + case '\\': + buffer[i] = '_'; + break; + } + } + + var trimmed = buffer.Trim(); + return new string(trimmed); + } + + private static double ValidateDouble(double value) + { + return double.IsNaN(value) || double.IsInfinity(value) || + value == double.MinValue || value == double.MaxValue ? 0.0 : value; + } + + private static int ValidateInt(uint value) + { + // Handle iRacing's invalid sentinel value (4294967295 = uint.MaxValue) + // QuestDB INT is signed 32-bit with max value 2,147,483,647 + if (value == uint.MaxValue || value > int.MaxValue) + { + return 0; + } + return (int)value; + } + + public static bool IsValidRecord(Telemetry record) + { + return !string.IsNullOrWhiteSpace(record.SessionId) || + !string.IsNullOrWhiteSpace(record.TrackName); + } + + #endregion +} diff --git a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestService.cs b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestService.cs deleted file mode 100644 index 1a0652f..0000000 --- a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/Persistance/QuestService.cs +++ /dev/null @@ -1,179 +0,0 @@ -using QuestDB; -using QuestDB.Senders; -using TelemetryService.Domain.Models; - -namespace TelemetryService.Infrastructure.Persistence; - -public class QuestDbService -{ - private ISender? _sender; - private QuestDbSchemaManager? _schemaManager; - - public QuestDbService() - { - string? url = Environment.GetEnvironmentVariable("QUESTDB_URL"); - - if (url == null) - { - return; - } - - _schemaManager = new QuestDbSchemaManager(url); - - _sender = Sender.New($"http::addr={url.Replace("http://", "")};auto_flush_rows=10000;auto_flush_interval=1000;"); - - _ = Task.Run(async () => - { - await Task.Delay(3000); - await InitializeSchema(); - }); - } - - private async Task InitializeSchema() - { - if (_schemaManager == null) return; - - try - { - Console.WriteLine("πŸ”§ Checking QuestDB schema optimization..."); - var success = await _schemaManager.EnsureOptimizedSchemaExists(); - - if (success) - { - var stats = await _schemaManager.GetTableStats(); - Console.WriteLine("πŸ“Š TelemetryTicks schema status:"); - foreach (var stat in stats) - { - Console.WriteLine($" {stat.Key}: {stat.Value}"); - } - } - else - { - Console.WriteLine("⚠️ Schema optimization failed, using existing table structure"); - } - } - catch (Exception ex) - { - Console.WriteLine($"⚠️ Schema initialization warning: {ex.Message}"); - } - } - - public async Task TriggerSchemaOptimization() - { - if (_schemaManager == null) - { - Console.WriteLine("⚠️ Schema manager not initialized"); - return false; - } - - try - { - Console.WriteLine("πŸ”§ Manually triggering schema optimization..."); - var result = await _schemaManager.EnsureOptimizedSchemaExists(); - - if (result) - { - var stats = await _schemaManager.GetTableStats(); - Console.WriteLine("πŸ“Š Schema optimization status:"); - foreach (var stat in stats) - { - Console.WriteLine($" {stat.Key}: {stat.Value}"); - } - } - - return result; - } - catch (Exception ex) - { - Console.WriteLine($"❌ Manual schema optimization failed: {ex.Message}"); - return false; - } - } - - public void Dispose() - { - _sender?.Dispose(); - _schemaManager?.Dispose(); - } - - public async Task WriteBatch(TelemetryBatch? telData) - { - if (telData == null) - { - Console.WriteLine("No telemetry data to write"); - return; - } - - if (_sender == null) - { - Console.WriteLine("ERROR: QuestDB sender is not initialized"); - return; - } - - try - { - foreach (var tel in telData.Records) - { - - _sender.Table("TelemetryTicks") - .Symbol("session_id", tel.SessionId) - .Symbol("track_name", tel.TrackName) - .Symbol("track_id", tel.TrackId) - - .Symbol("lap_id", tel.LapId ?? "unknown") - .Symbol("session_num", tel.SessionNum) - .Symbol("session_type", tel.SessionType ?? "Unknown") - .Symbol("session_name", tel.SessionName ?? "Unknown") - - .Column("car_id", tel.CarId) - - .Column("gear", tel.Gear) - .Column("player_car_position", (long)Math.Floor(tel.PlayerCarPosition)) - - .Column("speed", tel.Speed) - .Column("lap_dist_pct", tel.LapDistPct) - .Column("session_time", tel.SessionTime) - .Column("lat", tel.Lat) - .Column("lon", tel.Lon) - .Column("lap_current_lap_time", tel.LapCurrentLapTime) - .Column("lapLastLapTime", tel.LapLastLapTime) - .Column("lapDeltaToBestLap", tel.LapDeltaToBestLap) - - .Column("throttle", (float)tel.Throttle) - .Column("brake", (float)tel.Brake) - .Column("steering_wheel_angle", (float)tel.SteeringWheelAngle) - .Column("rpm", (float)tel.Rpm) - .Column("velocity_x", (float)tel.VelocityX) - .Column("velocity_y", (float)tel.VelocityY) - .Column("velocity_z", (float)tel.VelocityZ) - .Column("fuel_level", (float)tel.FuelLevel) - .Column("alt", (float)tel.Alt) - .Column("lat_accel", (float)tel.LatAccel) - .Column("long_accel", (float)tel.LongAccel) - .Column("vert_accel", (float)tel.VertAccel) - .Column("pitch", (float)tel.Pitch) - .Column("roll", (float)tel.Roll) - .Column("yaw", (float)tel.Yaw) - .Column("yaw_north", (float)tel.YawNorth) - .Column("voltage", (float)tel.Voltage) - .Column("waterTemp", (float)tel.WaterTemp) - .Column("lFpressure", (float)tel.LFpressure) - .Column("rFpressure", (float)tel.RFpressure) - .Column("lRpressure", (float)tel.LRpressure) - .Column("rRpressure", (float)tel.RRpressure) - .Column("lFtempM", (float)tel.LFtempM) - .Column("rFtempM", (float)tel.RFtempM) - .Column("lRtempM", (float)tel.LRtempM) - .Column("rRtempM", (float)tel.RRtempM) - .At(tel.TickTime.ToDateTime()); - } - - await _sender.SendAsync(); - Console.WriteLine($"Successfully wrote {telData.Records.Count} telemetry points"); - } - catch (Exception ex) - { - Console.WriteLine($"ERROR writing to QuestDB: {ex.Message}"); - } - } -} \ No newline at end of file diff --git a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/TelemetryService.Infrastructure.csproj b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/TelemetryService.Infrastructure.csproj index 7c08111..5d0af91 100644 --- a/telemetryService/telemetryService/src/TelemetryService.Infrastructure/TelemetryService.Infrastructure.csproj +++ b/telemetryService/telemetryService/src/TelemetryService.Infrastructure/TelemetryService.Infrastructure.csproj @@ -6,6 +6,7 @@ + diff --git a/telemetryService/telemetryService/src/TelemetryService.Worker/Program.cs b/telemetryService/telemetryService/src/TelemetryService.Worker/Program.cs index d9dc152..e0cc62f 100644 --- a/telemetryService/telemetryService/src/TelemetryService.Worker/Program.cs +++ b/telemetryService/telemetryService/src/TelemetryService.Worker/Program.cs @@ -1,9 +1,8 @@ ο»Ώusing Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using TelemetryService.Application.Services; -using TelemetryService.Configuration.Config; -using TelemetryService.Persistence.Services; -using TelemetryService.Messaging.Services; +using TelemetryService.Infrastructure.Messaging; +using TelemetryService.Infrastructure.Persistence; +using TelemetryService.Infrastructure.Configuration; namespace TelemetryService; @@ -13,20 +12,47 @@ private static async Task Main(string[] args) { try { + Console.WriteLine("πŸš€ Telemetry Service Starting..."); + + LoadEnvironmentVariables(); using var host = CreateHostBuilder(args).Build(); var subscriber = host.Services.GetRequiredService(); + var questDbSchemaManager = host.Services.GetRequiredService(); + + await questDbSchemaManager.EnsureOptimizedSchemaExists(); - Console.WriteLine("Starting telemetry service..."); + Console.WriteLine("Starting telemetry service subscriber..."); await subscriber.SubscribeAsync(); await host.RunAsync(); } + catch (OutOfMemoryException ex) + { + Console.WriteLine($"OutOfMemoryException during service startup"); + Console.WriteLine($" Message: {ex.Message}"); + Console.WriteLine($" Stack Trace: {ex.StackTrace}"); + + var process = System.Diagnostics.Process.GetCurrentProcess(); + var memoryUsageGB = (double)process.WorkingSet64 / (1024 * 1024 * 1024); + Console.WriteLine($" Memory Usage at Failure: {memoryUsageGB:F2}GB"); + + Environment.Exit(1); + } catch (Exception ex) { - Console.WriteLine($"Fatal error in telemetry service: {ex}"); + Console.WriteLine($" FATAL: Unhandled exception in telemetry service"); + Console.WriteLine($" Exception Type: {ex.GetType().Name}"); + Console.WriteLine($" Message: {ex.Message}"); + Console.WriteLine($" Stack Trace: {ex.StackTrace}"); + + if (ex.InnerException != null) + { + Console.WriteLine($" Inner Exception: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}"); + } + Environment.Exit(1); } } @@ -57,8 +83,15 @@ private static IHostBuilder CreateHostBuilder(string[] args) return Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => { - services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(sp => + { + var host = Environment.GetEnvironmentVariable("QUESTDB_HTTP_HOST") ?? "questdb"; + var port = Environment.GetEnvironmentVariable("QUESTDB_HTTP_PORT") ?? "9000"; + var questDbUrl = $"{host}:{port}"; + Console.WriteLine($"πŸ”§ Initializing QuestDbSchemaManager with HTTP URL: http://{questDbUrl}"); + return new QuestDbSchemaManager(questDbUrl); + }); }); } } \ No newline at end of file diff --git a/telemetryService/telemetryService/telemetryService.sln b/telemetryService/telemetryService/telemetryService.sln index 8a687ae..9f71645 100644 --- a/telemetryService/telemetryService/telemetryService.sln +++ b/telemetryService/telemetryService/telemetryService.sln @@ -1,4 +1,5 @@ -ο»ΏMicrosoft Visual Studio Solution File, Format Version 12.00 +ο»Ώ +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 @@ -6,51 +7,95 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelemetryService.Domain", "src\TelemetryService.Domain\TelemetryService.Domain.csproj", "{74FA1D50-DE09-4241-9BA1-1330DA121AD3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelemetryService.Application", "src\TelemetryService.Application\TelemetryService.Application.csproj", "{7D257D18-5742-4DC0-905C-91EC4587E35E}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelemetryService.Infrastructure", "src\TelemetryService.Infrastructure\TelemetryService.Infrastructure.csproj", "{11111111-1111-1111-1111-111111111111}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelemetryService.API", "src\TelemetryService.API\TelemetryService.API.csproj", "{8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelemetryService.Worker", "src\TelemetryService.Worker\TelemetryService.Worker.csproj", "{33333333-3333-3333-3333-333333333333}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelemetryService.Application.Tests", "tests\TelemetryService.Application.Tests\TelemetryService.Application.Tests.csproj", "{22222222-2222-2222-2222-222222222222}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelemetryService.InfrastructureTests", "tests\TelemetryService.InfrastructureTests\TelemetryService.InfrastructureTests.csproj", "{ACBC11B3-FF2E-47E2-A222-A1E8762EA332}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Debug|x64.ActiveCfg = Debug|Any CPU + {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Debug|x64.Build.0 = Debug|Any CPU + {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Debug|x86.Build.0 = Debug|Any CPU {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Release|Any CPU.Build.0 = Release|Any CPU - {7D257D18-5742-4DC0-905C-91EC4587E35E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7D257D18-5742-4DC0-905C-91EC4587E35E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7D257D18-5742-4DC0-905C-91EC4587E35E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7D257D18-5742-4DC0-905C-91EC4587E35E}.Release|Any CPU.Build.0 = Release|Any CPU + {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Release|x64.ActiveCfg = Release|Any CPU + {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Release|x64.Build.0 = Release|Any CPU + {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Release|x86.ActiveCfg = Release|Any CPU + {74FA1D50-DE09-4241-9BA1-1330DA121AD3}.Release|x86.Build.0 = Release|Any CPU {11111111-1111-1111-1111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11111111-1111-1111-1111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11111111-1111-1111-1111-111111111111}.Debug|x64.ActiveCfg = Debug|Any CPU + {11111111-1111-1111-1111-111111111111}.Debug|x64.Build.0 = Debug|Any CPU + {11111111-1111-1111-1111-111111111111}.Debug|x86.ActiveCfg = Debug|Any CPU + {11111111-1111-1111-1111-111111111111}.Debug|x86.Build.0 = Debug|Any CPU {11111111-1111-1111-1111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU {11111111-1111-1111-1111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU + {11111111-1111-1111-1111-111111111111}.Release|x64.ActiveCfg = Release|Any CPU + {11111111-1111-1111-1111-111111111111}.Release|x64.Build.0 = Release|Any CPU + {11111111-1111-1111-1111-111111111111}.Release|x86.ActiveCfg = Release|Any CPU + {11111111-1111-1111-1111-111111111111}.Release|x86.Build.0 = Release|Any CPU {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Debug|x64.Build.0 = Debug|Any CPU + {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Debug|x86.Build.0 = Debug|Any CPU {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Release|Any CPU.Build.0 = Release|Any CPU - {22222222-2222-2222-2222-222222222222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22222222-2222-2222-2222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU + {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Release|x64.ActiveCfg = Release|Any CPU + {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Release|x64.Build.0 = Release|Any CPU + {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Release|x86.ActiveCfg = Release|Any CPU + {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD}.Release|x86.Build.0 = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|x64.ActiveCfg = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|x64.Build.0 = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|x86.ActiveCfg = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|x86.Build.0 = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|Any CPU.Build.0 = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|x64.ActiveCfg = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|x64.Build.0 = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|x86.ActiveCfg = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|x86.Build.0 = Release|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Debug|x64.ActiveCfg = Debug|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Debug|x64.Build.0 = Debug|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Debug|x86.ActiveCfg = Debug|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Debug|x86.Build.0 = Debug|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Release|Any CPU.Build.0 = Release|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Release|x64.ActiveCfg = Release|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Release|x64.Build.0 = Release|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Release|x86.ActiveCfg = Release|Any CPU + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {74FA1D50-DE09-4241-9BA1-1330DA121AD3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {7D257D18-5742-4DC0-905C-91EC4587E35E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {11111111-1111-1111-1111-111111111111} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {8572DE66-3875-4AFF-B5FD-0A3D3E9FA3DD} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {22222222-2222-2222-2222-222222222222} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {33333333-3333-3333-3333-333333333333} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {ACBC11B3-FF2E-47E2-A222-A1E8762EA332} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/telemetryService/telemetryService/tests/TelemetryService.InfrastructureTests/DotEnvTest.cs b/telemetryService/telemetryService/tests/TelemetryService.InfrastructureTests/DotEnvTest.cs new file mode 100644 index 0000000..57d83bd --- /dev/null +++ b/telemetryService/telemetryService/tests/TelemetryService.InfrastructureTests/DotEnvTest.cs @@ -0,0 +1,85 @@ +ο»Ώusing TelemetryService.Infrastructure.Configuration; +namespace TelemetryService.InfrastructureTests; + +public class DotEnvTests +{ + private string _testDir = string.Empty; + private readonly Dictionary _originalEnvVars = new(); + + [SetUp] + public void Setup() + { + _testDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_testDir); + + CaptureOriginalEnvVar(); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_testDir)) + { + Directory.Delete(_testDir, true); + + RestoreOriginalEnvironmentVariables(); + } + } + + [Test] + public void Load_WhenFileDoesNotExist_DoesNotThrow() + { + var nonExistentPath = Path.Combine(_testDir, "nonExistant.env"); + + Assert.DoesNotThrow(() => DotEnv.Load(nonExistentPath)); + } + + [Test] + public void Load_WhenLineHasNoEquals_SkipLine() + { + var envFilePath = CreateTestEnvFile("INVALID_LINE_NO_EQUALS\nVALID_VAR=valid_value"); + + DotEnv.Load(envFilePath); + + Assert.That(Environment.GetEnvironmentVariable("INVALID_LINE_NO_EQUALS"), Is.Null); + Assert.That(Environment.GetEnvironmentVariable("VALID_VAR"), Is.EqualTo("valid_value")); + } + + private string CreateTestEnvFile(string envString) + { + var filePath = Path.Combine(_testDir, $"{Guid.NewGuid()}.env"); + File.WriteAllText(filePath, envString); + return filePath; + } + + private void CaptureOriginalEnvVar() + { + var testVars = new[] { "TEST_VAR", "ANOTHER_VAR", "VALID_VAR", "EXISTING_VAR", "EMPTY_VAR", "VAR_WITH_EQUALS", "FIRST_VAR", "SECOND_VAR" }; + + foreach (var variable in testVars) + { + var value = Environment.GetEnvironmentVariable(variable); + if (value != null) + { + _originalEnvVars[variable] = value; + } + } + } + + private void RestoreOriginalEnvironmentVariables() + { + var testVariables = new[] { "TEST_VAR", "ANOTHER_VAR", "VALID_VAR", "EXISTING_VAR", "EMPTY_VAR", "VAR_WITH_EQUALS", "FIRST_VAR", "SECOND_VAR" }; + + foreach (var variable in testVariables) + { + if (_originalEnvVars.TryGetValue(variable, out var originalValue)) + { + Environment.SetEnvironmentVariable(variable, originalValue); + } + else + { + Environment.SetEnvironmentVariable(variable, null); + } + } + } +} diff --git a/telemetryService/telemetryService/tests/TelemetryService.InfrastructureTests/TelemetryService.InfrastructureTests.csproj b/telemetryService/telemetryService/tests/TelemetryService.InfrastructureTests/TelemetryService.InfrastructureTests.csproj new file mode 100644 index 0000000..3785304 --- /dev/null +++ b/telemetryService/telemetryService/tests/TelemetryService.InfrastructureTests/TelemetryService.InfrastructureTests.csproj @@ -0,0 +1,27 @@ +ο»Ώ + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/traefik/middleware.yaml b/traefik/middleware.yaml index 2b7af6c..61b1105 100644 --- a/traefik/middleware.yaml +++ b/traefik/middleware.yaml @@ -1,25 +1,23 @@ http: middlewares: # Security Headers Middleware + # Works with both Tailscale certificates and local self-signed certificates security-headers: headers: customFrameOptionsValue: "SAMEORIGIN" - customRequestHeaders: - X-Forwarded-Proto: "https" customResponseHeaders: X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex" X-Frame-Options: "SAMEORIGIN" X-Content-Type-Options: "nosniff" Referrer-Policy: "same-origin" - Cross-Origin-Embedder-Policy: "require-corp" Cross-Origin-Opener-Policy: "same-origin" Cross-Origin-Resource-Policy: "same-origin" sslProxyHeaders: X-Forwarded-Proto: "https" - stsSeconds: 63072000 - stsIncludeSubdomains: true - stsPreload: true - forceSTSHeader: true + stsSeconds: 0 + stsIncludeSubdomains: false + stsPreload: false + forceSTSHeader: false contentTypeNosniff: true browserXssFilter: true referrerPolicy: "same-origin" @@ -81,4 +79,25 @@ http: middlewares: - local-allowlist - security-headers - - rate-limit \ No newline at end of file + - rate-limit + + # Path Prefix Stripping Middlewares + rabbitmq-stripprefix: + stripPrefix: + prefixes: + - "/rabbitmq" + + questdb-stripprefix: + stripPrefix: + prefixes: + - "/questdb" + + dashboard-stripprefix: + stripPrefix: + prefixes: + - "/dashboard" + + prometheus-stripprefix: + stripPrefix: + prefixes: + - "/prometheus" \ No newline at end of file diff --git a/traefik/tls.yaml b/traefik/tls.yaml index 0645066..2261271 100644 --- a/traefik/tls.yaml +++ b/traefik/tls.yaml @@ -1,7 +1,4 @@ tls: - certificates: - - certFile: /certs/localhost.crt - keyFile: /certs/localhost.key options: default: minVersion: "VersionTLS12" diff --git a/traefik/traefik.yml b/traefik/traefik.yml index 86bb9e3..579954d 100644 --- a/traefik/traefik.yml +++ b/traefik/traefik.yml @@ -5,37 +5,34 @@ global: # API and Dashboard api: dashboard: true - insecure: false + insecure: true + +# Ping endpoint for healthcheck +ping: {} # Entry Points entryPoints: web: address: ":80" - http: - redirections: - entrypoint: - to: websecure - scheme: https websecure: address: ":443" - http: - tls: - options: default # Providers providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false - network: "iracing-display_telemetry-network" watch: true + httpClientTimeout: 0 file: directory: /etc/traefik watch: true -# Certificates (none configured - using static certificates) -# certificatesResolvers: +# Certificate Resolvers +certificatesResolvers: + tailscale: + tailscale: {} # Logging log: