From 7daa9c5b3f54056ba43336e1c4d5e17e76e6f6f2 Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 10:48:24 -0400 Subject: [PATCH 01/33] add bounded series query primitives --- .../data-downloader/backend/series_queries.py | 105 ++++++++++++++++++ .../data-downloader/backend/tests/__init__.py | 0 .../backend/tests/test_series_queries.py | 66 +++++++++++ 3 files changed, 171 insertions(+) create mode 100644 server/installer/data-downloader/backend/series_queries.py create mode 100644 server/installer/data-downloader/backend/tests/__init__.py create mode 100644 server/installer/data-downloader/backend/tests/test_series_queries.py diff --git a/server/installer/data-downloader/backend/series_queries.py b/server/installer/data-downloader/backend/series_queries.py new file mode 100644 index 00000000..72f7e8f2 --- /dev/null +++ b/server/installer/data-downloader/backend/series_queries.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +import re +from typing import Literal + + +RAW_THRESHOLD = 100_000 +DEFAULT_TARGET_POINTS = 4_000 +MAX_TARGET_POINTS = 20_000 +MAX_SIGNALS = 12 +MAX_WINDOW_DAYS = 7 +MAX_TOTAL_POINTS = 1_500_000 +STATEMENT_TIMEOUT_MS = 15_000 + +_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +def normalize_utc(dt: datetime) -> datetime: + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc) + + +def validate_request( + signals: list[str], + start: datetime, + end: datetime, + target_points: int, +) -> None: + start_utc = normalize_utc(start) + end_utc = normalize_utc(end) + if not signals or len(signals) > MAX_SIGNALS: + raise ValueError(f"select between 1 and {MAX_SIGNALS} signals") + if len(set(signals)) != len(signals): + raise ValueError("signal names must be unique") + if any(not _IDENTIFIER.fullmatch(signal) for signal in signals): + raise ValueError("invalid signal name") + if start_utc >= end_utc: + raise ValueError("start must be before end") + if end_utc - start_utc > timedelta(days=MAX_WINDOW_DAYS): + raise ValueError("window cannot exceed seven days") + if not 1 <= target_points <= MAX_TARGET_POINTS: + raise ValueError(f"target_points must be between 1 and {MAX_TARGET_POINTS}") + + +def bucket_interval(start: datetime, end: datetime, target_points: int) -> str: + total_ms = int((normalize_utc(end) - normalize_utc(start)).total_seconds() * 1000) + return f"{max(1, (total_ms + target_points - 1) // target_points)} milliseconds" + + +def choose_mode(estimate: int) -> Literal["raw", "envelope"]: + return "raw" if estimate <= RAW_THRESHOLD else "envelope" + + +def _quoted(signal: str) -> str: + if not _IDENTIFIER.fullmatch(signal): + raise ValueError("invalid signal name") + return f'"{signal}"' + + +def _table(table: str) -> str: + if not re.fullmatch(r"[a-z][a-z0-9_]*", table): + raise ValueError("invalid season table") + return table + + +def build_estimate_sql(table: str, signal: str) -> str: + column = _quoted(signal) + return ( + "SELECT COUNT(*) FROM (" + f"SELECT 1 FROM {_table(table)} " + "WHERE time >= %(start)s AND time <= %(end)s " + f"AND {column} IS NOT NULL LIMIT {RAW_THRESHOLD + 1}" + ") AS bounded_count" + ) + + +def build_raw_sql(table: str, signal: str) -> str: + column = _quoted(signal) + return ( + f"SELECT time, {column} FROM {_table(table)} " + "WHERE time >= %(start)s AND time <= %(end)s " + f"AND {column} IS NOT NULL ORDER BY time" + ) + + +def build_envelope_sql(table: str, signal: str) -> str: + column = _quoted(signal) + return ( + "SELECT time_bucket(%(bucket)s::interval, time) AS bucket, " + f"MIN({column}), MAX({column}), AVG({column}) " + f"FROM {_table(table)} " + "WHERE time >= %(start)s AND time <= %(end)s " + f"AND {column} IS NOT NULL " + "GROUP BY bucket ORDER BY bucket" + ) + + +def response_estimate( + mode: Literal["raw", "envelope"], + raw_estimate: int, + target_points: int, +) -> int: + return raw_estimate if mode == "raw" else target_points diff --git a/server/installer/data-downloader/backend/tests/__init__.py b/server/installer/data-downloader/backend/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/installer/data-downloader/backend/tests/test_series_queries.py b/server/installer/data-downloader/backend/tests/test_series_queries.py new file mode 100644 index 00000000..ff77901a --- /dev/null +++ b/server/installer/data-downloader/backend/tests/test_series_queries.py @@ -0,0 +1,66 @@ +from datetime import datetime, timedelta, timezone + +import pytest + +from backend import series_queries as sq + + +UTC = timezone.utc +T0 = datetime(2026, 6, 20, 15, 0, tzinfo=UTC) + + +def test_validate_request_accepts_normal_request(): + sq.validate_request(["INV_Motor_Temp"], T0, T0 + timedelta(hours=1), 4000) + + +@pytest.mark.parametrize( + ("signals", "start", "end", "target", "match"), + [ + ([], T0, T0 + timedelta(hours=1), 4000, "signal"), + ([f"S{i}" for i in range(13)], T0, T0 + timedelta(hours=1), 4000, "signal"), + (["bad\"name"], T0, T0 + timedelta(hours=1), 4000, "signal"), + (["A"], T0, T0, 4000, "start"), + (["A"], T0, T0 + timedelta(days=8), 4000, "seven"), + (["A"], T0, T0 + timedelta(hours=1), 0, "target"), + (["A"], T0, T0 + timedelta(hours=1), 20_001, "target"), + ], +) +def test_validate_request_rejects_invalid_values(signals, start, end, target, match): + with pytest.raises(ValueError, match=match): + sq.validate_request(signals, start, end, target) + + +def test_bucket_interval_uses_integer_milliseconds(): + value = sq.bucket_interval(T0, T0 + timedelta(seconds=10_000), 4000) + assert value == "2500 milliseconds" + + +def test_choose_mode_uses_raw_threshold(): + assert sq.choose_mode(100_000) == "raw" + assert sq.choose_mode(100_001) == "envelope" + + +def test_sql_quotes_signal_and_bounds_time(): + for sql in ( + sq.build_estimate_sql("wfr26", "INV_Motor_Temp"), + sq.build_raw_sql("wfr26", "INV_Motor_Temp"), + sq.build_envelope_sql("wfr26", "INV_Motor_Temp"), + ): + assert '"INV_Motor_Temp"' in sql + assert "%(start)s" in sql + assert "%(end)s" in sql + assert '"INV_Motor_Temp" IS NOT NULL' in sql + assert "SELECT *" not in sql.upper() + + +def test_envelope_sql_uses_time_bucket_and_aggregates(): + sql = sq.build_envelope_sql("wfr26", "INV_Motor_Temp") + assert "time_bucket" in sql + assert 'MIN("INV_Motor_Temp")' in sql + assert 'MAX("INV_Motor_Temp")' in sql + assert 'AVG("INV_Motor_Temp")' in sql + + +def test_response_estimate_uses_target_for_envelope(): + assert sq.response_estimate("raw", 120, 4000) == 120 + assert sq.response_estimate("envelope", 100_001, 4000) == 4000 From 7d4f76e4ecf6d9c1b88ef11955dcd84d713ba4dd Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 10:51:07 -0400 Subject: [PATCH 02/33] add density-aware multi-signal series endpoint --- .../installer/data-downloader/backend/app.py | 49 ++++++++++ .../data-downloader/backend/series_queries.py | 72 ++++++++++++++ .../backend/tests/test_series_endpoint.py | 96 +++++++++++++++++++ .../backend/tests/test_series_queries.py | 81 ++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 server/installer/data-downloader/backend/tests/test_series_endpoint.py diff --git a/server/installer/data-downloader/backend/app.py b/server/installer/data-downloader/backend/app.py index 40bde0db..45034549 100644 --- a/server/installer/data-downloader/backend/app.py +++ b/server/installer/data-downloader/backend/app.py @@ -11,9 +11,11 @@ from fastapi import BackgroundTasks, FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware from fastapi.responses import HTMLResponse from pydantic import BaseModel +from backend import series_queries from backend.config import get_settings from backend.dbc_utils import group_sensors_by_message, load_dbc_db, refresh_dbc from backend.services import DataDownloaderService @@ -43,6 +45,14 @@ class CanFramesBatchPayload(BaseModel): frames: List[CanFramePayload] = [] +class SeriesPayload(BaseModel): + season: str + signals: List[str] + start: datetime + end: datetime + target_points: int = series_queries.DEFAULT_TARGET_POINTS + + settings = get_settings() service = DataDownloaderService(settings) logger = logging.getLogger(__name__) @@ -55,6 +65,7 @@ class CanFramesBatchPayload(BaseModel): allow_methods=["*"], allow_headers=["*"], ) +app.add_middleware(GZipMiddleware, minimum_size=1000) @app.get("/api/health") @@ -193,6 +204,44 @@ def query_signal(payload: DataQueryPayload, season: str | None = None) -> dict: raise HTTPException(status_code=503, detail=f"Database query failed: {exc}") +@app.post("/api/series") +def query_series(payload: SeriesPayload) -> dict: + try: + series_queries.validate_request( + payload.signals, payload.start, payload.end, payload.target_points + ) + # Metadata seasons use uppercase names; request season is the lowercase table. + season_by_table = {item["table"]: item for item in service.get_seasons()} + season_meta = season_by_table.get(payload.season) + if season_meta is None: + raise ValueError(f"unknown season table: {payload.season}") + known_signals = set( + service.get_sensors(season=season_meta["name"]).get("sensors", []) + ) + unknown = sorted(set(payload.signals) - known_signals) + if unknown: + raise ValueError(f"unknown signal(s): {', '.join(unknown)}") + result = series_queries.execute_series_query( + postgres_dsn=settings.postgres_dsn, + table=payload.season, + signals=payload.signals, + start=payload.start, + end=payload.end, + target_points=payload.target_points, + ) + return { + "season": payload.season, + "start": series_queries.normalize_utc(payload.start).isoformat(), + "end": series_queries.normalize_utc(payload.end).isoformat(), + "series": result, + } + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + except Exception as exc: + logger.exception("Series query failed for %s", payload.signals) + raise HTTPException(status_code=503, detail=f"Database query failed: {exc}") + + # ── CAN frames batch ingest (for flight-recorder sync) ───────────────────────── # Cache of known signal columns per table, thread-safe diff --git a/server/installer/data-downloader/backend/series_queries.py b/server/installer/data-downloader/backend/series_queries.py index 72f7e8f2..0a969772 100644 --- a/server/installer/data-downloader/backend/series_queries.py +++ b/server/installer/data-downloader/backend/series_queries.py @@ -4,6 +4,8 @@ import re from typing import Literal +import psycopg2 + RAW_THRESHOLD = 100_000 DEFAULT_TARGET_POINTS = 4_000 @@ -103,3 +105,73 @@ def response_estimate( target_points: int, ) -> int: return raw_estimate if mode == "raw" else target_points + + +def _epoch_ms(value: datetime) -> int: + return int(normalize_utc(value).timestamp() * 1000) + + +def execute_series_query( + *, + postgres_dsn: str, + table: str, + signals: list[str], + start: datetime, + end: datetime, + target_points: int, +) -> dict[str, dict]: + start_utc = normalize_utc(start) + end_utc = normalize_utc(end) + validate_request(signals, start_utc, end_utc, target_points) + params = {"start": start_utc, "end": end_utc} + result: dict[str, dict] = {} + + with psycopg2.connect(postgres_dsn) as conn: + with conn.cursor() as cur: + # Cap long-running analytics queries so the API stays responsive. + cur.execute(f"SET LOCAL statement_timeout = {STATEMENT_TIMEOUT_MS}") + estimates: dict[str, int] = {} + modes: dict[str, Literal["raw", "envelope"]] = {} + for signal in signals: + cur.execute(build_estimate_sql(table, signal), params) + estimates[signal] = int(cur.fetchone()[0]) + modes[signal] = choose_mode(estimates[signal]) + + projected = sum( + response_estimate(modes[s], estimates[s], target_points) for s in signals + ) + if projected > MAX_TOTAL_POINTS: + raise ValueError( + f"estimated response exceeds {MAX_TOTAL_POINTS:,} points" + ) + + bucket = bucket_interval(start_utc, end_utc, target_points) + resolution_ms = int(bucket.split()[0]) + for signal in signals: + mode = modes[signal] + if mode == "raw": + cur.execute(build_raw_sql(table, signal), params) + rows = cur.fetchall() + result[signal] = { + "mode": "raw", + "resolution_ms": None, + "point_count": len(rows), + "t": [_epoch_ms(row[0]) for row in rows], + "v": [float(row[1]) for row in rows], + } + else: + cur.execute( + build_envelope_sql(table, signal), + {**params, "bucket": bucket}, + ) + rows = cur.fetchall() + result[signal] = { + "mode": "envelope", + "resolution_ms": resolution_ms, + "point_count": len(rows), + "t": [_epoch_ms(row[0]) for row in rows], + "min": [float(row[1]) for row in rows], + "max": [float(row[2]) for row in rows], + "avg": [float(row[3]) for row in rows], + } + return result diff --git a/server/installer/data-downloader/backend/tests/test_series_endpoint.py b/server/installer/data-downloader/backend/tests/test_series_endpoint.py new file mode 100644 index 00000000..30591102 --- /dev/null +++ b/server/installer/data-downloader/backend/tests/test_series_endpoint.py @@ -0,0 +1,96 @@ +import gzip +import importlib +import os +import tempfile + +from fastapi.testclient import TestClient +import pytest + + +os.environ["DATA_DIR"] = tempfile.mkdtemp() +os.environ["POSTGRES_DSN"] = "postgresql://test:test@127.0.0.1:1/test" + +app_module = importlib.import_module("backend.app") +client = TestClient(app_module.app) + + +@pytest.fixture(autouse=True) +def metadata(monkeypatch): + monkeypatch.setattr( + app_module.service, + "get_seasons", + lambda: [{"name": "WFR26", "year": 2026, "table": "wfr26", "color": None}], + ) + monkeypatch.setattr( + app_module.service, + "get_sensors", + lambda season=None: {"updated_at": None, "sensors": ["INV_Motor_Temp", "INV_Coolant_Temp"]}, + ) + + +def payload(**overrides): + base = { + "season": "wfr26", + "signals": ["INV_Motor_Temp"], + "start": "2026-06-20T15:00:00Z", + "end": "2026-06-20T16:00:00Z", + "target_points": 4000, + } + return {**base, **overrides} + + +def test_series_endpoint_returns_query_result(monkeypatch): + expected = { + "INV_Motor_Temp": { + "mode": "raw", + "resolution_ms": None, + "point_count": 1, + "t": [1781967600000], + "v": [47.2], + } + } + monkeypatch.setattr(app_module.series_queries, "execute_series_query", lambda **kwargs: expected) + response = client.post("/api/series", json=payload()) + assert response.status_code == 200 + assert response.json()["series"] == expected + + +def test_series_endpoint_rejects_unknown_table(): + response = client.post("/api/series", json=payload(season="wfr99")) + assert response.status_code == 400 + assert "season" in response.json()["detail"].lower() + + +def test_series_endpoint_rejects_unknown_signal(): + response = client.post("/api/series", json=payload(signals=["Nope"])) + assert response.status_code == 400 + assert "signal" in response.json()["detail"].lower() + + +def test_series_endpoint_maps_database_failure_to_503(monkeypatch): + def fail(**kwargs): + raise RuntimeError("db unavailable") + monkeypatch.setattr(app_module.series_queries, "execute_series_query", fail) + response = client.post("/api/series", json=payload()) + assert response.status_code == 503 + assert "Database query failed" in response.json()["detail"] + + +def test_series_endpoint_gzips_large_response(monkeypatch): + large = { + "INV_Motor_Temp": { + "mode": "raw", + "resolution_ms": None, + "point_count": 500, + "t": list(range(500)), + "v": [47.2] * 500, + } + } + monkeypatch.setattr(app_module.series_queries, "execute_series_query", lambda **kwargs: large) + response = client.post( + "/api/series", + json=payload(), + headers={"Accept-Encoding": "gzip"}, + ) + assert response.status_code == 200 + assert response.headers["content-encoding"] == "gzip" diff --git a/server/installer/data-downloader/backend/tests/test_series_queries.py b/server/installer/data-downloader/backend/tests/test_series_queries.py index ff77901a..e2acc200 100644 --- a/server/installer/data-downloader/backend/tests/test_series_queries.py +++ b/server/installer/data-downloader/backend/tests/test_series_queries.py @@ -64,3 +64,84 @@ def test_envelope_sql_uses_time_bucket_and_aggregates(): def test_response_estimate_uses_target_for_envelope(): assert sq.response_estimate("raw", 120, 4000) == 120 assert sq.response_estimate("envelope", 100_001, 4000) == 4000 + + +class FakeCursor: + def __init__(self, estimate, rows): + self.estimate = estimate + self.rows = rows + self.executed = [] + self.last_sql = "" + + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def execute(self, sql, params=None): + self.last_sql = sql + self.executed.append((sql, params)) + + def fetchone(self): + return (self.estimate,) + + def fetchall(self): + return self.rows + + +class FakeConnection: + def __init__(self, cursor): + self._cursor = cursor + + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def cursor(self): + return self._cursor + + +def test_execute_series_query_sets_statement_timeout(monkeypatch): + cursor = FakeCursor(1, [(T0, 47.2)]) + monkeypatch.setattr( + sq.psycopg2, + "connect", + lambda _dsn: FakeConnection(cursor), + ) + result = sq.execute_series_query( + postgres_dsn="unused", + table="wfr26", + signals=["INV_Motor_Temp"], + start=T0, + end=T0 + timedelta(seconds=10), + target_points=4000, + ) + assert cursor.executed[0][0] == "SET LOCAL statement_timeout = 15000" + assert result["INV_Motor_Temp"]["mode"] == "raw" + assert result["INV_Motor_Temp"]["v"] == [47.2] + + +def test_execute_series_query_shapes_envelope(monkeypatch): + cursor = FakeCursor(100_001, [(T0, 1.0, 3.0, 2.0)]) + monkeypatch.setattr( + sq.psycopg2, + "connect", + lambda _dsn: FakeConnection(cursor), + ) + result = sq.execute_series_query( + postgres_dsn="unused", + table="wfr26", + signals=["INV_Motor_Temp"], + start=T0, + end=T0 + timedelta(seconds=10_000), + target_points=4000, + ) + series = result["INV_Motor_Temp"] + assert series["mode"] == "envelope" + assert series["resolution_ms"] == 2500 + assert series["min"] == [1.0] + assert series["max"] == [3.0] + assert series["avg"] == [2.0] From 44c3a92b6a9455dbb38776d6076b4172a7cbf67c Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 10:53:46 -0400 Subject: [PATCH 03/33] Use repeatable-read snapshot for series estimate and fetch --- .../data-downloader/backend/series_queries.py | 5 +- .../backend/tests/test_series_queries.py | 50 +++++++++++++++++-- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/server/installer/data-downloader/backend/series_queries.py b/server/installer/data-downloader/backend/series_queries.py index 0a969772..5fe5a202 100644 --- a/server/installer/data-downloader/backend/series_queries.py +++ b/server/installer/data-downloader/backend/series_queries.py @@ -128,7 +128,10 @@ def execute_series_query( with psycopg2.connect(postgres_dsn) as conn: with conn.cursor() as cur: - # Cap long-running analytics queries so the API stays responsive. + # One repeatable-read snapshot for estimate + fetch; cap query runtime. + cur.execute( + "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY" + ) cur.execute(f"SET LOCAL statement_timeout = {STATEMENT_TIMEOUT_MS}") estimates: dict[str, int] = {} modes: dict[str, Literal["raw", "envelope"]] = {} diff --git a/server/installer/data-downloader/backend/tests/test_series_queries.py b/server/installer/data-downloader/backend/tests/test_series_queries.py index e2acc200..12478846 100644 --- a/server/installer/data-downloader/backend/tests/test_series_queries.py +++ b/server/installer/data-downloader/backend/tests/test_series_queries.py @@ -67,11 +67,15 @@ def test_response_estimate_uses_target_for_envelope(): class FakeCursor: - def __init__(self, estimate, rows): - self.estimate = estimate - self.rows = rows + def __init__(self, estimate, rows=None): + if isinstance(estimate, list): + self._estimates = list(estimate) + else: + self._estimates = [estimate] + self.rows = rows if rows is not None else [] self.executed = [] self.last_sql = "" + self._estimate_idx = 0 def __enter__(self): return self @@ -84,7 +88,9 @@ def execute(self, sql, params=None): self.executed.append((sql, params)) def fetchone(self): - return (self.estimate,) + value = self._estimates[self._estimate_idx] + self._estimate_idx += 1 + return (value,) def fetchall(self): return self.rows @@ -119,7 +125,11 @@ def test_execute_series_query_sets_statement_timeout(monkeypatch): end=T0 + timedelta(seconds=10), target_points=4000, ) - assert cursor.executed[0][0] == "SET LOCAL statement_timeout = 15000" + assert ( + cursor.executed[0][0] + == "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY" + ) + assert cursor.executed[1][0] == "SET LOCAL statement_timeout = 15000" assert result["INV_Motor_Temp"]["mode"] == "raw" assert result["INV_Motor_Temp"]["v"] == [47.2] @@ -145,3 +155,33 @@ def test_execute_series_query_shapes_envelope(monkeypatch): assert series["min"] == [1.0] assert series["max"] == [3.0] assert series["avg"] == [2.0] + + +def test_execute_series_query_rejects_projected_total_before_fetch(monkeypatch): + monkeypatch.setattr(sq, "MAX_TOTAL_POINTS", 100) + # Two raw estimates: 60 + 60 = 120 > 100; never reach data fetch. + cursor = FakeCursor([60, 60]) + monkeypatch.setattr( + sq.psycopg2, + "connect", + lambda _dsn: FakeConnection(cursor), + ) + with pytest.raises(ValueError, match="estimated response exceeds"): + sq.execute_series_query( + postgres_dsn="unused", + table="wfr26", + signals=["A", "B"], + start=T0, + end=T0 + timedelta(seconds=10), + target_points=4000, + ) + sqls = [sql for sql, _ in cursor.executed] + assert sqls[0] == "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY" + assert sqls[1] == "SET LOCAL statement_timeout = 15000" + assert "COUNT(*)" in sqls[2] + assert '"A"' in sqls[2] + assert "COUNT(*)" in sqls[3] + assert '"B"' in sqls[3] + assert len(sqls) == 4 + assert not any("ORDER BY" in sql for sql in sqls) + assert cursor._estimate_idx == 2 From 50239e9e4080cea758f22f01f16628bf9ebf354c Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 10:57:27 -0400 Subject: [PATCH 04/33] add analysis API client types and bounded cache --- .../frontend/package-lock.json | 5854 +++++++++++++++++ .../data-downloader/frontend/package.json | 15 +- .../src/analysis/series-cache.test.ts | 23 + .../frontend/src/analysis/series-cache.ts | 60 + .../data-downloader/frontend/src/api.test.ts | 59 + .../data-downloader/frontend/src/api.ts | 22 +- .../frontend/src/test-setup.ts | 1 + .../data-downloader/frontend/src/types.ts | 37 +- .../frontend/src/vite-env.d.ts | 5 + .../data-downloader/frontend/tsconfig.json | 7 +- .../data-downloader/frontend/vite.config.ts | 8 +- 11 files changed, 6079 insertions(+), 12 deletions(-) create mode 100644 server/installer/data-downloader/frontend/package-lock.json create mode 100644 server/installer/data-downloader/frontend/src/analysis/series-cache.test.ts create mode 100644 server/installer/data-downloader/frontend/src/analysis/series-cache.ts create mode 100644 server/installer/data-downloader/frontend/src/api.test.ts create mode 100644 server/installer/data-downloader/frontend/src/test-setup.ts create mode 100644 server/installer/data-downloader/frontend/src/vite-env.d.ts diff --git a/server/installer/data-downloader/frontend/package-lock.json b/server/installer/data-downloader/frontend/package-lock.json new file mode 100644 index 00000000..10488d3b --- /dev/null +++ b/server/installer/data-downloader/frontend/package-lock.json @@ -0,0 +1,5854 @@ +{ + "name": "data-downloader-webapp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "data-downloader-webapp", + "version": "0.1.0", + "dependencies": { + "lucide-react": "^0.446.0", + "luxon": "^3.5.0", + "papaparse": "^5.4.1", + "plotly.js-basic-dist": "^2.35.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-plotly.js": "^2.6.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^18.2.45", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.3.2", + "jsdom": "^29.1.1", + "typescript": "^5.3.3", + "vite": "^5.1.0", + "vitest": "^4.1.10" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@choojs/findup": { + "version": "0.2.1", + "license": "MIT", + "peer": true, + "dependencies": { + "commander": "^2.15.1" + }, + "bin": { + "findup": "bin/findup.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "license": "ISC", + "peer": true, + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/geojson-types": { + "version": "1.0.2", + "license": "ISC", + "peer": true + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "1.5.0", + "license": "BSD-3-Clause", + "peer": true, + "peerDependencies": { + "mapbox-gl": ">=0.32.1 <2.0.0" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "license": "ISC", + "peer": true + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "1.2.5", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.0", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "20.4.0", + "license": "ISC", + "peer": true, + "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": "^2.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/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { + "version": "3.0.0", + "license": "ISC", + "peer": true + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@plotly/d3": { + "version": "3.8.2", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@plotly/d3-sankey": { + "version": "0.7.2", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "1", + "d3-collection": "1", + "d3-shape": "^1.2.0" + } + }, + "node_modules/@plotly/d3-sankey-circular": { + "version": "0.33.1", + "license": "MIT", + "peer": true, + "dependencies": { + "d3-array": "^1.2.1", + "d3-collection": "^1.0.4", + "d3-shape": "^1.2.0", + "elementary-circuits-directed-graph": "^1.0.4" + } + }, + "node_modules/@plotly/mapbox-gl": { + "version": "1.13.4", + "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/@plotly/point-cluster": { + "version": "3.1.9", + "license": "MIT", + "peer": true, + "dependencies": { + "array-bounds": "^1.0.1", + "binary-search-bounds": "^2.0.4", + "clamp": "^1.0.1", + "defined": "^1.0.0", + "dtype": "^2.0.0", + "flatten-vertex-data": "^1.0.2", + "is-obj": "^1.0.1", + "math-log2": "^1.0.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/@plotly/regl": { + "version": "2.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@turf/area": { + "version": "7.3.5", + "license": "MIT", + "peer": true, + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "7.3.5", + "license": "MIT", + "peer": true, + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/centroid": { + "version": "7.3.5", + "license": "MIT", + "peer": true, + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.5", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.3.5", + "license": "MIT", + "peer": true, + "dependencies": { + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "license": "MIT", + "peer": true + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "license": "MIT", + "peer": true + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "license": "MIT", + "peer": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.29", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "license": "MIT", + "peer": true + }, + "node_modules/acorn": { + "version": "7.4.1", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-bounds": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-normalize": { + "version": "1.1.4", + "license": "MIT", + "peer": true, + "dependencies": { + "array-bounds": "^1.0.0" + } + }, + "node_modules/array-range": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-search-bounds": { + "version": "2.0.5", + "license": "MIT", + "peer": true + }, + "node_modules/bit-twiddle": { + "version": "1.0.2", + "license": "MIT", + "peer": true + }, + "node_modules/bitmap-sdf": { + "version": "1.0.4", + "license": "MIT", + "peer": true + }, + "node_modules/bl": { + "version": "2.2.1", + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "dev": true, + "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/clamp": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/color-alpha": { + "version": "1.1.3", + "license": "MIT", + "peer": true, + "dependencies": { + "color-parse": "^1.4.1" + } + }, + "node_modules/color-alpha/node_modules/color-name": { + "version": "1.1.4", + "license": "MIT", + "peer": true + }, + "node_modules/color-alpha/node_modules/color-parse": { + "version": "1.4.3", + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-id": { + "version": "1.1.0", + "license": "MIT", + "peer": true, + "dependencies": { + "clamp": "^1.0.1" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-normalize": { + "version": "1.5.0", + "license": "MIT", + "peer": true, + "dependencies": { + "clamp": "^1.0.1", + "color-rgba": "^2.1.1", + "dtype": "^2.0.0" + } + }, + "node_modules/color-normalize/node_modules/color-name": { + "version": "1.1.4", + "license": "MIT", + "peer": true + }, + "node_modules/color-normalize/node_modules/color-parse": { + "version": "1.4.3", + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-normalize/node_modules/color-rgba": { + "version": "2.4.0", + "license": "MIT", + "peer": true, + "dependencies": { + "color-parse": "^1.4.2", + "color-space": "^2.0.0" + } + }, + "node_modules/color-parse": { + "version": "2.0.2", + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "^2.0.0" + } + }, + "node_modules/color-rgba": { + "version": "3.0.0", + "license": "MIT", + "peer": true, + "dependencies": { + "color-parse": "^2.0.0", + "color-space": "^2.0.0" + } + }, + "node_modules/color-space": { + "version": "2.3.2", + "license": "Unlicense", + "peer": true + }, + "node_modules/commander": { + "version": "2.20.3", + "license": "MIT", + "peer": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "license": "MIT", + "peer": true + }, + "node_modules/country-regex": { + "version": "1.1.0", + "license": "MIT", + "peer": true + }, + "node_modules/css-font": { + "version": "1.2.0", + "license": "MIT", + "peer": true, + "dependencies": { + "css-font-size-keywords": "^1.0.0", + "css-font-stretch-keywords": "^1.0.1", + "css-font-style-keywords": "^1.0.1", + "css-font-weight-keywords": "^1.0.0", + "css-global-keywords": "^1.0.1", + "css-system-font-keywords": "^1.0.0", + "pick-by-alias": "^1.2.0", + "string-split-by": "^1.0.0", + "unquote": "^1.1.0" + } + }, + "node_modules/css-font-size-keywords": { + "version": "1.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/css-font-stretch-keywords": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/css-font-style-keywords": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/css-font-weight-keywords": { + "version": "1.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/css-global-keywords": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/css-system-font-keywords": { + "version": "1.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "license": "MIT", + "peer": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.2", + "license": "ISC", + "peer": true, + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/d3-array": { + "version": "1.2.4", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-color": { + "version": "3.1.0", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "1.0.6", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-force": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/d3-format": { + "version": "1.4.5", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-geo": { + "version": "1.12.1", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-geo-projection": { + "version": "2.9.0", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "commander": "2", + "d3-array": "1", + "d3-geo": "^1.12.0", + "resolve": "^1.1.10" + }, + "bin": { + "geo2svg": "bin/geo2svg", + "geograticule": "bin/geograticule", + "geoproject": "bin/geoproject", + "geoquantize": "bin/geoquantize", + "geostitch": "bin/geostitch" + } + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "license": "ISC", + "peer": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-quadtree": { + "version": "1.0.7", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-time": { + "version": "1.1.0", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/d3-time-format": { + "version": "2.3.0", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/d3-timer": { + "version": "1.0.10", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/defined": { + "version": "1.0.1", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-kerning": { + "version": "2.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/draw-svg-path": { + "version": "1.0.0", + "license": "MIT", + "peer": true, + "dependencies": { + "abs-svg-path": "~0.1.1", + "normalize-svg-path": "~0.1.0" + } + }, + "node_modules/dtype": { + "version": "2.0.0", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/dup": { + "version": "1.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/duplexify": { + "version": "3.7.1", + "license": "MIT", + "peer": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/earcut": { + "version": "2.2.4", + "license": "ISC", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "dev": true, + "license": "ISC" + }, + "node_modules/elementary-circuits-directed-graph": { + "version": "1.3.1", + "license": "MIT", + "peer": true, + "dependencies": { + "strongly-connected-components": "^1.0.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "license": "MIT", + "peer": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "hasInstallScript": true, + "license": "ISC", + "peer": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "license": "MIT", + "peer": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "license": "ISC", + "peer": true, + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "license": "ISC", + "peer": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "license": "ISC", + "peer": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "license": "MIT", + "peer": true, + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/events": { + "version": "3.3.0", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "license": "ISC", + "peer": true, + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/falafel": { + "version": "2.2.5", + "license": "MIT", + "peer": true, + "dependencies": { + "acorn": "^7.1.1", + "isarray": "^2.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fast-isnumeric": { + "version": "1.1.4", + "license": "MIT", + "peer": true, + "dependencies": { + "is-string-blank": "^1.0.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flatten-vertex-data": { + "version": "1.0.2", + "license": "MIT", + "peer": true, + "dependencies": { + "dtype": "^2.0.0" + } + }, + "node_modules/font-atlas": { + "version": "2.1.0", + "license": "MIT", + "peer": true, + "dependencies": { + "css-font": "^1.0.0" + } + }, + "node_modules/font-measure": { + "version": "1.2.2", + "license": "MIT", + "peer": true, + "dependencies": { + "css-font": "^1.2.0" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/geojson-vt": { + "version": "3.2.1", + "license": "ISC", + "peer": true + }, + "node_modules/get-canvas-context": { + "version": "1.0.2", + "license": "MIT", + "peer": true + }, + "node_modules/get-stream": { + "version": "6.0.1", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gl-mat4": { + "version": "1.2.0", + "license": "Zlib", + "peer": true + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "license": "MIT", + "peer": true + }, + "node_modules/gl-text": { + "version": "1.4.0", + "license": "MIT", + "peer": true, + "dependencies": { + "bit-twiddle": "^1.0.2", + "color-normalize": "^1.5.0", + "css-font": "^1.2.0", + "detect-kerning": "^2.1.2", + "es6-weak-map": "^2.0.3", + "flatten-vertex-data": "^1.0.2", + "font-atlas": "^2.1.0", + "font-measure": "^1.2.2", + "gl-util": "^3.1.2", + "is-plain-obj": "^1.1.0", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "parse-unit": "^1.0.1", + "pick-by-alias": "^1.2.0", + "regl": "^2.0.0", + "to-px": "^1.0.1", + "typedarray-pool": "^1.1.0" + } + }, + "node_modules/gl-util": { + "version": "3.1.3", + "license": "MIT", + "peer": true, + "dependencies": { + "is-browser": "^2.0.1", + "is-firefox": "^1.0.3", + "is-plain-obj": "^1.1.0", + "number-is-integer": "^1.0.1", + "object-assign": "^4.1.0", + "pick-by-alias": "^1.2.0", + "weak-map": "^1.0.5" + } + }, + "node_modules/global-prefix": { + "version": "4.0.0", + "license": "MIT", + "peer": true, + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/glsl-inject-defines": { + "version": "1.0.3", + "license": "MIT", + "peer": true, + "dependencies": { + "glsl-token-inject-block": "^1.0.0", + "glsl-token-string": "^1.0.1", + "glsl-tokenizer": "^2.0.2" + } + }, + "node_modules/glsl-resolve": { + "version": "0.0.1", + "license": "MIT", + "peer": true, + "dependencies": { + "resolve": "^0.6.1", + "xtend": "^2.1.2" + } + }, + "node_modules/glsl-resolve/node_modules/resolve": { + "version": "0.6.3", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-resolve/node_modules/xtend": { + "version": "2.2.0", + "peer": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/glsl-token-assignments": { + "version": "2.0.2", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-token-defines": { + "version": "1.0.0", + "license": "MIT", + "peer": true, + "dependencies": { + "glsl-tokenizer": "^2.0.0" + } + }, + "node_modules/glsl-token-depth": { + "version": "1.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-token-descope": { + "version": "1.0.2", + "license": "MIT", + "peer": true, + "dependencies": { + "glsl-token-assignments": "^2.0.0", + "glsl-token-depth": "^1.1.0", + "glsl-token-properties": "^1.0.0", + "glsl-token-scope": "^1.1.0" + } + }, + "node_modules/glsl-token-inject-block": { + "version": "1.1.0", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-token-properties": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-token-scope": { + "version": "1.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-token-string": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-token-whitespace-trim": { + "version": "1.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-tokenizer": { + "version": "2.1.5", + "license": "MIT", + "peer": true, + "dependencies": { + "through2": "^0.6.3" + } + }, + "node_modules/glsl-tokenizer/node_modules/isarray": { + "version": "0.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-tokenizer/node_modules/readable-stream": { + "version": "1.0.34", + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/glsl-tokenizer/node_modules/string_decoder": { + "version": "0.10.31", + "license": "MIT", + "peer": true + }, + "node_modules/glsl-tokenizer/node_modules/through2": { + "version": "0.6.5", + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/glslify": { + "version": "7.1.1", + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^2.2.1", + "concat-stream": "^1.5.2", + "duplexify": "^3.4.5", + "falafel": "^2.1.0", + "from2": "^2.3.0", + "glsl-resolve": "0.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glslify-bundle": "^5.0.0", + "glslify-deps": "^1.2.5", + "minimist": "^1.2.5", + "resolve": "^1.1.5", + "stack-trace": "0.0.9", + "static-eval": "^2.0.5", + "through2": "^2.0.1", + "xtend": "^4.0.0" + }, + "bin": { + "glslify": "bin.js" + } + }, + "node_modules/glslify-bundle": { + "version": "5.1.1", + "license": "MIT", + "peer": true, + "dependencies": { + "glsl-inject-defines": "^1.0.1", + "glsl-token-defines": "^1.0.0", + "glsl-token-depth": "^1.1.1", + "glsl-token-descope": "^1.0.2", + "glsl-token-scope": "^1.1.1", + "glsl-token-string": "^1.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glsl-tokenizer": "^2.0.2", + "murmurhash-js": "^1.0.0", + "shallow-copy": "0.0.1" + } + }, + "node_modules/glslify-deps": { + "version": "1.3.2", + "license": "ISC", + "peer": true, + "dependencies": { + "@choojs/findup": "^0.2.0", + "events": "^3.2.0", + "glsl-resolve": "0.0.1", + "glsl-tokenizer": "^2.0.0", + "graceful-fs": "^4.1.2", + "inherits": "^2.0.1", + "map-limit": "0.0.1", + "resolve": "^1.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC", + "peer": true + }, + "node_modules/grid-index": { + "version": "1.1.0", + "license": "ISC", + "peer": true + }, + "node_modules/has-hover": { + "version": "1.0.1", + "license": "MIT", + "peer": true, + "dependencies": { + "is-browser": "^2.0.1" + } + }, + "node_modules/has-passive-events": { + "version": "1.0.0", + "license": "MIT", + "peer": true, + "dependencies": { + "is-browser": "^2.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC", + "peer": true + }, + "node_modules/ini": { + "version": "4.1.3", + "license": "ISC", + "peer": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-browser": { + "version": "2.1.0", + "license": "MIT", + "peer": true + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "license": "MIT", + "peer": true, + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-firefox": { + "version": "1.0.3", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-mobile": { + "version": "4.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/is-obj": { + "version": "1.0.1", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-string-blank": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/is-svg-path": { + "version": "1.0.2", + "license": "MIT", + "peer": true + }, + "node_modules/isarray": { + "version": "2.0.5", + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "3.1.5", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kdbush": { + "version": "4.1.0", + "license": "ISC", + "peer": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "license": "MIT", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.446.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/map-limit": { + "version": "0.0.1", + "license": "MIT", + "peer": true, + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/map-limit/node_modules/once": { + "version": "1.3.3", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/mapbox-gl": { + "version": "1.13.3", + "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/maplibre-gl": { + "version": "4.7.1", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/@mapbox/tiny-sdf": { + "version": "2.2.0", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/maplibre-gl/node_modules/earcut": { + "version": "3.0.2", + "license": "ISC", + "peer": true + }, + "node_modules/maplibre-gl/node_modules/geojson-vt": { + "version": "4.0.3", + "license": "ISC", + "peer": true + }, + "node_modules/maplibre-gl/node_modules/potpack": { + "version": "2.1.0", + "license": "ISC", + "peer": true + }, + "node_modules/maplibre-gl/node_modules/quickselect": { + "version": "3.0.0", + "license": "ISC", + "peer": true + }, + "node_modules/maplibre-gl/node_modules/supercluster": { + "version": "8.0.1", + "license": "ISC", + "peer": true, + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/maplibre-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "license": "ISC", + "peer": true + }, + "node_modules/math-log2": { + "version": "1.0.1", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mouse-event-offset": { + "version": "3.0.2", + "license": "MIT", + "peer": true + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/nanoid": { + "version": "3.3.12", + "dev": true, + "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/native-promise-only": { + "version": "0.8.1", + "license": "MIT", + "peer": true + }, + "node_modules/needle": { + "version": "2.9.1", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "license": "ISC", + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.46", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-svg-path": { + "version": "0.1.0", + "license": "MIT", + "peer": true + }, + "node_modules/number-is-integer": { + "version": "1.0.1", + "license": "MIT", + "peer": true, + "dependencies": { + "is-finite": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/papaparse": { + "version": "5.5.3", + "license": "MIT" + }, + "node_modules/parenthesis": { + "version": "3.1.8", + "license": "MIT", + "peer": true + }, + "node_modules/parse-rect": { + "version": "1.2.0", + "license": "MIT", + "peer": true, + "dependencies": { + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/parse-unit": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "license": "MIT", + "peer": true + }, + "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": "3.3.0", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "license": "MIT", + "peer": true + }, + "node_modules/pick-by-alias": { + "version": "1.2.0", + "license": "MIT", + "peer": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plotly.js": { + "version": "3.5.1", + "license": "MIT", + "peer": true, + "dependencies": { + "@plotly/d3": "3.8.2", + "@plotly/d3-sankey": "0.7.2", + "@plotly/d3-sankey-circular": "0.33.1", + "@plotly/mapbox-gl": "1.13.4", + "@plotly/regl": "^2.1.2", + "@turf/area": "^7.1.0", + "@turf/bbox": "^7.1.0", + "@turf/centroid": "^7.1.0", + "base64-arraybuffer": "^1.0.2", + "color-normalize": "1.5.0", + "color-rgba": "3.0.0", + "country-regex": "^1.1.0", + "d3-force": "^1.2.1", + "d3-format": "^1.4.5", + "d3-geo": "^1.12.1", + "d3-geo-projection": "^2.9.0", + "d3-hierarchy": "^1.1.9", + "d3-interpolate": "^3.0.1", + "d3-time": "^1.1.0", + "d3-time-format": "^2.2.3", + "fast-isnumeric": "^1.1.4", + "gl-mat4": "^1.2.0", + "gl-text": "^1.4.0", + "has-hover": "^1.0.1", + "has-passive-events": "^1.0.0", + "is-mobile": "^4.0.0", + "maplibre-gl": "^4.7.1", + "mouse-event-offset": "^3.0.2", + "native-promise-only": "^0.8.1", + "parse-svg-path": "^0.1.2", + "point-in-polygon": "^1.1.0", + "polybooljs": "^1.2.2", + "probe-image-size": "^7.2.3", + "regl-error2d": "^2.0.12", + "regl-line2d": "^3.1.3", + "regl-scatter2d": "^3.3.1", + "regl-splom": "^1.0.14", + "strongly-connected-components": "^1.0.1", + "svg-path-sdf": "^1.1.3", + "tinycolor2": "^1.4.2", + "topojson-client": "^3.1.0", + "webgl-context": "^2.2.0", + "world-calendars": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/plotly.js-basic-dist": { + "version": "2.35.3", + "license": "MIT" + }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "license": "MIT", + "peer": true + }, + "node_modules/polybooljs": { + "version": "1.2.2", + "license": "MIT", + "peer": true + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "license": "ISC", + "peer": true + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/probe-image-size": { + "version": "7.3.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "lodash.merge": "^4.6.2", + "needle": "^2.5.2", + "stream-parser": "~0.3.1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/prop-types": { + "version": "15.8.1", + "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.1", + "license": "MIT", + "peer": true + }, + "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/quickselect": { + "version": "2.0.0", + "license": "ISC", + "peer": true + }, + "node_modules/raf": { + "version": "3.4.1", + "license": "MIT", + "peer": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, + "node_modules/react-plotly.js": { + "version": "2.6.0", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "plotly.js": ">1.34.0", + "react": ">0.13.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regl": { + "version": "2.1.1", + "license": "MIT", + "peer": true + }, + "node_modules/regl-error2d": { + "version": "2.0.12", + "license": "MIT", + "peer": true, + "dependencies": { + "array-bounds": "^1.0.1", + "color-normalize": "^1.5.0", + "flatten-vertex-data": "^1.0.2", + "object-assign": "^4.1.1", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0", + "update-diff": "^1.1.0" + } + }, + "node_modules/regl-line2d": { + "version": "3.1.3", + "license": "MIT", + "peer": true, + "dependencies": { + "array-bounds": "^1.0.1", + "array-find-index": "^1.0.2", + "array-normalize": "^1.1.4", + "color-normalize": "^1.5.0", + "earcut": "^2.1.5", + "es6-weak-map": "^2.0.3", + "flatten-vertex-data": "^1.0.2", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0" + } + }, + "node_modules/regl-scatter2d": { + "version": "3.4.0", + "license": "MIT", + "peer": true, + "dependencies": { + "@plotly/point-cluster": "^3.1.9", + "array-bounds": "^1.0.1", + "color-id": "^1.1.0", + "color-normalize": "^1.5.0", + "flatten-vertex-data": "^1.0.2", + "glslify": "^7.0.0", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0", + "update-diff": "^1.1.0" + } + }, + "node_modules/regl-splom": { + "version": "1.0.14", + "license": "MIT", + "peer": true, + "dependencies": { + "array-bounds": "^1.0.1", + "array-range": "^1.0.1", + "color-alpha": "^1.0.4", + "flatten-vertex-data": "^1.0.2", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "raf": "^3.4.1", + "regl-scatter2d": "^3.2.3" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "license": "MIT", + "peer": true, + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "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", + "peer": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/sax": { + "version": "1.6.0", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shallow-copy": { + "version": "0.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.9", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/static-eval": { + "version": "2.1.1", + "license": "MIT", + "peer": true, + "dependencies": { + "escodegen": "^2.1.0" + } + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-parser": { + "version": "0.3.1", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2" + } + }, + "node_modules/stream-parser/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/stream-parser/node_modules/ms": { + "version": "2.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "license": "MIT", + "peer": true + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/string-split-by": { + "version": "1.0.0", + "license": "MIT", + "peer": true, + "dependencies": { + "parenthesis": "^3.1.5" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strongly-connected-components": { + "version": "1.0.1", + "license": "MIT", + "peer": true + }, + "node_modules/supercluster": { + "version": "7.1.5", + "license": "ISC", + "peer": true, + "dependencies": { + "kdbush": "^3.0.0" + } + }, + "node_modules/supercluster/node_modules/kdbush": { + "version": "3.0.0", + "license": "ISC", + "peer": true + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "license": "ISC", + "peer": true + }, + "node_modules/svg-path-bounds": { + "version": "1.0.2", + "license": "MIT", + "peer": true, + "dependencies": { + "abs-svg-path": "^0.1.1", + "is-svg-path": "^1.0.1", + "normalize-svg-path": "^1.0.0", + "parse-svg-path": "^0.1.2" + } + }, + "node_modules/svg-path-bounds/node_modules/normalize-svg-path": { + "version": "1.1.0", + "license": "MIT", + "peer": true, + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, + "node_modules/svg-path-sdf": { + "version": "1.1.3", + "license": "MIT", + "peer": true, + "dependencies": { + "bitmap-sdf": "^1.0.0", + "draw-svg-path": "^1.0.0", + "is-svg-path": "^1.0.1", + "parse-svg-path": "^0.1.2", + "svg-path-bounds": "^1.0.1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "license": "MIT", + "peer": true + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "license": "ISC", + "peer": true + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-float32": { + "version": "1.1.0", + "license": "MIT", + "peer": true + }, + "node_modules/to-px": { + "version": "1.1.0", + "license": "MIT", + "peer": true, + "dependencies": { + "parse-unit": "^1.0.1" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "license": "ISC", + "peer": true, + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/type": { + "version": "2.7.3", + "license": "ISC", + "peer": true + }, + "node_modules/typedarray": { + "version": "0.0.6", + "license": "MIT", + "peer": true + }, + "node_modules/typedarray-pool": { + "version": "1.2.0", + "license": "MIT", + "peer": true, + "dependencies": { + "bit-twiddle": "^1.0.0", + "dup": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "license": "MIT", + "peer": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "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/update-diff": { + "version": "1.1.0", + "license": "MIT", + "peer": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT", + "peer": true + }, + "node_modules/vite": { + "version": "5.4.21", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "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", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.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 + }, + "@vitejs/devtools": { + "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/vt-pbf": { + "version": "3.1.3", + "license": "MIT", + "peer": true, + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/weak-map": { + "version": "1.0.8", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/webgl-context": { + "version": "2.2.0", + "license": "MIT", + "peer": true, + "dependencies": { + "get-canvas-context": "^1.0.1" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "4.0.0", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/world-calendars": { + "version": "1.0.4", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4.1.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC", + "peer": true + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + } + } +} diff --git a/server/installer/data-downloader/frontend/package.json b/server/installer/data-downloader/frontend/package.json index 011057ab..834ddcb6 100644 --- a/server/installer/data-downloader/frontend/package.json +++ b/server/installer/data-downloader/frontend/package.json @@ -6,22 +6,29 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "test:ci": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { - "luxon": "^3.5.0", "lucide-react": "^0.446.0", + "luxon": "^3.5.0", "papaparse": "^5.4.1", + "plotly.js-basic-dist": "^2.35.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "plotly.js-basic-dist": "^2.35.3", "react-plotly.js": "^2.6.0" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.3.2", + "jsdom": "^29.1.1", "typescript": "^5.3.3", - "vite": "^5.1.0" + "vite": "^5.1.0", + "vitest": "^4.1.10" } } diff --git a/server/installer/data-downloader/frontend/src/analysis/series-cache.test.ts b/server/installer/data-downloader/frontend/src/analysis/series-cache.test.ts new file mode 100644 index 00000000..dfd789b0 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/series-cache.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { SeriesCache, seriesCacheKey, targetPointsFor } from "./series-cache"; + +describe("series cache", () => { + it("makes signal order irrelevant and preserves exact range", () => { + expect(seriesCacheKey("wfr26", ["B", "A"], 1, 2, 4000)).toBe( + seriesCacheKey("wfr26", ["A", "B"], 1, 2, 4000), + ); + }); + + it("evicts least-recently-used entries to stay under budget", () => { + const cache = new SeriesCache(3); + cache.set("a", { A: { mode: "raw", resolution_ms: null, point_count: 2, t: [1, 2], v: [1, 2] } }); + cache.set("b", { B: { mode: "raw", resolution_ms: null, point_count: 2, t: [1, 2], v: [1, 2] } }); + expect(cache.get("a")).toBeUndefined(); + expect(cache.get("b")).toBeDefined(); + }); + + it("shares the million-point budget across signals", () => { + expect(targetPointsFor(1)).toBe(20_000); + expect(targetPointsFor(12)).toBeLessThanOrEqual(Math.floor(1_000_000 / 12)); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/analysis/series-cache.ts b/server/installer/data-downloader/frontend/src/analysis/series-cache.ts new file mode 100644 index 00000000..e6376972 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/series-cache.ts @@ -0,0 +1,60 @@ +import type { SignalSeries } from "../types"; + +export type SeriesMap = Record; + +const CLIENT_POINT_BUDGET = 1_000_000; +const MAX_TARGET_POINTS = 20_000; + +export function targetPointsFor(signalCount: number): number { + if (signalCount <= 0) return 0; + // Cap per-request density so the client stays within its shared point budget. + return Math.min(MAX_TARGET_POINTS, Math.floor(CLIENT_POINT_BUDGET / signalCount)); +} + +export function seriesCacheKey( + season: string, + signals: string[], + startMs: number, + endMs: number, + targetPoints: number, +): string { + // Sort signals so cache hits ignore selection order while keeping exact ranges. + return JSON.stringify([season, [...signals].sort(), startMs, endMs, targetPoints]); +} + +function representedPoints(series: SeriesMap): number { + return Object.values(series).reduce((sum, item) => sum + item.point_count, 0); +} + +export class SeriesCache { + private readonly entries = new Map(); + private points = 0; + + constructor(private readonly budget = CLIENT_POINT_BUDGET) {} + + get(key: string): SeriesMap | undefined { + const entry = this.entries.get(key); + if (!entry) return undefined; + // Re-insert to mark this key as most recently used. + this.entries.delete(key); + this.entries.set(key, entry); + return entry.value; + } + + set(key: string, value: SeriesMap): void { + const prior = this.entries.get(key); + if (prior) this.points -= prior.points; + this.entries.delete(key); + const points = representedPoints(value); + this.entries.set(key, { value, points }); + this.points += points; + while (this.points > this.budget && this.entries.size > 0) { + const oldest = this.entries.entries().next().value as + | [string, { value: SeriesMap; points: number }] + | undefined; + if (!oldest) break; + this.entries.delete(oldest[0]); + this.points -= oldest[1].points; + } + } +} diff --git a/server/installer/data-downloader/frontend/src/api.test.ts b/server/installer/data-downloader/frontend/src/api.test.ts new file mode 100644 index 00000000..d2d1f519 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/api.test.ts @@ -0,0 +1,59 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { querySeries } from "./api"; + +describe("querySeries", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("POSTs JSON body to /api/series", async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + season: "wfr26", + start: "2026-01-01T00:00:00Z", + end: "2026-01-01T01:00:00Z", + series: {}, + }), + }); + vi.stubGlobal("fetch", fetchMock); + + const payload = { + season: "wfr26", + signals: ["A", "B"], + start: "2026-01-01T00:00:00Z", + end: "2026-01-01T01:00:00Z", + target_points: 4000, + }; + await querySeries(payload); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe("/api/series"); + expect(init.method).toBe("POST"); + expect(JSON.parse(init.body)).toEqual(payload); + }); + + it("surfaces server detail on errors", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: false, + status: 503, + text: async () => JSON.stringify({ detail: "TimescaleDB unavailable" }), + }), + ); + + await expect( + querySeries({ + season: "wfr26", + signals: ["A"], + start: "2026-01-01T00:00:00Z", + end: "2026-01-01T01:00:00Z", + target_points: 4000, + }), + ).rejects.toThrow("TimescaleDB unavailable"); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/api.ts b/server/installer/data-downloader/frontend/src/api.ts index 30e4a50b..e2a7d21b 100644 --- a/server/installer/data-downloader/frontend/src/api.ts +++ b/server/installer/data-downloader/frontend/src/api.ts @@ -5,7 +5,9 @@ import { Season, SensorDataResponse, SensorsGroupedResponse, - SensorsResponse + SensorsResponse, + SeriesRequest, + SeriesResponse } from "./types"; const RAW_API_BASE = import.meta.env.VITE_API_BASE_URL?.trim() ?? ""; @@ -26,8 +28,15 @@ async function request(path: string, init?: RequestInit): Promise { ...init }); if (!response.ok) { - const message = await response.text(); - throw new Error(message || `Request failed (${response.status})`); + const body = await response.text(); + let message = body || `Request failed (${response.status})`; + try { + const parsed = JSON.parse(body) as { detail?: string }; + if (parsed.detail) message = parsed.detail; + } catch { + // Preserve non-JSON server errors. + } + throw new Error(message); } if (response.status === 204) { return {} as T; @@ -90,3 +99,10 @@ export function querySensorData(payload: DataQueryPayload, season?: string): Pro body: JSON.stringify(payload) }); } + +export function querySeries(payload: SeriesRequest): Promise { + return request("/api/series", { + method: "POST", + body: JSON.stringify(payload), + }); +} diff --git a/server/installer/data-downloader/frontend/src/test-setup.ts b/server/installer/data-downloader/frontend/src/test-setup.ts new file mode 100644 index 00000000..f149f27a --- /dev/null +++ b/server/installer/data-downloader/frontend/src/test-setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/server/installer/data-downloader/frontend/src/types.ts b/server/installer/data-downloader/frontend/src/types.ts index e39195f7..7f9fa238 100644 --- a/server/installer/data-downloader/frontend/src/types.ts +++ b/server/installer/data-downloader/frontend/src/types.ts @@ -49,10 +49,45 @@ export interface ScannerStatus { export interface Season { name: string; year: number; - database: string; + table: string; color?: string; } +export interface RawSignalSeries { + mode: "raw"; + resolution_ms: null; + point_count: number; + t: number[]; + v: number[]; +} + +export interface EnvelopeSignalSeries { + mode: "envelope"; + resolution_ms: number; + point_count: number; + t: number[]; + min: number[]; + max: number[]; + avg: number[]; +} + +export type SignalSeries = RawSignalSeries | EnvelopeSignalSeries; + +export interface SeriesRequest { + season: string; + signals: string[]; + start: string; + end: string; + target_points: number; +} + +export interface SeriesResponse { + season: string; + start: string; + end: string; + series: Record; +} + export interface MessageGroup { name: string; subsystem: string; diff --git a/server/installer/data-downloader/frontend/src/vite-env.d.ts b/server/installer/data-downloader/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..8f3b5cee --- /dev/null +++ b/server/installer/data-downloader/frontend/src/vite-env.d.ts @@ -0,0 +1,5 @@ +/// + +declare module "luxon"; +declare module "papaparse"; +declare module "react-plotly.js"; diff --git a/server/installer/data-downloader/frontend/tsconfig.json b/server/installer/data-downloader/frontend/tsconfig.json index 769017e4..9c7ab563 100644 --- a/server/installer/data-downloader/frontend/tsconfig.json +++ b/server/installer/data-downloader/frontend/tsconfig.json @@ -1,9 +1,12 @@ { "extends": "./tsconfig.node.json", "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], "jsx": "react-jsx", - "types": ["vite/client"], - "strict": true + "types": ["vite/client", "vitest/globals"], + "strict": true, + "noEmit": true }, "include": ["src"] } diff --git a/server/installer/data-downloader/frontend/vite.config.ts b/server/installer/data-downloader/frontend/vite.config.ts index e8968ec6..259534fa 100644 --- a/server/installer/data-downloader/frontend/vite.config.ts +++ b/server/installer/data-downloader/frontend/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from "vite"; +import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; const apiBase = process.env.VITE_API_BASE_URL || "http://localhost:8000"; @@ -14,5 +14,9 @@ export default defineConfig({ changeOrigin: true } } - } + }, + test: { + environment: "jsdom", + setupFiles: ["./src/test-setup.ts"], + }, }); From 23032267c4ebc2250f0558a5c30de7b86c2c3eb6 Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:01:17 -0400 Subject: [PATCH 05/33] Pin Vitest/jsdom for Node 18 and harden SeriesCache oversize inserts --- .../frontend/package-lock.json | 2722 ++++++++--------- .../data-downloader/frontend/package.json | 8 +- .../src/analysis/series-cache.test.ts | 57 +- .../frontend/src/analysis/series-cache.ts | 5 +- .../data-downloader/frontend/src/api.test.ts | 42 + .../frontend/src/components/data-download.tsx | 92 +- .../frontend/src/vite-env.d.ts | 4 - 7 files changed, 1475 insertions(+), 1455 deletions(-) diff --git a/server/installer/data-downloader/frontend/package-lock.json b/server/installer/data-downloader/frontend/package-lock.json index 10488d3b..5ce7637e 100644 --- a/server/installer/data-downloader/frontend/package-lock.json +++ b/server/installer/data-downloader/frontend/package-lock.json @@ -19,13 +19,17 @@ "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@types/luxon": "^3.4.2", + "@types/papaparse": "^5.3.15", + "@types/plotly.js": "^2.33.4", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", + "@types/react-plotly.js": "^2.6.3", "@vitejs/plugin-react": "^4.3.2", - "jsdom": "^29.1.1", + "jsdom": "24.1.3", "typescript": "^5.3.3", "vite": "^5.1.0", - "vitest": "^4.1.10" + "vitest": "1.6.1" } }, "node_modules/@adobe/css-tools": { @@ -36,55 +40,25 @@ "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, "node_modules/@babel/code-frame": { "version": "7.29.7", @@ -340,19 +314,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, "node_modules/@choojs/findup": { "version": "0.2.1", "license": "MIT", @@ -365,9 +326,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, "funding": [ { @@ -381,13 +342,13 @@ ], "license": "MIT-0", "engines": { - "node": ">=20.19.0" + "node": ">=18" } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, "funding": [ { @@ -401,17 +362,17 @@ ], "license": "MIT", "engines": { - "node": ">=20.19.0" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", - "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, "funding": [ { @@ -425,21 +386,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.2.1" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { - "node": ">=20.19.0" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -453,41 +414,16 @@ ], "license": "MIT", "engines": { - "node": ">=20.19.0" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", - "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -501,41 +437,7 @@ ], "license": "MIT", "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "node": ">=18" } }, "node_modules/@esbuild/aix-ppc64": { @@ -927,22 +829,17 @@ "node": ">=12" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" + "dependencies": { + "@sinclair/typebox": "^0.27.8" }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1077,35 +974,6 @@ "license": "ISC", "peer": true }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@plotly/d3": { "version": "3.8.2", "license": "BSD-3-Clause", @@ -1186,27 +1054,41 @@ "license": "MIT", "peer": true }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", "cpu": [ "arm64" ], @@ -1215,15 +1097,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ "x64" ], @@ -1232,51 +1111,42 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "freebsd" + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ - "arm64" + "arm" ], "dev": true, "libc": [ @@ -1286,17 +1156,14 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", "cpu": [ - "arm64" + "arm" ], "dev": true, "libc": [ @@ -1306,17 +1173,14 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "libc": [ @@ -1326,37 +1190,31 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "libc": [ - "glibc" + "musl" ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", "cpu": [ - "x64" + "loong64" ], "dev": true, "libc": [ @@ -1366,17 +1224,14 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", "cpu": [ - "x64" + "loong64" ], "dev": true, "libc": [ @@ -1386,297 +1241,35 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "linux" + ] }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", "cpu": [ - "wasm32" + "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" + "libc": [ + "musl" ], "license": "MIT", "optional": true, @@ -1853,10 +1446,10 @@ "win32" ] }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", "dev": true, "license": "MIT" }, @@ -2003,17 +1596,6 @@ "url": "https://opencollective.com/turf" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -2059,24 +1641,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "dev": true, @@ -2095,6 +1659,13 @@ "@types/geojson": "*" } }, + "node_modules/@types/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-gW+Oib+vUtGJBtNC8V9Reww0oIpusw+4m81uncg9REGZAJfqOQHfo/nkabnc7w0QReXyPqjrbWMJk6NuAkiX3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mapbox__point-geometry": { "version": "0.1.4", "license": "MIT", @@ -2110,11 +1681,38 @@ "@types/pbf": "*" } }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pbf": { "version": "3.0.5", "license": "MIT", "peer": true }, + "node_modules/@types/plotly.js": { + "version": "2.35.14", + "resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-2.35.14.tgz", + "integrity": "sha512-CcD/32JcK19+xWH4FFpmYez/5X9kOjUcBr8Hxh7gQ/3Z32gIoLLy/L9xvC7DG5YikPvJjq6QN05B9+MCRu/Ncw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "dev": true, @@ -2137,6 +1735,17 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-plotly.js": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/react-plotly.js/-/react-plotly.js-2.6.4.tgz", + "integrity": "sha512-AU6w1u3qEGM0NmBA69PaOgNc0KPFA/+qkH6Uu9EBTJ45/WYOUoXi9AF5O15PRM2klpHSiHAAs4WnlI+OZAFmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/plotly.js": "*", + "@types/react": "*" + } + }, "node_modules/@types/supercluster": { "version": "7.1.3", "license": "MIT", @@ -2165,91 +1774,123 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "node_modules/@vitest/snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@vitest/snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", "dev": true, "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/abs-svg-path": { "version": "0.1.1", "license": "MIT", @@ -2266,6 +1907,42 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk/node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2283,7 +1960,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -2328,15 +2004,22 @@ "peer": true }, "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": "*" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "license": "MIT", @@ -2356,16 +2039,6 @@ "node": ">=6.0.0" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/binary-search-bounds": { "version": "2.0.5", "license": "MIT", @@ -2427,6 +2100,30 @@ "license": "MIT", "peer": true }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "dev": true, @@ -2447,13 +2144,35 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { - "node": ">=18" + "node": "*" } }, "node_modules/clamp": { @@ -2552,6 +2271,19 @@ "license": "Unlicense", "peer": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "license": "MIT", @@ -2571,6 +2303,13 @@ "typedarray": "^0.0.6" } }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "dev": true, @@ -2586,6 +2325,44 @@ "license": "MIT", "peer": true }, + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/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/cross-spawn/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/css-font": { "version": "1.2.0", "license": "MIT", @@ -2632,20 +2409,6 @@ "license": "MIT", "peer": true }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -2658,6 +2421,27 @@ "license": "MIT", "peer": true }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "dev": true, @@ -2793,17 +2577,17 @@ "peer": true }, "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" } }, "node_modules/debug": { @@ -2829,6 +2613,19 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/defined": { "version": "1.0.1", "license": "MIT", @@ -2837,6 +2634,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2852,14 +2659,14 @@ "license": "MIT", "peer": true }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/dom-accessibility-api": { @@ -2887,6 +2694,21 @@ "node": ">= 0.8.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/dup": { "version": "1.0.0", "license": "MIT", @@ -2930,32 +2752,63 @@ } }, "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=20.19.0" + "node": ">=0.12" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, + "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", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, - "license": "MIT" + "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/es5-ext": { "version": "0.10.64", @@ -3139,14 +2992,41 @@ "node": ">=0.8.x" } }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ext": { @@ -3177,24 +3057,6 @@ "is-string-blank": "^1.0.1" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/flatten-vertex-data": { "version": "1.0.2", "license": "MIT", @@ -3219,6 +3081,23 @@ "css-font": "^1.2.0" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/from2": { "version": "2.3.0", "license": "MIT", @@ -3246,29 +3125,77 @@ "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/geojson-vt": { + "version": "3.2.1", + "license": "ISC", + "peer": true + }, + "node_modules/get-canvas-context": { + "version": "1.0.2", + "license": "MIT", + "peer": true + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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": ">=6.9.0" + "node": ">= 0.4" } }, - "node_modules/geojson-vt": { - "version": "3.2.1", - "license": "ISC", - "peer": true - }, - "node_modules/get-canvas-context": { - "version": "1.0.2", - "license": "MIT", - "peer": true - }, "node_modules/get-stream": { "version": "6.0.1", "license": "MIT", @@ -3521,6 +3448,19 @@ "resolve": "^1.0.0" } }, + "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", "license": "ISC", @@ -3547,10 +3487,38 @@ "is-browser": "^2.0.1" } }, + "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.4", "license": "MIT", - "peer": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -3559,16 +3527,54 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.6.0" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" } }, "node_modules/iconv-lite": { @@ -3690,6 +3696,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string-blank": { "version": "1.0.1", "license": "MIT", @@ -3718,367 +3737,101 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-stringify-pretty-compact": { - "version": "4.0.0", - "license": "MIT", - "peer": true - }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kdbush": { - "version": "4.1.0", - "license": "ISC", - "peer": true - }, - "node_modules/kind-of": { - "version": "6.0.3", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], + "node_modules/jsesc": { + "version": "3.1.0", "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=6" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "license": "MIT", + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=6" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "node_modules/kdbush": { + "version": "4.1.0", + "license": "ISC", + "peer": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "license": "MIT", + "peer": true, "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=0.10.0" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=14" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/antfu" } }, "node_modules/lodash.merge": { @@ -4096,6 +3849,16 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "dev": true, @@ -4270,6 +4033,16 @@ "license": "ISC", "peer": true }, + "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/math-log2": { "version": "1.0.1", "license": "MIT", @@ -4278,12 +4051,48 @@ "node": ">=0.10.0" } }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "CC0-1.0" + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/min-indent": { "version": "1.0.1", @@ -4303,6 +4112,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/mlly/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/mouse-event-offset": { "version": "3.0.2", "license": "MIT", @@ -4381,6 +4223,35 @@ "license": "MIT", "peer": true }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/number-is-integer": { "version": "1.0.1", "license": "MIT", @@ -4392,6 +4263,13 @@ "node": ">=0.10.0" } }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -4399,20 +4277,6 @@ "node": ">=0.10.0" } }, - "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/once": { "version": "1.4.0", "license": "ISC", @@ -4421,6 +4285,38 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/papaparse": { "version": "5.5.3", "license": "MIT" @@ -4449,30 +4345,50 @@ "peer": true }, "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^8.0.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "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", "license": "MIT", "peer": true }, "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/pbf": { "version": "3.3.0", "license": "BSD-3-Clause", @@ -4500,19 +4416,25 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, + "node_modules/pkg-types/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/plotly.js": { "version": "3.5.1", "license": "MIT", @@ -4677,6 +4599,19 @@ "license": "MIT", "peer": true }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4687,6 +4622,13 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/quickselect": { "version": "2.0.0", "license": "ISC", @@ -4851,15 +4793,12 @@ "regl-scatter2d": "^3.2.3" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/resolve": { "version": "1.22.12", @@ -4889,47 +4828,6 @@ "protocol-buffers-schema": "^3.3.1" } }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, "node_modules/rollup": { "version": "4.60.4", "dev": true, @@ -4973,6 +4871,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/rw": { "version": "1.3.3", "license": "BSD-3-Clause", @@ -4999,8 +4904,7 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/sax": { "version": "1.6.0", @@ -5043,6 +4947,29 @@ "license": "MIT", "peer": true }, + "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/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -5050,6 +4977,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map": { "version": "0.6.1", "license": "BSD-3-Clause", @@ -5090,9 +5030,9 @@ } }, "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -5143,6 +5083,19 @@ "parenthesis": "^3.1.5" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -5156,6 +5109,26 @@ "node": ">=8" } }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strongly-connected-components": { "version": "1.0.1", "license": "MIT", @@ -5249,31 +5222,14 @@ "license": "MIT", "peer": true }, - "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=14.0.0" } }, "node_modules/tinyqueue": { @@ -5281,36 +5237,16 @@ "license": "ISC", "peer": true }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", - "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.9" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.9", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", - "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", - "dev": true, - "license": "MIT" - }, "node_modules/to-float32": { "version": "1.1.0", "license": "MIT", @@ -5338,40 +5274,54 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "tldts": "^7.0.5" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=16" + "node": ">=6" } }, "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/tslib": { "version": "2.8.1", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/type": { "version": "2.7.3", "license": "ISC", "peer": true }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/typedarray": { "version": "0.0.6", "license": "MIT", @@ -5398,14 +5348,28 @@ "node": ">=14.17" } }, - "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">= 4.0.0" } }, "node_modules/unquote": { @@ -5447,6 +5411,17 @@ "license": "MIT", "peer": true }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "license": "MIT", @@ -5510,80 +5485,82 @@ } } }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@opentelemetry/api": { - "optional": true - }, "@types/node": { "optional": true }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { + "@vitest/browser": { "optional": true }, "@vitest/ui": { @@ -5594,114 +5571,6 @@ }, "jsdom": { "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", - "tinyglobby": "^0.2.17" - }, - "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", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.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 - }, - "@vitejs/devtools": { - "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 } } }, @@ -5742,38 +5611,64 @@ } }, "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=20" + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" } }, "node_modules/which": { @@ -5820,6 +5715,28 @@ "license": "ISC", "peer": true }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -5849,6 +5766,19 @@ "version": "3.1.1", "dev": true, "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/server/installer/data-downloader/frontend/package.json b/server/installer/data-downloader/frontend/package.json index 834ddcb6..68b22316 100644 --- a/server/installer/data-downloader/frontend/package.json +++ b/server/installer/data-downloader/frontend/package.json @@ -23,12 +23,16 @@ "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@types/luxon": "^3.4.2", + "@types/papaparse": "^5.3.15", + "@types/plotly.js": "^2.33.4", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", + "@types/react-plotly.js": "^2.6.3", "@vitejs/plugin-react": "^4.3.2", - "jsdom": "^29.1.1", + "jsdom": "24.1.3", "typescript": "^5.3.3", "vite": "^5.1.0", - "vitest": "^4.1.10" + "vitest": "1.6.1" } } diff --git a/server/installer/data-downloader/frontend/src/analysis/series-cache.test.ts b/server/installer/data-downloader/frontend/src/analysis/series-cache.test.ts index dfd789b0..0a9d0050 100644 --- a/server/installer/data-downloader/frontend/src/analysis/series-cache.test.ts +++ b/server/installer/data-downloader/frontend/src/analysis/series-cache.test.ts @@ -1,6 +1,16 @@ import { describe, expect, it } from "vitest"; import { SeriesCache, seriesCacheKey, targetPointsFor } from "./series-cache"; +function rawSeries(pointCount: number) { + return { + mode: "raw" as const, + resolution_ms: null, + point_count: pointCount, + t: Array.from({ length: pointCount }, (_, i) => i), + v: Array.from({ length: pointCount }, (_, i) => i), + }; +} + describe("series cache", () => { it("makes signal order irrelevant and preserves exact range", () => { expect(seriesCacheKey("wfr26", ["B", "A"], 1, 2, 4000)).toBe( @@ -8,14 +18,57 @@ describe("series cache", () => { ); }); + it("keys differ by exact range and target", () => { + const base = seriesCacheKey("wfr26", ["A"], 1, 2, 4000); + expect(seriesCacheKey("wfr26", ["A"], 1, 3, 4000)).not.toBe(base); + expect(seriesCacheKey("wfr26", ["A"], 0, 2, 4000)).not.toBe(base); + expect(seriesCacheKey("wfr26", ["A"], 1, 2, 5000)).not.toBe(base); + }); + it("evicts least-recently-used entries to stay under budget", () => { const cache = new SeriesCache(3); - cache.set("a", { A: { mode: "raw", resolution_ms: null, point_count: 2, t: [1, 2], v: [1, 2] } }); - cache.set("b", { B: { mode: "raw", resolution_ms: null, point_count: 2, t: [1, 2], v: [1, 2] } }); + cache.set("a", { A: rawSeries(2) }); + cache.set("b", { B: rawSeries(2) }); expect(cache.get("a")).toBeUndefined(); expect(cache.get("b")).toBeDefined(); }); + it("promotes an entry to most-recently-used on read", () => { + const cache = new SeriesCache(4); + cache.set("a", { A: rawSeries(2) }); + cache.set("b", { B: rawSeries(2) }); + expect(cache.get("a")).toBeDefined(); + cache.set("c", { C: rawSeries(2) }); + expect(cache.get("a")).toBeDefined(); + expect(cache.get("b")).toBeUndefined(); + expect(cache.get("c")).toBeDefined(); + }); + + it("accounts for replacement points when updating the same key", () => { + const cache = new SeriesCache(5); + cache.set("a", { A: rawSeries(3) }); + cache.set("a", { A: rawSeries(1) }); + cache.set("b", { B: rawSeries(4) }); + expect(cache.get("a")).toBeDefined(); + expect(cache.get("b")).toBeDefined(); + }); + + it("rejects an oversize insert without mutating the cache", () => { + const cache = new SeriesCache(3); + cache.set("a", { A: rawSeries(2) }); + cache.set("oversize", { X: rawSeries(4) }); + expect(cache.get("a")).toEqual({ A: rawSeries(2) }); + expect(cache.get("oversize")).toBeUndefined(); + }); + + it("preserves the prior same-key value when a replacement is oversize", () => { + const cache = new SeriesCache(3); + const prior = { A: rawSeries(2) }; + cache.set("a", prior); + cache.set("a", { A: rawSeries(4) }); + expect(cache.get("a")).toEqual(prior); + }); + it("shares the million-point budget across signals", () => { expect(targetPointsFor(1)).toBe(20_000); expect(targetPointsFor(12)).toBeLessThanOrEqual(Math.floor(1_000_000 / 12)); diff --git a/server/installer/data-downloader/frontend/src/analysis/series-cache.ts b/server/installer/data-downloader/frontend/src/analysis/series-cache.ts index e6376972..917a5ccf 100644 --- a/server/installer/data-downloader/frontend/src/analysis/series-cache.ts +++ b/server/installer/data-downloader/frontend/src/analysis/series-cache.ts @@ -42,10 +42,13 @@ export class SeriesCache { } set(key: string, value: SeriesMap): void { + const points = representedPoints(value); + // Reject oversize inserts before mutating so useful entries stay cached. + if (points > this.budget) return; + const prior = this.entries.get(key); if (prior) this.points -= prior.points; this.entries.delete(key); - const points = representedPoints(value); this.entries.set(key, { value, points }); this.points += points; while (this.points > this.budget && this.entries.size > 0) { diff --git a/server/installer/data-downloader/frontend/src/api.test.ts b/server/installer/data-downloader/frontend/src/api.test.ts index d2d1f519..8fc288fa 100644 --- a/server/installer/data-downloader/frontend/src/api.test.ts +++ b/server/installer/data-downloader/frontend/src/api.test.ts @@ -56,4 +56,46 @@ describe("querySeries", () => { }), ).rejects.toThrow("TimescaleDB unavailable"); }); + + it("surfaces non-JSON error bodies as plain text", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: false, + status: 502, + text: async () => "Bad Gateway from upstream", + }), + ); + + await expect( + querySeries({ + season: "wfr26", + signals: ["A"], + start: "2026-01-01T00:00:00Z", + end: "2026-01-01T01:00:00Z", + target_points: 4000, + }), + ).rejects.toThrow("Bad Gateway from upstream"); + }); + + it("falls back when the error body is empty", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: false, + status: 500, + text: async () => "", + }), + ); + + await expect( + querySeries({ + season: "wfr26", + signals: ["A"], + start: "2026-01-01T00:00:00Z", + end: "2026-01-01T01:00:00Z", + target_points: 4000, + }), + ).rejects.toThrow("Request failed (500)"); + }); }); diff --git a/server/installer/data-downloader/frontend/src/components/data-download.tsx b/server/installer/data-downloader/frontend/src/components/data-download.tsx index 6e2996b5..f06a4e68 100644 --- a/server/installer/data-downloader/frontend/src/components/data-download.tsx +++ b/server/installer/data-downloader/frontend/src/components/data-download.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { DateTime } from "luxon"; import Papa from "papaparse"; import { Download } from "lucide-react"; +import type { Config, Data, Layout } from "plotly.js"; import Plot from "react-plotly.js"; import { RunRecord, SensorDataPoint, SensorDataResponse, SensorsGroupedResponse } from "../types"; @@ -226,57 +227,48 @@ export function DataDownload({ runs, sensors, sensorsGrouped, season, externalSe const chartGrid = isDark ? "#2c313a" : "#e5e7eb"; const chartLine = isDark ? "#60a5fa" : "#2563eb"; - const plotData = useMemo( - () => - series.length === 0 - ? [] - : [ - { - x: series.map((point) => point.time), - y: series.map((point) => point.value), - customdata: series.map((point) => toUtcTooltip(point.time)), - type: "scatter", - mode: "lines", - line: { color: chartLine, width: 2 }, - hovertemplate: "%{y}
%{customdata}", - name: selectedSensor || "Sensor" - } - ], - [series, selectedSensor, chartLine] - ); - - const plotLayout = useMemo( - () => ({ - autosize: true, - margin: { t: 10, r: 20, b: 40, l: 50, pad: 4 }, - hovermode: "x unified", - font: { color: chartFont }, - xaxis: { - title: "Time (UTC)", - type: "date", - tickformat: "%H:%M\n%b %d", - gridcolor: chartGrid, - zerolinecolor: chartGrid - }, - yaxis: { - title: selectedSensor || "Value", - zeroline: false, - gridcolor: chartGrid + const plotData = useMemo((): Data[] => { + if (series.length === 0) return []; + return [ + { + x: series.map((point) => point.time), + y: series.map((point) => point.value), + customdata: series.map((point) => toUtcTooltip(point.time)), + type: "scatter", + mode: "lines", + line: { color: chartLine, width: 2 }, + hovertemplate: "%{y}
%{customdata}", + name: selectedSensor || "Sensor", }, - paper_bgcolor: "rgba(0,0,0,0)", - plot_bgcolor: "rgba(0,0,0,0)" - }), - [selectedSensor, chartFont, chartGrid] - ); - - const plotConfig = useMemo( - () => ({ - responsive: true, - displaylogo: false, - modeBarButtonsToRemove: ["select2d", "lasso2d"] - }), - [] - ); + ]; + }, [series, selectedSensor, chartLine]); + + const plotLayout = useMemo((): Partial => ({ + autosize: true, + margin: { t: 10, r: 20, b: 40, l: 50, pad: 4 }, + hovermode: "x unified", + font: { color: chartFont }, + xaxis: { + title: "Time (UTC)", + type: "date", + tickformat: "%H:%M\n%b %d", + gridcolor: chartGrid, + zerolinecolor: chartGrid, + }, + yaxis: { + title: selectedSensor || "Value", + zeroline: false, + gridcolor: chartGrid, + }, + paper_bgcolor: "rgba(0,0,0,0)", + plot_bgcolor: "rgba(0,0,0,0)", + }), [selectedSensor, chartFont, chartGrid]); + + const plotConfig = useMemo((): Partial => ({ + responsive: true, + displaylogo: false, + modeBarButtonsToRemove: ["select2d", "lasso2d"], + }), []); const handleDownload = () => { if (series.length === 0) return; diff --git a/server/installer/data-downloader/frontend/src/vite-env.d.ts b/server/installer/data-downloader/frontend/src/vite-env.d.ts index 8f3b5cee..11f02fe2 100644 --- a/server/installer/data-downloader/frontend/src/vite-env.d.ts +++ b/server/installer/data-downloader/frontend/src/vite-env.d.ts @@ -1,5 +1 @@ /// - -declare module "luxon"; -declare module "papaparse"; -declare module "react-plotly.js"; From ae4f1510964da7aaa0a43bfd945a055189f2caa8 Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:03:00 -0400 Subject: [PATCH 06/33] Redirect TypeScript incremental build info under node_modules cache --- server/installer/data-downloader/frontend/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/installer/data-downloader/frontend/tsconfig.json b/server/installer/data-downloader/frontend/tsconfig.json index 9c7ab563..ba7e46ad 100644 --- a/server/installer/data-downloader/frontend/tsconfig.json +++ b/server/installer/data-downloader/frontend/tsconfig.json @@ -6,7 +6,8 @@ "jsx": "react-jsx", "types": ["vite/client", "vitest/globals"], "strict": true, - "noEmit": true + "noEmit": true, + "tsBuildInfoFile": "node_modules/.cache/tsconfig.tsbuildinfo" }, "include": ["src"] } From 04bd3baaeeb19ed72d1c86244f48b3d538d9c325 Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:06:35 -0400 Subject: [PATCH 07/33] add debounced cached series data hook --- .../src/analysis/use-series-data.test.tsx | 204 ++++++++++++++++++ .../frontend/src/analysis/use-series-data.ts | 128 +++++++++++ 2 files changed, 332 insertions(+) create mode 100644 server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx create mode 100644 server/installer/data-downloader/frontend/src/analysis/use-series-data.ts diff --git a/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx b/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx new file mode 100644 index 00000000..386e83b1 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx @@ -0,0 +1,204 @@ +import { act, renderHook } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { SeriesMap } from "./series-cache"; +import type { SeriesResponse } from "../types"; +import { useSeriesData } from "./use-series-data"; + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +function seriesResponse( + season: string, + signal: string, + value: number, +): SeriesResponse { + return { + season, + start: "s", + end: "e", + series: { + [signal]: { + mode: "raw", + resolution_ms: null, + point_count: 1, + t: [0], + v: [value], + }, + }, + }; +} + +function rawValues(seriesBySignal: SeriesMap, signal: string): number[] | undefined { + const series = seriesBySignal[signal]; + return series?.mode === "raw" ? series.v : undefined; +} + +vi.mock("../api", () => ({ + querySeries: vi.fn(), +})); + +import { querySeries } from "../api"; + +const querySeriesMock = vi.mocked(querySeries); + +describe("useSeriesData", () => { + beforeEach(() => { + vi.useFakeTimers(); + querySeriesMock.mockReset(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("debounces three rapid ranges into one latest-only fetch after 300ms", async () => { + querySeriesMock.mockResolvedValue(seriesResponse("wfr26", "A", 1)); + const { result } = renderHook(() => useSeriesData()); + + act(() => { + result.current.requestRange("wfr26", ["A"], 0, 1000); + result.current.requestRange("wfr26", ["A"], 0, 2000); + result.current.requestRange("wfr26", ["A"], 0, 3000); + }); + + expect(querySeriesMock).not.toHaveBeenCalled(); + await act(() => vi.advanceTimersByTimeAsync(299)); + expect(querySeriesMock).not.toHaveBeenCalled(); + await act(() => vi.advanceTimersByTimeAsync(1)); + + expect(querySeriesMock).toHaveBeenCalledTimes(1); + expect(querySeriesMock.mock.calls[0][0]).toMatchObject({ + season: "wfr26", + signals: ["A"], + start: new Date(0).toISOString(), + end: new Date(3000).toISOString(), + }); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([1]); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBeNull(); + }); + + it("serves an exact repeated range from cache without another fetch", async () => { + querySeriesMock.mockResolvedValue(seriesResponse("cache-hit", "A", 7)); + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("cache-hit", ["A"], 10, 20)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + expect(result.current.loading).toBe(false); + + act(() => result.current.requestRange("cache-hit", ["A"], 10, 20)); + await act(() => vi.advanceTimersByTimeAsync(300)); + + expect(querySeriesMock).toHaveBeenCalledTimes(1); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([7]); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBeNull(); + }); + + it("suppresses an older pending response after a newer request", async () => { + const first = deferred(); + const second = deferred(); + querySeriesMock + .mockReturnValueOnce(first.promise) + .mockReturnValueOnce(second.promise); + + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("stale", ["A"], 0, 1000)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + + act(() => result.current.requestRange("stale", ["A"], 0, 2000)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(querySeriesMock).toHaveBeenCalledTimes(2); + + await act(async () => { + first.resolve(seriesResponse("stale", "A", 1)); + await first.promise; + }); + expect(result.current.seriesBySignal.A).toBeUndefined(); + + await act(async () => { + second.resolve(seriesResponse("stale", "A", 2)); + await second.promise; + }); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([2]); + expect(result.current.loading).toBe(false); + }); + + it("retains prior seriesBySignal while a later range is loading", async () => { + const second = deferred(); + querySeriesMock + .mockResolvedValueOnce(seriesResponse("retain", "A", 11)) + .mockReturnValueOnce(second.promise); + + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("retain", ["A"], 100, 200)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([11]); + + act(() => result.current.requestRange("retain", ["A"], 100, 300)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(result.current.loading).toBe(true); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([11]); + + await act(async () => { + second.resolve(seriesResponse("retain", "A", 22)); + await second.promise; + }); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([22]); + expect(result.current.loading).toBe(false); + }); + + it("includes the request range in errors and retry repeats the latest request", async () => { + const startMs = 1_000; + const endMs = 2_000; + querySeriesMock.mockRejectedValueOnce(new Error("TimescaleDB unavailable")); + + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("retry-me", ["A"], startMs, endMs)); + await act(() => vi.advanceTimersByTimeAsync(300)); + + expect(result.current.error).toBe( + `TimescaleDB unavailable (${new Date(startMs).toISOString()} to ${new Date(endMs).toISOString()})`, + ); + expect(result.current.loading).toBe(false); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + + querySeriesMock.mockResolvedValueOnce(seriesResponse("retry-me", "A", 99)); + act(() => result.current.retry()); + await act(() => vi.advanceTimersByTimeAsync(300)); + + expect(querySeriesMock).toHaveBeenCalledTimes(2); + expect(querySeriesMock.mock.calls[1][0]).toMatchObject({ + season: "retry-me", + signals: ["A"], + start: new Date(startMs).toISOString(), + end: new Date(endMs).toISOString(), + }); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([99]); + expect(result.current.error).toBeNull(); + }); + + it("clears the debounce timer on unmount so no fetch runs", async () => { + querySeriesMock.mockResolvedValue(seriesResponse("unmount", "A", 1)); + const { result, unmount } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("unmount", ["A"], 0, 5000)); + unmount(); + await act(() => vi.advanceTimersByTimeAsync(500)); + + expect(querySeriesMock).not.toHaveBeenCalled(); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts b/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts new file mode 100644 index 00000000..1cf4e67a --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts @@ -0,0 +1,128 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { querySeries } from "../api"; +import { + SeriesCache, + seriesCacheKey, + targetPointsFor, + type SeriesMap, +} from "./series-cache"; + +export interface UseSeriesData { + seriesBySignal: SeriesMap; + loading: boolean; + error: string | null; + requestRange( + seasonTable: string, + signals: string[], + startMs: number, + endMs: number, + ): void; + retry(): void; +} + +interface SeriesRequestArgs { + seasonTable: string; + signals: string[]; + startMs: number; + endMs: number; +} + +const DEBOUNCE_MS = 300; +// Shared across hook instances so zoom revisits hit the same window budget. +const seriesCache = new SeriesCache(); + +export function useSeriesData(): UseSeriesData { + const [seriesBySignal, setSeriesBySignal] = useState({}); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const timerRef = useRef | null>(null); + const sequenceRef = useRef(0); + const mountedRef = useRef(true); + const lastRequestRef = useRef(null); + + const executeRequest = useCallback(async (req: SeriesRequestArgs) => { + const { seasonTable, signals, startMs, endMs } = req; + const targetPoints = targetPointsFor(signals.length); + const key = seriesCacheKey(seasonTable, signals, startMs, endMs, targetPoints); + const seq = ++sequenceRef.current; + + const cached = seriesCache.get(key); + if (cached) { + // Clear loading so a later cache hit cannot leave a prior fetch stuck true. + if (mountedRef.current && seq === sequenceRef.current) { + setSeriesBySignal(cached); + setLoading(false); + setError(null); + } + return; + } + + if (mountedRef.current && seq === sequenceRef.current) { + setLoading(true); + } + + try { + const response = await querySeries({ + season: seasonTable, + signals, + start: new Date(startMs).toISOString(), + end: new Date(endMs).toISOString(), + target_points: targetPoints, + }); + if (!mountedRef.current || seq !== sequenceRef.current) return; + seriesCache.set(key, response.series); + setSeriesBySignal(response.series); + setError(null); + } catch (err) { + if (!mountedRef.current || seq !== sequenceRef.current) return; + const message = err instanceof Error ? err.message : "failed to fetch series"; + setError( + `${message} (${new Date(startMs).toISOString()} to ${new Date(endMs).toISOString()})`, + ); + } finally { + if (mountedRef.current && seq === sequenceRef.current) { + setLoading(false); + } + } + }, []); + + const requestRange = useCallback( + (seasonTable: string, signals: string[], startMs: number, endMs: number) => { + // Copy signals so callers can mutate their array without changing the pending request. + const req: SeriesRequestArgs = { + seasonTable, + signals: [...signals], + startMs, + endMs, + }; + lastRequestRef.current = req; + if (timerRef.current !== null) clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => { + timerRef.current = null; + void executeRequest(req); + }, DEBOUNCE_MS); + }, + [executeRequest], + ); + + const retry = useCallback(() => { + const last = lastRequestRef.current; + if (!last) return; + // Resubmit the latest intentional range; cache applies only if that fetch already succeeded. + requestRange(last.seasonTable, last.signals, last.startMs, last.endMs); + }, [requestRange]); + + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + if (timerRef.current !== null) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }; + }, []); + + return { seriesBySignal, loading, error, requestRange, retry }; +} From e9139f7a49586a417777deb53ae40ff921972825 Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:09:16 -0400 Subject: [PATCH 08/33] Bump series request generation at schedule time to block debounce races Allocate generation when a range is queued, clear stale errors immediately, and reject superseded state updates so in-flight responses cannot overwrite the current UI. --- .../src/analysis/use-series-data.test.tsx | 110 ++++++++++++++++++ .../frontend/src/analysis/use-series-data.ts | 26 +++-- 2 files changed, 127 insertions(+), 9 deletions(-) diff --git a/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx b/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx index 386e83b1..41f2e72a 100644 --- a/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx +++ b/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx @@ -135,6 +135,71 @@ describe("useSeriesData", () => { expect(result.current.loading).toBe(false); }); + it("does not render A when A resolves after uncached B is scheduled but before B's timer", async () => { + const first = deferred(); + const second = deferred(); + querySeriesMock + .mockReturnValueOnce(first.promise) + .mockReturnValueOnce(second.promise); + + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("race-uncached", ["A"], 0, 1000)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + + act(() => result.current.requestRange("race-uncached", ["A"], 0, 2000)); + await act(async () => { + first.resolve(seriesResponse("race-uncached", "A", 1)); + await first.promise; + }); + expect(result.current.seriesBySignal.A).toBeUndefined(); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(querySeriesMock).toHaveBeenCalledTimes(2); + + await act(async () => { + second.resolve(seriesResponse("race-uncached", "A", 2)); + await second.promise; + }); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([2]); + expect(result.current.loading).toBe(false); + }); + + it("does not render A when A resolves after cached B is scheduled but before cache-hit timer", async () => { + const inflightA = deferred(); + querySeriesMock + .mockResolvedValueOnce(seriesResponse("race-cached", "A", 20)) + .mockReturnValueOnce(inflightA.promise); + + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("race-cached", ["A"], 0, 2000)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([20]); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + + act(() => result.current.requestRange("race-cached", ["A"], 0, 1000)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(querySeriesMock).toHaveBeenCalledTimes(2); + expect(result.current.loading).toBe(true); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([20]); + + act(() => result.current.requestRange("race-cached", ["A"], 0, 2000)); + await act(async () => { + inflightA.resolve(seriesResponse("race-cached", "A", 1)); + await inflightA.promise; + }); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([20]); + expect(querySeriesMock).toHaveBeenCalledTimes(2); + + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(querySeriesMock).toHaveBeenCalledTimes(2); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([20]); + expect(result.current.loading).toBe(false); + }); + it("retains prior seriesBySignal while a later range is loading", async () => { const second = deferred(); querySeriesMock @@ -160,6 +225,31 @@ describe("useSeriesData", () => { expect(result.current.loading).toBe(false); }); + it("clears stale error immediately when a new range or retry is scheduled", async () => { + const startMs = 1_000; + const endMs = 2_000; + const errorText = `TimescaleDB unavailable (${new Date(startMs).toISOString()} to ${new Date(endMs).toISOString()})`; + querySeriesMock.mockRejectedValueOnce(new Error("TimescaleDB unavailable")); + + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("clear-err", ["A"], startMs, endMs)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(result.current.error).toBe(errorText); + + act(() => result.current.requestRange("clear-err", ["A"], startMs, endMs + 1)); + expect(result.current.error).toBeNull(); + + querySeriesMock.mockRejectedValueOnce(new Error("TimescaleDB unavailable")); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(result.current.error).toBe( + `TimescaleDB unavailable (${new Date(startMs).toISOString()} to ${new Date(endMs + 1).toISOString()})`, + ); + + act(() => result.current.retry()); + expect(result.current.error).toBeNull(); + }); + it("includes the request range in errors and retry repeats the latest request", async () => { const startMs = 1_000; const endMs = 2_000; @@ -178,6 +268,7 @@ describe("useSeriesData", () => { querySeriesMock.mockResolvedValueOnce(seriesResponse("retry-me", "A", 99)); act(() => result.current.retry()); + expect(result.current.error).toBeNull(); await act(() => vi.advanceTimersByTimeAsync(300)); expect(querySeriesMock).toHaveBeenCalledTimes(2); @@ -191,6 +282,25 @@ describe("useSeriesData", () => { expect(result.current.error).toBeNull(); }); + it("retry after success serves the cached range without another network fetch", async () => { + querySeriesMock.mockResolvedValueOnce(seriesResponse("retry-cache", "A", 5)); + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("retry-cache", ["A"], 50, 60)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([5]); + expect(result.current.error).toBeNull(); + + act(() => result.current.retry()); + await act(() => vi.advanceTimersByTimeAsync(300)); + + expect(querySeriesMock).toHaveBeenCalledTimes(1); + expect(rawValues(result.current.seriesBySignal, "A")).toEqual([5]); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBeNull(); + }); + it("clears the debounce timer on unmount so no fetch runs", async () => { querySeriesMock.mockResolvedValue(seriesResponse("unmount", "A", 1)); const { result, unmount } = renderHook(() => useSeriesData()); diff --git a/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts b/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts index 1cf4e67a..c02e7d04 100644 --- a/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts +++ b/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts @@ -37,20 +37,22 @@ export function useSeriesData(): UseSeriesData { const [error, setError] = useState(null); const timerRef = useRef | null>(null); - const sequenceRef = useRef(0); + const generationRef = useRef(0); const mountedRef = useRef(true); const lastRequestRef = useRef(null); - const executeRequest = useCallback(async (req: SeriesRequestArgs) => { + const executeRequest = useCallback(async (req: SeriesRequestArgs, generation: number) => { + // Drop work that was superseded while the debounce timer was pending. + if (generation !== generationRef.current) return; + const { seasonTable, signals, startMs, endMs } = req; const targetPoints = targetPointsFor(signals.length); const key = seriesCacheKey(seasonTable, signals, startMs, endMs, targetPoints); - const seq = ++sequenceRef.current; const cached = seriesCache.get(key); if (cached) { // Clear loading so a later cache hit cannot leave a prior fetch stuck true. - if (mountedRef.current && seq === sequenceRef.current) { + if (mountedRef.current && generation === generationRef.current) { setSeriesBySignal(cached); setLoading(false); setError(null); @@ -58,7 +60,7 @@ export function useSeriesData(): UseSeriesData { return; } - if (mountedRef.current && seq === sequenceRef.current) { + if (mountedRef.current && generation === generationRef.current) { setLoading(true); } @@ -70,18 +72,20 @@ export function useSeriesData(): UseSeriesData { end: new Date(endMs).toISOString(), target_points: targetPoints, }); - if (!mountedRef.current || seq !== sequenceRef.current) return; + // Cache under the request's own key even if superseded — that cannot corrupt + // current UI because state updates below still require a matching generation. seriesCache.set(key, response.series); + if (!mountedRef.current || generation !== generationRef.current) return; setSeriesBySignal(response.series); setError(null); } catch (err) { - if (!mountedRef.current || seq !== sequenceRef.current) return; + if (!mountedRef.current || generation !== generationRef.current) return; const message = err instanceof Error ? err.message : "failed to fetch series"; setError( `${message} (${new Date(startMs).toISOString()} to ${new Date(endMs).toISOString()})`, ); } finally { - if (mountedRef.current && seq === sequenceRef.current) { + if (mountedRef.current && generation === generationRef.current) { setLoading(false); } } @@ -97,10 +101,14 @@ export function useSeriesData(): UseSeriesData { endMs, }; lastRequestRef.current = req; + // Allocate generation at schedule time so an in-flight older fetch cannot + // win state updates during this request's debounce window. + const generation = ++generationRef.current; + setError(null); if (timerRef.current !== null) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { timerRef.current = null; - void executeRequest(req); + void executeRequest(req, generation); }, DEBOUNCE_MS); }, [executeRequest], From db7c5ab70561e00185b7f386e87714bbc1b30f47 Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:13:11 -0400 Subject: [PATCH 09/33] add analysis range and signal selection controls --- .../src/analysis/analysis-range.test.ts | 83 +++++++ .../frontend/src/analysis/analysis-range.ts | 42 ++++ .../src/components/AnalysisSignalPicker.tsx | 227 ++++++++++++++++++ .../src/components/AnalysisToolbar.tsx | 188 +++++++++++++++ .../src/components/SensorGroupedGrid.tsx | 74 +----- .../frontend/src/components/sensor-palette.ts | 71 ++++++ 6 files changed, 613 insertions(+), 72 deletions(-) create mode 100644 server/installer/data-downloader/frontend/src/analysis/analysis-range.test.ts create mode 100644 server/installer/data-downloader/frontend/src/analysis/analysis-range.ts create mode 100644 server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.tsx create mode 100644 server/installer/data-downloader/frontend/src/components/AnalysisToolbar.tsx create mode 100644 server/installer/data-downloader/frontend/src/components/sensor-palette.ts diff --git a/server/installer/data-downloader/frontend/src/analysis/analysis-range.test.ts b/server/installer/data-downloader/frontend/src/analysis/analysis-range.test.ts new file mode 100644 index 00000000..71c9bac9 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/analysis-range.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { findSeasonWithData, runRange } from "./analysis-range"; +import type { RunRecord } from "../types"; + +function run(partial: Partial & Pick): RunRecord { + const { start_utc, end_utc } = partial; + return { + bins: 1, + timezone: "UTC", + start_local: start_utc, + end_local: end_utc, + key: `${start_utc}_${end_utc}`, + ...partial, + start_utc, + end_utc, + }; +} + +describe("runRange", () => { + it("converts run UTC timestamps to millisecond bounds", () => { + const record = run({ + start_utc: "2026-04-12T10:00:00+00:00", + end_utc: "2026-04-12T11:00:00+00:00", + }); + expect(runRange(record)).toEqual([ + Date.parse("2026-04-12T10:00:00Z"), + Date.parse("2026-04-12T11:00:00Z"), + ]); + }); +}); + +describe("findSeasonWithData", () => { + const runsBySeason: Record = { + wfr25: [run({ start_utc: "2025-09-09T03:00:00+00:00", end_utc: "2025-09-09T04:00:00+00:00" })], + wfr26: [run({ start_utc: "2026-04-12T10:00:00+00:00", end_utc: "2026-04-12T11:00:00+00:00" })], + }; + + it("finds a season whose runs strictly overlap the window", () => { + const start = Date.parse("2025-09-09T03:21:08Z"); + const end = Date.parse("2025-09-09T03:23:38Z"); + expect(findSeasonWithData(runsBySeason, start, end, "wfr26")).toBe("wfr25"); + }); + + it("uses strict overlap (touching endpoints do not count)", () => { + const start = Date.parse("2025-09-09T04:00:00Z"); + const end = Date.parse("2025-09-09T05:00:00Z"); + expect(findSeasonWithData(runsBySeason, start, end, "wfr26")).toBeNull(); + }); + + it("excludes the current season even when it overlaps", () => { + const start = Date.parse("2026-04-12T10:15:00Z"); + const end = Date.parse("2026-04-12T10:45:00Z"); + expect(findSeasonWithData(runsBySeason, start, end, "wfr26")).toBeNull(); + }); + + it("returns null when the query range is invalid", () => { + const start = Date.parse("2025-09-09T03:21:08Z"); + const end = Date.parse("2025-09-09T03:23:38Z"); + expect(findSeasonWithData(runsBySeason, Number.NaN, end, "wfr26")).toBeNull(); + expect(findSeasonWithData(runsBySeason, start, Number.NaN, "wfr26")).toBeNull(); + expect(findSeasonWithData(runsBySeason, end, start, "wfr26")).toBeNull(); + expect(findSeasonWithData(runsBySeason, start, start, "wfr26")).toBeNull(); + }); + + it("skips runs with invalid timestamps and keeps scanning", () => { + const withBad = { + wfr25: [ + run({ key: "bad", start_utc: "not-a-date", end_utc: "also-bad" }), + run({ start_utc: "2025-09-09T03:00:00+00:00", end_utc: "2025-09-09T04:00:00+00:00" }), + ], + wfr26: runsBySeason.wfr26, + }; + const start = Date.parse("2025-09-09T03:21:08Z"); + const end = Date.parse("2025-09-09T03:23:38Z"); + expect(findSeasonWithData(withBad, start, end, "wfr26")).toBe("wfr25"); + }); + + it("returns null when nothing overlaps", () => { + const start = Date.parse("2024-01-01T00:00:00Z"); + const end = Date.parse("2024-01-02T00:00:00Z"); + expect(findSeasonWithData(runsBySeason, start, end, "wfr26")).toBeNull(); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/analysis/analysis-range.ts b/server/installer/data-downloader/frontend/src/analysis/analysis-range.ts new file mode 100644 index 00000000..1bbc274d --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/analysis-range.ts @@ -0,0 +1,42 @@ +import type { RunRecord } from "../types"; + +/** Convert a run's UTC bounds to millisecond timestamps. */ +export function runRange(run: RunRecord): [number, number] { + return [Date.parse(run.start_utc), Date.parse(run.end_utc)]; +} + +function isValidRange(startMs: number, endMs: number): boolean { + return Number.isFinite(startMs) && Number.isFinite(endMs) && startMs < endMs; +} + +/** + * Find another season whose indexed runs strictly overlap [startMs, endMs). + * Excludes `currentSeason`. Returns null for invalid query ranges or no overlap. + */ +export function findSeasonWithData( + runsBySeason: Record, + startMs: number, + endMs: number, + currentSeason: string, +): string | null { + if (!isValidRange(startMs, endMs)) { + return null; + } + + for (const [season, runs] of Object.entries(runsBySeason)) { + if (season === currentSeason) continue; + + const overlaps = runs.some((r) => { + const runStart = Date.parse(r.start_utc); + const runEnd = Date.parse(r.end_utc); + if (!Number.isFinite(runStart) || !Number.isFinite(runEnd)) { + return false; + } + return runStart < endMs && runEnd > startMs; + }); + + if (overlaps) return season; + } + + return null; +} diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.tsx new file mode 100644 index 00000000..6fb57796 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.tsx @@ -0,0 +1,227 @@ +import { useMemo, useState, type ReactNode } from "react"; + +import type { MessageGroup, SensorsGroupedResponse } from "../types"; +import { OTHER_PALETTE, subsystemColor, type PaletteEntry } from "./sensor-palette"; + +function readTheme(): "light" | "dark" { + if (typeof document === "undefined") return "light"; + return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light"; +} + +function matchesQuery(haystacks: Array, query: string): boolean { + const q = query.trim().toLowerCase(); + if (!q) return true; + return haystacks.some((value) => String(value).toLowerCase().includes(q)); +} + +export interface AnalysisSignalPickerProps { + grouped: SensorsGroupedResponse; + selected: ReadonlySet; + onToggle: (signal: string) => void; + maxSelected?: number; +} + +interface PickerGroupProps { + groupKey: string; + name: string; + signals: string[]; + colors: PaletteEntry; + collapsed: boolean; + onCollapseToggle: () => void; + selected: ReadonlySet; + onToggle: (signal: string) => void; + atCap: boolean; + badge?: ReactNode; +} + +function PickerGroup({ + groupKey, + name, + signals, + colors, + collapsed, + onCollapseToggle, + selected, + onToggle, + atCap, + badge, +}: PickerGroupProps) { + return ( +
+ + {!collapsed && ( +
+
+ {signals.map((signal) => { + const isSelected = selected.has(signal); + const disabled = atCap && !isSelected; + return ( + + ); + })} +
+
+ )} +
+ ); +} + +export function AnalysisSignalPicker({ + grouped, + selected, + onToggle, + maxSelected = 12, +}: AnalysisSignalPickerProps) { + const [query, setQuery] = useState(""); + const [collapsed, setCollapsed] = useState>(new Set()); + const theme = readTheme(); + const atCap = selected.size >= maxSelected; + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) { + return { messages: grouped.messages, ungrouped: grouped.ungrouped }; + } + + const messages: MessageGroup[] = []; + for (const msg of grouped.messages) { + const groupMatches = matchesQuery( + [msg.name, msg.subsystem, msg.can_id, msg.can_id_hex], + q, + ); + const signals = groupMatches + ? msg.signals + : msg.signals.filter((signal) => matchesQuery([signal], q)); + if (signals.length > 0) { + messages.push({ ...msg, signals }); + } + } + + const ungrouped = grouped.ungrouped.filter((signal) => matchesQuery([signal, "Other"], q)); + return { messages, ungrouped }; + }, [grouped, query]); + + const toggleCollapsed = (key: string) => + setCollapsed((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + + return ( +
+
+ +

+ {selected.size} / {maxSelected} selected +

+
+ + setQuery(event.target.value)} + aria-label="Search signals" + /> + +
+ {filtered.messages.map((msg) => { + const colors = subsystemColor(msg.subsystem, theme); + return ( + toggleCollapsed(msg.name)} + selected={selected} + onToggle={onToggle} + atCap={atCap} + badge={ + <> + + {msg.subsystem} + + + {msg.can_id_hex} · {msg.can_id} + + + } + /> + ); + })} + + {filtered.ungrouped.length > 0 && ( + toggleCollapsed("__ungrouped__")} + selected={selected} + onToggle={onToggle} + atCap={atCap} + /> + )} +
+
+ ); +} diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.tsx new file mode 100644 index 00000000..1e2c1191 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.tsx @@ -0,0 +1,188 @@ +import { useEffect, useMemo, useState } from "react"; +import { DateTime } from "luxon"; +import { Download } from "lucide-react"; + +import { runRange } from "../analysis/analysis-range"; +import type { RunRecord } from "../types"; + +const INPUT_FORMAT = "yyyy-LL-dd'T'HH:mm"; +const CUSTOM_VALUE = ""; + +function getLocalTimeZone(): string { + if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat === "undefined") { + return "UTC"; + } + return Intl.DateTimeFormat().resolvedOptions().timeZone; +} + +function formatLocalInput(ms: number, zone: string): string { + if (!Number.isFinite(ms)) return ""; + return DateTime.fromMillis(ms, { zone: "utc" }).setZone(zone).toFormat(INPUT_FORMAT); +} + +function parseLocalInput(value: string, zone: string): number { + if (!value) return Number.NaN; + const dt = DateTime.fromFormat(value, INPUT_FORMAT, { zone }); + return dt.isValid ? dt.toUTC().toMillis() : Number.NaN; +} + +function formatRunOption(run: RunRecord): string { + const start = new Date(run.start_local).toLocaleString(undefined, { hour12: false }); + const end = new Date(run.end_local).toLocaleString(undefined, { hour12: false }); + return `${start} → ${end}`; +} + +export interface AnalysisToolbarProps { + runs: RunRecord[]; + selectedRunKey: string; + range: [number, number]; + loading: boolean; + exportDisabled: boolean; + onRunChange: (runKey: string, startMs: number, endMs: number) => void; + onCustomRange: (startMs: number, endMs: number) => void; + onExport: () => void; +} + +export function AnalysisToolbar({ + runs, + selectedRunKey, + range, + loading, + exportDisabled, + onRunChange, + onCustomRange, + onExport, +}: AnalysisToolbarProps) { + const zone = useMemo(() => getLocalTimeZone(), []); + const [startInput, setStartInput] = useState(() => formatLocalInput(range[0], zone)); + const [endInput, setEndInput] = useState(() => formatLocalInput(range[1], zone)); + const [rangeError, setRangeError] = useState(null); + + // Keep custom inputs aligned with the controlled range (run selection or parent reset). + useEffect(() => { + setStartInput(formatLocalInput(range[0], zone)); + setEndInput(formatLocalInput(range[1], zone)); + setRangeError(null); + }, [range, zone]); + + const selectValue = selectedRunKey || CUSTOM_VALUE; + + const handleRunSelect = (value: string) => { + if (value === CUSTOM_VALUE) { + // Switch to custom mode while keeping the current bounds (parent clears run key). + const [startMs, endMs] = range; + if (Number.isFinite(startMs) && Number.isFinite(endMs) && startMs < endMs) { + setRangeError(null); + onCustomRange(startMs, endMs); + } + return; + } + const run = runs.find((r) => r.key === value); + if (!run) return; + const [startMs, endMs] = runRange(run); + if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) { + setRangeError("Selected run has invalid timestamps."); + return; + } + setRangeError(null); + onRunChange(run.key, startMs, endMs); + }; + + const handleApplyCustom = () => { + const startMs = parseLocalInput(startInput, zone); + const endMs = parseLocalInput(endInput, zone); + if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) { + setRangeError("Enter valid start and end times."); + return; + } + if (!(startMs < endMs)) { + setRangeError("Start must be before end."); + return; + } + setRangeError(null); + onCustomRange(startMs, endMs); + }; + + return ( +
+
+
+ + +
+ +
+ + { + setStartInput(event.target.value); + setRangeError(null); + }} + /> +
+ +
+ + { + setEndInput(event.target.value); + setRangeError(null); + }} + /> +
+ +
+ + + {loading && ( + + Loading… + + )} +
+
+ + {rangeError && ( +

+ {rangeError} +

+ )} +
+ ); +} diff --git a/server/installer/data-downloader/frontend/src/components/SensorGroupedGrid.tsx b/server/installer/data-downloader/frontend/src/components/SensorGroupedGrid.tsx index faa2615b..142d7f7f 100644 --- a/server/installer/data-downloader/frontend/src/components/SensorGroupedGrid.tsx +++ b/server/installer/data-downloader/frontend/src/components/SensorGroupedGrid.tsx @@ -1,71 +1,6 @@ import { useState, type ReactNode } from "react"; import { MessageGroup, SensorsGroupedResponse } from "../types"; - -// ── Subsystem colour palette (8 hues, light + dark) ────────────────────────── - -interface PaletteEntry { - border: string; - bg: string; - badgeBg: string; - badgeText: string; -} - -const PALETTE: Array<{ light: PaletteEntry; dark: PaletteEntry }> = [ - // 0 Blue - { - light: { border: "#2563eb", bg: "#eef2ff", badgeBg: "#dbeafe", badgeText: "#1d4ed8" }, - dark: { border: "#60a5fa", bg: "#1e2638", badgeBg: "#1e3150", badgeText: "#93c5fd" }, - }, - // 1 Orange - { - light: { border: "#c2410c", bg: "#fff7ed", badgeBg: "#ffedd5", badgeText: "#9a3412" }, - dark: { border: "#fb923c", bg: "#2c1a10", badgeBg: "#3a2010", badgeText: "#fdba74" }, - }, - // 2 Teal - { - light: { border: "#0d9488", bg: "#f0fdfa", badgeBg: "#ccfbf1", badgeText: "#0f766e" }, - dark: { border: "#2dd4bf", bg: "#0d2522", badgeBg: "#123230", badgeText: "#5eead4" }, - }, - // 3 Violet - { - light: { border: "#7c3aed", bg: "#f5f3ff", badgeBg: "#ede9fe", badgeText: "#5b21b6" }, - dark: { border: "#a78bfa", bg: "#1e1530", badgeBg: "#261b3e", badgeText: "#c4b5fd" }, - }, - // 4 Amber - { - light: { border: "#b45309", bg: "#fffbeb", badgeBg: "#fef3c7", badgeText: "#92400e" }, - dark: { border: "#fbbf24", bg: "#2a1f0a", badgeBg: "#352710", badgeText: "#fde68a" }, - }, - // 5 Green - { - light: { border: "#15803d", bg: "#f0fdf4", badgeBg: "#dcfce7", badgeText: "#14532d" }, - dark: { border: "#4ade80", bg: "#0d2118", badgeBg: "#122818", badgeText: "#86efac" }, - }, - // 6 Rose - { - light: { border: "#be185d", bg: "#fdf2f8", badgeBg: "#fce7f3", badgeText: "#9d174d" }, - dark: { border: "#f472b6", bg: "#2c1020", badgeBg: "#3a1428", badgeText: "#f9a8d4" }, - }, - // 7 Cyan - { - light: { border: "#0e7490", bg: "#ecfeff", badgeBg: "#cffafe", badgeText: "#155e75" }, - dark: { border: "#22d3ee", bg: "#0a2028", badgeBg: "#0f2a38", badgeText: "#67e8f9" }, - }, -]; - -/** djb2 hash → stable palette index for a subsystem name. */ -function paletteIndex(subsystem: string): number { - let h = 5381; - const s = subsystem.toUpperCase(); - for (let i = 0; i < s.length; i++) { - h = (((h << 5) + h) ^ s.charCodeAt(i)) >>> 0; - } - return h % PALETTE.length; -} - -function subsystemColor(subsystem: string, theme: "light" | "dark"): PaletteEntry { - return PALETTE[paletteIndex(subsystem)][theme]; -} +import { OTHER_PALETTE, subsystemColor, type PaletteEntry } from "./sensor-palette"; // ── Component ───────────────────────────────────────────────────────────────── @@ -194,12 +129,7 @@ export function SensorGroupedGrid({ grouped, theme, onPick }: Props) { groupKey="__ungrouped__" name="Other" signals={grouped.ungrouped} - colors={{ - border: "var(--border-strong)", - bg: "var(--surface-2)", - badgeBg: "var(--surface-2)", - badgeText: "var(--text-muted)", - }} + colors={OTHER_PALETTE} count={grouped.ungrouped.length} collapsed={collapsed.has("__ungrouped__")} onToggle={() => toggle("__ungrouped__")} diff --git a/server/installer/data-downloader/frontend/src/components/sensor-palette.ts b/server/installer/data-downloader/frontend/src/components/sensor-palette.ts new file mode 100644 index 00000000..0e2ba31e --- /dev/null +++ b/server/installer/data-downloader/frontend/src/components/sensor-palette.ts @@ -0,0 +1,71 @@ +export interface PaletteEntry { + border: string; + bg: string; + badgeBg: string; + badgeText: string; +} + +const PALETTE: Array<{ light: PaletteEntry; dark: PaletteEntry }> = [ + // 0 Blue + { + light: { border: "#2563eb", bg: "#eef2ff", badgeBg: "#dbeafe", badgeText: "#1d4ed8" }, + dark: { border: "#60a5fa", bg: "#1e2638", badgeBg: "#1e3150", badgeText: "#93c5fd" }, + }, + // 1 Orange + { + light: { border: "#c2410c", bg: "#fff7ed", badgeBg: "#ffedd5", badgeText: "#9a3412" }, + dark: { border: "#fb923c", bg: "#2c1a10", badgeBg: "#3a2010", badgeText: "#fdba74" }, + }, + // 2 Teal + { + light: { border: "#0d9488", bg: "#f0fdfa", badgeBg: "#ccfbf1", badgeText: "#0f766e" }, + dark: { border: "#2dd4bf", bg: "#0d2522", badgeBg: "#123230", badgeText: "#5eead4" }, + }, + // 3 Violet + { + light: { border: "#7c3aed", bg: "#f5f3ff", badgeBg: "#ede9fe", badgeText: "#5b21b6" }, + dark: { border: "#a78bfa", bg: "#1e1530", badgeBg: "#261b3e", badgeText: "#c4b5fd" }, + }, + // 4 Amber + { + light: { border: "#b45309", bg: "#fffbeb", badgeBg: "#fef3c7", badgeText: "#92400e" }, + dark: { border: "#fbbf24", bg: "#2a1f0a", badgeBg: "#352710", badgeText: "#fde68a" }, + }, + // 5 Green + { + light: { border: "#15803d", bg: "#f0fdf4", badgeBg: "#dcfce7", badgeText: "#14532d" }, + dark: { border: "#4ade80", bg: "#0d2118", badgeBg: "#122818", badgeText: "#86efac" }, + }, + // 6 Rose + { + light: { border: "#be185d", bg: "#fdf2f8", badgeBg: "#fce7f3", badgeText: "#9d174d" }, + dark: { border: "#f472b6", bg: "#2c1020", badgeBg: "#3a1428", badgeText: "#f9a8d4" }, + }, + // 7 Cyan + { + light: { border: "#0e7490", bg: "#ecfeff", badgeBg: "#cffafe", badgeText: "#155e75" }, + dark: { border: "#22d3ee", bg: "#0a2028", badgeBg: "#0f2a38", badgeText: "#67e8f9" }, + }, +]; + +/** Neutral palette for the ungrouped "Other" bucket. */ +export const OTHER_PALETTE: PaletteEntry = { + border: "var(--border-strong)", + bg: "var(--surface-2)", + badgeBg: "var(--surface-2)", + badgeText: "var(--text-muted)", +}; + +/** djb2 hash → stable palette index for a subsystem name. */ +export function paletteIndex(subsystem: string): number { + let h = 5381; + const s = subsystem.toUpperCase(); + for (let i = 0; i < s.length; i++) { + h = (((h << 5) + h) ^ s.charCodeAt(i)) >>> 0; + } + return h % PALETTE.length; +} + +export function subsystemColor(subsystem: string, theme: "light" | "dark"): PaletteEntry { + return PALETTE[paletteIndex(subsystem)][theme]; +} From be7dd5775d5995912926f09360bc2088e89c7f2d Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:17:53 -0400 Subject: [PATCH 10/33] make analysis selection theme and custom-range callbacks controlled --- .../components/AnalysisSignalPicker.test.tsx | 56 +++++++++ .../src/components/AnalysisSignalPicker.tsx | 8 +- .../src/components/AnalysisToolbar.test.tsx | 112 ++++++++++++++++++ .../src/components/AnalysisToolbar.tsx | 11 +- 4 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.test.tsx create mode 100644 server/installer/data-downloader/frontend/src/components/AnalysisToolbar.test.tsx diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.test.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.test.tsx new file mode 100644 index 00000000..c0e7b784 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.test.tsx @@ -0,0 +1,56 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { SensorsGroupedResponse } from "../types"; +import { AnalysisSignalPicker } from "./AnalysisSignalPicker"; +import { subsystemColor } from "./sensor-palette"; + +const grouped: SensorsGroupedResponse = { + updated_at: null, + dbc_source: "test.dbc", + messages: [ + { + name: "PDU_Status", + subsystem: "Powertrain", + can_id: 256, + can_id_hex: "0x100", + signals: ["battery_voltage"], + }, + ], + ungrouped: [], +}; + +afterEach(() => { + cleanup(); +}); + +describe("AnalysisSignalPicker", () => { + it("updates subsystem palette styles when theme prop changes", () => { + const lightBorder = subsystemColor("Powertrain", "light").border; + const darkBorder = subsystemColor("Powertrain", "dark").border; + expect(lightBorder).not.toBe(darkBorder); + + const { rerender } = render( + , + ); + + const header = screen.getByRole("button", { name: /PDU_Status/i }); + expect(header).toHaveStyle({ borderLeftColor: lightBorder }); + + rerender( + , + ); + + expect(header).toHaveStyle({ borderLeftColor: darkBorder }); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.tsx index 6fb57796..304ef582 100644 --- a/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.tsx +++ b/server/installer/data-downloader/frontend/src/components/AnalysisSignalPicker.tsx @@ -3,11 +3,6 @@ import { useMemo, useState, type ReactNode } from "react"; import type { MessageGroup, SensorsGroupedResponse } from "../types"; import { OTHER_PALETTE, subsystemColor, type PaletteEntry } from "./sensor-palette"; -function readTheme(): "light" | "dark" { - if (typeof document === "undefined") return "light"; - return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light"; -} - function matchesQuery(haystacks: Array, query: string): boolean { const q = query.trim().toLowerCase(); if (!q) return true; @@ -18,6 +13,7 @@ export interface AnalysisSignalPickerProps { grouped: SensorsGroupedResponse; selected: ReadonlySet; onToggle: (signal: string) => void; + theme: "light" | "dark"; maxSelected?: number; } @@ -105,11 +101,11 @@ export function AnalysisSignalPicker({ grouped, selected, onToggle, + theme, maxSelected = 12, }: AnalysisSignalPickerProps) { const [query, setQuery] = useState(""); const [collapsed, setCollapsed] = useState>(new Set()); - const theme = readTheme(); const atCap = selected.size >= maxSelected; const filtered = useMemo(() => { diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.test.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.test.tsx new file mode 100644 index 00000000..4c445c1d --- /dev/null +++ b/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.test.tsx @@ -0,0 +1,112 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { ComponentProps } from "react"; + +import type { RunRecord } from "../types"; +import { AnalysisToolbar } from "./AnalysisToolbar"; + +const run: RunRecord = { + key: "run-1", + start_utc: "2026-01-01T12:00:00Z", + end_utc: "2026-01-01T13:00:00Z", + start_local: "2026-01-01T07:00:00-05:00", + end_local: "2026-01-01T08:00:00-05:00", + bins: 60, +}; + +const range: [number, number] = [ + Date.parse(run.start_utc), + Date.parse(run.end_utc), +]; + +afterEach(() => { + cleanup(); +}); + +function renderToolbar(overrides: Partial> = {}) { + const props = { + runs: [run], + selectedRunKey: run.key, + range, + loading: false, + exportDisabled: true, + onRunChange: vi.fn(), + onSelectCustom: vi.fn(), + onCustomRange: vi.fn(), + onExport: vi.fn(), + ...overrides, + }; + const view = render(); + return { ...view, props }; +} + +describe("AnalysisToolbar", () => { + it("selecting Custom calls onSelectCustom once and does not call onCustomRange", () => { + const { props } = renderToolbar(); + + fireEvent.change(screen.getByLabelText(/Run window/i), { + target: { value: "" }, + }); + + expect(props.onSelectCustom).toHaveBeenCalledTimes(1); + expect(props.onCustomRange).not.toHaveBeenCalled(); + }); + + it("keeps the select value controlled by selectedRunKey", () => { + const onSelectCustom = vi.fn(); + const onCustomRange = vi.fn(); + const base = { + runs: [run], + range, + loading: false, + exportDisabled: true, + onRunChange: vi.fn(), + onSelectCustom, + onCustomRange, + onExport: vi.fn(), + }; + + const { rerender } = render( + , + ); + const select = screen.getByLabelText(/Run window/i) as HTMLSelectElement; + expect(select.value).toBe(run.key); + + rerender(); + expect(select.value).toBe(""); + }); + + it("Apply with valid bounds calls onCustomRange", () => { + const onCustomRange = vi.fn(); + renderToolbar({ selectedRunKey: "", onCustomRange }); + + fireEvent.change(screen.getByLabelText(/Start \(local/i), { + target: { value: "2026-06-01T10:00" }, + }); + fireEvent.change(screen.getByLabelText(/End \(local/i), { + target: { value: "2026-06-01T11:00" }, + }); + fireEvent.click(screen.getByRole("button", { name: /Apply range/i })); + + expect(onCustomRange).toHaveBeenCalledTimes(1); + const [startMs, endMs] = onCustomRange.mock.calls[0]; + expect(startMs).toBeLessThan(endMs); + expect(Number.isFinite(startMs)).toBe(true); + expect(Number.isFinite(endMs)).toBe(true); + }); + + it("Apply with invalid bounds does not call onCustomRange", () => { + const { props } = renderToolbar({ selectedRunKey: "" }); + + fireEvent.change(screen.getByLabelText(/Start \(local/i), { + target: { value: "2026-06-01T12:00" }, + }); + fireEvent.change(screen.getByLabelText(/End \(local/i), { + target: { value: "2026-06-01T11:00" }, + }); + fireEvent.click(screen.getByRole("button", { name: /Apply range/i })); + + expect(props.onCustomRange).not.toHaveBeenCalled(); + expect(screen.getByRole("alert")).toHaveTextContent(/Start must be before end/i); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.tsx index 1e2c1191..dd2656b9 100644 --- a/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.tsx +++ b/server/installer/data-downloader/frontend/src/components/AnalysisToolbar.tsx @@ -39,6 +39,7 @@ export interface AnalysisToolbarProps { loading: boolean; exportDisabled: boolean; onRunChange: (runKey: string, startMs: number, endMs: number) => void; + onSelectCustom: () => void; onCustomRange: (startMs: number, endMs: number) => void; onExport: () => void; } @@ -50,6 +51,7 @@ export function AnalysisToolbar({ loading, exportDisabled, onRunChange, + onSelectCustom, onCustomRange, onExport, }: AnalysisToolbarProps) { @@ -69,12 +71,9 @@ export function AnalysisToolbar({ const handleRunSelect = (value: string) => { if (value === CUSTOM_VALUE) { - // Switch to custom mode while keeping the current bounds (parent clears run key). - const [startMs, endMs] = range; - if (Number.isFinite(startMs) && Number.isFinite(endMs) && startMs < endMs) { - setRangeError(null); - onCustomRange(startMs, endMs); - } + // Enter custom mode without re-emitting the current bounds. + setRangeError(null); + onSelectCustom(); return; } const run = runs.find((r) => r.key === value); From b4bd701759490f59ff72ca95ce752b07c85c489a Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:22:21 -0400 Subject: [PATCH 11/33] add linked analysis plots and CSV export --- .../frontend/src/analysis/export-csv.test.ts | 124 ++++++++++ .../frontend/src/analysis/export-csv.ts | 110 +++++++++ .../frontend/src/analysis/plot-traces.test.ts | 73 ++++++ .../frontend/src/analysis/plot-traces.ts | 117 ++++++++++ .../src/components/AnalysisPlotStack.tsx | 218 ++++++++++++++++++ 5 files changed, 642 insertions(+) create mode 100644 server/installer/data-downloader/frontend/src/analysis/export-csv.test.ts create mode 100644 server/installer/data-downloader/frontend/src/analysis/export-csv.ts create mode 100644 server/installer/data-downloader/frontend/src/analysis/plot-traces.test.ts create mode 100644 server/installer/data-downloader/frontend/src/analysis/plot-traces.ts create mode 100644 server/installer/data-downloader/frontend/src/components/AnalysisPlotStack.tsx diff --git a/server/installer/data-downloader/frontend/src/analysis/export-csv.test.ts b/server/installer/data-downloader/frontend/src/analysis/export-csv.test.ts new file mode 100644 index 00000000..5c4f3d87 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/export-csv.test.ts @@ -0,0 +1,124 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { SignalSeries } from "../types"; +import { downloadSeriesCsv, seriesToCsv } from "./export-csv"; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("seriesToCsv", () => { + it("merges non-aligned raw timestamps sorted by exact numeric keys", () => { + const seriesBySignal: Record = { + A: { + mode: "raw", + resolution_ms: null, + point_count: 2, + t: [0, 1000], + v: [1, 2], + }, + B: { + mode: "raw", + resolution_ms: null, + point_count: 1, + t: [1000], + v: [9], + }, + }; + + const { csv, containsEnvelope } = seriesToCsv(seriesBySignal); + expect(containsEnvelope).toBe(false); + expect(csv).toBe( + [ + "time_utc,A,B", + "1970-01-01T00:00:00.000Z,1,", + "1970-01-01T00:00:01.000Z,2,9", + ].join("\n"), + ); + }); + + it("emits avg/min/max envelope columns and flags containsEnvelope", () => { + const seriesBySignal: Record = { + A: { + mode: "envelope", + resolution_ms: 1000, + point_count: 2, + t: [0, 1000], + avg: [1.5, 2.5], + min: [1, 2], + max: [2, 3], + }, + }; + + const { csv, containsEnvelope } = seriesToCsv(seriesBySignal); + expect(containsEnvelope).toBe(true); + expect(csv).toBe( + [ + "time_utc,A_avg,A_min,A_max", + "1970-01-01T00:00:00.000Z,1.5,1,2", + "1970-01-01T00:00:01.000Z,2.5,2,3", + ].join("\n"), + ); + }); + + it("escapes headers that contain commas, quotes, or newlines", () => { + const seriesBySignal: Record = { + 'weird,"name"\n': { + mode: "raw", + resolution_ms: null, + point_count: 1, + t: [0], + v: [1], + }, + }; + + const { csv } = seriesToCsv(seriesBySignal); + // Quoted header may embed a literal newline; assert the escaped field, not split lines. + expect(csv.startsWith('time_utc,"weird,""name""\n"\n')).toBe(true); + expect(csv).toContain("1970-01-01T00:00:00.000Z,1"); + }); +}); + +describe("downloadSeriesCsv", () => { + it("creates a blob download and revokes the object URL", () => { + const click = vi.fn(); + const revokeObjectURL = vi.fn(); + const createObjectURL = vi.fn(() => "blob:mock-url"); + + vi.stubGlobal("URL", { + createObjectURL, + revokeObjectURL, + }); + + const anchor = { + href: "", + download: "", + click, + } as unknown as HTMLAnchorElement; + const createElement = vi.spyOn(document, "createElement").mockReturnValue(anchor); + + downloadSeriesCsv( + { + A: { + mode: "raw", + resolution_ms: null, + point_count: 1, + t: [0], + v: [1], + }, + }, + "wfr26", + Date.parse("2026-06-20T15:00:00.000Z"), + Date.parse("2026-06-20T16:00:00.000Z"), + ); + + expect(createElement).toHaveBeenCalledWith("a"); + expect(createObjectURL).toHaveBeenCalledTimes(1); + expect(anchor.href).toBe("blob:mock-url"); + expect(anchor.download).toBe( + "wfr26_2026-06-20T15-00-00.000Z_2026-06-20T16-00-00.000Z.csv", + ); + expect(click).toHaveBeenCalledTimes(1); + expect(revokeObjectURL).toHaveBeenCalledWith("blob:mock-url"); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/analysis/export-csv.ts b/server/installer/data-downloader/frontend/src/analysis/export-csv.ts new file mode 100644 index 00000000..e5ecde35 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/export-csv.ts @@ -0,0 +1,110 @@ +import type { SignalSeries } from "../types"; +import type { SeriesMap } from "./series-cache"; + +function escapeCsvField(value: string): string { + if (/[",\n\r]/.test(value)) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; +} + +interface ColumnSpec { + header: string; + values: Map; +} + +function columnsForSignal(signal: string, series: SignalSeries): ColumnSpec[] { + if (series.mode === "raw") { + const values = new Map(); + for (let i = 0; i < series.t.length; i++) { + values.set(series.t[i], series.v[i]); + } + return [{ header: signal, values }]; + } + + const avg = new Map(); + const min = new Map(); + const max = new Map(); + for (let i = 0; i < series.t.length; i++) { + const t = series.t[i]; + avg.set(t, series.avg[i]); + min.set(t, series.min[i]); + max.set(t, series.max[i]); + } + // Header order matches the product contract: avg, min, max. + return [ + { header: `${signal}_avg`, values: avg }, + { header: `${signal}_min`, values: min }, + { header: `${signal}_max`, values: max }, + ]; +} + +/** + * Merge loaded series into a CSV string keyed by exact numeric timestamps. + * Signal iteration order follows `Object.keys(seriesBySignal)`. + */ +export function seriesToCsv(seriesBySignal: SeriesMap): { + csv: string; + containsEnvelope: boolean; +} { + const columns: ColumnSpec[] = []; + let containsEnvelope = false; + const timestamps = new Set(); + + for (const [signal, series] of Object.entries(seriesBySignal)) { + if (series.mode === "envelope") containsEnvelope = true; + for (const col of columnsForSignal(signal, series)) { + columns.push(col); + for (const t of col.values.keys()) timestamps.add(t); + } + } + + const sortedTimes = [...timestamps].sort((a, b) => a - b); + const header = ["time_utc", ...columns.map((c) => escapeCsvField(c.header))].join(","); + const rows = sortedTimes.map((t) => { + const cells = [ + new Date(t).toISOString(), + ...columns.map((c) => { + const value = c.values.get(t); + return value === undefined ? "" : String(value); + }), + ]; + return cells.join(","); + }); + + return { + csv: [header, ...rows].join("\n"), + containsEnvelope, + }; +} + +/** Make a filesystem-safe filename fragment from an ISO / season string. */ +export function safeFilenamePart(value: string): string { + return value.replace(/[:/\\?%*|<>"]/g, "-").replace(/\s+/g, "_"); +} + +export function seriesCsvFilename(season: string, startMs: number, endMs: number): string { + const start = safeFilenamePart(new Date(startMs).toISOString()); + const end = safeFilenamePart(new Date(endMs).toISOString()); + return `${safeFilenamePart(season)}_${start}_${end}.csv`; +} + +/** Trigger a browser download for the current series map; always revokes the blob URL. */ +export function downloadSeriesCsv( + seriesBySignal: SeriesMap, + season: string, + startMs: number, + endMs: number, +): void { + const { csv } = seriesToCsv(seriesBySignal); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); + const url = URL.createObjectURL(blob); + try { + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = seriesCsvFilename(season, startMs, endMs); + anchor.click(); + } finally { + URL.revokeObjectURL(url); + } +} diff --git a/server/installer/data-downloader/frontend/src/analysis/plot-traces.test.ts b/server/installer/data-downloader/frontend/src/analysis/plot-traces.test.ts new file mode 100644 index 00000000..7c8a8aa0 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/plot-traces.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import type { Data } from "plotly.js"; + +import type { SignalSeries } from "../types"; +import { buildTraces } from "./plot-traces"; + +const raw: SignalSeries = { + mode: "raw", + resolution_ms: null, + point_count: 2, + t: [0, 1000], + v: [1, 2], +}; + +const env: SignalSeries = { + mode: "envelope", + resolution_ms: 2500, + point_count: 2, + t: [0, 2500], + min: [1, 2], + max: [3, 4], + avg: [2, 3], +}; + +function asTraces(data: Data[]): Array> { + return data as Array>; +} + +describe("buildTraces", () => { + it("builds one scattergl line for raw series with Date x-values", () => { + const traces = asTraces(buildTraces("A", raw, "#f47560")); + expect(traces).toHaveLength(1); + expect(traces[0].type).toBe("scattergl"); + expect(traces[0].mode).toBe("lines"); + expect(traces[0].name).toBe("A"); + const x = traces[0].x as Date[]; + expect(x).toHaveLength(2); + expect(x[0]).toBeInstanceOf(Date); + expect(x[0].getTime()).toBe(0); + expect(x[1].getTime()).toBe(1000); + expect(traces[0].y).toEqual([1, 2]); + expect((traces[0].line as { color: string }).color).toBe("#f47560"); + }); + + it("builds max, min band, and avg traces for envelope series", () => { + const traces = asTraces(buildTraces("A", env, "#f47560")); + expect(traces).toHaveLength(3); + const [maxT, minT, avgT] = traces; + + expect(maxT.type).toBe("scattergl"); + expect(minT.type).toBe("scattergl"); + expect(avgT.type).toBe("scattergl"); + + expect(minT.fill).toBe("tonexty"); + expect(maxT.showlegend).toBe(false); + expect(minT.showlegend).toBe(false); + expect(avgT.name).toContain("A"); + + const maxLine = maxT.line as { color: string }; + const minLine = minT.line as { color: string }; + const avgLine = avgT.line as { color: string }; + expect(avgLine.color).toBe("#f47560"); + // Band edges share the signal color family (transparent stroke / tinted fill). + expect(maxLine.color).toContain("rgba"); + expect(minLine.color).toContain("rgba"); + expect(String(minT.fillcolor)).toContain("rgba"); + expect(minT.fillcolor).toContain("244, 117, 96"); + + expect(maxT.y).toEqual([3, 4]); + expect(minT.y).toEqual([1, 2]); + expect(avgT.y).toEqual([2, 3]); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/analysis/plot-traces.ts b/server/installer/data-downloader/frontend/src/analysis/plot-traces.ts new file mode 100644 index 00000000..1f9680b8 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/analysis/plot-traces.ts @@ -0,0 +1,117 @@ +import type { Data, PlotData, PlotRelayoutEvent } from "plotly.js"; + +import type { SignalSeries } from "../types"; + +/** Hex (#rgb / #rrggbb) → rgba() with the given alpha. */ +export function withAlpha(hex: string, alpha: number): string { + const raw = hex.replace("#", "").trim(); + const full = + raw.length === 3 + ? raw + .split("") + .map((c) => c + c) + .join("") + : raw; + const n = Number.parseInt(full, 16); + if (!Number.isFinite(n) || full.length !== 6) { + return `rgba(0, 0, 0, ${alpha})`; + } + const r = (n >> 16) & 255; + const g = (n >> 8) & 255; + const b = n & 255; + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +function toDateX(t: number[]): Date[] { + return t.map((ms) => new Date(ms)); +} + +/** + * Build Plotly scattergl traces for one signal. + * Envelope order is max → min (fill tonexty) → avg so the band fills correctly. + */ +export function buildTraces(signal: string, series: SignalSeries, color: string): Data[] { + const x = toDateX(series.t); + + if (series.mode === "raw") { + const trace: Partial = { + type: "scattergl", + mode: "lines", + name: signal, + x, + y: series.v, + line: { color, width: 1.5 }, + hovertemplate: "%{y:.4g}" + signal + "", + }; + return [trace as Data]; + } + + const transparent = withAlpha(color, 0); + const fill = withAlpha(color, 0.25); + + const maxTrace: Partial = { + type: "scattergl", + mode: "lines", + name: `${signal} max`, + x, + y: series.max, + line: { color: transparent, width: 0 }, + showlegend: false, + hoverinfo: "skip", + }; + + const minTrace: Partial = { + type: "scattergl", + mode: "lines", + name: `${signal} min`, + x, + y: series.min, + line: { color: transparent, width: 0 }, + fill: "tonexty", + fillcolor: fill, + showlegend: false, + hoverinfo: "skip", + }; + + const avgTrace: Partial = { + type: "scattergl", + mode: "lines", + name: `${signal} (avg)`, + x, + y: series.avg, + line: { color, width: 1.5 }, + hovertemplate: "%{y:.4g}" + signal + "", + }; + + return [maxTrace as Data, minTrace as Data, avgTrace as Data]; +} + +/** + * Parse a user x-axis relayout into millisecond bounds. + * Returns `null` when the event has no x-range / autorange keys (ignore to avoid feedback loops). + * Returns `[NaN, NaN]` when autorange is requested (double-click reset). + */ +export function parseXRangeRelayout( + event: Readonly, +): [number, number] | null { + const record = event as Readonly>; + + if (record["xaxis.autorange"] === true) { + return [Number.NaN, Number.NaN]; + } + + if ("xaxis.range[0]" in record && "xaxis.range[1]" in record) { + const start = Date.parse(String(record["xaxis.range[0]"])); + const end = Date.parse(String(record["xaxis.range[1]"])); + return [start, end]; + } + + const range = record["xaxis.range"]; + if (Array.isArray(range) && range.length >= 2) { + const start = Date.parse(String(range[0])); + const end = Date.parse(String(range[1])); + return [start, end]; + } + + return null; +} diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisPlotStack.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisPlotStack.tsx new file mode 100644 index 00000000..d4126fd9 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/components/AnalysisPlotStack.tsx @@ -0,0 +1,218 @@ +import { useMemo } from "react"; +import type { Config, Data, Layout, PlotRelayoutEvent } from "plotly.js"; +import Plot from "react-plotly.js"; + +import { buildTraces, parseXRangeRelayout } from "../analysis/plot-traces"; +import type { SeriesMap } from "../analysis/series-cache"; +import type { SignalSeries } from "../types"; + +/** Season-independent plot stroke colors (light / dark pairs by hash index). */ +const PLOT_PALETTE: Array<{ light: string; dark: string }> = [ + { light: "#2563eb", dark: "#60a5fa" }, + { light: "#c2410c", dark: "#fb923c" }, + { light: "#0d9488", dark: "#2dd4bf" }, + { light: "#7c3aed", dark: "#a78bfa" }, + { light: "#b45309", dark: "#fbbf24" }, + { light: "#15803d", dark: "#4ade80" }, + { light: "#be185d", dark: "#f472b6" }, + { light: "#0e7490", dark: "#22d3ee" }, +]; + +/** djb2 → stable palette index for a signal name (not season). */ +export function signalPlotColor(signal: string, theme: "light" | "dark"): string { + let h = 5381; + for (let i = 0; i < signal.length; i++) { + h = (((h << 5) + h) ^ signal.charCodeAt(i)) >>> 0; + } + return PLOT_PALETTE[h % PLOT_PALETTE.length][theme]; +} + +function formatResolution(ms: number): string { + if (ms >= 60_000 && ms % 60_000 === 0) return `${ms / 60_000}m`; + if (ms >= 1000 && ms % 1000 === 0) return `${ms / 1000}s`; + return `${ms}ms`; +} + +function seriesStatus(series: SignalSeries | undefined): { + modeLabel: string; + countLabel: string; + resolutionLabel: string | null; + emptyMessage: string | null; +} { + if (!series) { + return { + modeLabel: "—", + countLabel: "no data", + resolutionLabel: null, + emptyMessage: "No series loaded for this signal yet.", + }; + } + if (series.point_count === 0 || series.t.length === 0) { + return { + modeLabel: series.mode === "envelope" ? "envelope" : "raw", + countLabel: "0 pts", + resolutionLabel: + series.mode === "envelope" ? formatResolution(series.resolution_ms) : null, + emptyMessage: "No samples in the visible range.", + }; + } + return { + modeLabel: series.mode === "envelope" ? "envelope" : "raw", + countLabel: `${series.point_count.toLocaleString()} pts`, + resolutionLabel: + series.mode === "envelope" ? formatResolution(series.resolution_ms) : null, + emptyMessage: null, + }; +} + +export interface AnalysisPlotStackProps { + seriesBySignal: SeriesMap; + signals: string[]; + range: [number, number]; + onRangeChange: (startMs: number, endMs: number) => void; + theme: "light" | "dark"; +} + +function PlotCard({ + signal, + series, + color, + range, + isBottom, + theme, + onRangeChange, +}: { + signal: string; + series: SignalSeries | undefined; + color: string; + range: [number, number]; + isBottom: boolean; + theme: "light" | "dark"; + onRangeChange: (startMs: number, endMs: number) => void; +}) { + const status = seriesStatus(series); + const isDark = theme === "dark"; + const chartFont = isDark ? "#e6e8eb" : "#111827"; + const chartGrid = isDark ? "#2c313a" : "#e5e7eb"; + + const data = useMemo((): Data[] => { + if (!series || status.emptyMessage) return []; + return buildTraces(signal, series, color); + }, [series, signal, color, status.emptyMessage]); + + const layout = useMemo((): Partial => { + return { + autosize: true, + margin: { t: 8, r: 16, b: isBottom ? 36 : 8, l: 52, pad: 2 }, + hovermode: "x unified", + showlegend: false, + uirevision: signal, + font: { color: chartFont, size: 11 }, + paper_bgcolor: "rgba(0,0,0,0)", + plot_bgcolor: "rgba(0,0,0,0)", + xaxis: { + type: "date", + range: [new Date(range[0]), new Date(range[1])], + showticklabels: isBottom, + ticks: isBottom ? "outside" : "", + gridcolor: chartGrid, + zeroline: false, + showspikes: true, + spikemode: "across", + }, + yaxis: { + zeroline: false, + gridcolor: chartGrid, + fixedrange: true, + }, + }; + }, [signal, range, isBottom, chartFont, chartGrid]); + + const config = useMemo( + (): Partial => ({ + responsive: true, + displaylogo: false, + modeBarButtonsToRemove: ["select2d", "lasso2d", "autoScale2d"], + }), + [], + ); + + const handleRelayout = (event: Readonly) => { + const next = parseXRangeRelayout(event); + if (next === null) return; + onRangeChange(next[0], next[1]); + }; + + return ( +
+
+

{signal}

+
+ {status.modeLabel} + {status.countLabel} + {status.resolutionLabel && ( + {status.resolutionLabel} + )} +
+
+
+ {status.emptyMessage ? ( +
+ {status.emptyMessage} +
+ ) : ( + + )} +
+
+ ); +} + +/** Shared Plot sizing — Task 7 CSS owns card chrome; Plotly still needs explicit dimensions. */ +const PLOT_STYLE = { width: "100%", height: "100%" } as const; + +/** + * Vertically stacked linked plots: one card per selected signal, shared controlled x-range. + */ +export function AnalysisPlotStack({ + seriesBySignal, + signals, + range, + onRangeChange, + theme, +}: AnalysisPlotStackProps) { + if (signals.length === 0) { + return ( +
+
+ Select one or more signals to plot. +
+
+ ); + } + + return ( +
+ {signals.map((signal, index) => ( + + ))} +
+ ); +} From 60cc38358a6b663dc66c609c40270b582735ef6a Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:25:09 -0400 Subject: [PATCH 12/33] harden plotly x-range relayout parsing by bound type --- .../frontend/src/analysis/plot-traces.test.ts | 128 +++++++++++++++++- .../frontend/src/analysis/plot-traces.ts | 48 +++++-- 2 files changed, 162 insertions(+), 14 deletions(-) diff --git a/server/installer/data-downloader/frontend/src/analysis/plot-traces.test.ts b/server/installer/data-downloader/frontend/src/analysis/plot-traces.test.ts index 7c8a8aa0..efd9a055 100644 --- a/server/installer/data-downloader/frontend/src/analysis/plot-traces.test.ts +++ b/server/installer/data-downloader/frontend/src/analysis/plot-traces.test.ts @@ -1,8 +1,13 @@ import { describe, expect, it } from "vitest"; -import type { Data } from "plotly.js"; +import type { Data, PlotRelayoutEvent } from "plotly.js"; import type { SignalSeries } from "../types"; -import { buildTraces } from "./plot-traces"; +import { buildTraces, parseXRangeRelayout } from "./plot-traces"; + +/** Plotly can emit numbers, Date, or ISO strings; typings only allow numbers. */ +function asRelayout(event: Record): PlotRelayoutEvent { + return event as unknown as PlotRelayoutEvent; +} const raw: SignalSeries = { mode: "raw", @@ -71,3 +76,122 @@ describe("buildTraces", () => { expect(avgT.y).toEqual([2, 3]); }); }); + +describe("parseXRangeRelayout", () => { + it("uses finite numbers as epoch ms (including 0)", () => { + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range[0]": 0, + "xaxis.range[1]": 1000, + }), + ), + ).toEqual([0, 1000]); + + expect(parseXRangeRelayout(asRelayout({ "xaxis.range": [0, 1000] }))).toEqual([ + 0, 1000, + ]); + }); + + it("accepts Date bounds via getTime", () => { + const start = new Date(0); + const end = new Date(1000); + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range[0]": start, + "xaxis.range[1]": end, + }), + ), + ).toEqual([0, 1000]); + expect(parseXRangeRelayout(asRelayout({ "xaxis.range": [start, end] }))).toEqual([ + 0, 1000, + ]); + }); + + it("parses ISO date strings", () => { + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range[0]": "1970-01-01T00:00:00.000Z", + "xaxis.range[1]": "1970-01-01T00:00:01.000Z", + }), + ), + ).toEqual([0, 1000]); + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range": ["1970-01-01T00:00:00.000Z", "1970-01-01T00:00:01.000Z"], + }), + ), + ).toEqual([0, 1000]); + }); + + it("returns null for malformed values", () => { + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range[0]": "not-a-date", + "xaxis.range[1]": 1000, + }), + ), + ).toBeNull(); + expect(parseXRangeRelayout(asRelayout({ "xaxis.range": ["bad", "worse"] }))).toBeNull(); + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range[0]": { foo: 1 }, + "xaxis.range[1]": 1000, + }), + ), + ).toBeNull(); + }); + + it("returns null for non-finite values", () => { + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range[0]": Number.NaN, + "xaxis.range[1]": 1000, + }), + ), + ).toBeNull(); + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range": [0, Number.POSITIVE_INFINITY], + }), + ), + ).toBeNull(); + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range[0]": new Date(Number.NaN), + "xaxis.range[1]": new Date(1000), + }), + ), + ).toBeNull(); + }); + + it("returns null for reversed or equal bounds", () => { + expect( + parseXRangeRelayout( + asRelayout({ + "xaxis.range[0]": 1000, + "xaxis.range[1]": 0, + }), + ), + ).toBeNull(); + expect(parseXRangeRelayout(asRelayout({ "xaxis.range": [500, 500] }))).toBeNull(); + }); + + it("returns [NaN, NaN] only for explicit autorange", () => { + const auto = parseXRangeRelayout(asRelayout({ "xaxis.autorange": true })); + expect(auto).not.toBeNull(); + expect(Number.isNaN(auto![0])).toBe(true); + expect(Number.isNaN(auto![1])).toBe(true); + + expect(parseXRangeRelayout(asRelayout({}))).toBeNull(); + expect(parseXRangeRelayout(asRelayout({ "yaxis.range": [0, 1] }))).toBeNull(); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/analysis/plot-traces.ts b/server/installer/data-downloader/frontend/src/analysis/plot-traces.ts index 1f9680b8..721b4bb9 100644 --- a/server/installer/data-downloader/frontend/src/analysis/plot-traces.ts +++ b/server/installer/data-downloader/frontend/src/analysis/plot-traces.ts @@ -86,10 +86,27 @@ export function buildTraces(signal: string, series: SignalSeries, color: string) return [maxTrace as Data, minTrace as Data, avgTrace as Data]; } +/** Normalize a Plotly relayout bound to finite epoch ms, or null if invalid. */ +function normalizeRelayoutBound(value: unknown): number | null { + if (typeof value === "number") { + return Number.isFinite(value) ? value : null; + } + if (value instanceof Date) { + const ms = value.getTime(); + return Number.isFinite(ms) ? ms : null; + } + if (typeof value === "string") { + const ms = Date.parse(value); + return Number.isFinite(ms) ? ms : null; + } + return null; +} + /** * Parse a user x-axis relayout into millisecond bounds. - * Returns `null` when the event has no x-range / autorange keys (ignore to avoid feedback loops). - * Returns `[NaN, NaN]` when autorange is requested (double-click reset). + * Returns `null` when the event has no x-range / autorange keys (ignore to avoid feedback loops), + * or when bounds are missing/invalid/non-finite/reversed-or-equal. + * Returns `[NaN, NaN]` only when autorange is requested (double-click reset). */ export function parseXRangeRelayout( event: Readonly, @@ -100,18 +117,25 @@ export function parseXRangeRelayout( return [Number.NaN, Number.NaN]; } + let startRaw: unknown; + let endRaw: unknown; + if ("xaxis.range[0]" in record && "xaxis.range[1]" in record) { - const start = Date.parse(String(record["xaxis.range[0]"])); - const end = Date.parse(String(record["xaxis.range[1]"])); - return [start, end]; + startRaw = record["xaxis.range[0]"]; + endRaw = record["xaxis.range[1]"]; + } else { + const range = record["xaxis.range"]; + if (!Array.isArray(range) || range.length < 2) { + return null; + } + startRaw = range[0]; + endRaw = range[1]; } - const range = record["xaxis.range"]; - if (Array.isArray(range) && range.length >= 2) { - const start = Date.parse(String(range[0])); - const end = Date.parse(String(range[1])); - return [start, end]; + const start = normalizeRelayoutBound(startRaw); + const end = normalizeRelayoutBound(endRaw); + if (start === null || end === null || !(start < end)) { + return null; } - - return null; + return [start, end]; } From 8033d19054771382a7de2c70c9317c92779c7a84 Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:32:42 -0400 Subject: [PATCH 13/33] add downloader analysis workspace and tab --- .../data-downloader/frontend/src/App.tsx | 340 ++++++++++------ .../src/analysis/use-series-data.test.tsx | 22 ++ .../frontend/src/analysis/use-series-data.ts | 18 +- .../src/components/AnalysisWorkspace.test.tsx | 374 ++++++++++++++++++ .../src/components/AnalysisWorkspace.tsx | 289 ++++++++++++++ .../data-downloader/frontend/src/styles.css | 328 +++++++++++++++ 6 files changed, 1260 insertions(+), 111 deletions(-) create mode 100644 server/installer/data-downloader/frontend/src/components/AnalysisWorkspace.test.tsx create mode 100644 server/installer/data-downloader/frontend/src/components/AnalysisWorkspace.tsx diff --git a/server/installer/data-downloader/frontend/src/App.tsx b/server/installer/data-downloader/frontend/src/App.tsx index a58c2bd2..18e1625c 100644 --- a/server/installer/data-downloader/frontend/src/App.tsx +++ b/server/installer/data-downloader/frontend/src/App.tsx @@ -5,15 +5,25 @@ import { RunRecord, RunsResponse, ScannerStatus, SensorsGroupedResponse, Sensors import { RunTable } from "./components/RunTable"; import { DataDownload } from "./components/data-download"; import { SensorGroupedGrid } from "./components/SensorGroupedGrid"; +import { AnalysisWorkspace } from "./components/AnalysisWorkspace"; type ScanState = "idle" | "running" | "success" | "error"; type Theme = "light" | "dark"; +type Workspace = "downloader" | "analysis"; function getInitialTheme(): Theme { if (typeof window === "undefined") return "light"; - const saved = window.localStorage.getItem("theme"); - if (saved === "light" || saved === "dark") return saved; - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + try { + const saved = window.localStorage?.getItem("theme"); + if (saved === "light" || saved === "dark") return saved; + } catch { + // jsdom / privacy modes may omit or block localStorage. + } + try { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } catch { + return "light"; + } } interface DownloaderSelection { @@ -30,6 +40,7 @@ export default function App() { const [runs, setRuns] = useState(null); const [sensors, setSensors] = useState(null); const [sensorsGrouped, setSensorsGrouped] = useState(null); + const [runsBySeason, setRunsBySeason] = useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [noteDrafts, setNoteDrafts] = useState>({}); @@ -39,6 +50,7 @@ export default function App() { const [downloaderSelection, setDownloaderSelection] = useState(null); const [scannerStatus, setScannerStatus] = useState(null); const [theme, setTheme] = useState(getInitialTheme); + const [workspace, setWorkspace] = useState("downloader"); const sensorsSectionRef = useRef(null); const downloaderSectionRef = useRef(null); const statusFinishedRef = useRef(null); @@ -71,6 +83,9 @@ export default function App() { setRuns(runsData); setSensors(sensorsData); setSensorsGrouped(groupedData); + if (currentSeason) { + setRunsBySeason((prev) => ({ ...prev, [currentSeason]: runsData.runs })); + } setError(null); } catch (err) { console.error(err); @@ -80,6 +95,37 @@ export default function App() { } }, [selectedSeason, seasons.length]); + // Prefetch runs for every season for Analysis wrong-season hints — never surfaces as the page error. + useEffect(() => { + if (seasons.length === 0) return; + let cancelled = false; + + void (async () => { + const entries = await Promise.all( + seasons.map(async (s) => { + try { + const data = await fetchRuns(s.name); + return [s.name, data.runs] as const; + } catch { + return [s.name, [] as RunRecord[]] as const; + } + }), + ); + if (cancelled) return; + setRunsBySeason((prev) => { + const next = { ...prev }; + for (const [name, seasonRuns] of entries) { + next[name] = seasonRuns; + } + return next; + }); + })(); + + return () => { + cancelled = true; + }; + }, [seasons]); + const loadStatus = useCallback( async (syncOnFinishChange: boolean) => { try { @@ -105,7 +151,11 @@ export default function App() { useEffect(() => { document.documentElement.setAttribute("data-theme", theme); - window.localStorage.setItem("theme", theme); + try { + window.localStorage?.setItem("theme", theme); + } catch { + // Ignore persistence failures in restricted environments. + } }, [theme]); useEffect(() => { @@ -238,8 +288,13 @@ export default function App() { }; const seasonColor = (name: string) => SEASON_COLORS[name] || "#0bf"; + const activeSeason = useMemo( + () => seasons.find((s) => s.name === selectedSeason) ?? null, + [seasons, selectedSeason], + ); + return ( -
+
@@ -292,121 +347,186 @@ export default function App() {
- {scanningActive && ( -
- Scanning database. Do not click again. -
- )} - -
- {seasons.length > 1 && ( - - )} - - -

- Use the top right season selector to switch the active season. -

- {scanState !== "idle" && ( - - {scanState === "running" && "Scan in progress..."} - {scanState === "success" && "Scan queued and data refreshed"} - {scanState === "error" && "Scan failed"} - - )}
- {error && ( -
- Heads up: {error} -
- )} + {workspace === "downloader" && ( +
+ {scanningActive && ( +
+ Scanning database. Do not click again. +
+ )} -
-

Past Runs

-

Last refresh: {lastRunsRefresh}

- {loading && !runs ? ( -

Loading runs...

- ) : runs ? ( - - ) : ( -

No data yet.

- )} -
- -
-
-

Unique Sensors

- {sensorsGrouped?.dbc_source && sensorsGrouped.dbc_source !== "none" && ( - - DBC: {sensorsGrouped.dbc_source} - +
+ {seasons.length > 1 && ( + + )} + + +

+ Use the top right season selector to switch the active season. +

+ {scanState !== "idle" && ( + + {scanState === "running" && "Scan in progress..."} + {scanState === "success" && "Scan queued and data refreshed"} + {scanState === "error" && "Scan failed"} + + )} +
+ + {error && ( +
+ Heads up: {error} +
)} + +
+

Past Runs

+

Last refresh: {lastRunsRefresh}

+ {loading && !runs ? ( +

Loading runs...

+ ) : runs ? ( + + ) : ( +

No data yet.

+ )} +
+ +
+
+

Unique Sensors

+ {sensorsGrouped?.dbc_source && sensorsGrouped.dbc_source !== "none" && ( + + DBC: {sensorsGrouped.dbc_source} + + )} +
+

Last refresh: {lastSensorRefresh}

+ {loading && !sensors ? ( +

Loading sensors...

+ ) : sensorsGrouped && sensorsGrouped.messages.length > 0 ? ( + + ) : ( +
+ {sensorsPreview.length === 0 &&

No sensors captured.

} + {sensorsPreview.map((sensor) => ( + + ))} +
+ )} +
+ +
+ +
-

Last refresh: {lastSensorRefresh}

- {loading && !sensors ? ( -

Loading sensors...

- ) : sensorsGrouped && sensorsGrouped.messages.length > 0 ? ( - + - ) : ( -
- {sensorsPreview.length === 0 &&

No sensors captured.

} - {sensorsPreview.map((sensor) => ( - - ))} -
- )} -
- -
- -
+
+ )}
); } diff --git a/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx b/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx index 41f2e72a..7f78b6aa 100644 --- a/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx +++ b/server/installer/data-downloader/frontend/src/analysis/use-series-data.test.tsx @@ -311,4 +311,26 @@ describe("useSeriesData", () => { expect(querySeriesMock).not.toHaveBeenCalled(); }); + + it("clear drops pending work and wipes series/error/loading", async () => { + const pending = deferred(); + querySeriesMock.mockReturnValueOnce(pending.promise); + const { result } = renderHook(() => useSeriesData()); + + act(() => result.current.requestRange("clear-me", ["A"], 0, 1000)); + await act(() => vi.advanceTimersByTimeAsync(300)); + expect(result.current.loading).toBe(true); + + act(() => result.current.clear()); + expect(result.current.seriesBySignal).toEqual({}); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBeNull(); + + await act(async () => { + pending.resolve(seriesResponse("clear-me", "A", 9)); + await pending.promise; + }); + expect(result.current.seriesBySignal).toEqual({}); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts b/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts index c02e7d04..eadd35dd 100644 --- a/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts +++ b/server/installer/data-downloader/frontend/src/analysis/use-series-data.ts @@ -18,6 +18,8 @@ export interface UseSeriesData { endMs: number, ): void; retry(): void; + /** Drop pending/in-flight work and wipe results (e.g. season change). */ + clear(): void; } interface SeriesRequestArgs { @@ -121,6 +123,20 @@ export function useSeriesData(): UseSeriesData { requestRange(last.seasonTable, last.signals, last.startMs, last.endMs); }, [requestRange]); + const clear = useCallback(() => { + generationRef.current += 1; + lastRequestRef.current = null; + if (timerRef.current !== null) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + if (mountedRef.current) { + setSeriesBySignal({}); + setLoading(false); + setError(null); + } + }, []); + useEffect(() => { mountedRef.current = true; return () => { @@ -132,5 +148,5 @@ export function useSeriesData(): UseSeriesData { }; }, []); - return { seriesBySignal, loading, error, requestRange, retry }; + return { seriesBySignal, loading, error, requestRange, retry, clear }; } diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisWorkspace.test.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisWorkspace.test.tsx new file mode 100644 index 00000000..c078f7f9 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/components/AnalysisWorkspace.test.tsx @@ -0,0 +1,374 @@ +import { act, cleanup, fireEvent, render, screen, within } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { RunRecord, Season, SensorsGroupedResponse, SeriesResponse } from "../types"; + +vi.mock("../api", () => ({ + fetchSeasons: vi.fn(), + fetchRuns: vi.fn(), + fetchSensors: vi.fn(), + fetchSensorsGrouped: vi.fn(), + fetchScannerStatus: vi.fn(), + triggerScan: vi.fn(), + updateNote: vi.fn(), + querySeries: vi.fn(), + querySensorData: vi.fn(), +})); + +vi.mock("react-plotly.js", () => ({ + default: () =>
, +})); + +vi.mock("../analysis/export-csv", async () => { + const actual = await vi.importActual( + "../analysis/export-csv", + ); + return { + ...actual, + downloadSeriesCsv: vi.fn(), + }; +}); + +import App from "../App"; +import { + fetchRuns, + fetchScannerStatus, + fetchSeasons, + fetchSensors, + fetchSensorsGrouped, + querySeries, +} from "../api"; +import { downloadSeriesCsv } from "../analysis/export-csv"; +import { AnalysisWorkspace } from "./AnalysisWorkspace"; + +const fetchSeasonsMock = vi.mocked(fetchSeasons); +const fetchRunsMock = vi.mocked(fetchRuns); +const fetchSensorsMock = vi.mocked(fetchSensors); +const fetchSensorsGroupedMock = vi.mocked(fetchSensorsGrouped); +const fetchScannerStatusMock = vi.mocked(fetchScannerStatus); +const querySeriesMock = vi.mocked(querySeries); +const downloadSeriesCsvMock = vi.mocked(downloadSeriesCsv); + +const seasons: Season[] = [ + { name: "WFR26", year: 2026, table: "wfr26" }, + { name: "WFR25", year: 2025, table: "wfr25" }, +]; + +const run: RunRecord = { + key: "run-1", + start_utc: "2026-06-08T21:00:00.000Z", + end_utc: "2026-06-09T03:00:00.000Z", + start_local: "2026-06-08T17:00:00-04:00", + end_local: "2026-06-08T23:00:00-04:00", + bins: 60, +}; + +const grouped: SensorsGroupedResponse = { + updated_at: null, + dbc_source: "github", + messages: [ + { + name: "M160_Temperature_Set_1", + subsystem: "INV", + can_id: 160, + can_id_hex: "0x0A0", + signals: ["INV_Analog_Input_2"], + }, + ], + ungrouped: [], +}; + +const emptyGrouped: SensorsGroupedResponse = { + updated_at: null, + dbc_source: "none", + messages: [], + ungrouped: [], +}; + +function seriesResponse( + season: string, + signal: string, + value: number, + mode: "raw" | "envelope" = "raw", +): SeriesResponse { + if (mode === "envelope") { + return { + season, + start: "s", + end: "e", + series: { + [signal]: { + mode: "envelope", + resolution_ms: 1000, + point_count: 1, + t: [Date.parse(run.start_utc)], + min: [value - 1], + max: [value + 1], + avg: [value], + }, + }, + }; + } + return { + season, + start: "s", + end: "e", + series: { + [signal]: { + mode: "raw", + resolution_ms: null, + point_count: 1, + t: [Date.parse(run.start_utc)], + v: [value], + }, + }, + }; +} + +function emptySeriesResponse(season: string, signal: string): SeriesResponse { + return { + season, + start: "s", + end: "e", + series: { + [signal]: { + mode: "raw", + resolution_ms: null, + point_count: 0, + t: [], + v: [], + }, + }, + }; +} + +function stubAppApis() { + fetchSeasonsMock.mockResolvedValue(seasons); + fetchRunsMock.mockImplementation(async (season?: string) => { + if (season === "WFR25") { + return { updated_at: null, runs: [] }; + } + return { updated_at: "2026-07-16T10:00:00Z", runs: [run] }; + }); + fetchSensorsMock.mockResolvedValue({ + updated_at: "2026-07-16T10:00:00Z", + sensors: ["INV_Analog_Input_2"], + }); + fetchSensorsGroupedMock.mockResolvedValue(grouped); + fetchScannerStatusMock.mockResolvedValue({ + scanning: false, + started_at: null, + finished_at: null, + source: null, + updated_at: null, + }); + querySeriesMock.mockResolvedValue(seriesResponse("wfr26", "INV_Analog_Input_2", 5)); +} + +async function flushInitialLoad() { + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); +} + +afterEach(() => { + cleanup(); + vi.useRealTimers(); + vi.clearAllMocks(); +}); + +describe("Analysis workspace tabs (App)", () => { + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + stubAppApis(); + }); + + it("shows Downloader and Analysis tabs with Past Runs initially", async () => { + render(); + await flushInitialLoad(); + + expect(screen.getByRole("tab", { name: "Downloader" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Analysis" })).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "Past Runs" })).toBeInTheDocument(); + expect(screen.queryByLabelText(/Run window/i)).not.toBeInTheDocument(); + }); + + it("switches to Analysis workspace while keeping the shared season selected", async () => { + render(); + await flushInitialLoad(); + + fireEvent.click(screen.getByRole("tab", { name: "Analysis" })); + + expect(screen.getByLabelText(/Run window/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Search signals/i)).toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: "Past Runs" })).not.toBeInTheDocument(); + + const activeSeason = screen.getByRole("button", { name: "WFR26" }); + expect(activeSeason).toHaveStyle({ fontWeight: "bold" }); + }); + + it("queries the season table with exact run UTC bounds and selected signal", async () => { + render(); + await flushInitialLoad(); + + fireEvent.click(screen.getByRole("tab", { name: "Analysis" })); + + fireEvent.change(screen.getByLabelText(/Run window/i), { + target: { value: run.key }, + }); + fireEvent.click(screen.getByRole("checkbox", { name: "INV_Analog_Input_2" })); + + await act(async () => { + await vi.advanceTimersByTimeAsync(300); + }); + + expect(querySeriesMock).toHaveBeenCalledWith( + expect.objectContaining({ + season: "wfr26", + signals: ["INV_Analog_Input_2"], + start: run.start_utc, + end: run.end_utc, + }), + ); + }); +}); + +describe("AnalysisWorkspace", () => { + const runsBySeason: Record = { + WFR26: [run], + WFR25: [], + }; + + beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + querySeriesMock.mockReset(); + downloadSeriesCsvMock.mockReset(); + }); + + it("clears prior season plots when the season prop changes", async () => { + const clearSeason: Season = { name: "WFR26", year: 2026, table: "wfr26-clear" }; + querySeriesMock.mockResolvedValue(seriesResponse("wfr26-clear", "INV_Analog_Input_2", 5)); + + const { rerender } = render( + , + ); + + fireEvent.change(screen.getByLabelText(/Run window/i), { + target: { value: run.key }, + }); + fireEvent.click(screen.getByRole("checkbox", { name: "INV_Analog_Input_2" })); + await act(async () => { + await vi.advanceTimersByTimeAsync(300); + }); + + expect(await screen.findByTestId("plotly-mock")).toBeInTheDocument(); + expect(querySeriesMock).toHaveBeenCalledTimes(1); + + querySeriesMock.mockClear(); + rerender( + , + ); + + expect(screen.queryByTestId("plotly-mock")).not.toBeInTheDocument(); + expect(screen.queryByRole("checkbox", { name: "INV_Analog_Input_2" })).not.toBeInTheDocument(); + expect(querySeriesMock).not.toHaveBeenCalled(); + }); + + it("does not show the neutral empty state before the first response completes", async () => { + let resolveSeries!: (value: SeriesResponse) => void; + querySeriesMock.mockReturnValue( + new Promise((resolve) => { + resolveSeries = resolve; + }), + ); + + const emptySeason: Season = { name: "WFR26", year: 2026, table: "wfr26-empty-timing" }; + + render( + , + ); + + fireEvent.change(screen.getByLabelText(/Run window/i), { + target: { value: run.key }, + }); + fireEvent.click(screen.getByRole("checkbox", { name: "INV_Analog_Input_2" })); + + expect(screen.queryByTestId("analysis-empty")).not.toBeInTheDocument(); + + await act(async () => { + await vi.advanceTimersByTimeAsync(300); + }); + + expect(screen.queryByTestId("analysis-empty")).not.toBeInTheDocument(); + expect(screen.getByText(/Loading series/i)).toBeInTheDocument(); + + await act(async () => { + resolveSeries(emptySeriesResponse("wfr26-empty-timing", "INV_Analog_Input_2")); + await Promise.resolve(); + }); + + expect(screen.getByTestId("analysis-empty")).toBeInTheDocument(); + }); + + it("asks for inline confirmation before exporting envelope data", async () => { + const envelopeSeason: Season = { name: "WFR26", year: 2026, table: "wfr26-envelope" }; + querySeriesMock.mockResolvedValue( + seriesResponse("wfr26-envelope", "INV_Analog_Input_2", 5, "envelope"), + ); + const confirmSpy = vi.spyOn(window, "confirm"); + + render( + , + ); + + fireEvent.change(screen.getByLabelText(/Run window/i), { + target: { value: run.key }, + }); + fireEvent.click(screen.getByRole("checkbox", { name: "INV_Analog_Input_2" })); + await act(async () => { + await vi.advanceTimersByTimeAsync(300); + }); + + expect(await screen.findByTestId("plotly-mock")).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /Export CSV/i })); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(downloadSeriesCsvMock).not.toHaveBeenCalled(); + expect(screen.getByTestId("analysis-export-confirm")).toBeInTheDocument(); + + fireEvent.click( + within(screen.getByTestId("analysis-export-confirm")).getByRole("button", { + name: /Export anyway/i, + }), + ); + + expect(downloadSeriesCsvMock).toHaveBeenCalledTimes(1); + confirmSpy.mockRestore(); + }); +}); diff --git a/server/installer/data-downloader/frontend/src/components/AnalysisWorkspace.tsx b/server/installer/data-downloader/frontend/src/components/AnalysisWorkspace.tsx new file mode 100644 index 00000000..10df3ef9 --- /dev/null +++ b/server/installer/data-downloader/frontend/src/components/AnalysisWorkspace.tsx @@ -0,0 +1,289 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { findSeasonWithData } from "../analysis/analysis-range"; +import { downloadSeriesCsv, seriesToCsv } from "../analysis/export-csv"; +import { useSeriesData } from "../analysis/use-series-data"; +import type { RunRecord, Season, SensorsGroupedResponse } from "../types"; +import { AnalysisPlotStack } from "./AnalysisPlotStack"; +import { AnalysisSignalPicker } from "./AnalysisSignalPicker"; +import { AnalysisToolbar } from "./AnalysisToolbar"; + +export interface AnalysisWorkspaceProps { + season: Season; + runs: RunRecord[]; + grouped: SensorsGroupedResponse | null; + theme: "light" | "dark"; + runsBySeason: Record; +} + +const EMPTY_GROUPED: SensorsGroupedResponse = { + updated_at: null, + dbc_source: "none", + messages: [], + ungrouped: [], +}; + +function isValidRange(startMs: number, endMs: number): boolean { + return Number.isFinite(startMs) && Number.isFinite(endMs) && startMs < endMs; +} + +function hasPlottableSeries( + seriesBySignal: Record, + signals: string[], +): boolean { + return signals.some((signal) => { + const series = seriesBySignal[signal]; + return series != null && series.point_count > 0; + }); +} + +export function AnalysisWorkspace({ + season, + runs, + grouped, + theme, + runsBySeason, +}: AnalysisWorkspaceProps) { + const [selectedRunKey, setSelectedRunKey] = useState(""); + const [selectedSignals, setSelectedSignals] = useState([]); + const [fullRange, setFullRange] = useState<[number, number] | null>(null); + const [viewRange, setViewRange] = useState<[number, number] | null>(null); + const [awaitingFirstResponse, setAwaitingFirstResponse] = useState(false); + const [exportConfirmOpen, setExportConfirmOpen] = useState(false); + + const { seriesBySignal, loading, error, requestRange, retry, clear } = useSeriesData(); + + const seasonName = season.name; + const seasonTable = season.table; + const prevSeasonRef = useRef(seasonName); + + // Season change: wipe selection state and hook results so prior season data never shows. + useEffect(() => { + if (prevSeasonRef.current === seasonName) return; + prevSeasonRef.current = seasonName; + setSelectedRunKey(""); + setSelectedSignals([]); + setFullRange(null); + setViewRange(null); + setAwaitingFirstResponse(false); + setExportConfirmOpen(false); + clear(); + }, [seasonName, clear]); + + const selectedSet = useMemo(() => new Set(selectedSignals), [selectedSignals]); + + const handleToggleSignal = useCallback((signal: string) => { + setSelectedSignals((prev) => + prev.includes(signal) ? prev.filter((s) => s !== signal) : [...prev, signal], + ); + }, []); + + const handleRunChange = useCallback((runKey: string, startMs: number, endMs: number) => { + setSelectedRunKey(runKey); + setFullRange([startMs, endMs]); + setViewRange([startMs, endMs]); + setExportConfirmOpen(false); + }, []); + + const handleSelectCustom = useCallback(() => { + setSelectedRunKey(""); + }, []); + + const handleCustomRange = useCallback((startMs: number, endMs: number) => { + setSelectedRunKey(""); + setFullRange([startMs, endMs]); + setViewRange([startMs, endMs]); + setExportConfirmOpen(false); + }, []); + + const handlePlotRangeChange = useCallback( + (startMs: number, endMs: number) => { + if (Number.isNaN(startMs) && Number.isNaN(endMs)) { + if (fullRange) setViewRange(fullRange); + return; + } + if (!isValidRange(startMs, endMs)) return; + setViewRange([startMs, endMs]); + }, + [fullRange], + ); + + // Request series whenever table + signals + view range are valid. + useEffect(() => { + if (!viewRange || !isValidRange(viewRange[0], viewRange[1]) || selectedSignals.length === 0) { + return; + } + setAwaitingFirstResponse(true); + setExportConfirmOpen(false); + requestRange(seasonTable, selectedSignals, viewRange[0], viewRange[1]); + }, [seasonTable, selectedSignals, viewRange, requestRange]); + + // Mark first response complete once loading settles after a request. + const wasLoadingRef = useRef(false); + useEffect(() => { + if (loading) { + wasLoadingRef.current = true; + return; + } + if (wasLoadingRef.current || Object.keys(seriesBySignal).length > 0 || error) { + setAwaitingFirstResponse(false); + wasLoadingRef.current = false; + } + }, [loading, seriesBySignal, error]); + + const toolbarRange: [number, number] = viewRange ?? [Number.NaN, Number.NaN]; + const exportDisabled = + selectedSignals.length === 0 || + !viewRange || + !hasPlottableSeries(seriesBySignal, selectedSignals); + + const wrongSeasonHint = useMemo(() => { + if (!viewRange || !isValidRange(viewRange[0], viewRange[1])) return null; + if (loading || awaitingFirstResponse) return null; + if (hasPlottableSeries(seriesBySignal, selectedSignals)) return null; + if (selectedSignals.length === 0) return null; + return findSeasonWithData(runsBySeason, viewRange[0], viewRange[1], seasonName); + }, [ + viewRange, + loading, + awaitingFirstResponse, + seriesBySignal, + selectedSignals, + runsBySeason, + seasonName, + ]); + + const showEmpty = + !error && + !awaitingFirstResponse && + !loading && + selectedSignals.length > 0 && + viewRange != null && + !hasPlottableSeries(seriesBySignal, selectedSignals); + + const keepPreviousPlots = + !error && + selectedSignals.length > 0 && + viewRange != null && + hasPlottableSeries(seriesBySignal, selectedSignals); + + const performExport = useCallback(() => { + if (!viewRange) return; + downloadSeriesCsv(seriesBySignal, seasonName, viewRange[0], viewRange[1]); + setExportConfirmOpen(false); + }, [seriesBySignal, seasonName, viewRange]); + + const handleExport = useCallback(() => { + if (!viewRange || exportDisabled) return; + const { containsEnvelope } = seriesToCsv(seriesBySignal); + if (containsEnvelope) { + setExportConfirmOpen(true); + return; + } + performExport(); + }, [viewRange, exportDisabled, seriesBySignal, performExport]); + + const pickerGrouped = grouped ?? EMPTY_GROUPED; + + return ( +
+ + + {exportConfirmOpen && ( +
+

+ This export includes envelope (downsampled) data with avg/min/max columns — not raw + samples. +

+
+ + +
+
+ )} + +
+ + +
+ {error && ( +
+ Could not load series. +

{error}

+ +
+ )} + + {!error && awaitingFirstResponse && !keepPreviousPlots && ( +
+ Loading series… +
+ )} + + {!error && showEmpty && ( +
+

No samples in this range for the selected signals.

+ {wrongSeasonHint && ( +

+ Overlapping indexed runs exist in {wrongSeasonHint}. Try switching seasons. +

+ )} +
+ )} + + {!error && keepPreviousPlots && viewRange && ( + + )} + + {!error && + !awaitingFirstResponse && + !loading && + !showEmpty && + !keepPreviousPlots && + selectedSignals.length === 0 && ( +
+ Select a run window and one or more signals to load linked plots. +
+ )} +
+
+
+ ); +} diff --git a/server/installer/data-downloader/frontend/src/styles.css b/server/installer/data-downloader/frontend/src/styles.css index c589b77e..f16e3619 100644 --- a/server/installer/data-downloader/frontend/src/styles.css +++ b/server/installer/data-downloader/frontend/src/styles.css @@ -561,3 +561,331 @@ body { max-height: none; margin-top: 0; } + +/* ── Workspace tabs ───────────────────────────────────────────────────────── */ + +.app-shell.is-analysis { + max-width: 1400px; +} + +.workspace-tabs { + display: flex; + gap: 0.25rem; + margin: 0 0 1.25rem; + padding: 0.25rem; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface-2); + width: fit-content; + max-width: 100%; +} + +.workspace-tab { + appearance: none; + border: none; + background: transparent; + color: var(--text-muted); + font: inherit; + font-size: 0.9rem; + font-weight: 600; + padding: 0.45rem 1rem; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; +} + +.workspace-tab:hover { + color: var(--text); + background: var(--surface); +} + +.workspace-tab.is-selected { + background: var(--surface); + color: var(--accent); + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08); +} + +.workspace-tab:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-ring); +} + +/* ── Analysis workspace ───────────────────────────────────────────────────── */ + +.analysis-workspace { + display: flex; + flex-direction: column; + gap: 0.9rem; +} + +.analysis-toolbar { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 0.85rem 1rem; + box-shadow: var(--shadow-card); +} + +.analysis-toolbar-row { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 0.75rem 1rem; +} + +.analysis-toolbar-run { + flex: 1 1 220px; + min-width: 180px; +} + +.analysis-toolbar-start, +.analysis-toolbar-end { + flex: 1 1 180px; + min-width: 160px; +} + +.analysis-toolbar-actions { + flex: 0 0 auto; + align-items: center; +} + +.analysis-toolbar-loading { + margin: 0; +} + +.analysis-toolbar-error { + margin: 0.5rem 0 0; +} + +.analysis-export-confirm { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.75rem 1rem; + border-radius: 10px; + border: 1px solid var(--warn-border); + background: var(--warn-bg); + color: var(--warn-text); + font-size: 0.9rem; +} + +.analysis-export-confirm p { + margin: 0; + flex: 1 1 240px; +} + +.analysis-export-confirm-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.analysis-grid { + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + gap: 1rem; + align-items: start; + min-height: 420px; +} + +.analysis-rail { + position: sticky; + top: 0.75rem; + max-height: calc(100vh - 2rem); + overflow: hidden; + display: flex; + flex-direction: column; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: var(--shadow-card); + padding: 0.85rem; +} + +.analysis-signal-picker { + display: flex; + flex-direction: column; + gap: 0.55rem; + min-height: 0; + flex: 1; +} + +.analysis-signal-picker-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.5rem; +} + +.analysis-signal-picker-count { + margin: 0; + font-size: 0.8rem; +} + +.analysis-signal-search { + width: 100%; +} + +.analysis-signal-groups { + margin-top: 0.25rem; + max-height: none; + flex: 1; + min-height: 0; + overflow-y: auto; +} + +.analysis-signal-chip.is-selected { + background: var(--accent-soft-bg); + border-color: var(--accent); + color: var(--accent-soft-text); + font-weight: 600; +} + +.analysis-plots { + display: flex; + flex-direction: column; + gap: 0.85rem; + min-width: 0; +} + +.analysis-plot-stack { + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.analysis-plot-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: var(--shadow-card); + padding: 0.75rem 0.85rem 0.5rem; + display: flex; + flex-direction: column; + min-height: 220px; +} + +.analysis-plot-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 0.35rem; +} + +.analysis-plot-title { + margin: 0; + font-size: 0.95rem; + font-weight: 700; + color: var(--text); +} + +.analysis-plot-meta { + display: flex; + align-items: center; + gap: 0.4rem; + flex-wrap: wrap; +} + +.analysis-plot-badge, +.analysis-status-badge { + display: inline-flex; + align-items: center; + padding: 0.15rem 0.45rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.02em; + background: var(--accent-soft-bg); + color: var(--accent-soft-text); +} + +.analysis-plot-badge.is-envelope { + background: var(--warn-bg); + color: var(--warn-text); +} + +.analysis-plot-count, +.analysis-plot-resolution { + font-size: 0.75rem; + color: var(--text-muted); +} + +.analysis-plot-body { + flex: 1; + min-height: 180px; +} + +.analysis-plotly { + width: 100%; + height: 180px; +} + +.analysis-plot-empty, +.analysis-empty, +.analysis-idle { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 0.35rem; + min-height: 180px; + padding: 1.25rem 1.35rem; + border: 1px dashed var(--border-strong); + border-radius: 12px; + background: var(--surface-2); + color: var(--text-muted); + font-size: 0.92rem; +} + +.analysis-empty p, +.analysis-idle { + margin: 0; +} + +.analysis-empty-hint { + color: var(--warn-text); + font-size: 0.85rem; +} + +.analysis-error { + border: 1px solid var(--error-card-border); + background: var(--error-card-bg); + color: var(--error-text); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.65rem; +} + +.analysis-error p { + margin: 0; + font-size: 0.9rem; +} + +.analysis-status-badge { + align-self: flex-start; +} + +@media (max-width: 849px) { + .analysis-grid { + grid-template-columns: 1fr; + } + + .analysis-rail { + position: static; + max-height: 320px; + } + + .analysis-toolbar-row { + flex-direction: column; + align-items: stretch; + } + + .analysis-toolbar-actions { + width: 100%; + } +} From 6cdc5d1a898e16e1bd2609b7e93b3e93862d2822 Mon Sep 17 00:00:00 2001 From: Haorui Zhou Date: Thu, 16 Jul 2026 11:40:25 -0400 Subject: [PATCH 14/33] fix analysis season remount, export guards, and tab persistence --- .../data-downloader/frontend/src/App.tsx | 244 ++++++++-------- .../frontend/src/analysis/use-series-data.ts | 24 +- .../src/components/AnalysisWorkspace.test.tsx | 276 +++++++++++++++++- .../src/components/AnalysisWorkspace.tsx | 72 +++-- 4 files changed, 467 insertions(+), 149 deletions(-) diff --git a/server/installer/data-downloader/frontend/src/App.tsx b/server/installer/data-downloader/frontend/src/App.tsx index 18e1625c..d36023aa 100644 --- a/server/installer/data-downloader/frontend/src/App.tsx +++ b/server/installer/data-downloader/frontend/src/App.tsx @@ -95,14 +95,16 @@ export default function App() { } }, [selectedSeason, seasons.length]); - // Prefetch runs for every season for Analysis wrong-season hints — never surfaces as the page error. + // Prefetch non-active seasons for Analysis wrong-season hints — never surfaces as the page error. + // Active season is seeded by loadData; never let an older prefetch overwrite a fresher entry. useEffect(() => { - if (seasons.length === 0) return; + if (seasons.length === 0 || !selectedSeason) return; let cancelled = false; void (async () => { + const targets = seasons.filter((s) => s.name !== selectedSeason); const entries = await Promise.all( - seasons.map(async (s) => { + targets.map(async (s) => { try { const data = await fetchRuns(s.name); return [s.name, data.runs] as const; @@ -115,6 +117,7 @@ export default function App() { setRunsBySeason((prev) => { const next = { ...prev }; for (const [name, seasonRuns] of entries) { + if (Object.prototype.hasOwnProperty.call(next, name)) continue; next[name] = seasonRuns; } return next; @@ -124,7 +127,7 @@ export default function App() { return () => { cancelled = true; }; - }, [seasons]); + }, [seasons, selectedSeason]); const loadStatus = useCallback( async (syncOnFinishChange: boolean) => { @@ -388,137 +391,138 @@ export default function App() {
- {workspace === "downloader" && ( -
- {scanningActive && ( -
- Scanning database. Do not click again. -
+ - {workspace === "analysis" && activeSeason && ( + {activeSeason && (