From 76435a7260aecc18811dc504e0a71edaa6b278f4 Mon Sep 17 00:00:00 2001 From: runsystem-hiro Date: Sun, 2 Aug 2026 21:35:54 +0900 Subject: [PATCH] feat: release LightWatch 1.1.0 --- .env.example | 8 +- .github/workflows/ci.yml | 17 +- .gitignore | 20 +- .python-version | 1 + CHANGELOG.md | 20 + CONTRIBUTING.md | 65 +- README.md | 282 +-- VERSION | 2 +- config/default.toml | 3 + config/development.toml.example | 41 + config/production.toml.example | 76 + docs/.gitkeep | 0 docs/development.md | 147 ++ docs/lighting-roi.md | 119 + docs/local-web-preview.md | 69 + docs/operations.md | 259 ++ docs/systemd.md | 129 + frontend/package-lock.json | 3038 +----------------------- frontend/package.json | 2 +- frontend/src/App.tsx | 720 ++++-- frontend/src/index.css | 251 +- pyproject.toml | 37 +- requirements-dev.txt | 3 - requirements.txt | 3 - scripts/create-web-sample-data.py | 13 + scripts/reclassify_lighting.py | 13 +- scripts/sync-to-nas.sh | 6 +- scripts/sync-version.ps1 | 18 + scripts/verify.ps1 | 74 + src/lightwatch/analysis.py | 26 +- src/lightwatch/capture.py | 10 +- src/lightwatch/cleanup.py | 9 +- src/lightwatch/config.py | 32 +- src/lightwatch/database.py | 73 +- src/lightwatch/database_maintenance.py | 7 +- src/lightwatch/health.py | 10 +- src/lightwatch/nas_sync_state.py | 2 +- src/lightwatch/notification.py | 4 +- src/lightwatch/report.py | 23 +- src/lightwatch/sample_data.py | 172 ++ src/lightwatch/web.py | 65 +- systemd/README.md | 67 - systemd/lightwatch-capture.service | 6 +- systemd/lightwatch-cleanup.service | 5 +- systemd/lightwatch-health.service | 6 +- systemd/lightwatch-nas-sync.service | 3 +- systemd/lightwatch-report.service | 6 +- systemd/lightwatch-web.service | 6 +- tests/test_analysis.py | 4 +- tests/test_capture_retention.py | 4 +- tests/test_cleanup.py | 4 +- tests/test_config.py | 48 +- tests/test_database.py | 2 +- tests/test_database_indexes.py | 2 +- tests/test_database_lighting.py | 2 +- tests/test_database_transition.py | 2 +- tests/test_health.py | 18 +- tests/test_health_notification.py | 2 +- tests/test_lighting_classification.py | 85 +- tests/test_nas_sync_monitoring.py | 24 +- tests/test_nas_sync_state.py | 2 +- tests/test_notification.py | 6 +- tests/test_reclassify_lighting.py | 6 +- tests/test_reclassify_lighting_cli.py | 3 +- tests/test_report.py | 14 +- tests/test_sample_data.py | 61 + tests/test_slack_file.py | 8 +- tests/test_transition.py | 2 +- tests/test_version.py | 2 +- tests/test_web.py | 102 +- uv.lock | 656 +++++ 71 files changed, 3381 insertions(+), 3646 deletions(-) create mode 100644 .python-version create mode 100644 config/development.toml.example create mode 100644 config/production.toml.example delete mode 100644 docs/.gitkeep create mode 100644 docs/development.md create mode 100644 docs/lighting-roi.md create mode 100644 docs/local-web-preview.md create mode 100644 docs/operations.md create mode 100644 docs/systemd.md delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt create mode 100644 scripts/create-web-sample-data.py create mode 100644 scripts/verify.ps1 create mode 100644 src/lightwatch/sample_data.py delete mode 100644 systemd/README.md create mode 100644 tests/test_sample_data.py create mode 100644 uv.lock diff --git a/.env.example b/.env.example index 0da47ce..904057a 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ -# Slack integration (optional) +# Configuration +LIGHTWATCH_CONFIG=/opt/lightwatch/config/production.toml + +# Slack SLACK_BOT_TOKEN= SLACK_CHANNEL_ID= - -# Optional configuration override -# LIGHTWATCH_CONFIG=/opt/lightwatch/config/local.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f663ed..32eed87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,15 +12,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true - uses: actions/setup-python@v5 with: - python-version: '3.13' - cache: pip - - run: python -m pip install --upgrade pip - - run: python -m pip install -e ".[dev]" - - run: python scripts/verify-version.py - - run: ruff check . - - run: pytest + python-version-file: .python-version + - run: uv sync --frozen + - run: uv lock --check + - run: uv run --locked python scripts/verify-version.py + - run: uv run --locked ruff check . + - run: uv run --locked ruff format --check . + - run: uv run --locked pytest -q frontend: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index cfc6e5c..4a6fabb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,12 +12,10 @@ htmlcov/ # Virtual environment .venv/ venv/ - # Environment variables / secrets .env .env.* !.env.example - # Runtime data data/ logs/ @@ -25,19 +23,17 @@ tmp/ *.log *.pid *.lock - +!uv.lock # SQLite *.db *.db-shm *.db-wal *.sqlite *.sqlite3 - # Captured images *.jpg *.jpeg *.png - # OS / Editor .DS_Store Thumbs.db @@ -45,27 +41,27 @@ desktop.ini .vscode/* !.vscode/extensions.json !.vscode/settings.json - # Raspberry Pi / deployment *.service.local *.timer.local - # Backup / archive *.zip *.tar *.tar.gz *.bak - - +# Internal reference images +docs/reference/* +!docs/reference/.gitkeep *.egg-info/ - # Frontend frontend/node_modules/ frontend/dist/ - +# Local development and production-specific configuration +.local/ +config/development.toml +config/production.toml # TypeScript build cache *.tsbuildinfo - # Generated Vite config files frontend/vite.config.js frontend/vite.config.d.ts diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fd354e..91a7c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to LightWatch are documented in this file. +## [1.1.0] - 2026-08-02 + +### Added + +- Detailed lighting-state timeline in the Web UI +- Image modal with previous and next navigation +- Local Web preview with generated sample data +- uv-based locked development and verification workflow +- Sample production and development configuration files +- Expanded documentation for development, operation, lighting ROI, local Web preview, and systemd + +### Changed + +- Timeline positions now follow actual capture times +- Web API and dashboard status information were expanded +- Frontend dependencies were updated and pinned +- Python dependencies are now managed through `pyproject.toml` and `uv.lock` +- systemd units, configuration examples, and documentation were updated for LightWatch +- Public documentation now uses generic Raspberry Pi, NAS, and deployment examples + ## [1.0.0] - 2026-07-22 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a6aa31..22285ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,17 +2,70 @@ Contributions are welcome through issues and pull requests. -## Development checks +## Development setup + +Install the Python dependencies and create the virtual environment: + +```bash +uv sync --frozen +``` + +Install the frontend dependencies: ```bash -python scripts/verify-version.py -ruff check . -pytest cd frontend npm ci +cd .. +``` + +## Development checks + +On Windows with PowerShell 7, run the complete verification script: + +```powershell +.\scripts\verify.ps1 +``` + +To verify only the Python backend: + +```powershell +.\scripts\verify.ps1 -SkipFrontend +``` + +The equivalent individual commands are: + +```bash +uv lock --check +uv run --locked python scripts/verify-version.py +uv run --locked ruff check . +uv run --locked ruff format --check . +uv run --locked pytest -q + +cd frontend npm run lint npm run build -npm audit +cd .. ``` -Keep changes focused, include tests for behavior changes, and do not commit credentials, private host names, internal paths, production images, or generated runtime data. +Before submitting a pull request, also confirm that the working tree contains only the intended changes: + +```bash +git diff --check +git status +git diff --stat +``` + +Keep changes focused and include tests for behavior changes. + +Do not commit: + +- Credentials, tokens, passwords, or private keys +- Private host names, IP addresses, or internal paths +- Production configuration files +- Captured images +- SQLite databases +- Logs or state files +- Generated runtime data +- `.venv/` +- `frontend/node_modules/` +- `frontend/dist/` diff --git a/README.md b/README.md index 98159b9..c655308 100644 --- a/README.md +++ b/README.md @@ -1,224 +1,228 @@ # LightWatch -Raspberry Piを使用したオフィス照明監視・タイムラプスシステム +LightWatchは、Raspberry Piと対応カメラを使用して室内照明の状態を定期撮影・分類・記録する、オープンソースの監視・タイムラプスシステムです。 -LightWatchは、Raspberry Pi Cameraで定期的に画像を撮影し、オフィスの照明状態を判定するシステムです。判定結果をSQLiteへ記録し、必要に応じてSlackへ通知します。撮影画像はNASへ同期でき、FastAPIとReactで構成されたWebダッシュボードから現在の状態や履歴を確認できます。 +観測結果をSQLiteへ保存し、状態変化時や定時の画像を保持します。保存画像のNAS同期、Slack通知、定期レポート、Health Check、FastAPIとReactによるWeb UIをsystemdで運用できます。 ## 主な機能 -- Raspberry Pi Camera Module 3による定期撮影 -- 4段階の照明状態判定:`BRIGHT`、`PARTIAL`、`DIM`、`DARK` -- ROI(関心領域)を使用した照明状態の判定 -- 毎時の基準画像と、状態変化時のイベント画像を保存 -- SQLiteによる観測履歴の記録 -- `DARK`以外から`DARK`へ変化した際のSlack通知 -- 最新画像を添付した定期Slackレポート -- SSH/rsyncによるNASへの画像同期 -- NAS同期成功後のローカル保存画像の自動整理 -- 撮影、NAS同期、ストレージ容量のヘルスチェック -- FastAPI APIとReactダッシュボード -- `flock`による撮影処理とライブビューのカメラ排他制御 -- systemdサービス/タイマーの設定例 +- Raspberry Piカメラによる定期撮影 +- `BRIGHT` / `PARTIAL` / `DIM` / `DARK` の4段階分類 +- ROIを使用した照明状態判定 +- SQLiteへの観測結果保存 +- 初回観測、定時、状態変化時、解析失敗時の画像保存 +- 非DARKからDARKへの遷移時のSlack通知 +- 画像付き定期レポート +- SSH / rsyncによるNAS同期 +- 同期済みローカル画像の保持期間管理 +- 撮影、NAS同期、ディスク容量などのHealth Check +- FastAPI + ReactによるWeb UI +- タイムライン表示と画像履歴閲覧 +- 画像モーダルと前後画像への移動 +- `flock` による撮影とライブビューの排他制御 +- systemd service / timerの設定例 + +同梱の設定例では、5分ごとの撮影、毎時10分のNAS同期、毎時15分のHealth Check、06:30・12:30・18:30の定期レポートを使用します。必要に応じて設定ファイルとsystemd timerを変更してください。 ## システム構成 ```text -Raspberry Pi Camera - | - v +対応カメラ + │ + ▼ Raspberry Pi - |- 定期撮影・照明状態判定 - |- SQLite観測データベース - |- イベント画像・毎時画像の保存 - |- Slack通知・定期レポート - |- NAS同期 - |- ヘルス監視 - `- FastAPI + Reactダッシュボード + ├─ 撮影・画像解析・照明分類 + ├─ SQLite記録 + ├─ Slack通知・定期レポート + ├─ NAS同期・ローカル画像整理 + ├─ Health Check + └─ FastAPI + React Web UI(既定ポート8000) ``` -## 動作要件 +フロントエンドは開発用PCでビルドし、生成された `frontend/dist/` をRaspberry Piへ配置します。 -### Raspberry Pi +## 動作環境 + +### 開発環境 + +- Python 3.11以上、3.14未満 +- uv +- Node.js / npm +- Git + +付属のPowerShellスクリプトを使用する場合は、次の環境を推奨します。 + +- Windows 11 +- PowerShell 7 + +### 実行環境 - Raspberry Pi Zero 2 W以降 - Raspberry Pi OS Lite 64-bit -- Raspberry Pi Camera Module 3、または`rpicam-apps`対応カメラ -- Python 3.11~3.13 -- `rpicam-still`、`rpicam-vid`、`rsync`、`sqlite3`、`flock` +- `rpicam-apps` 対応カメラ +- Python 3.11以上、3.14未満 +- `rpicam-still` +- `rpicam-vid` +- `rsync` +- `sqlite3` +- `flock` -### 開発環境 +同梱の設定例とsystemdユニットでは、配置先として `/opt/lightwatch`、実行ユーザーとして `pi` を使用します。 -- Python 3.11~3.13 -- Node.jsおよびnpm +## 開発環境のセットアップ -## リポジトリ構成 +プロジェクトルートでPython依存関係を準備します。 -```text -lightwatch/ -|- config/default.toml -|- frontend/ -|- scripts/ -|- src/lightwatch/ -|- systemd/ -|- tests/ -|- VERSION -|- pyproject.toml -`- README.md +```powershell +uv sync --frozen ``` -## 開発環境のセットアップ +フロントエンド依存関係をlockfileに基づいてインストールします。 ```powershell -cd C:\dev\lightwatch -python -m venv .venv -.\.venv\Scripts\Activate.ps1 -python -m pip install --upgrade pip -python -m pip install -e ".[dev]" - Push-Location frontend npm ci Pop-Location ``` -検証を実行します。 +フロントエンドを含む標準検証を実行します。 ```powershell -python .\scripts\verify-version.py -ruff check . -pytest +.\scripts\verify.ps1 +``` -Push-Location frontend -npm run lint -npm run build -npm audit -Pop-Location +Python側だけ確認する場合: + +```powershell +.\scripts\verify.ps1 -SkipFrontend ``` -## Raspberry Piへのセットアップ +詳細は [開発環境と検証](docs/development.md) を参照してください。 -同梱のsystemd設定は、プロジェクトを`/opt/lightwatch`へ配置し、ユーザー`pi`で実行する前提です。 +## Raspberry Piの初期セットアップ + +以下は、同梱のsystemdユニットに合わせて `/opt/lightwatch` へ配置する例です。 ```bash sudo mkdir -p /opt/lightwatch sudo chown pi:pi /opt/lightwatch cd /opt/lightwatch - -python3 -m venv .venv -source .venv/bin/activate -python -m pip install --upgrade pip -python -m pip install -r requirements.txt ``` -フロントエンドは開発用PCでビルドし、生成された`frontend/dist/`をRaspberry Piへコピーします。通常運用ではRaspberry Pi側にNode.jsは不要です。 - -## 設定 - -サンプル設定をコピーして編集します。 +uvをインストールし、Python実行依存関係を準備します。 ```bash -cp config/default.toml config/local.toml +curl -LsSf https://astral.sh/uv/install.sh | sh +export PATH="$HOME/.local/bin:$PATH" + +uv sync --frozen --no-dev ``` -標準以外の設定ファイルを使用する場合は、`LIGHTWATCH_CONFIG`を指定します。 +本番設定はGit管理外の `config/production.toml` を使用します。 + +サンプルから作成します。 ```bash -export LIGHTWATCH_CONFIG=/opt/lightwatch/config/local.toml +cp config/production.toml.example config/production.toml ``` -主な設定項目: - -```toml -[storage] -data_dir = "/opt/lightwatch/data/images" -temp_dir = "/dev/shm/lightwatch" -state_dir = "/opt/lightwatch/data/state" +設定ファイルは、環境変数 `LIGHTWATCH_CONFIG` で指定できます。systemd運用では `/etc/lightwatch.env` へ記載します。 -[database] -path = "/opt/lightwatch/data/timelapse.db" +```dotenv +LIGHTWATCH_CONFIG=/opt/lightwatch/config/production.toml +``` -[operations] -capture_interval_minutes = 5 -local_retention_days = 30 +実際の配置先や実行ユーザーが異なる場合は、設定ファイルとsystemdユニットを変更してください。 -[nas] -user = "backup" -host = "nas.local" -destination_dir = "/srv/timelapse/lightwatch" -ssh_key = "/home/pi/.ssh/id_ed25519" -``` +systemdの導入と確認は [systemd運用](docs/systemd.md) を参照してください。 -初期設定のROI座標は、2304×1296画像向けのサンプルです。照明状態の判定を運用に使用する前に、実際のカメラ設置位置と室内レイアウトに合わせて再調整してください。 +## ローカルWebプレビュー -## Slack通知 +Raspberry Pi、本番DB、NAS、Slack、カメラを使用せず、サンプルDBと画像を生成してWeb UIを確認できます。 -Raspberry Pi上に`/etc/lightwatch.env`を作成します。 +開発用設定を作成します。 -```dotenv -SLACK_BOT_TOKEN=xoxb-your-token -SLACK_CHANNEL_ID=C0123456789 +```powershell +Copy-Item ` + .\config\development.toml.example ` + .\config\development.toml ``` -アクセス権を設定します。 +サンプルデータを生成します。 -```bash -sudo chown root:root /etc/lightwatch.env -sudo chmod 600 /etc/lightwatch.env +```powershell +uv run --locked python .\scripts\create-web-sample-data.py ``` -実際のトークン、チャンネルID、SSH秘密鍵、本番環境の設定ファイルは、Gitへコミットしないでください。 +Web UIを起動します。 -## 手動実行 - -1回だけ撮影します。 +```powershell +$env:LIGHTWATCH_CONFIG = "config/development.toml" -```bash -cd /opt/lightwatch -.venv/bin/python -m lightwatch.capture +uv run --locked python -m uvicorn lightwatch.web:app ` + --host 127.0.0.1 ` + --port 8000 ` + --reload ``` -ドライラン: +ブラウザで `http://127.0.0.1:8000` を開きます。 -```bash -.venv/bin/python -m lightwatch.capture --dry-run -``` +詳細は [ローカルWebプレビュー](docs/local-web-preview.md) を参照してください。 -Web APIを起動します。 +## 設定 -```bash -.venv/bin/python -m uvicorn lightwatch.web:app --host 0.0.0.0 --port 8000 -``` +設定ファイルの解決順は次のとおりです。 -ブラウザで`http://:8000`を開きます。 +1. アプリ作成時に明示した設定パス +2. 環境変数 `LIGHTWATCH_CONFIG` +3. `config/default.toml` -ライブビュー: +主な設定項目: -```bash -./scripts/live-view.sh -``` +| セクション | 内容 | +| -------------- | ------------------------------------------ | +| `[camera]` | 撮影コマンド、解像度、画質、フォーカスなど | +| `[storage]` | 画像、一時ファイル、状態ファイルの保存先 | +| `[database]` | SQLiteデータベースのパス | +| `[lighting]` | 照明判定の閾値とROI | +| `[operations]` | 撮影間隔、画像保持期間 | +| `[nas]` | NAS接続先、SSH鍵、タイムアウト | -別のPCから確認します。 +同梱のROI座標と閾値は2304×1296画像向けのサンプルです。実際のカメラ位置、画角、照明配置に合わせて調整してください。 -```bash -ffplay -fflags nobuffer -flags low_delay -framedrop tcp://raspberrypi.local:8888 -``` +## ドキュメント -## systemdへの登録 +| 資料 | 内容 | +| -------------------------------------------------- | ---------------------------------------------- | +| [開発環境と検証](docs/development.md) | 開発セットアップ、テスト、フロントエンドビルド | +| [運用ガイド](docs/operations.md) | 撮影、通知、NAS、Health、Web UI、SQLite | +| [照明状態判定とROI](docs/lighting-roi.md) | 画像指標、判定順序、ROI座標と閾値 | +| [systemd運用](docs/systemd.md) | service / timerの構成、導入、確認、ログ | +| [ローカルWebプレビュー](docs/local-web-preview.md) | サンプルDB生成とローカルUI確認 | -詳細は[systemd/README.md](systemd/README.md)を参照してください。インストール前に、パス、実行ユーザー、スケジュール、環境変数ファイルを実際の環境に合わせて確認・変更してください。 +## セキュリティ -## バージョン管理 +LightWatchのWeb UIには、アプリケーション単体でのユーザー認証やTLS終端は含まれていません。信頼できるネットワーク内で使用するか、リバースプロキシやVPNなどでアクセス制御とHTTPSを追加してください。 -`VERSION`、`pyproject.toml`、`frontend/package.json`、`frontend/package-lock.json`のバージョンは一致させます。 +次の情報や生成物はGitへコミットしないでください。 -```powershell -.\scripts\sync-version.ps1 1.0.1 -python .\scripts\verify-version.py -``` +- `.env` とSlack認証情報 +- 本番用設定ファイル +- SSH秘密鍵とNAS認証情報 +- 撮影画像 +- SQLiteデータベース +- 状態ファイル +- ログ +- `.venv/` +- `frontend/node_modules/` +- ローカルWebプレビュー生成物 -## セキュリティ +脆弱性の報告方法とサポート対象バージョンは [SECURITY.md](SECURITY.md) を参照してください。 + +## コントリビューション -詳細は[SECURITY.md](SECURITY.md)を参照してください。LightWatchは、認証とTLSをリバースプロキシなどで追加しない限り、信頼できるネットワーク内での利用を想定しています。 +IssueやPull Requestを作成する前に [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。 ## ライセンス -MIT Licenseです。詳細は[LICENSE](LICENSE)を参照してください。 +LightWatchはMIT Licenseで公開されています。詳細は [LICENSE](LICENSE) を参照してください。 diff --git a/VERSION b/VERSION index 3eefcb9..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/config/default.toml b/config/default.toml index c1ef099..005931f 100644 --- a/config/default.toml +++ b/config/default.toml @@ -35,6 +35,7 @@ partial_min_bright_ratio = 0.02 # A/B/C correspond to the left, center, and right ceiling-lighting rows. [[lighting.rois]] name = "left_ceiling" +required_for_bright = false x1 = 0 y1 = 325 x2 = 760 @@ -44,6 +45,7 @@ max_dark_ratio = 0.12 [[lighting.rois]] name = "center_ceiling" +required_for_bright = true x1 = 760 y1 = 295 x2 = 1730 @@ -53,6 +55,7 @@ max_dark_ratio = 0.10 [[lighting.rois]] name = "right_ceiling" +required_for_bright = true x1 = 1730 y1 = 175 x2 = 2303 diff --git a/config/development.toml.example b/config/development.toml.example new file mode 100644 index 0000000..de06c63 --- /dev/null +++ b/config/development.toml.example @@ -0,0 +1,41 @@ +[camera] +command = "disabled-for-local-preview" +width = 2304 +height = 1296 +quality = 90 +timeout_ms = 3000 +autofocus_mode = "manual" +lens_position = 0.8 +awb = "fluorescent" +metering = "centre" +rotation = 0 + +[storage] +data_dir = ".local/web-preview/images" +temp_dir = ".local/web-preview/tmp" +state_dir = ".local/web-preview/state" + +[database] +path = ".local/web-preview/timelapse.db" + +[lighting] +dark_max_mean = 45.0 +dark_max_median = 45 +dark_min_dark_ratio = 0.70 +bright_min_mean = 100.0 +bright_min_median = 100 +bright_min_bright_ratio = 0.05 +partial_min_mean = 80.0 +partial_min_bright_ratio = 0.02 + +[operations] +capture_interval_minutes = 5 +local_retention_days = 30 + +[nas] +user = "disabled" +host = "localhost" +destination_dir = "/disabled/local-preview" +ssh_key = ".local/web-preview/disabled-key" +rsync_timeout_seconds = 1 +ssh_connect_timeout_seconds = 1 diff --git a/config/production.toml.example b/config/production.toml.example new file mode 100644 index 0000000..005931f --- /dev/null +++ b/config/production.toml.example @@ -0,0 +1,76 @@ +[camera] +command = "rpicam-still" +width = 2304 +height = 1296 +quality = 90 +timeout_ms = 3000 + +autofocus_mode = "manual" +lens_position = 0.8 +awb = "fluorescent" +metering = "centre" +rotation = 0 + +[storage] +data_dir = "/opt/lightwatch/data/images" +temp_dir = "/dev/shm/lightwatch" +state_dir = "/opt/lightwatch/data/state" + +[database] +path = "/opt/lightwatch/data/timelapse.db" + +[lighting] +dark_max_mean = 45.0 +dark_max_median = 45 +dark_min_dark_ratio = 0.70 + +bright_min_mean = 100.0 +bright_min_median = 100 +bright_min_bright_ratio = 0.05 + +partial_min_mean = 80.0 +partial_min_bright_ratio = 0.02 + +# Fixed ROIs for the 2304x1296 camera view. +# A/B/C correspond to the left, center, and right ceiling-lighting rows. +[[lighting.rois]] +name = "left_ceiling" +required_for_bright = false +x1 = 0 +y1 = 325 +x2 = 760 +y2 = 465 +min_mean = 120.0 +max_dark_ratio = 0.12 + +[[lighting.rois]] +name = "center_ceiling" +required_for_bright = true +x1 = 760 +y1 = 295 +x2 = 1730 +y2 = 430 +min_mean = 124.0 +max_dark_ratio = 0.10 + +[[lighting.rois]] +name = "right_ceiling" +required_for_bright = true +x1 = 1730 +y1 = 175 +x2 = 2303 +y2 = 330 +min_mean = 165.0 +max_dark_ratio = 0.10 + +[operations] +capture_interval_minutes = 5 +local_retention_days = 30 + +[nas] +user = "backup" +host = "nas.local" +destination_dir = "/srv/timelapse/lightwatch" +ssh_key = "/home/pi/.ssh/id_ed25519" +rsync_timeout_seconds = 120 +ssh_connect_timeout_seconds = 10 diff --git a/docs/.gitkeep b/docs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..bd8dfa3 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,147 @@ +# 開発環境と検証 + +## 前提 + +- Python 3.11以上、3.14未満 +- uv +- Node.js / npm +- Git + +Windowsで付属のPowerShellスクリプトを使用する場合は、次の環境を推奨します。 + +- Windows 11 +- PowerShell 7 + +以下のコマンドは、特記がない限りプロジェクトルートで実行します。 + +## Python環境 + +依存関係と仮想環境を準備します。 + +```powershell +uv sync --frozen +``` + +`.python-version` では、開発時の推奨バージョンとしてPython 3.13系を指定しています。 + +Pythonパッケージのバージョンは `uv.lock` で固定し、開発用依存関係は `pyproject.toml` の `dependency-groups.dev` で管理します。 + +仮想環境を手動で有効化せず、`uv run` 経由でコマンドを実行できます。 + +```powershell +uv run --locked python --version +uv run --locked pytest -q +uv run --locked ruff check . +``` + +## フロントエンド + +`frontend` ディレクトリで、lockfileに基づいて依存関係をインストールします。 + +```powershell +Push-Location frontend +npm ci +Pop-Location +``` + +利用可能なnpmスクリプトは次のとおりです。 + +```text +npm run dev +npm run lint +npm run build +npm run preview +``` + +## 標準検証 + +フロントエンドを含む完全検証を実行します。 + +```powershell +.\scripts\verify.ps1 +``` + +Python側だけを確認する場合は、`-SkipFrontend` を指定します。 + +```powershell +.\scripts\verify.ps1 -SkipFrontend +``` + +検証内容は次のとおりです。 + +```text +uv lock --check +バージョン整合性 +Ruff lint +Ruff format +pytest +フロントエンドlint +フロントエンドbuild +git diff --check +``` + +正常に完了すると、次のメッセージが表示されます。 + +```text +All verification steps passed. +``` + +差分と作業ツリーの状態も確認してください。 + +```powershell +git status +git diff --stat +git diff --check +``` + +pytestは、`pyproject.toml` の `addopts = "-q"` によりquietモードで実行されます。 + +## 個別の検証コマンド + +PowerShellスクリプトを使用しない場合は、各検証を個別に実行できます。 + +```powershell +uv lock --check +uv run --locked python .\scripts\verify-version.py +uv run --locked ruff check . +uv run --locked ruff format --check . +uv run --locked pytest -q +``` + +フロントエンドを検証します。 + +```powershell +Push-Location frontend +npm run lint +npm run build +Pop-Location +``` + +## バージョン整合性 + +ルートの `VERSION` を基準として、次のファイルとのバージョン一致を検証します。 + +- `pyproject.toml` +- `frontend/package.json` +- `frontend/package-lock.json` +- `uv.lock` + +確認コマンド: + +```powershell +uv run --locked python .\scripts\verify-version.py +``` + +バージョンを更新する場合: + +```powershell +.\scripts\sync-version.ps1 X.Y.Z +``` + +このスクリプトは、次の処理を行います。 + +1. `VERSION` を更新 +2. `pyproject.toml` を更新 +3. `frontend/package.json` と `frontend/package-lock.json` を更新 +4. `uv.lock` を再生成 +5. バージョン整合性を検証 diff --git a/docs/lighting-roi.md b/docs/lighting-roi.md new file mode 100644 index 0000000..48b2247 --- /dev/null +++ b/docs/lighting-roi.md @@ -0,0 +1,119 @@ +# 照明状態判定とROI + +## 設定と実装 + +照明判定の標準設定は `config/default.toml` にあります。 + +実際の運用では、カメラの設置位置、画角、照明配置に合わせて、Git管理外の設定ファイルでROI座標と閾値を調整してください。 + +主な実装は次のファイルにあります。 + +- `src/lightwatch/analysis.py` +- `src/lightwatch/capture.py` + +同梱のサンプル設定では、2304×1296の画像を前提としています。画像サイズが設定と一致しない場合、ROI解析は失敗し、撮影結果は `analysis_failed` として記録されます。 + +## 画像指標 + +画像をグレースケールへ変換し、画像全体と各ROIについて次の値を算出します。 + +- `mean_brightness`: 平均輝度 +- `median_brightness`: 中央値 +- `dark_ratio`: 輝度40以下の画素割合 +- `bright_ratio`: 輝度220以上の画素割合 + +各ROIのON判定では、次の2条件を同時に満たす必要があります。 + +```text +mean_brightness >= min_mean +dark_ratio <= max_dark_ratio +``` + +## サンプルROI + +座標はPillowのcrop範囲 `(x1, y1, x2, y2)` として使用されます。`x2` と `y2` は範囲の終端で、切り出しには含まれません。 + +次の値は、2304×1296画像向けのサンプルです。実際の環境に合わせて調整してください。 + +| ROI | 用途 | 必須 | 座標 | `min_mean` | `max_dark_ratio` | +| ---------------- | -------------- | ------ | --------------------------- | ---------: | ---------------: | +| `left_ceiling` | 左側領域の観測 | いいえ | `(0, 325) - (760, 465)` | 120.0 | 0.12 | +| `center_ceiling` | 中央領域の観測 | はい | `(760, 295) - (1730, 430)` | 124.0 | 0.10 | +| `right_ceiling` | 右側領域の観測 | はい | `(1730, 175) - (2303, 330)` | 165.0 | 0.10 | + +`left_ceiling` は判定理由へ記録されますが、サンプル設定ではBRIGHT成立の必須条件ではありません。 + +## 判定順序 + +新しい撮影では `classify_lighting_with_rois()` を使用し、次の順序で判定します。 + +### 1. DARK + +画像全体が次の3条件をすべて満たす場合は `DARK` です。 + +```text +mean_brightness <= 45.0 +median_brightness <= 45 +dark_ratio >= 0.70 +``` + +DARK判定はROI判定より先に行います。 + +### 2. BRIGHT + +DARKでなく、`required_for_bright = true` の全ROIがONの場合は `BRIGHT` です。 + +サンプル設定で必須としているROIは次の2つです。 + +```text +center_ceiling +right_ceiling +``` + +### 3. PARTIAL + +必須ROIのいずれかがOFFで、画像全体が次のどちらかを満たす場合は `PARTIAL` です。 + +```text +mean_brightness >= 80.0 +または +bright_ratio >= 0.02 +``` + +### 4. DIM + +DARK、BRIGHT、PARTIALのいずれにも該当しない場合は `DIM` です。 + +## 判定理由 + +SQLiteの `lighting_reason` には、各ROIについて次の情報が記録されます。 + +- ON / OFF +- 実測平均輝度と閾値 +- 実測暗部率と閾値 +- BRIGHT判定で必須かどうか +- 画像全体の4指標 + +Web UIとAPIは、この文字列を参照して判定詳細を表示します。 + +## 履歴再分類との違い + +`lightwatch.analysis.classify_lighting()` と `scripts/reclassify_lighting.py` は、SQLiteへ保存済みの画像全体4指標だけを使用する従来の全体判定です。 + +この経路では `bright_min_mean`、`bright_min_median`、`bright_min_bright_ratio` を使用します。一方、新規撮影におけるBRIGHT判定は、必須ROIのON/OFFで決まります。 + +保存済みの画像全体指標だけではROIごとの局所状態を復元できないため、履歴再分類はROI判定と同じ結果になるとは限りません。 + +## 変更時の注意 + +ROI座標または閾値を変更する場合は、少なくとも次を確認してください。 + +1. カメラの解像度、位置、向き、画角が固定されている +2. 設定ファイルの座標が画像範囲内にある +3. 必須ROIが1つ以上存在する +4. 明るい状態、部分消灯、暗い状態の実画像で判定結果を確認する +5. `lighting_reason` の実測値と閾値を確認する +6. 関連する分析・分類テストを実行する + +画角やカメラ位置を変更した場合も、既存のROI座標と閾値を再評価してください。 +s diff --git a/docs/local-web-preview.md b/docs/local-web-preview.md new file mode 100644 index 0000000..c986191 --- /dev/null +++ b/docs/local-web-preview.md @@ -0,0 +1,69 @@ +# ローカルWebプレビュー + +Raspberry Pi、本番DB、NAS、Slack、カメラを使用せず、Windows上でWeb UIを確認します。 + +生成物は `.local/web-preview/` に保存され、Git管理対象外です。 + +## 初回準備 + +プロジェクトルートで、開発用設定ファイルを作成します。 + +```powershell +Copy-Item ` + .\config\development.toml.example ` + .\config\development.toml +``` + +`config/development.toml` はGit管理対象外です。 + +Python依存関係を準備していない場合は、次を実行します。 + +```powershell +uv sync --frozen +``` + +## サンプルデータ生成 + +```powershell +uv run --locked python .\scripts\create-web-sample-data.py +``` + +既存のサンプルデータを上書きして再生成する場合: + +```powershell +uv run --locked python .\scripts\create-web-sample-data.py --force +``` + +サンプルには次のデータが含まれます。 + +- `BRIGHT` / `PARTIAL` / `DIM` / `DARK` +- 長い判定理由 +- 保存画像あり/なし +- 状態遷移タイムライン + +## Web起動 + +```powershell +$env:LIGHTWATCH_CONFIG = "config/development.toml" + +uv run --locked python -m uvicorn lightwatch.web:app ` + --host 127.0.0.1 ` + --port 8000 ` + --reload +``` + +ブラウザで `http://127.0.0.1:8000` を開きます。 + +設定ファイルの解決順は次のとおりです。 + +1. アプリ作成時に明示した設定パス +2. 環境変数 `LIGHTWATCH_CONFIG` +3. `config/default.toml` + +## 終了後 + +環境変数を削除します。 + +```powershell +Remove-Item Env:LIGHTWATCH_CONFIG -ErrorAction SilentlyContinue +``` diff --git a/docs/operations.md b/docs/operations.md new file mode 100644 index 0000000..d56fbc3 --- /dev/null +++ b/docs/operations.md @@ -0,0 +1,259 @@ +# 運用ガイド + +## 設定 + +本番設定はGit管理外の `config/production.toml` で管理し、`/etc/lightwatch.env` の `LIGHTWATCH_CONFIG` で明示します。 + +| セクション | 用途 | +| -------------- | ------------------------------------------------------- | +| `[camera]` | 撮影コマンド、解像度、画質、フォーカス、AWB、測光、回転 | +| `[storage]` | 正式画像、RAM一時領域、状態ファイルの保存先 | +| `[database]` | SQLite DBのパス | +| `[lighting]` | 全体判定閾値と固定ROI | +| `[operations]` | 撮影間隔とローカル保持日数 | +| `[nas]` | NAS接続先、SSH鍵、タイムアウト | + +撮影間隔を変更する場合は、次の両方を同じ間隔へ変更します。 + +- `config/production.toml` の `operations.capture_interval_minutes` +- `systemd/lightwatch-capture.timer` の `OnCalendar` + +照明判定の詳細は [照明状態判定とROI](lighting-roi.md) を参照してください。 + +## 撮影と保存 + +通常実行: + +```bash +cd /opt/lightwatch +source .venv/bin/activate +python -m lightwatch.capture +``` + +カメラコマンドの表示だけを行うdry-run: + +```bash +python -m lightwatch.capture --dry-run +``` + +状態にかかわらず正式画像を保存: + +```bash +python -m lightwatch.capture --force-save +``` + +正式保存条件: + +- 初回の成功観測 +- 毎時00分の成功観測 +- 前回の成功観測から照明状態が変化 +- `--force-save` を指定 +- 画像解析に失敗 + +状態変化がない通常観測画像は、SQLite記録後に正式保存せず削除します。成功観測の最新スナップショットは、定期レポート用として `storage.temp_dir` 配下の `latest-observation/` に更新されます。 + +## Slack通知 + +認証情報は `/etc/lightwatch.env` に保存します。 + +```dotenv +SLACK_BOT_TOKEN= +SLACK_CHANNEL_ID=Cxxxxxxxxxx +``` + +```bash +sudo chown root:root /etc/lightwatch.env +sudo chmod 600 /etc/lightwatch.env +``` + +DARK通知は、非DARKからDARKへの遷移時だけ送信します。初回観測、DARK継続、DARKからの復旧では送信しません。 + +接続テスト: + +```bash +cd /opt/lightwatch +source .venv/bin/activate +set -a +source <(sudo cat /etc/lightwatch.env) +set +a +python -m lightwatch.notification --send-test +``` + +遷移判定だけを確認する場合は `--dry-run` と前後状態を指定します。 + +```bash +python -m lightwatch.notification \ + --previous-state BRIGHT \ + --current-state DARK \ + --dry-run +``` + +## 定期レポート + +`lightwatch-report.timer` により06:30 / 12:30 / 18:30に送信します。 + +レポートは、RAM上の最新観測画像とそのcapture IDに対応するSQLiteレコードを優先します。利用できない場合は、ファイルが存在する最新の正式保存画像へフォールバックします。 + +送信せず内容を確認: + +```bash +cd /opt/lightwatch +source .venv/bin/activate +python -m lightwatch.report --dry-run +``` + +## NAS同期 + +手動実行: + +```bash +cd /opt/lightwatch +./scripts/sync-to-nas.sh +``` + +同期スクリプトは `LIGHTWATCH_CONFIG` が設定されている場合はその設定を、未設定時は `/opt/lightwatch/config/default.toml` を使用します。 + +状態ファイル: + +```text +/opt/lightwatch/data/state/nas-sync-status.json +/opt/lightwatch/data/state/nas-sync-success +``` + +rsyncと状態記録が正常終了した場合だけ、同期対象レコードの `nas_synced` を更新します。 + +## ローカル画像整理 + +削除対象は次をすべて満たす正式保存画像です。 + +- 撮影成功 +- NAS同期済み +- `operations.local_retention_days` を経過 +- 設定された画像ディレクトリ配下 + +DBレコードは削除しません。 + +送信せず対象を確認: + +```bash +cd /opt/lightwatch +source .venv/bin/activate +python -m lightwatch.cleanup --dry-run +``` + +## Health Check + +```bash +cd /opt/lightwatch +source .venv/bin/activate +python -m lightwatch.health +python -m lightwatch.health --json +``` + +確認項目: + +- 最新成功観測の経過時間 +- NAS同期の最終試行・最終成功 +- 直近NAS同期失敗 +- 未同期画像件数 +- ディスク使用率 +- 総合状態 + +Health状態は `data/state/health-status.json` に保存します。初回は状態保存のみ行い、以降は悪化または復旧時にSlack通知します。同一状態が続く場合は通知しません。 + +## Web UIとAPI + +Web UIは `lightwatch-web.service` によりポート8000で稼働します。 + +実装されているAPI: + +```text +GET /api/status +GET /api/images +GET /api/images/latest +GET /api/images/file/{capture_id} +GET /api/images/{date_value} +GET /api/observations +GET /api/timeline +GET /api/health +GET /api/system +``` + +確認: + +```bash +curl -fsS http://127.0.0.1:8000/api/status +curl -fsS http://127.0.0.1:8000/api/health +``` + +## ライブビュー + +Pi側: + +```bash +cd /opt/lightwatch +./scripts/live-view.sh +``` + +Windows側: + +```powershell +ffplay -fflags nobuffer -flags low_delay -framedrop tcp://raspberrypi.local:8888 +``` + +撮影とライブビューは `/run/lock/lightwatch-camera.lock` を使用して排他制御します。ロック取得済みの場合、後から起動した処理はメッセージを出して終了します。 + +## SQLite確認 + +DB: + +```text +/opt/lightwatch/data/timelapse.db +``` + +直近の観測: + +```bash +cd /opt/lightwatch +sqlite3 -header -column data/timelapse.db \ + "SELECT captured_at, status, lighting_state, lighting_reason FROM captures ORDER BY id DESC LIMIT 10;" +``` + +DBメンテナンスCLI: + +```bash +python -m lightwatch.database_maintenance --help +``` + +保存済みの全体指標を使用した過去データ再分類: + +```bash +python scripts/reclassify_lighting.py --help +``` + +この再分類は保存済みの全体4指標を使用し、現在のROI判定を再現しません。 + +## 更新とロールバック + +ソースコードを更新する前に、次の本番データをバックアップしてください。 + +- 本番設定ファイル +- SQLiteデータベース +- 撮影画像 +- 状態ファイル +- 必要なログ + +本番固有の設定ファイル、データベース、撮影画像、状態ファイル、ログは、ソースコードの更新時に上書きしないでください。 + +更新またはロールバックでは、対象バージョンのGitタグまたはGitHub Releaseを取得し、依存関係とフロントエンドを再構築します。systemdユニットを変更した場合は、ユニットファイルを反映して `daemon-reload` を実行してください。 + +反映後は、少なくとも次を確認します。 + +```bash +cat /opt/lightwatch/VERSION +systemctl status lightwatch-web.service --no-pager +systemctl list-timers --all | grep lightwatch +curl -fsS http://127.0.0.1:8000/api/status +``` + +DBスキーマや設定形式に互換性がない場合は、必要な移行手順と復旧方法を確認せずにロールバックしないでください。 diff --git a/docs/systemd.md b/docs/systemd.md new file mode 100644 index 0000000..5e3e82f --- /dev/null +++ b/docs/systemd.md @@ -0,0 +1,129 @@ +# systemd運用 + +## 正本と配置先 + +systemdユニットの正本はリポジトリの `systemd/` 配下です。Raspberry Piでは `/etc/systemd/system/` へコピーして使用します。 + +同梱のsystemdユニットは、既定例として `User=pi`、`Group=pi`、`WorkingDirectory=/opt/lightwatch` を使用します。実際の配置先や実行ユーザーが異なる場合は、インストール前に各ユニットを変更してください。 + +## ユニット一覧 + +| service | timer | 実行内容 | +| ----------------------------- | --------------------------- | ------------------------------------------------ | +| `lightwatch-capture.service` | `lightwatch-capture.timer` | カメラロックを取得して撮影、解析、保存、DARK通知 | +| `lightwatch-nas-sync.service` | `lightwatch-nas-sync.timer` | `scripts/sync-to-nas.sh` によるNAS同期 | +| `lightwatch-health.service` | `lightwatch-health.timer` | Health Checkと状態変化通知 | +| `lightwatch-report.service` | `lightwatch-report.timer` | 画像付きSlack定期レポート | +| `lightwatch-cleanup.service` | `lightwatch-cleanup.timer` | 同期済みローカル画像の整理 | +| `lightwatch-web.service` | なし | FastAPI + React Web UIを常時起動 | + +## スケジュール + +次のスケジュールは、同梱timerユニットの既定値です。運用要件に合わせて変更してください。 + +| timer | `OnCalendar` | `Persistent` | 処理 | +| --------------------------- | ---------------------- | ------------ | ----------------------------------- | +| `lightwatch-capture.timer` | `*:0/5` | `true` | 5分ごとの撮影 | +| `lightwatch-nas-sync.timer` | `*-*-* *:10:00` | `true` | 毎時10分のNAS同期 | +| `lightwatch-health.timer` | `*-*-* *:15:00` | `true` | 毎時15分のHealth Check | +| `lightwatch-report.timer` | `*-*-* 06,12,18:30:00` | `false` | 06:30 / 12:30 / 18:30の定期レポート | +| `lightwatch-cleanup.timer` | `*-*-* 03:30:00` | `true` | 毎日03:30のローカル画像整理 | + +## service固有の設定 + +- capture / health / report / webは `EnvironmentFile=-/etc/lightwatch.env` を読み込みます +- captureは `scripts/run-with-camera-lock.sh` を経由します +- NAS同期は `HOME=/home/pi` を設定し、`TimeoutStartSec=10min` です +- cleanupは `After=lightwatch-nas-sync.service` です +- webは `Restart=on-failure`、`RestartSec=5` です +- `EnvironmentFile` の先頭の `-` により、ファイルが存在しない場合もserviceの起動自体は継続します + +## インストール・更新 + +同梱ユニット内の実行ユーザー、グループ、配置先、環境変数ファイル、実行スケジュールを確認してからインストールします。 + +Raspberry Pi上のプロジェクトルートで実行します。 + +```bash +cd /opt/lightwatch +sudo cp systemd/*.service systemd/*.timer /etc/systemd/system/ +sudo systemctl daemon-reload +``` + +タイマーを有効化・起動: + +```bash +sudo systemctl enable --now \ + lightwatch-capture.timer \ + lightwatch-nas-sync.timer \ + lightwatch-health.timer \ + lightwatch-report.timer \ + lightwatch-cleanup.timer +``` + +Web UIを有効化・起動: + +```bash +sudo systemctl enable --now lightwatch-web.service +``` + +ユニット更新後は `daemon-reload` の後、変更対象を再起動します。 + +```bash +sudo systemctl daemon-reload +sudo systemctl restart lightwatch-web.service +sudo systemctl restart lightwatch-capture.timer +systemctl list-timers --all | grep lightwatch +``` + +## 状態確認 + +```bash +systemctl list-timers --all | grep lightwatch +systemctl status lightwatch-web.service --no-pager +``` + +設定内容の確認: + +```bash +systemctl cat lightwatch-web.service +systemctl cat lightwatch-capture.timer +``` + +個別処理を手動実行: + +```bash +sudo systemctl start lightwatch-capture.service +sudo systemctl start lightwatch-nas-sync.service +sudo systemctl start lightwatch-health.service +sudo systemctl start lightwatch-report.service +sudo systemctl start lightwatch-cleanup.service +``` + +oneshot serviceは、正常終了後も `inactive (dead)` へ戻ります。この表示だけでは失敗とは判断できません。`Result=success`、`ExecMainStatus=0`、およびjournalを確認してください。 + +```bash +systemctl show lightwatch-nas-sync.service \ + -p Result \ + -p ExecMainStatus \ + -p ActiveState +``` + +## ログ確認 + +```bash +journalctl -u lightwatch-web.service -n 100 --no-pager +journalctl -u lightwatch-capture.service -n 100 --no-pager +journalctl -u lightwatch-nas-sync.service -n 100 --no-pager +journalctl -u lightwatch-health.service -n 100 --no-pager +journalctl -u lightwatch-report.service -n 100 --no-pager +journalctl -u lightwatch-cleanup.service -n 100 --no-pager +``` + +リアルタイム表示: + +```bash +journalctl -u lightwatch-web.service -f +``` + +処理内容と手動実行方法は [運用ガイド](operations.md) を参照してください。 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b8ded11..2bcacb5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,3037 +1 @@ -{ - "name": "lightwatch-web", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "lightwatch-web", - "version": "1.0.0", - "dependencies": { - "@tailwindcss/vite": "^4.3.3", - "@vitejs/plugin-react": "latest", - "lucide-react": "latest", - "react": "latest", - "react-dom": "latest", - "tailwindcss": "^4.3.3", - "typescript": "latest", - "vite": "latest" - }, - "devDependencies": { - "@eslint/js": "latest", - "@types/react": "latest", - "@types/react-dom": "latest", - "eslint": "latest", - "eslint-plugin-react-hooks": "latest", - "eslint-plugin-react-refresh": "latest", - "globals": "latest", - "typescript-eslint": "latest" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "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", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "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", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "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", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "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", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "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", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "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-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "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", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "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", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "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", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "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/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "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==", - "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==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@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==", - "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==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/@tailwindcss/node": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", - "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.24.1", - "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.3" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", - "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-x64": "4.3.3", - "@tailwindcss/oxide-freebsd-x64": "4.3.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-x64-musl": "4.3.3", - "@tailwindcss/oxide-wasm32-wasi": "4.3.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", - "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", - "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", - "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", - "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", - "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", - "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", - "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", - "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", - "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", - "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", - "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", - "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", - "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "tailwindcss": "4.3.3" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, - "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==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", - "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/type-utils": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.64.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", - "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", - "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", - "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", - "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", - "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", - "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", - "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.64.0", - "@typescript-eslint/tsconfig-utils": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", - "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", - "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.64.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, - "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/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.43", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", - "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/browserslist": { - "version": "4.28.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", - "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", - "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.42", - "caniuse-lite": "^1.0.30001803", - "electron-to-chromium": "^1.5.389", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", - "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/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.393", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz", - "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==", - "dev": true, - "license": "ISC" - }, - "node_modules/enhanced-resolve": { - "version": "5.24.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", - "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", - "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", - "dev": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", - "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", - "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/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/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz", - "integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "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/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", - "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", - "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.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/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/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "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/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "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/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tailwindcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz", - "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.64.0", - "@typescript-eslint/parser": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "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/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", - "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/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/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} +{"name":"lightwatch-web","version":"1.1.0","lockfileVersion":3,"requires":true,"packages":{"":{"name":"lightwatch-web","version":"1.1.0","dependencies":{"@tailwindcss/vite":"4.3.3","@vitejs/plugin-react":"6.0.3","lucide-react":"1.25.0","react":"19.2.7","react-dom":"19.2.7","tailwindcss":"4.3.3","typescript":"6.0.3","vite":"8.1.5"},"devDependencies":{"@eslint/js":"10.0.1","@types/react":"19.2.17","@types/react-dom":"19.2.3","eslint":"10.7.0","eslint-plugin-react-hooks":"7.1.1","eslint-plugin-react-refresh":"0.5.3","globals":"17.7.0","typescript-eslint":"8.64.0"}},"node_modules/@babel/code-frame":{"version":"7.29.7","resolved":"https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz","integrity":"sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==","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","resolved":"https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz","integrity":"sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==","dev":true,"license":"MIT","engines":{"node":">=6.9.0"}},"node_modules/@babel/core":{"version":"7.29.7","resolved":"https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz","integrity":"sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==","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","resolved":"https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz","integrity":"sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==","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","resolved":"https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz","integrity":"sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==","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","resolved":"https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz","integrity":"sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==","dev":true,"license":"MIT","engines":{"node":">=6.9.0"}},"node_modules/@babel/helper-module-imports":{"version":"7.29.7","resolved":"https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz","integrity":"sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==","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","resolved":"https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz","integrity":"sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==","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-string-parser":{"version":"7.29.7","resolved":"https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz","integrity":"sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==","dev":true,"license":"MIT","engines":{"node":">=6.9.0"}},"node_modules/@babel/helper-validator-identifier":{"version":"7.29.7","resolved":"https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz","integrity":"sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==","dev":true,"license":"MIT","engines":{"node":">=6.9.0"}},"node_modules/@babel/helper-validator-option":{"version":"7.29.7","resolved":"https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz","integrity":"sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==","dev":true,"license":"MIT","engines":{"node":">=6.9.0"}},"node_modules/@babel/helpers":{"version":"7.29.7","resolved":"https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz","integrity":"sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==","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","resolved":"https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz","integrity":"sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==","dev":true,"license":"MIT","dependencies":{"@babel/types":"^7.29.7"},"bin":{"parser":"bin/babel-parser.js"},"engines":{"node":">=6.0.0"}},"node_modules/@babel/template":{"version":"7.29.7","resolved":"https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz","integrity":"sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==","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","resolved":"https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz","integrity":"sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==","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","resolved":"https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz","integrity":"sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==","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/@emnapi/core":{"version":"1.11.1","resolved":"https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz","integrity":"sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==","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==","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==","license":"MIT","optional":true,"dependencies":{"tslib":"^2.4.0"}},"node_modules/@eslint-community/eslint-utils":{"version":"4.9.1","resolved":"https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz","integrity":"sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==","dev":true,"license":"MIT","dependencies":{"eslint-visitor-keys":"^3.4.3"},"engines":{"node":"^12.22.0 || ^14.17.0 || >=16.0.0"},"funding":{"url":"https://opencollective.com/eslint"},"peerDependencies":{"eslint":"^6.0.0 || ^7.0.0 || >=8.0.0"}},"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys":{"version":"3.4.3","resolved":"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz","integrity":"sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==","dev":true,"license":"Apache-2.0","engines":{"node":"^12.22.0 || ^14.17.0 || >=16.0.0"},"funding":{"url":"https://opencollective.com/eslint"}},"node_modules/@eslint-community/regexpp":{"version":"4.12.2","resolved":"https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz","integrity":"sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==","dev":true,"license":"MIT","engines":{"node":"^12.0.0 || ^14.0.0 || >=16.0.0"}},"node_modules/@eslint/config-array":{"version":"0.23.5","resolved":"https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz","integrity":"sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==","dev":true,"license":"Apache-2.0","dependencies":{"@eslint/object-schema":"^3.0.5","debug":"^4.3.1","minimatch":"^10.2.4"},"engines":{"node":"^20.19.0 || ^22.13.0 || >=24"}},"node_modules/@eslint/config-helpers":{"version":"0.6.0","resolved":"https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz","integrity":"sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==","dev":true,"license":"Apache-2.0","dependencies":{"@eslint/core":"^1.2.1"},"engines":{"node":"^20.19.0 || ^22.13.0 || >=24"}},"node_modules/@eslint/core":{"version":"1.2.1","resolved":"https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz","integrity":"sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==","dev":true,"license":"Apache-2.0","dependencies":{"@types/json-schema":"^7.0.15"},"engines":{"node":"^20.19.0 || ^22.13.0 || >=24"}},"node_modules/@eslint/js":{"version":"10.0.1","resolved":"https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz","integrity":"sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==","dev":true,"license":"MIT","engines":{"node":"^20.19.0 || ^22.13.0 || >=24"},"funding":{"url":"https://eslint.org/donate"},"peerDependencies":{"eslint":"^10.0.0"},"peerDependenciesMeta":{"eslint":{"optional":true}}},"node_modules/@eslint/object-schema":{"version":"3.0.5","resolved":"https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz","integrity":"sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==","dev":true,"license":"Apache-2.0","engines":{"node":"^20.19.0 || ^22.13.0 || >=24"}},"node_modules/@eslint/plugin-kit":{"version":"0.7.2","resolved":"https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz","integrity":"sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==","dev":true,"license":"Apache-2.0","dependencies":{"@eslint/core":"^1.2.1","levn":"^0.4.1"},"engines":{"node":"^20.19.0 || ^22.13.0 || >=24"}},"node_modules/@humanfs/core":{"version":"0.19.2","resolved":"https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz","integrity":"sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==","dev":true,"license":"Apache-2.0","dependencies":{"@humanfs/types":"^0.15.0"},"engines":{"node":">=18.18.0"}},"node_modules/@humanfs/node":{"version":"0.16.8","resolved":"https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz","integrity":"sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==","dev":true,"license":"Apache-2.0","dependencies":{"@humanfs/core":"^0.19.2","@humanfs/types":"^0.15.0","@humanwhocodes/retry":"^0.4.0"},"engines":{"node":">=18.18.0"}},"node_modules/@humanfs/types":{"version":"0.15.0","resolved":"https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz","integrity":"sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==","dev":true,"license":"Apache-2.0","engines":{"node":">=18.18.0"}},"node_modules/@humanwhocodes/module-importer":{"version":"1.0.1","resolved":"https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz","integrity":"sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==","dev":true,"license":"Apache-2.0","engines":{"node":">=12.22"},"funding":{"type":"github","url":"https://github.com/sponsors/nzakas"}},"node_modules/@humanwhocodes/retry":{"version":"0.4.3","resolved":"https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz","integrity":"sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==","dev":true,"license":"Apache-2.0","engines":{"node":">=18.18"},"funding":{"type":"github","url":"https://github.com/sponsors/nzakas"}},"node_modules/@jridgewell/gen-mapping":{"version":"0.3.13","resolved":"https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz","integrity":"sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==","license":"MIT","dependencies":{"@jridgewell/sourcemap-codec":"^1.5.0","@jridgewell/trace-mapping":"^0.3.24"}},"node_modules/@jridgewell/remapping":{"version":"2.3.5","resolved":"https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz","integrity":"sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==","license":"MIT","dependencies":{"@jridgewell/gen-mapping":"^0.3.5","@jridgewell/trace-mapping":"^0.3.24"}},"node_modules/@jridgewell/resolve-uri":{"version":"3.1.2","resolved":"https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz","integrity":"sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==","license":"MIT","engines":{"node":">=6.0.0"}},"node_modules/@jridgewell/sourcemap-codec":{"version":"1.5.5","resolved":"https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz","integrity":"sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==","license":"MIT"},"node_modules/@jridgewell/trace-mapping":{"version":"0.3.31","resolved":"https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz","integrity":"sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==","license":"MIT","dependencies":{"@jridgewell/resolve-uri":"^3.1.0","@jridgewell/sourcemap-codec":"^1.4.14"}},"node_modules/@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==","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==","license":"MIT","funding":{"url":"https://github.com/sponsors/Boshen"}},"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"],"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"],"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"],"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"],"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"],"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"],"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"],"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"],"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"],"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"],"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"],"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"],"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"],"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"],"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"],"license":"MIT","optional":true,"os":["win32"],"engines":{"node":"^20.19.0 || >=22.12.0"}},"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==","license":"MIT"},"node_modules/@tailwindcss/node":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz","integrity":"sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==","license":"MIT","dependencies":{"@jridgewell/remapping":"^2.3.5","enhanced-resolve":"^5.24.1","jiti":"^2.7.0","lightningcss":"1.32.0","magic-string":"^0.30.21","source-map-js":"^1.2.1","tailwindcss":"4.3.3"}},"node_modules/@tailwindcss/oxide":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz","integrity":"sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==","license":"MIT","engines":{"node":">= 20"},"optionalDependencies":{"@tailwindcss/oxide-android-arm64":"4.3.3","@tailwindcss/oxide-darwin-arm64":"4.3.3","@tailwindcss/oxide-darwin-x64":"4.3.3","@tailwindcss/oxide-freebsd-x64":"4.3.3","@tailwindcss/oxide-linux-arm-gnueabihf":"4.3.3","@tailwindcss/oxide-linux-arm64-gnu":"4.3.3","@tailwindcss/oxide-linux-arm64-musl":"4.3.3","@tailwindcss/oxide-linux-x64-gnu":"4.3.3","@tailwindcss/oxide-linux-x64-musl":"4.3.3","@tailwindcss/oxide-wasm32-wasi":"4.3.3","@tailwindcss/oxide-win32-arm64-msvc":"4.3.3","@tailwindcss/oxide-win32-x64-msvc":"4.3.3"}},"node_modules/@tailwindcss/oxide-android-arm64":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz","integrity":"sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==","cpu":["arm64"],"license":"MIT","optional":true,"os":["android"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-darwin-arm64":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz","integrity":"sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==","cpu":["arm64"],"license":"MIT","optional":true,"os":["darwin"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-darwin-x64":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz","integrity":"sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==","cpu":["x64"],"license":"MIT","optional":true,"os":["darwin"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-freebsd-x64":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz","integrity":"sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==","cpu":["x64"],"license":"MIT","optional":true,"os":["freebsd"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz","integrity":"sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==","cpu":["arm"],"license":"MIT","optional":true,"os":["linux"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-linux-arm64-gnu":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz","integrity":"sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==","cpu":["arm64"],"libc":["glibc"],"license":"MIT","optional":true,"os":["linux"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-linux-arm64-musl":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz","integrity":"sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==","cpu":["arm64"],"libc":["musl"],"license":"MIT","optional":true,"os":["linux"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-linux-x64-gnu":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz","integrity":"sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==","cpu":["x64"],"libc":["glibc"],"license":"MIT","optional":true,"os":["linux"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-linux-x64-musl":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz","integrity":"sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==","cpu":["x64"],"libc":["musl"],"license":"MIT","optional":true,"os":["linux"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-wasm32-wasi":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz","integrity":"sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==","bundleDependencies":["@napi-rs/wasm-runtime","@emnapi/core","@emnapi/runtime","@tybys/wasm-util","@emnapi/wasi-threads","tslib"],"cpu":["wasm32"],"license":"MIT","optional":true,"dependencies":{"@emnapi/core":"^1.11.1","@emnapi/runtime":"^1.11.1","@emnapi/wasi-threads":"^1.2.2","@napi-rs/wasm-runtime":"^1.1.4","@tybys/wasm-util":"^0.10.2","tslib":"^2.8.1"},"engines":{"node":">=14.0.0"}},"node_modules/@tailwindcss/oxide-win32-arm64-msvc":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz","integrity":"sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==","cpu":["arm64"],"license":"MIT","optional":true,"os":["win32"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/oxide-win32-x64-msvc":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz","integrity":"sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==","cpu":["x64"],"license":"MIT","optional":true,"os":["win32"],"engines":{"node":">= 20"}},"node_modules/@tailwindcss/vite":{"version":"4.3.3","resolved":"https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz","integrity":"sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==","license":"MIT","dependencies":{"@tailwindcss/node":"4.3.3","@tailwindcss/oxide":"4.3.3","tailwindcss":"4.3.3"},"peerDependencies":{"vite":"^5.2.0 || ^6 || ^7 || ^8"}},"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==","license":"MIT","optional":true,"dependencies":{"tslib":"^2.4.0"}},"node_modules/@types/esrecurse":{"version":"4.3.1","resolved":"https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz","integrity":"sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==","dev":true,"license":"MIT"},"node_modules/@types/estree":{"version":"1.0.9","resolved":"https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz","integrity":"sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==","dev":true,"license":"MIT"},"node_modules/@types/json-schema":{"version":"7.0.15","resolved":"https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz","integrity":"sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==","dev":true,"license":"MIT"},"node_modules/@types/react":{"version":"19.2.17","resolved":"https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz","integrity":"sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==","dev":true,"license":"MIT","dependencies":{"csstype":"^3.2.2"}},"node_modules/@types/react-dom":{"version":"19.2.3","resolved":"https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz","integrity":"sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==","dev":true,"license":"MIT","peerDependencies":{"@types/react":"^19.2.0"}},"node_modules/@typescript-eslint/eslint-plugin":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz","integrity":"sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==","dev":true,"license":"MIT","dependencies":{"@eslint-community/regexpp":"^4.12.2","@typescript-eslint/scope-manager":"8.64.0","@typescript-eslint/type-utils":"8.64.0","@typescript-eslint/utils":"8.64.0","@typescript-eslint/visitor-keys":"8.64.0","ignore":"^7.0.5","natural-compare":"^1.4.0","ts-api-utils":"^2.5.0"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"},"peerDependencies":{"@typescript-eslint/parser":"^8.64.0","eslint":"^8.57.0 || ^9.0.0 || ^10.0.0","typescript":">=4.8.4 <6.1.0"}},"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore":{"version":"7.0.6","resolved":"https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz","integrity":"sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==","dev":true,"license":"MIT","engines":{"node":">= 4"}},"node_modules/@typescript-eslint/parser":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz","integrity":"sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==","dev":true,"license":"MIT","dependencies":{"@typescript-eslint/scope-manager":"8.64.0","@typescript-eslint/types":"8.64.0","@typescript-eslint/typescript-estree":"8.64.0","@typescript-eslint/visitor-keys":"8.64.0","debug":"^4.4.3"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"},"peerDependencies":{"eslint":"^8.57.0 || ^9.0.0 || ^10.0.0","typescript":">=4.8.4 <6.1.0"}},"node_modules/@typescript-eslint/project-service":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz","integrity":"sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==","dev":true,"license":"MIT","dependencies":{"@typescript-eslint/tsconfig-utils":"^8.64.0","@typescript-eslint/types":"^8.64.0","debug":"^4.4.3"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"},"peerDependencies":{"typescript":">=4.8.4 <6.1.0"}},"node_modules/@typescript-eslint/scope-manager":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz","integrity":"sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==","dev":true,"license":"MIT","dependencies":{"@typescript-eslint/types":"8.64.0","@typescript-eslint/visitor-keys":"8.64.0"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"}},"node_modules/@typescript-eslint/tsconfig-utils":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz","integrity":"sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==","dev":true,"license":"MIT","engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"},"peerDependencies":{"typescript":">=4.8.4 <6.1.0"}},"node_modules/@typescript-eslint/type-utils":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz","integrity":"sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==","dev":true,"license":"MIT","dependencies":{"@typescript-eslint/types":"8.64.0","@typescript-eslint/typescript-estree":"8.64.0","@typescript-eslint/utils":"8.64.0","debug":"^4.4.3","ts-api-utils":"^2.5.0"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"},"peerDependencies":{"eslint":"^8.57.0 || ^9.0.0 || ^10.0.0","typescript":">=4.8.4 <6.1.0"}},"node_modules/@typescript-eslint/types":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz","integrity":"sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==","dev":true,"license":"MIT","engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"}},"node_modules/@typescript-eslint/typescript-estree":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz","integrity":"sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==","dev":true,"license":"MIT","dependencies":{"@typescript-eslint/project-service":"8.64.0","@typescript-eslint/tsconfig-utils":"8.64.0","@typescript-eslint/types":"8.64.0","@typescript-eslint/visitor-keys":"8.64.0","debug":"^4.4.3","minimatch":"^10.2.2","semver":"^7.7.3","tinyglobby":"^0.2.15","ts-api-utils":"^2.5.0"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"},"peerDependencies":{"typescript":">=4.8.4 <6.1.0"}},"node_modules/@typescript-eslint/typescript-estree/node_modules/semver":{"version":"7.8.5","resolved":"https://registry.npmjs.org/semver/-/semver-7.8.5.tgz","integrity":"sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==","dev":true,"license":"ISC","bin":{"semver":"bin/semver.js"},"engines":{"node":">=10"}},"node_modules/@typescript-eslint/utils":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz","integrity":"sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==","dev":true,"license":"MIT","dependencies":{"@eslint-community/eslint-utils":"^4.9.1","@typescript-eslint/scope-manager":"8.64.0","@typescript-eslint/types":"8.64.0","@typescript-eslint/typescript-estree":"8.64.0"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"},"peerDependencies":{"eslint":"^8.57.0 || ^9.0.0 || ^10.0.0","typescript":">=4.8.4 <6.1.0"}},"node_modules/@typescript-eslint/visitor-keys":{"version":"8.64.0","resolved":"https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz","integrity":"sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==","dev":true,"license":"MIT","dependencies":{"@typescript-eslint/types":"8.64.0","eslint-visitor-keys":"^5.0.0"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"}},"node_modules/@vitejs/plugin-react":{"version":"6.0.3","resolved":"https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz","integrity":"sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==","license":"MIT","dependencies":{"@rolldown/pluginutils":"^1.0.1"},"engines":{"node":"^20.19.0 || >=22.12.0"},"peerDependencies":{"@rolldown/plugin-babel":"^0.1.7 || ^0.2.0","babel-plugin-react-compiler":"^1.0.0","vite":"^8.0.0"},"peerDependenciesMeta":{"@rolldown/plugin-babel":{"optional":true},"babel-plugin-react-compiler":{"optional":true}}},"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/acorn-jsx":{"version":"5.3.2","resolved":"https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz","integrity":"sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==","dev":true,"license":"MIT","peerDependencies":{"acorn":"^6.0.0 || ^7.0.0 || ^8.0.0"}},"node_modules/ajv":{"version":"6.15.0","resolved":"https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz","integrity":"sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==","dev":true,"license":"MIT","dependencies":{"fast-deep-equal":"^3.1.1","fast-json-stable-stringify":"^2.0.0","json-schema-traverse":"^0.4.1","uri-js":"^4.2.2"},"funding":{"type":"github","url":"https://github.com/sponsors/epoberezkin"}},"node_modules/balanced-match":{"version":"4.0.4","resolved":"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz","integrity":"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==","dev":true,"license":"MIT","engines":{"node":"18 || 20 || >=22"}},"node_modules/baseline-browser-mapping":{"version":"2.10.43","resolved":"https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz","integrity":"sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==","dev":true,"license":"Apache-2.0","bin":{"baseline-browser-mapping":"dist/cli.cjs"},"engines":{"node":">=6.0.0"}},"node_modules/brace-expansion":{"version":"5.0.8","resolved":"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz","integrity":"sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==","dev":true,"license":"MIT","dependencies":{"balanced-match":"^4.0.2"},"engines":{"node":"20 || >=22"}},"node_modules/browserslist":{"version":"4.28.6","resolved":"https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz","integrity":"sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==","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.42","caniuse-lite":"^1.0.30001803","electron-to-chromium":"^1.5.389","node-releases":"^2.0.51","update-browserslist-db":"^1.2.3"},"bin":{"browserslist":"cli.js"},"engines":{"node":"^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"}},"node_modules/caniuse-lite":{"version":"1.0.30001806","resolved":"https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz","integrity":"sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==","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/convert-source-map":{"version":"2.0.0","resolved":"https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz","integrity":"sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==","dev":true,"license":"MIT"},"node_modules/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/csstype":{"version":"3.2.3","resolved":"https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz","integrity":"sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==","dev":true,"license":"MIT"},"node_modules/debug":{"version":"4.4.3","resolved":"https://registry.npmjs.org/debug/-/debug-4.4.3.tgz","integrity":"sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==","dev":true,"license":"MIT","dependencies":{"ms":"^2.1.3"},"engines":{"node":">=6.0"},"peerDependenciesMeta":{"supports-color":{"optional":true}}},"node_modules/deep-is":{"version":"0.1.4","resolved":"https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz","integrity":"sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==","dev":true,"license":"MIT"},"node_modules/detect-libc":{"version":"2.1.2","resolved":"https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz","integrity":"sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==","license":"Apache-2.0","engines":{"node":">=8"}},"node_modules/electron-to-chromium":{"version":"1.5.393","resolved":"https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz","integrity":"sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==","dev":true,"license":"ISC"},"node_modules/enhanced-resolve":{"version":"5.24.2","resolved":"https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz","integrity":"sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==","license":"MIT","dependencies":{"graceful-fs":"^4.2.4","tapable":"^2.3.3"},"engines":{"node":">=10.13.0"}},"node_modules/escalade":{"version":"3.2.0","resolved":"https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz","integrity":"sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==","dev":true,"license":"MIT","engines":{"node":">=6"}},"node_modules/escape-string-regexp":{"version":"4.0.0","resolved":"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz","integrity":"sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==","dev":true,"license":"MIT","engines":{"node":">=10"},"funding":{"url":"https://github.com/sponsors/sindresorhus"}},"node_modules/eslint":{"version":"10.7.0","resolved":"https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz","integrity":"sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==","dev":true,"license":"MIT","workspaces":["packages/*"],"dependencies":{"@eslint-community/eslint-utils":"^4.8.0","@eslint-community/regexpp":"^4.12.2","@eslint/config-array":"^0.23.5","@eslint/config-helpers":"^0.6.0","@eslint/core":"^1.2.1","@eslint/plugin-kit":"^0.7.2","@humanfs/node":"^0.16.6","@humanwhocodes/module-importer":"^1.0.1","@humanwhocodes/retry":"^0.4.2","@types/estree":"^1.0.6","ajv":"^6.14.0","cross-spawn":"^7.0.6","debug":"^4.3.2","escape-string-regexp":"^4.0.0","eslint-scope":"^9.1.2","eslint-visitor-keys":"^5.0.1","espree":"^11.2.0","esquery":"^1.7.0","esutils":"^2.0.2","fast-deep-equal":"^3.1.3","file-entry-cache":"^8.0.0","find-up":"^5.0.0","glob-parent":"^6.0.2","ignore":"^5.2.0","imurmurhash":"^0.1.4","is-glob":"^4.0.0","json-stable-stringify-without-jsonify":"^1.0.1","minimatch":"^10.2.4","natural-compare":"^1.4.0","optionator":"^0.9.3"},"bin":{"eslint":"bin/eslint.js"},"engines":{"node":"^20.19.0 || ^22.13.0 || >=24"},"funding":{"url":"https://eslint.org/donate"},"peerDependencies":{"jiti":"*"},"peerDependenciesMeta":{"jiti":{"optional":true}}},"node_modules/eslint-plugin-react-hooks":{"version":"7.1.1","resolved":"https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz","integrity":"sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==","dev":true,"license":"MIT","dependencies":{"@babel/core":"^7.24.4","@babel/parser":"^7.24.4","hermes-parser":"^0.25.1","zod":"^3.25.0 || ^4.0.0","zod-validation-error":"^3.5.0 || ^4.0.0"},"engines":{"node":">=18"},"peerDependencies":{"eslint":"^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"}},"node_modules/eslint-plugin-react-refresh":{"version":"0.5.3","resolved":"https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz","integrity":"sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==","dev":true,"license":"MIT","peerDependencies":{"eslint":"^9 || ^10"}},"node_modules/eslint-scope":{"version":"9.1.2","resolved":"https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz","integrity":"sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==","dev":true,"license":"BSD-2-Clause","dependencies":{"@types/esrecurse":"^4.3.1","@types/estree":"^1.0.8","esrecurse":"^4.3.0","estraverse":"^5.2.0"},"engines":{"node":"^20.19.0 || ^22.13.0 || >=24"},"funding":{"url":"https://opencollective.com/eslint"}},"node_modules/eslint-visitor-keys":{"version":"5.0.1","resolved":"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz","integrity":"sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==","dev":true,"license":"Apache-2.0","engines":{"node":"^20.19.0 || ^22.13.0 || >=24"},"funding":{"url":"https://opencollective.com/eslint"}},"node_modules/espree":{"version":"11.2.0","resolved":"https://registry.npmjs.org/espree/-/espree-11.2.0.tgz","integrity":"sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==","dev":true,"license":"BSD-2-Clause","dependencies":{"acorn":"^8.16.0","acorn-jsx":"^5.3.2","eslint-visitor-keys":"^5.0.1"},"engines":{"node":"^20.19.0 || ^22.13.0 || >=24"},"funding":{"url":"https://opencollective.com/eslint"}},"node_modules/esquery":{"version":"1.7.0","resolved":"https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz","integrity":"sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==","dev":true,"license":"BSD-3-Clause","dependencies":{"estraverse":"^5.1.0"},"engines":{"node":">=0.10"}},"node_modules/esrecurse":{"version":"4.3.0","resolved":"https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz","integrity":"sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==","dev":true,"license":"BSD-2-Clause","dependencies":{"estraverse":"^5.2.0"},"engines":{"node":">=4.0"}},"node_modules/estraverse":{"version":"5.3.0","resolved":"https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz","integrity":"sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==","dev":true,"license":"BSD-2-Clause","engines":{"node":">=4.0"}},"node_modules/esutils":{"version":"2.0.3","resolved":"https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz","integrity":"sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==","dev":true,"license":"BSD-2-Clause","engines":{"node":">=0.10.0"}},"node_modules/fast-deep-equal":{"version":"3.1.3","resolved":"https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz","integrity":"sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==","dev":true,"license":"MIT"},"node_modules/fast-json-stable-stringify":{"version":"2.1.0","resolved":"https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz","integrity":"sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==","dev":true,"license":"MIT"},"node_modules/fast-levenshtein":{"version":"2.0.6","resolved":"https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz","integrity":"sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==","dev":true,"license":"MIT"},"node_modules/fdir":{"version":"6.5.0","resolved":"https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz","integrity":"sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==","license":"MIT","engines":{"node":">=12.0.0"},"peerDependencies":{"picomatch":"^3 || ^4"},"peerDependenciesMeta":{"picomatch":{"optional":true}}},"node_modules/file-entry-cache":{"version":"8.0.0","resolved":"https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz","integrity":"sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==","dev":true,"license":"MIT","dependencies":{"flat-cache":"^4.0.0"},"engines":{"node":">=16.0.0"}},"node_modules/find-up":{"version":"5.0.0","resolved":"https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz","integrity":"sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==","dev":true,"license":"MIT","dependencies":{"locate-path":"^6.0.0","path-exists":"^4.0.0"},"engines":{"node":">=10"},"funding":{"url":"https://github.com/sponsors/sindresorhus"}},"node_modules/flat-cache":{"version":"4.0.1","resolved":"https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz","integrity":"sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==","dev":true,"license":"MIT","dependencies":{"flatted":"^3.2.9","keyv":"^4.5.4"},"engines":{"node":">=16"}},"node_modules/flatted":{"version":"3.4.2","resolved":"https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz","integrity":"sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==","dev":true,"license":"ISC"},"node_modules/fsevents":{"version":"2.3.3","resolved":"https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz","integrity":"sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==","hasInstallScript":true,"license":"MIT","optional":true,"os":["darwin"],"engines":{"node":"^8.16.0 || ^10.6.0 || >=11.0.0"}},"node_modules/gensync":{"version":"1.0.0-beta.2","resolved":"https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz","integrity":"sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==","dev":true,"license":"MIT","engines":{"node":">=6.9.0"}},"node_modules/glob-parent":{"version":"6.0.2","resolved":"https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz","integrity":"sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==","dev":true,"license":"ISC","dependencies":{"is-glob":"^4.0.3"},"engines":{"node":">=10.13.0"}},"node_modules/globals":{"version":"17.7.0","resolved":"https://registry.npmjs.org/globals/-/globals-17.7.0.tgz","integrity":"sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==","dev":true,"license":"MIT","engines":{"node":">=18"},"funding":{"url":"https://github.com/sponsors/sindresorhus"}},"node_modules/graceful-fs":{"version":"4.2.11","resolved":"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz","integrity":"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==","license":"ISC"},"node_modules/hermes-estree":{"version":"0.25.1","resolved":"https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz","integrity":"sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==","dev":true,"license":"MIT"},"node_modules/hermes-parser":{"version":"0.25.1","resolved":"https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz","integrity":"sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==","dev":true,"license":"MIT","dependencies":{"hermes-estree":"0.25.1"}},"node_modules/ignore":{"version":"5.3.2","resolved":"https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz","integrity":"sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==","dev":true,"license":"MIT","engines":{"node":">= 4"}},"node_modules/imurmurhash":{"version":"0.1.4","resolved":"https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz","integrity":"sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==","dev":true,"license":"MIT","engines":{"node":">=0.8.19"}},"node_modules/is-extglob":{"version":"2.1.1","resolved":"https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz","integrity":"sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==","dev":true,"license":"MIT","engines":{"node":">=0.10.0"}},"node_modules/is-glob":{"version":"4.0.3","resolved":"https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz","integrity":"sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==","dev":true,"license":"MIT","dependencies":{"is-extglob":"^2.1.1"},"engines":{"node":">=0.10.0"}},"node_modules/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/jiti":{"version":"2.7.0","resolved":"https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz","integrity":"sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==","license":"MIT","bin":{"jiti":"lib/jiti-cli.mjs"}},"node_modules/js-tokens":{"version":"4.0.0","resolved":"https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz","integrity":"sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==","dev":true,"license":"MIT"},"node_modules/jsesc":{"version":"3.1.0","resolved":"https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz","integrity":"sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==","dev":true,"license":"MIT","bin":{"jsesc":"bin/jsesc"},"engines":{"node":">=6"}},"node_modules/json-buffer":{"version":"3.0.1","resolved":"https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz","integrity":"sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==","dev":true,"license":"MIT"},"node_modules/json-schema-traverse":{"version":"0.4.1","resolved":"https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz","integrity":"sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==","dev":true,"license":"MIT"},"node_modules/json-stable-stringify-without-jsonify":{"version":"1.0.1","resolved":"https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz","integrity":"sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==","dev":true,"license":"MIT"},"node_modules/json5":{"version":"2.2.3","resolved":"https://registry.npmjs.org/json5/-/json5-2.2.3.tgz","integrity":"sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==","dev":true,"license":"MIT","bin":{"json5":"lib/cli.js"},"engines":{"node":">=6"}},"node_modules/keyv":{"version":"4.5.4","resolved":"https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz","integrity":"sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==","dev":true,"license":"MIT","dependencies":{"json-buffer":"3.0.1"}},"node_modules/levn":{"version":"0.4.1","resolved":"https://registry.npmjs.org/levn/-/levn-0.4.1.tgz","integrity":"sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==","dev":true,"license":"MIT","dependencies":{"prelude-ls":"^1.2.1","type-check":"~0.4.0"},"engines":{"node":">= 0.8.0"}},"node_modules/lightningcss":{"version":"1.32.0","resolved":"https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz","integrity":"sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==","license":"MPL-2.0","dependencies":{"detect-libc":"^2.0.3"},"engines":{"node":">= 12.0.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/parcel"},"optionalDependencies":{"lightningcss-android-arm64":"1.32.0","lightningcss-darwin-arm64":"1.32.0","lightningcss-darwin-x64":"1.32.0","lightningcss-freebsd-x64":"1.32.0","lightningcss-linux-arm-gnueabihf":"1.32.0","lightningcss-linux-arm64-gnu":"1.32.0","lightningcss-linux-arm64-musl":"1.32.0","lightningcss-linux-x64-gnu":"1.32.0","lightningcss-linux-x64-musl":"1.32.0","lightningcss-win32-arm64-msvc":"1.32.0","lightningcss-win32-x64-msvc":"1.32.0"}},"node_modules/lightningcss-android-arm64":{"version":"1.32.0","resolved":"https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz","integrity":"sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==","cpu":["arm64"],"license":"MPL-2.0","optional":true,"os":["android"],"engines":{"node":">= 12.0.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/parcel"}},"node_modules/lightningcss-darwin-arm64":{"version":"1.32.0","resolved":"https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz","integrity":"sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==","cpu":["arm64"],"license":"MPL-2.0","optional":true,"os":["darwin"],"engines":{"node":">= 12.0.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/parcel"}},"node_modules/lightningcss-darwin-x64":{"version":"1.32.0","resolved":"https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz","integrity":"sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==","cpu":["x64"],"license":"MPL-2.0","optional":true,"os":["darwin"],"engines":{"node":">= 12.0.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/parcel"}},"node_modules/lightningcss-freebsd-x64":{"version":"1.32.0","resolved":"https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz","integrity":"sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==","cpu":["x64"],"license":"MPL-2.0","optional":true,"os":["freebsd"],"engines":{"node":">= 12.0.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/parcel"}},"node_modules/lightningcss-linux-arm-gnueabihf":{"version":"1.32.0","resolved":"https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz","integrity":"sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==","cpu":["arm"],"license":"MPL-2.0","optional":true,"os":["linux"],"engines":{"node":">= 12.0.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/parcel"}},"node_modules/lightningcss-linux-arm64-gnu":{"version":"1.32.0","resolved":"https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz","integrity":"sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==","cpu":["arm64"],"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"],"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"],"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"],"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"],"license":"MPL-2.0","optional":true,"os":["win32"],"engines":{"node":">= 12.0.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/parcel"}},"node_modules/lightningcss-win32-x64-msvc":{"version":"1.32.0","resolved":"https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz","integrity":"sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==","cpu":["x64"],"license":"MPL-2.0","optional":true,"os":["win32"],"engines":{"node":">= 12.0.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/parcel"}},"node_modules/locate-path":{"version":"6.0.0","resolved":"https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz","integrity":"sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==","dev":true,"license":"MIT","dependencies":{"p-locate":"^5.0.0"},"engines":{"node":">=10"},"funding":{"url":"https://github.com/sponsors/sindresorhus"}},"node_modules/lru-cache":{"version":"5.1.1","resolved":"https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz","integrity":"sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==","dev":true,"license":"ISC","dependencies":{"yallist":"^3.0.2"}},"node_modules/lucide-react":{"version":"1.25.0","resolved":"https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz","integrity":"sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==","license":"ISC","peerDependencies":{"react":"^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"}},"node_modules/magic-string":{"version":"0.30.21","resolved":"https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz","integrity":"sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==","license":"MIT","dependencies":{"@jridgewell/sourcemap-codec":"^1.5.5"}},"node_modules/minimatch":{"version":"10.2.5","resolved":"https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz","integrity":"sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==","dev":true,"license":"BlueOak-1.0.0","dependencies":{"brace-expansion":"^5.0.5"},"engines":{"node":"18 || 20 || >=22"},"funding":{"url":"https://github.com/sponsors/isaacs"}},"node_modules/ms":{"version":"2.1.3","resolved":"https://registry.npmjs.org/ms/-/ms-2.1.3.tgz","integrity":"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==","dev":true,"license":"MIT"},"node_modules/nanoid":{"version":"3.3.16","resolved":"https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz","integrity":"sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==","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/natural-compare":{"version":"1.4.0","resolved":"https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz","integrity":"sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==","dev":true,"license":"MIT"},"node_modules/node-releases":{"version":"2.0.51","resolved":"https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz","integrity":"sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==","dev":true,"license":"MIT","engines":{"node":">=18"}},"node_modules/optionator":{"version":"0.9.4","resolved":"https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz","integrity":"sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==","dev":true,"license":"MIT","dependencies":{"deep-is":"^0.1.3","fast-levenshtein":"^2.0.6","levn":"^0.4.1","prelude-ls":"^1.2.1","type-check":"^0.4.0","word-wrap":"^1.2.5"},"engines":{"node":">= 0.8.0"}},"node_modules/p-limit":{"version":"3.1.0","resolved":"https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz","integrity":"sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==","dev":true,"license":"MIT","dependencies":{"yocto-queue":"^0.1.0"},"engines":{"node":">=10"},"funding":{"url":"https://github.com/sponsors/sindresorhus"}},"node_modules/p-locate":{"version":"5.0.0","resolved":"https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz","integrity":"sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==","dev":true,"license":"MIT","dependencies":{"p-limit":"^3.0.2"},"engines":{"node":">=10"},"funding":{"url":"https://github.com/sponsors/sindresorhus"}},"node_modules/path-exists":{"version":"4.0.0","resolved":"https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz","integrity":"sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==","dev":true,"license":"MIT","engines":{"node":">=8"}},"node_modules/path-key":{"version":"3.1.1","resolved":"https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz","integrity":"sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==","dev":true,"license":"MIT","engines":{"node":">=8"}},"node_modules/picocolors":{"version":"1.1.1","resolved":"https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz","integrity":"sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==","license":"ISC"},"node_modules/picomatch":{"version":"4.0.5","resolved":"https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz","integrity":"sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==","license":"MIT","engines":{"node":">=12"},"funding":{"url":"https://github.com/sponsors/jonschlinkert"}},"node_modules/postcss":{"version":"8.5.20","resolved":"https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz","integrity":"sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==","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.16","picocolors":"^1.1.1","source-map-js":"^1.2.1"},"engines":{"node":"^10 || ^12 || >=14"}},"node_modules/prelude-ls":{"version":"1.2.1","resolved":"https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz","integrity":"sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==","dev":true,"license":"MIT","engines":{"node":">= 0.8.0"}},"node_modules/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/react":{"version":"19.2.7","resolved":"https://registry.npmjs.org/react/-/react-19.2.7.tgz","integrity":"sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==","license":"MIT","engines":{"node":">=0.10.0"}},"node_modules/react-dom":{"version":"19.2.7","resolved":"https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz","integrity":"sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==","license":"MIT","dependencies":{"scheduler":"^0.27.0"},"peerDependencies":{"react":"^19.2.7"}},"node_modules/rolldown":{"version":"1.1.5","resolved":"https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz","integrity":"sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==","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/scheduler":{"version":"0.27.0","resolved":"https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz","integrity":"sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==","license":"MIT"},"node_modules/semver":{"version":"6.3.1","resolved":"https://registry.npmjs.org/semver/-/semver-6.3.1.tgz","integrity":"sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==","dev":true,"license":"ISC","bin":{"semver":"bin/semver.js"}},"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/source-map-js":{"version":"1.2.1","resolved":"https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz","integrity":"sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==","license":"BSD-3-Clause","engines":{"node":">=0.10.0"}},"node_modules/tailwindcss":{"version":"4.3.3","resolved":"https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz","integrity":"sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==","license":"MIT"},"node_modules/tapable":{"version":"2.3.3","resolved":"https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz","integrity":"sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==","license":"MIT","engines":{"node":">=6"},"funding":{"type":"opencollective","url":"https://opencollective.com/webpack"}},"node_modules/tinyglobby":{"version":"0.2.17","resolved":"https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz","integrity":"sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==","license":"MIT","dependencies":{"fdir":"^6.5.0","picomatch":"^4.0.4"},"engines":{"node":">=12.0.0"},"funding":{"url":"https://github.com/sponsors/SuperchupuDev"}},"node_modules/ts-api-utils":{"version":"2.5.0","resolved":"https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz","integrity":"sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==","dev":true,"license":"MIT","engines":{"node":">=18.12"},"peerDependencies":{"typescript":">=4.8.4"}},"node_modules/tslib":{"version":"2.8.1","resolved":"https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz","integrity":"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==","license":"0BSD","optional":true},"node_modules/type-check":{"version":"0.4.0","resolved":"https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz","integrity":"sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==","dev":true,"license":"MIT","dependencies":{"prelude-ls":"^1.2.1"},"engines":{"node":">= 0.8.0"}},"node_modules/typescript":{"version":"6.0.3","resolved":"https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz","integrity":"sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==","license":"Apache-2.0","bin":{"tsc":"bin/tsc","tsserver":"bin/tsserver"},"engines":{"node":">=14.17"}},"node_modules/typescript-eslint":{"version":"8.64.0","resolved":"https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz","integrity":"sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==","dev":true,"license":"MIT","dependencies":{"@typescript-eslint/eslint-plugin":"8.64.0","@typescript-eslint/parser":"8.64.0","@typescript-eslint/typescript-estree":"8.64.0","@typescript-eslint/utils":"8.64.0"},"engines":{"node":"^18.18.0 || ^20.9.0 || >=21.1.0"},"funding":{"type":"opencollective","url":"https://opencollective.com/typescript-eslint"},"peerDependencies":{"eslint":"^8.57.0 || ^9.0.0 || ^10.0.0","typescript":">=4.8.4 <6.1.0"}},"node_modules/update-browserslist-db":{"version":"1.2.3","resolved":"https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz","integrity":"sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==","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/uri-js":{"version":"4.4.1","resolved":"https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz","integrity":"sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==","dev":true,"license":"BSD-2-Clause","dependencies":{"punycode":"^2.1.0"}},"node_modules/vite":{"version":"8.1.5","resolved":"https://registry.npmjs.org/vite/-/vite-8.1.5.tgz","integrity":"sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==","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/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/word-wrap":{"version":"1.2.5","resolved":"https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz","integrity":"sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==","dev":true,"license":"MIT","engines":{"node":">=0.10.0"}},"node_modules/yallist":{"version":"3.1.1","resolved":"https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz","integrity":"sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==","dev":true,"license":"ISC"},"node_modules/yocto-queue":{"version":"0.1.0","resolved":"https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz","integrity":"sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==","dev":true,"license":"MIT","engines":{"node":">=10"},"funding":{"url":"https://github.com/sponsors/sindresorhus"}},"node_modules/zod":{"version":"4.4.3","resolved":"https://registry.npmjs.org/zod/-/zod-4.4.3.tgz","integrity":"sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==","dev":true,"license":"MIT","funding":{"url":"https://github.com/sponsors/colinhacks"}},"node_modules/zod-validation-error":{"version":"4.0.2","resolved":"https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz","integrity":"sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==","dev":true,"license":"MIT","engines":{"node":">=18.0.0"},"peerDependencies":{"zod":"^3.25.0 || ^4.0.0"}}}} diff --git a/frontend/package.json b/frontend/package.json index 9f76b64..bace83a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1 +1 @@ -{"name":"lightwatch-web","private":true,"version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"tsc -b && vite build","lint":"eslint .","preview":"vite preview"},"dependencies":{"@tailwindcss/vite":"^4.3.3","@vitejs/plugin-react":"latest","lucide-react":"latest","react":"latest","react-dom":"latest","tailwindcss":"^4.3.3","typescript":"latest","vite":"latest"},"devDependencies":{"@eslint/js":"latest","@types/react":"latest","@types/react-dom":"latest","eslint":"latest","eslint-plugin-react-hooks":"latest","eslint-plugin-react-refresh":"latest","globals":"latest","typescript-eslint":"latest"}} \ No newline at end of file +{"name":"lightwatch-web","private":true,"version":"1.1.0","type":"module","scripts":{"dev":"vite","build":"tsc -b && vite build","lint":"eslint .","preview":"vite preview"},"dependencies":{"@tailwindcss/vite":"4.3.3","@vitejs/plugin-react":"6.0.3","lucide-react":"1.25.0","react":"19.2.7","react-dom":"19.2.7","tailwindcss":"4.3.3","typescript":"6.0.3","vite":"8.1.5"},"devDependencies":{"@eslint/js":"10.0.1","@types/react":"19.2.17","@types/react-dom":"19.2.3","eslint":"10.7.0","eslint-plugin-react-hooks":"7.1.1","eslint-plugin-react-refresh":"0.5.3","globals":"17.7.0","typescript-eslint":"8.64.0"}} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0eac471..3e2f7a6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,12 +1,17 @@ -import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; +import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { Activity, Camera, + ChevronDown, + ChevronLeft, + ChevronRight, Database, HardDrive, + Pause, + Play, RefreshCw, Server, - Wifi, + X, } from "lucide-react"; type Capture = { @@ -14,35 +19,81 @@ type Capture = { captured_at: string; status: string; lighting_state?: string | null; + lighting_reason?: string | null; mean_brightness?: number | null; median_brightness?: number | null; + dark_ratio?: number | null; + bright_ratio?: number | null; nas_synced?: boolean; image_url?: string | null; }; type Status = { + version: string; + server_time: string; + state_started_at?: string | null; latest_observation: Capture | null; latest_image: (Capture & { image_url: string }) | null; unsynced_count: number; - nas_sync: { result: string; last_success_at?: string | null } | null; + nas_sync: { + result: string; + attempted_at?: string | null; + last_success_at?: string | null; + message?: string | null; + } | null; }; type Health = { status: string; - capture: { message: string }; - storage: { status: string; used_percent: number }; + checked_at: string; + capture: { status: string; message: string; age_minutes?: number | null; last_success_at?: string | null }; + nas_sync: { status: string; message: string; unsynced_count: number; last_attempt_at?: string | null; last_success_at?: string | null }; + storage: { status: string; used_percent: number; total_bytes: number; free_bytes: number; message: string }; }; +type TimelineSegment = { + state: string; + started_at: string; + ended_at: string; + samples: number; +}; + +const STATES = ["ALL", "BRIGHT", "PARTIAL", "DIM", "DARK"] as const; +const AUTO_REFRESH_SECONDS = 60; + const fmt = (value?: string | null) => value ? new Date(value).toLocaleString("ja-JP") : "—"; +const fmtTime = (value?: Date | null) => + value ? value.toLocaleTimeString("ja-JP", { hour12: false }) : "—"; const cls = (value?: string | null) => `badge ${String(value ?? "unknown").toLowerCase()}`; +function formatDuration(start?: string | null, end?: string | null) { + if (!start) return "—"; + const milliseconds = Math.max(0, new Date(end ?? Date.now()).getTime() - new Date(start).getTime()); + const minutes = Math.floor(milliseconds / 60_000); + if (minutes < 60) return `${minutes}分`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}時間${minutes % 60}分`; + const days = Math.floor(hours / 24); + return `${days}日${hours % 24}時間`; +} + +function formatBytes(value?: number | null) { + if (value == null) return "—"; + let size = value; + const units = ["B", "KiB", "MiB", "GiB", "TiB"]; + let index = 0; + while (size >= 1024 && index < units.length - 1) { + size /= 1024; + index += 1; + } + return `${size.toFixed(1)} ${units[index]}`; +} + async function fetchJson(url: string, signal: AbortSignal): Promise { const response = await fetch(url, { cache: "no-store", signal }); - if (!response.ok) { - throw new Error(`${url}: HTTP ${response.status}`); - } + if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`); return response.json() as Promise; } @@ -55,303 +106,584 @@ function preloadImage(url: string): Promise { }); } +function useGalleryColumns() { + const getColumns = () => { + if (window.matchMedia("(max-width: 560px)").matches) return 1; + if (window.matchMedia("(max-width: 900px)").matches) return 2; + return 4; + }; + + const [columns, setColumns] = useState(getColumns); + + useEffect(() => { + const narrow = window.matchMedia("(max-width: 560px)"); + const medium = window.matchMedia("(max-width: 900px)"); + const update = () => setColumns(getColumns()); + + narrow.addEventListener("change", update); + medium.addEventListener("change", update); + return () => { + narrow.removeEventListener("change", update); + medium.removeEventListener("change", update); + }; + }, []); + + return columns; +} + export default function App() { const [status, setStatus] = useState(null); const [health, setHealth] = useState(null); const [observations, setObservations] = useState([]); const [images, setImages] = useState([]); + const [timeline, setTimeline] = useState([]); const [latestImageUrl, setLatestImageUrl] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [imageError, setImageError] = useState(""); + const [lastUpdatedAt, setLastUpdatedAt] = useState(null); + const [autoRefresh, setAutoRefresh] = useState(true); + const [stateFilter, setStateFilter] = useState<(typeof STATES)[number]>("ALL"); + const [savedOnly, setSavedOnly] = useState(false); + const [expandedObservation, setExpandedObservation] = useState(null); + const [expandedImage, setExpandedImage] = useState(null); + const [modalImageKey, setModalImageKey] = useState(null); const requestId = useRef(0); const activeController = useRef(null); + const galleryColumns = useGalleryColumns(); + + const observationUrl = useMemo(() => { + const params = new URLSearchParams({ limit: "50" }); + if (stateFilter !== "ALL") params.set("lighting_state", stateFilter); + if (savedOnly) params.set("saved_only", "true"); + return `/api/observations?${params.toString()}`; + }, [savedOnly, stateFilter]); const load = useCallback(async () => { const currentRequestId = ++requestId.current; activeController.current?.abort(); const controller = new AbortController(); activeController.current = controller; - setLoading(true); setError(""); setImageError(""); - const [statusResult, healthResult, observationsResult, imagesResult] = + const [statusResult, healthResult, observationsResult, imagesResult, timelineResult] = await Promise.allSettled([ fetchJson("/api/status", controller.signal), fetchJson("/api/health", controller.signal), - fetchJson<{ items: Capture[] }>( - "/api/observations?limit=20", - controller.signal, - ), + fetchJson<{ items: Capture[] }>(observationUrl, controller.signal), fetchJson<{ items: Capture[] }>("/api/images?limit=12", controller.signal), + fetchJson<{ segments: TimelineSegment[] }>("/api/timeline", controller.signal), ]); - if (controller.signal.aborted || currentRequestId !== requestId.current) { - return; - } - + if (controller.signal.aborted || currentRequestId !== requestId.current) return; const failures: string[] = []; if (statusResult.status === "fulfilled") { const nextStatus = statusResult.value; setStatus(nextStatus); - const nextImageUrl = nextStatus.latest_image?.image_url ?? null; if (nextImageUrl) { try { - const loadedUrl = await preloadImage(nextImageUrl); - if ( - !controller.signal.aborted && - currentRequestId === requestId.current - ) { - setLatestImageUrl(loadedUrl); - } + setLatestImageUrl(await preloadImage(nextImageUrl)); } catch (imageLoadError) { - if (currentRequestId === requestId.current) { - setImageError( - imageLoadError instanceof Error - ? imageLoadError.message - : "最新画像の取得に失敗しました", - ); - } + setImageError(imageLoadError instanceof Error ? imageLoadError.message : "最新画像の取得に失敗しました"); } - } else { - setLatestImageUrl(null); - } - } else if (statusResult.reason?.name !== "AbortError") { - failures.push("状態"); - } + } else setLatestImageUrl(null); + } else if (statusResult.reason?.name !== "AbortError") failures.push("状態"); - if (healthResult.status === "fulfilled") { - setHealth(healthResult.value); - } else if (healthResult.reason?.name !== "AbortError") { - failures.push("ヘルスチェック"); - } + if (healthResult.status === "fulfilled") setHealth(healthResult.value); + else if (healthResult.reason?.name !== "AbortError") failures.push("ヘルスチェック"); - if (observationsResult.status === "fulfilled") { - setObservations(observationsResult.value.items); - } else if (observationsResult.reason?.name !== "AbortError") { - failures.push("観測履歴"); - } + if (observationsResult.status === "fulfilled") setObservations(observationsResult.value.items); + else if (observationsResult.reason?.name !== "AbortError") failures.push("観測履歴"); - if (imagesResult.status === "fulfilled") { - setImages(imagesResult.value.items); - } else if (imagesResult.reason?.name !== "AbortError") { - failures.push("画像一覧"); - } + if (imagesResult.status === "fulfilled") setImages(imagesResult.value.items); + else if (imagesResult.reason?.name !== "AbortError") failures.push("画像一覧"); - if (failures.length > 0) { - setError(`${failures.join("・")}の更新に失敗しました。前回の表示を維持しています。`); - } + if (timelineResult.status === "fulfilled") setTimeline(timelineResult.value.segments); + else if (timelineResult.reason?.name !== "AbortError") failures.push("タイムライン"); + + if (failures.length > 0) setError(`${failures.join("・")}の更新に失敗しました。前回の表示を維持しています。`); + else setLastUpdatedAt(new Date()); setLoading(false); - }, []); + }, [observationUrl]); useEffect(() => { void load(); - const timer = window.setInterval(() => void load(), 60_000); + if (!autoRefresh) return () => activeController.current?.abort(); + const timer = window.setInterval(() => void load(), AUTO_REFRESH_SECONDS * 1000); return () => { window.clearInterval(timer); activeController.current?.abort(); }; - }, [load]); + }, [autoRefresh, load]); const latest = status?.latest_observation; const state = latest?.lighting_state ?? "UNKNOWN"; - const label: Record = { - BRIGHT: "BRIGHT", - PARTIAL: "PARTIAL", - DIM: "DIM", - DARK: "DARK", - UNKNOWN: "UNKNOWN", - }; + const galleryItems = useMemo( + () => + images.map((item, index) => ({ + item, + key: String(item.id ?? `${item.captured_at}-${index}`), + })), + [images], + ); + const latestImage = status?.latest_image; + const modalItems = useMemo(() => { + const candidates = latestImage ? [latestImage, ...images] : images; + const unique = new Map(); + for (const item of candidates) { + if (!item.image_url) continue; + unique.set(item.image_url, item); + } + return Array.from(unique.entries()) + .map(([key, item]) => ({ key, item })) + .sort((left, right) => + new Date(left.item.captured_at).getTime() - new Date(right.item.captured_at).getTime(), + ); + }, [images, latestImage]); + const imageRows = useMemo(() => { + const rows: typeof galleryItems[] = []; + for (let index = 0; index < galleryItems.length; index += galleryColumns) { + rows.push(galleryItems.slice(index, index + galleryColumns)); + } + return rows; + }, [galleryColumns, galleryItems]); + const selectedImage = galleryItems.find(({ key }) => key === expandedImage)?.item; + const modalIndex = modalItems.findIndex(({ key }) => key === modalImageKey); + const modalImage = modalIndex >= 0 ? modalItems[modalIndex].item : null; + + const openImageModal = useCallback((imageUrl?: string | null) => { + if (imageUrl) setModalImageKey(imageUrl); + }, []); + const closeImageModal = useCallback(() => setModalImageKey(null), []); + const showPreviousImage = useCallback(() => { + if (modalIndex > 0) setModalImageKey(modalItems[modalIndex - 1].key); + }, [modalIndex, modalItems]); + const showNextImage = useCallback(() => { + if (modalIndex >= 0 && modalIndex < modalItems.length - 1) { + setModalImageKey(modalItems[modalIndex + 1].key); + } + }, [modalIndex, modalItems]); + + useEffect(() => { + if (!modalImage) return; + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") closeImageModal(); + if (event.key === "ArrowLeft") showPreviousImage(); + if (event.key === "ArrowRight") showNextImage(); + }; + window.addEventListener("keydown", handleKeyDown); + return () => { + document.body.style.overflow = previousOverflow; + window.removeEventListener("keydown", handleKeyDown); + }; + }, [closeImageModal, modalImage, showNextImage, showPreviousImage]); return (

OFFICE LIGHTING OBSERVATORY

-

LightWatch

+
+

LightWatch

+ v{status?.version ?? "—"} +
+
+
+
+ {autoRefresh ? `AUTO ${AUTO_REFRESH_SECONDS}s` : "PAUSED"} + 最終更新 {fmtTime(lastUpdatedAt)} +
+ +
-
- {(error || imageError) && ( -
- {error || imageError} -
- )} + {(error || imageError) &&
{error || imageError}
}
- {label[state] ?? state} + {state}

{fmt(latest?.captured_at)}

+

継続 {formatDuration(status?.state_started_at, status?.server_time)}

5分ごとの観測から、オフィス照明の状態と保存画像を確認できます。

- - - + + +
+ {latest?.lighting_reason && }
{latestImageUrl ? ( - 最新の保存画像 - ) : ( -
- - {loading ? "画像を読み込み中" : "保存画像なし"} -
- )} + + ) :
{loading ? "画像を読み込み中" : "保存画像なし"}
}
- } - title="Health" - value={health?.status.toUpperCase() ?? "—"} - detail={health?.capture.message ?? "取得中"} - /> - } - title="NAS Sync" - value={status?.nas_sync?.result?.toUpperCase() ?? "—"} - detail={fmt(status?.nas_sync?.last_success_at)} - /> - } - title="Storage" - value={health ? `${health.storage.used_percent.toFixed(1)}%` : "—"} - detail="ローカル使用率" - /> - } - title="Database" - value={`${observations.length}件`} - detail="直近の観測" - /> + } title="Health" value={health?.status.toUpperCase() ?? "—"} detail={health?.capture.message ?? "取得中"}> + + + + + } title="NAS Sync" value={status?.nas_sync?.result?.toUpperCase() ?? "—"} detail={fmt(status?.nas_sync?.last_success_at)}> + + + + + } title="Storage" value={health ? `${health.storage.used_percent.toFixed(1)}%` : "—"} detail="ローカル使用率"> + + + + + } title="Database" value="最新50件" detail={`現在の表示件数: ${observations.length}件`}> + + + + +
+ +
+ +
-
- {images.map((item) => ( -
- {item.captured_at} -
- {fmt(item.captured_at)} - - {item.lighting_state ?? "—"} - -
-
- ))} +
+ {imageRows.map((row, rowIndex) => { + const selectedInRow = row.some(({ key }) => key === expandedImage); + return ( + +
+ {row.map(({ item, key: imageKey }) => { + const isExpanded = expandedImage === imageKey; + return ( +
+ +
+ {fmt(item.captured_at)} + {item.lighting_state ?? "—"} +
+ {item.lighting_reason && ( + + )} +
+ ); + })} +
+ {selectedInRow && selectedImage?.lighting_reason && ( +
+
+
+

選択した画像の判定詳細

+

{fmt(selectedImage.captured_at)}

+
+
+ + {selectedImage.lighting_state ?? "—"} + + +
+
+ +
+ )} +
+ ); + })}
- } - /> +
+

OBSERVATIONS

観測履歴

+ +
+
+
+ {STATES.map((item) => )} +
+ +
- - - - - - - - - + - {observations.map((item, index) => ( - - - - - - - - ))} + {observations.map((item, index) => { + const rowKey = item.id ?? `${item.captured_at}-${index}`; + const isExpanded = expandedObservation === rowKey; + return ( + + + + + + + + + + {item.lighting_reason && isExpanded && ( + + + + )} + + ); + })}
時刻状態輝度保存NAS
時刻状態輝度保存NAS詳細
{fmt(item.captured_at)} - - {item.lighting_state ?? item.status} - - {item.mean_brightness?.toFixed(1) ?? "—"}{item.image_url ? "あり" : "破棄"}{item.nas_synced ? "同期済" : "—"}
{fmt(item.captured_at)}{item.lighting_state ?? item.status}{item.mean_brightness?.toFixed(1) ?? "—"}{item.image_url ? "あり" : "破棄"}{item.nas_synced ? "同期済" : "—"} + {item.lighting_reason ? ( + + ) : "—"} +
+ +
-
LightWatch · Raspberry Pi Zero 2 W
+ {modalImage && ( + 0} + canNext={modalIndex < modalItems.length - 1} + onPrevious={showPreviousImage} + onNext={showNextImage} + onClose={closeImageModal} + /> + )} +
LightWatch · v{status?.version ?? "—"} · Raspberry Pi Zero 2 W
); } function Metric({ name, value }: { name: string; value: string }) { + return
{name}{value}
; +} + +function DetailRow({ label, value }: { label: string; value: string }) { + return
{label}{value}
; +} + +function InfoCard({ icon, title, value, detail, children }: { icon: ReactNode; title: string; value: string; detail: string; children: ReactNode }) { return ( -
- {name} - {value} +
+
{icon}
{title}{value}

{detail}

+
{children}
+
+ ); +} + +function ReasonPanel({ capture }: { capture: Capture }) { + return ( +
+

{capture.lighting_reason}

+
+
平均
{capture.mean_brightness?.toFixed(2) ?? "—"}
+
中央値
{capture.median_brightness ?? "—"}
+
暗部率
{capture.dark_ratio?.toFixed(4) ?? "—"}
+
明部率
{capture.bright_ratio?.toFixed(4) ?? "—"}
+
); } -function Card({ - icon, - title, - value, - detail, -}: { - icon: ReactNode; - title: string; - value: string; - detail: string; -}) { +function ReasonDetails({ capture, compact = false }: { capture: Capture; compact?: boolean }) { return ( -
-
{icon}
-
- {title} - {value} -

{detail}

+
+ 判定詳細 + +
+ ); +} + +function Timeline({ segments }: { segments: TimelineSegment[] }) { + if (segments.length === 0) return
本日の観測データはまだありません。
; + + const ordered = [...segments].sort( + (left, right) => new Date(left.started_at).getTime() - new Date(right.started_at).getTime(), + ); + const firstStartedAt = new Date(ordered[0].started_at); + const dayStart = new Date(firstStartedAt); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(dayStart); + dayEnd.setDate(dayEnd.getDate() + 1); + const dayStartMs = dayStart.getTime(); + const dayEndMs = dayEnd.getTime(); + const toPercent = (value: string) => { + const timestamp = new Date(value).getTime(); + if (!Number.isFinite(timestamp)) return 0; + const clamped = Math.min(dayEndMs, Math.max(dayStartMs, timestamp)); + return ((clamped - dayStartMs) / (dayEndMs - dayStartMs)) * 100; + }; + + const positionedSegments = ordered.map((segment, index) => { + const left = toPercent(segment.started_at); + const visualEnd = ordered[index + 1]?.started_at ?? segment.ended_at; + const right = Math.max(left, toPercent(visualEnd)); + return { + ...segment, + key: `${segment.started_at}-${index}`, + left: `${left}%`, + width: `${right - left}%`, + visualEnd, + }; + }); + const transitions = ordered.slice(1).map((segment, index) => { + const transitionAt = new Date(segment.started_at); + return { + key: `${segment.started_at}-${index}`, + left: `${toPercent(segment.started_at)}%`, + time: transitionAt.toLocaleTimeString("ja-JP", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), + from: ordered[index].state, + to: segment.state, + }; + }); + return ( + <> +
+
+ {positionedSegments.map((segment) => ( +
+ {segment.state} +
+ ))} +
+
+ {transitions.map((transition) => ( + + ))} +
-
+
00:0006:0012:0018:0024:00
+
{["BRIGHT", "PARTIAL", "DIM", "DARK"].map((item) => {item})}
+ ); } -function Head({ - over, - title, - icon, +function ImageModal({ + image, + position, + total, + canPrevious, + canNext, + onPrevious, + onNext, + onClose, }: { - over: string; - title: string; - icon?: ReactNode; + image: Capture; + position: number; + total: number; + canPrevious: boolean; + canNext: boolean; + onPrevious: () => void; + onNext: () => void; + onClose: () => void; }) { return ( -
-
-

{over}

-

{title}

+
+
event.stopPropagation()} + > +
+
+

{fmt(image.captured_at)}

+ {image.lighting_state ?? "—"} +
+ +
+
+ + {`${fmt(image.captured_at)}の保存画像`} + +
+
+ {position} / {total} + ← → で移動 · Escで閉じる +
- {icon}
); } + +function Head({ over, title, icon }: { over: string; title: string; icon?: ReactNode }) { + return

{over}

{title}

{icon}
; +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 4a96d46..c2c5847 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,2 +1,251 @@ @import "tailwindcss"; -:root{font-family:Inter,"Noto Sans JP",system-ui,sans-serif;color:#dce8f5;background:#07101d}*{box-sizing:border-box}body{margin:0;min-width:320px;background:radial-gradient(circle at 20% 0%,#122b45 0,transparent 34%),#07101d}.app{min-height:100vh}header{height:88px;padding:0 max(24px,calc((100vw - 1240px)/2));display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #233447;background:#07101dcc;backdrop-filter:blur(16px);position:sticky;top:0;z-index:10}h1{margin:2px 0 0;font-size:26px}h2{font-size:36px;margin:18px 0 8px}h3{margin:3px 0 0;font-size:22px}.eyebrow{font-size:10px;font-weight:800;letter-spacing:.2em;color:#55b8ff;margin:0}button{display:flex;gap:8px;align-items:center;background:#13263a;color:#dce8f5;border:1px solid #31506c;border-radius:10px;padding:10px 14px}main{max-width:1240px;margin:auto;padding:30px 24px 60px}.hero{display:grid;grid-template-columns:1fr 1.35fr;min-height:390px;background:linear-gradient(135deg,#10253b,#091421);border:1px solid #26415c;border-radius:22px;overflow:hidden;box-shadow:0 24px 80px #0007}.hero-copy{padding:42px;display:flex;flex-direction:column;justify-content:center}.hero-copy>p{color:#93a9bd;line-height:1.8}.hero-image{min-height:350px;background:#02070d}.hero-image img{width:100%;height:100%;object-fit:cover;display:block}.empty{height:100%;display:grid;place-content:center;gap:12px;color:#6e8497;text-align:center}.metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:25px}.metrics div{background:#091522;border:1px solid #20364b;padding:15px;border-radius:12px}.metrics span,.card span{display:block;color:#8398aa;font-size:12px}.metrics strong{display:block;font-size:22px;margin-top:5px}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0}.card{display:flex;gap:14px;background:#0b1724;border:1px solid #20374d;border-radius:16px;padding:20px}.card .icon{color:#52b8ff}.card strong{display:block;font-size:21px;margin:4px 0}.card p{margin:0;color:#71879a;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}.panel{background:#0a1522;border:1px solid #20374d;border-radius:18px;padding:24px;margin-top:18px}.section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}.gallery{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.gallery figure{margin:0;background:#07101a;border:1px solid #1c3348;border-radius:12px;overflow:hidden}.gallery img{display:block;width:100%;aspect-ratio:16/9;object-fit:cover}.gallery figcaption{padding:10px;display:flex;align-items:center;justify-content:space-between;font-size:11px;color:#91a7ba}.badge{display:inline-flex;width:max-content;padding:5px 9px;border-radius:999px;font-size:11px;font-weight:800;border:1px solid}.bright,.ok,.healthy,.success{color:#6ee7b7;background:#064e3b55;border-color:#0b7a5c}.partial,.warning{color:#fcd34d;background:#713f1255;border-color:#a16207}.dark,.error,.failure{color:#fca5a5;background:#7f1d1d55;border-color:#b91c1c}.unknown{color:#aab9c7;background:#33415555;border-color:#475569}.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;color:#7890a5;padding:12px;border-bottom:1px solid #273b4e}td{padding:13px 12px;border-bottom:1px solid #14283a;color:#c4d3df}footer{text-align:center;padding:26px;color:#51677a;font-size:12px}.error{max-width:1240px;margin:18px auto 0;padding:12px 24px;color:#fecaca}.spin{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@media(max-width:900px){.hero{grid-template-columns:1fr}.cards{grid-template-columns:repeat(2,1fr)}.gallery{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){header{padding:0 16px}.hero-copy{padding:26px}h2{font-size:26px}.metrics,.cards,.gallery{grid-template-columns:1fr}.panel{padding:16px}main{padding:18px 12px 40px}} + +:root { + font-family: Inter, "Noto Sans JP", system-ui, sans-serif; + color: #dce8f5; + background: #07101d; + + --state-bright-text: #c7f9ff; + --state-bright-bg: rgb(103 232 249 / 12%); + --state-bright-border: rgb(125 211 252 / 42%); + --state-bright-glow: rgb(103 232 249 / 15%); + + --state-partial-text: #f6d99a; + --state-partial-bg: rgb(180 125 45 / 14%); + --state-partial-border: rgb(217 164 65 / 42%); + --state-partial-glow: rgb(217 164 65 / 13%); + + --state-dim-text: #b8c8d8; + --state-dim-bg: rgb(100 116 139 / 16%); + --state-dim-border: rgb(148 163 184 / 38%); + --state-dim-glow: rgb(148 163 184 / 10%); + + --state-dark-text: #b8c9e8; + --state-dark-bg: rgb(51 65 110 / 22%); + --state-dark-border: rgb(99 116 166 / 44%); + --state-dark-glow: rgb(71 85 140 / 16%); + + --state-unknown-text: #aab9c7; + --state-unknown-bg: rgb(51 65 85 / 34%); + --state-unknown-border: #475569; + --state-unknown-glow: transparent; +} + +* { box-sizing: border-box; } +body { margin: 0; min-width: 320px; background: radial-gradient(circle at 20% 0%, #122b45 0, transparent 34%), #07101d; } +.app { min-height: 100vh; } +header { min-height: 88px; padding: 12px max(24px, calc((100vw - 1240px) / 2)); display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #233447; background: #07101dcc; backdrop-filter: blur(16px); position: sticky; top: 0; z-index: 10; } +h1 { margin: 2px 0 0; font-size: 26px; } +h2 { font-size: 36px; margin: 18px 0 4px; } +h3 { margin: 3px 0 0; font-size: 22px; } +.eyebrow { font-size: 10px; font-weight: 800; letter-spacing: .2em; color: #55b8ff; margin: 0; } +.title-row, .header-actions { display: flex; align-items: center; gap: 10px; } +.version-badge { font-size: 11px; font-weight: 800; color: #7dd3fc; background: #0c4a6e55; border: 1px solid #38bdf855; border-radius: 999px; padding: 4px 8px; } +.refresh-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; } +.refresh-meta span { font-size: 10px; font-weight: 800; letter-spacing: .08em; color: #7dd3fc; } +.refresh-meta small { font-size: 11px; color: #71879a; } +button { display: flex; gap: 8px; align-items: center; background: #13263a; color: #dce8f5; border: 1px solid #31506c; border-radius: 10px; padding: 10px 14px; cursor: pointer; } +button:disabled { opacity: .6; cursor: wait; } +.icon-button { padding: 10px; } +main { max-width: 1240px; margin: auto; padding: 30px 24px 60px; } +.hero { display: grid; grid-template-columns: 1fr 1.35fr; min-height: 390px; background: linear-gradient(135deg, #10253b, #091421); border: 1px solid #26415c; border-radius: 22px; overflow: hidden; box-shadow: 0 24px 80px #0007; } +.hero-copy { padding: 42px; display: flex; flex-direction: column; justify-content: center; } +.hero-copy > p { color: #93a9bd; line-height: 1.8; } +.hero-copy .duration { margin: 0; color: #7dd3fc; font-size: 13px; font-weight: 700; } +.hero-image { min-height: 350px; background: #02070d; } +.hero-image img { width: 100%; height: 100%; object-fit: cover; display: block; } +.hero-image-button { width: 100%; height: 100%; padding: 0; border: 0; border-radius: 0; background: transparent; display: block; } +.hero-image-button:hover img, .hero-image-button:focus-visible img { filter: brightness(1.06); } +.empty { height: 100%; display: grid; place-content: center; gap: 12px; color: #6e8497; text-align: center; } +.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 25px; } +.metrics div { background: #091522; border: 1px solid #20364b; padding: 15px; border-radius: 12px; } +.metrics span, .card summary span { display: block; color: #8398aa; font-size: 12px; } +.metrics strong { display: block; font-size: 22px; margin-top: 5px; } +.cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin: 18px 0; } +.card { background: #0b1724; border: 1px solid #20374d; border-radius: 16px; padding: 0; overflow: hidden; } +.card summary { list-style: none; display: flex; gap: 14px; align-items: flex-start; padding: 20px; cursor: pointer; } +.card summary::-webkit-details-marker { display: none; } +.card .icon { color: #52b8ff; } +.card strong { display: block; font-size: 21px; margin: 4px 0; } +.card p { margin: 0; color: #71879a; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px; } +.chevron { margin-left: auto; color: #60788d; transition: transform .2s; } +.card[open] .chevron { transform: rotate(180deg); } +.card-details { border-top: 1px solid #1a3043; padding: 12px 18px 16px; } +.detail-row { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; font-size: 11px; } +.detail-row span { color: #71879a; } +.detail-row b { font-weight: 600; color: #c8d7e4; text-align: right; } +.panel { background: #0a1522; border: 1px solid #20374d; border-radius: 18px; padding: 24px; margin-top: 18px; } +.section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; } +.gallery-stack { display: grid; gap: 14px; } +.gallery-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; } +.gallery-row figure { margin: 0; background: #07101a; border: 1px solid #1c3348; border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; min-width: 0; transition: border-color .2s, box-shadow .2s; } +.gallery-row figure.selected { border-color: #38bdf8aa; box-shadow: 0 0 0 1px #38bdf833, 0 0 24px #0ea5e922; } +.gallery-row img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; transition: transform .2s, filter .2s; } +.image-open-button { width: 100%; padding: 0; border: 0; border-radius: 0; background: transparent; overflow: hidden; display: block; } +.image-open-button:hover img, .image-open-button:focus-visible img { transform: scale(1.02); filter: brightness(1.08); } +.gallery-row figcaption { padding: 10px 10px 6px; display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 11px; color: #91a7ba; } +.gallery-row figcaption span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.image-detail-toggle { width: 100%; margin-top: auto; justify-content: flex-start; border: 0; border-radius: 0; padding: 8px 10px 10px; background: transparent; color: #7dd3fc; font-size: 11px; font-weight: 700; } +.image-detail-toggle:hover { background: #0b1d2c; } +.image-detail-toggle svg { margin-left: 2px; transition: transform .2s; } +.image-detail-toggle[aria-expanded="true"] svg { transform: rotate(180deg); } +.gallery-detail { position: relative; padding: 18px; background: #081522; border: 1px solid #284760; border-radius: 14px; box-shadow: inset 0 1px rgb(255 255 255 / 4%); } +.gallery-detail-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 12px; } +.gallery-detail-head h4 { margin: 3px 0 0; font-size: 17px; } +.gallery-detail-actions { display: flex; align-items: center; gap: 10px; } +.detail-close { padding: 7px 10px; font-size: 11px; } +.gallery-detail .reason-panel { margin-top: 0; background: #06101a; } + +.badge { display: inline-flex; width: max-content; padding: 5px 9px; border-radius: 999px; font-size: 11px; font-weight: 800; border: 1px solid; box-shadow: 0 0 18px var(--state-glow); } +.bright { --state-text: var(--state-bright-text); --state-bg: var(--state-bright-bg); --state-border: var(--state-bright-border); --state-glow: var(--state-bright-glow); } +.partial { --state-text: var(--state-partial-text); --state-bg: var(--state-partial-bg); --state-border: var(--state-partial-border); --state-glow: var(--state-partial-glow); } +.dim { --state-text: var(--state-dim-text); --state-bg: var(--state-dim-bg); --state-border: var(--state-dim-border); --state-glow: var(--state-dim-glow); } +.dark { --state-text: var(--state-dark-text); --state-bg: var(--state-dark-bg); --state-border: var(--state-dark-border); --state-glow: var(--state-dark-glow); } +.unknown { --state-text: var(--state-unknown-text); --state-bg: var(--state-unknown-bg); --state-border: var(--state-unknown-border); --state-glow: var(--state-unknown-glow); } +.badge.bright, .badge.partial, .badge.dim, .badge.dark, .badge.unknown { color: var(--state-text); background: var(--state-bg); border-color: var(--state-border); } +.ok, .healthy, .success { color: #6ee7b7; background: #064e3b55; border-color: #0b7a5c; } +.warning { color: #fcd34d; background: #713f1255; border-color: #a16207; } +.error, .failure { color: #fca5a5; background: #7f1d1d55; border-color: #b91c1c; } + +.reason { margin-top: 14px; } +.reason summary { cursor: pointer; color: #7dd3fc; font-size: 12px; font-weight: 700; } +.reason.compact { margin: 0; padding: 0 10px 10px; } +.reason-panel { margin-top: 8px; background: #07101a; border: 1px solid #1c3348; border-radius: 10px; padding: 14px; min-width: 0; } +.reason-panel p { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; margin: 0 0 12px; color: #a8bac9; font: 12px/1.7 ui-monospace, SFMono-Regular, Consolas, monospace; } +.reason-panel dl { margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: 8px; } +.reason-panel dl div { background: #0b1724; border: 1px solid #172d40; border-radius: 8px; padding: 9px; min-width: 0; } +.reason-panel dt { color: #71879a; font-size: 10px; } +.reason-panel dd { margin: 3px 0 0; font-size: 12px; overflow-wrap: anywhere; } + +.filters { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; } +.segmented { display: flex; flex-wrap: wrap; gap: 6px; } +.segmented button { font-size: 11px; padding: 7px 10px; background: #091522; } +.segmented button.active { color: #7dd3fc; border-color: #38bdf8; background: #0c4a6e66; } +.segmented button.active.bright, .segmented button.active.partial, .segmented button.active.dim, .segmented button.active.dark { color: var(--state-text); border-color: var(--state-border); background: var(--state-bg); box-shadow: 0 0 16px var(--state-glow); } +.switch { display: flex; align-items: center; gap: 8px; color: #91a7ba; font-size: 12px; cursor: pointer; } +.switch input { display: none; } +.switch span { width: 34px; height: 18px; border-radius: 999px; background: #334155; position: relative; } +.switch span:after { content: ""; position: absolute; width: 14px; height: 14px; left: 2px; top: 2px; border-radius: 50%; background: #cbd5e1; transition: .2s; } +.switch input:checked + span { background: #0369a1; } +.switch input:checked + span:after { transform: translateX(16px); } +.table-wrap { overflow-x: auto; } +table { width: 100%; border-collapse: collapse; font-size: 13px; } +th { text-align: left; color: #7890a5; padding: 12px; border-bottom: 1px solid #273b4e; } +td { padding: 13px 12px; border-bottom: 1px solid #14283a; color: #c4d3df; } +.observation-row.expanded td { border-bottom-color: transparent; background: #0b1825; } +.detail-toggle { padding: 6px 9px; font-size: 11px; white-space: nowrap; background: #0d2031; } +.detail-toggle[aria-expanded="true"] svg { transform: rotate(180deg); } +.detail-toggle svg { transition: transform .2s; } +.observation-detail-row td { padding: 0 12px 14px; background: #0b1825; } +.observation-detail-row .reason-panel { margin: 0; width: 100%; max-height: 360px; overflow-y: auto; } + +.timeline-wrap { position: relative; padding-top: 18px; } +.timeline { position: relative; height: 42px; border-radius: 10px; overflow: hidden; background: #07101a; border: 1px solid #1c3348; } +.timeline-segment { position: absolute; inset-block: 0; min-width: 3px; display: flex; align-items: center; justify-content: center; overflow: hidden; border-right: 1px solid rgb(7 16 29 / 55%); box-shadow: inset 0 1px rgb(255 255 255 / 7%); background: var(--state-bg); } +.timeline-segment span { font-size: 9px; font-weight: 800; color: var(--state-text); overflow: hidden; } +.timeline-segment.bright, .timeline-segment.partial, .timeline-segment.dim, .timeline-segment.dark, .timeline-segment.unknown { background: var(--state-bg); } +.timeline-markers { position: absolute; inset: 18px 0 0; height: 42px; pointer-events: none; } +.timeline-marker { position: absolute; top: -7px; width: 13px; height: 56px; padding: 0; border: 0; border-radius: 999px; background: transparent; transform: translateX(-50%); pointer-events: auto; } +.timeline-marker::before { content: ""; position: absolute; left: 5px; top: 7px; width: 3px; height: 42px; border-radius: 999px; background: #7dd3fc; box-shadow: 0 0 9px #38bdf8cc; } +.timeline-marker::after { content: ""; position: absolute; left: 2px; top: 3px; width: 9px; height: 9px; border-radius: 50%; background: #dff7ff; border: 2px solid #38bdf8; box-shadow: 0 0 10px #38bdf8; } +.timeline-marker > span { position: absolute; left: 50%; bottom: calc(100% + 4px); min-width: 132px; padding: 7px 9px; border: 1px solid #31506c; border-radius: 8px; background: #07101df2; color: #dce8f5; font-size: 10px; line-height: 1.4; text-align: left; box-shadow: 0 8px 24px #0009; opacity: 0; visibility: hidden; transform: translate(-50%, 4px); transition: opacity .15s, transform .15s; z-index: 3; } +.timeline-marker > span b { display: block; color: #7dd3fc; white-space: nowrap; } +.timeline-marker:hover > span, .timeline-marker:focus-visible > span { opacity: 1; visibility: visible; transform: translate(-50%, 0); } +.timeline-scale { display: flex; justify-content: space-between; color: #60788d; font-size: 10px; margin-top: 7px; } +.timeline-legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 12px; color: #91a7ba; font-size: 10px; } +.timeline-legend span { display: flex; align-items: center; gap: 5px; } +.timeline-legend i { width: 9px; height: 9px; border-radius: 3px; background: var(--state-bg); border: 1px solid var(--state-border); box-shadow: 0 0 10px var(--state-glow); } +.timeline-empty { color: #71879a; font-size: 13px; } + +.image-modal { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 24px; background: rgb(2 7 13 / 88%); backdrop-filter: blur(10px); } +.image-modal-dialog { width: min(1180px, 100%); max-height: calc(100vh - 48px); display: flex; flex-direction: column; overflow: hidden; border: 1px solid #31506c; border-radius: 18px; background: #07101d; box-shadow: 0 28px 100px #000d; } +.image-modal-head, .image-modal-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 18px; } +.image-modal-head { border-bottom: 1px solid #1c3348; } +.image-modal-head > div { display: flex; align-items: center; gap: 12px; min-width: 0; } +.image-modal-head p { margin: 0; color: #c4d3df; font-size: 13px; } +.modal-close { padding: 8px; } +.image-modal-stage { min-height: 0; display: grid; grid-template-columns: 56px minmax(0, 1fr) 56px; align-items: center; background: #02070d; } +.image-modal-stage img { display: block; width: 100%; height: auto; max-height: calc(100vh - 190px); object-fit: contain; } +.modal-nav { height: 100%; min-height: 88px; justify-content: center; padding: 0; border: 0; border-radius: 0; background: rgb(7 16 29 / 72%); } +.modal-nav:hover:not(:disabled), .modal-nav:focus-visible:not(:disabled) { background: rgb(14 58 90 / 72%); } +.modal-nav:disabled { opacity: .22; cursor: not-allowed; } +.image-modal-foot { border-top: 1px solid #1c3348; color: #91a7ba; font-size: 12px; } +.image-modal-foot small { color: #60788d; } + +footer { text-align: center; padding: 26px; color: #51677a; font-size: 12px; } +.error { max-width: 1240px; margin: 18px auto 0; padding: 12px 24px; color: #fecaca; } +.spin { animation: spin 1s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +@media (max-width: 900px) { + header { align-items: flex-start; } + .header-actions { flex-wrap: wrap; justify-content: flex-end; } + .hero { grid-template-columns: 1fr; } + .cards { grid-template-columns: repeat(2, 1fr); } + .gallery-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .gallery-detail { padding: 16px; } + .image-modal { padding: 16px; } + .image-modal-dialog { max-height: calc(100vh - 32px); } +} + +@media (max-width: 560px) { + header { padding: 12px 16px; gap: 12px; } + .title-row { align-items: flex-start; flex-direction: column; gap: 4px; } + .refresh-meta { display: none; } + .hero-copy { padding: 26px; } + h1 { font-size: 21px; } + h2 { font-size: 26px; } + .metrics, .cards, .gallery-row { grid-template-columns: 1fr; } + .panel { padding: 16px; } + main { padding: 18px 12px 40px; } + .filters { align-items: flex-start; flex-direction: column; } + .gallery-detail-head { align-items: flex-start; flex-direction: column; } + .gallery-detail-actions { width: 100%; justify-content: space-between; } + .image-modal { padding: 0; } + .image-modal-dialog { width: 100%; max-height: 100vh; border-radius: 0; border-left: 0; border-right: 0; } + .image-modal-head, .image-modal-foot { padding: 10px 12px; } + .image-modal-head > div { align-items: flex-start; flex-direction: column; gap: 6px; } + .image-modal-stage { grid-template-columns: 44px minmax(0, 1fr) 44px; } + .image-modal-stage img { max-height: calc(100vh - 170px); } + .image-modal-foot small { display: none; } + .table-wrap { + width: 100%; + max-width: 100%; + overflow-x: hidden; + } + table { + width: 100%; + table-layout: fixed; + } + th, td { + min-width: 0; + padding-left: 6px; + padding-right: 6px; + overflow-wrap: anywhere; + } + th:nth-child(1), td:nth-child(1) { width: 25%; } + th:nth-child(2), td:nth-child(2) { width: 14%; } + th:nth-child(3), td:nth-child(3) { width: 12%; } + th:nth-child(4), td:nth-child(4) { width: 11%; } + th:nth-child(5), td:nth-child(5) { width: 14%; } + th:nth-child(6), td:nth-child(6) { width: 24%; } + .observation-detail-row td { + width: auto; + max-width: 100%; + padding: 0 0 12px; + } + .observation-detail-row .reason-panel { + width: auto; + max-width: 100%; + margin: 0; + overflow-x: hidden; + } + .observation-detail-row .reason-panel dl { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 360px) { + .reason-panel dl { grid-template-columns: 1fr; } + .gallery-detail { padding: 12px; } +} diff --git a/pyproject.toml b/pyproject.toml index c8b81f3..97e8d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,39 @@ [build-system] -requires = ["setuptools>=68"] +requires = ["setuptools>=75", "wheel"] build-backend = "setuptools.build_meta" [project] name = "lightwatch" -version = "1.0.0" -description = "Raspberry Pi-based office lighting monitoring and timelapse system" +version = "1.1.0" +description = "Raspberry Pi lighting monitoring and timelapse system" requires-python = ">=3.11,<3.14" dependencies = [ - "Pillow", + "Pillow>=11.0,<13.0", + "fastapi>=0.115,<1.0", + "uvicorn[standard]>=0.34,<1.0", ] -[project.optional-dependencies] -dev = [ - "pytest", - "ruff", - "httpx2", - "fastapi", -] +[dependency-groups] +dev = ["httpx2>=2.0,<3.0", "pytest>=9.0,<10.0", "ruff>=0.12,<1.0"] + +[tool.setuptools] +package-dir = { "" = "src" } + +[tool.setuptools.packages.find] +where = ["src"] [tool.ruff] line-length = 88 target-version = "py311" +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP", "SIM"] +ignore = ["E501"] + +[tool.ruff.lint.per-file-ignores] +"src/lightwatch/web.py" = ["B008"] + [tool.pytest.ini_options] +pythonpath = ["."] testpaths = ["tests"] - -[tool.setuptools.packages.find] -where = ["src"] +addopts = "-q" diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 6b6ad8f..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ --r requirements.txt -pytest>=9.0,<10.0 -httpx>=0.28,<1.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cd42ef6..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Pillow>=11.0,<13.0 -fastapi>=0.115,<1.0 -uvicorn[standard]>=0.34,<1.0 diff --git a/scripts/create-web-sample-data.py b/scripts/create-web-sample-data.py new file mode 100644 index 0000000..4018988 --- /dev/null +++ b/scripts/create-web-sample-data.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from src.lightwatch.sample_data import main # noqa: E402 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/reclassify_lighting.py b/scripts/reclassify_lighting.py index 5fc3552..624c2cf 100644 --- a/scripts/reclassify_lighting.py +++ b/scripts/reclassify_lighting.py @@ -2,7 +2,6 @@ import argparse import sys -import tomllib from datetime import date from pathlib import Path from typing import Any @@ -11,13 +10,9 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -from lightwatch.analysis import LightingThresholds # noqa: E402 -from lightwatch.reclassify import reclassify_lighting_history # noqa: E402 - - -def load_config(path: Path) -> dict[str, Any]: - with path.open("rb") as file: - return tomllib.load(file) +from src.lightwatch.analysis import LightingThresholds # noqa: E402 +from src.lightwatch.config import load_config # noqa: E402 +from src.lightwatch.reclassify import reclassify_lighting_history # noqa: E402 def build_lighting_thresholds(config: dict[str, Any]) -> LightingThresholds: @@ -49,7 +44,7 @@ def main() -> int: parser.add_argument( "--config", type=Path, - default=Path("config/default.toml"), + default=None, help="Path to the TOML configuration file.", ) diff --git a/scripts/sync-to-nas.sh b/scripts/sync-to-nas.sh index 33caec5..aa67ba7 100644 --- a/scripts/sync-to-nas.sh +++ b/scripts/sync-to-nas.sh @@ -10,7 +10,7 @@ CONFIG_OUTPUT="$( import sys from pathlib import Path -from lightwatch.config import get_nas_settings, load_config +from src.lightwatch.config import get_nas_settings, load_config config = load_config(Path(sys.argv[1])) nas = get_nas_settings(config) @@ -39,7 +39,7 @@ readonly SYNC_CUTOFF="$(date '+%Y-%m-%dT%H:%M:%S')" record_failure() { local exit_code=$? set +e - "${PROJECT_ROOT}/.venv/bin/python" -m lightwatch.nas_sync_state \ + "${PROJECT_ROOT}/.venv/bin/python" -m src.lightwatch.nas_sync_state \ --database "${DATABASE_PATH}" \ --state "${SYNC_STATE}" \ --result failure \ @@ -65,7 +65,7 @@ rsync \ "${SOURCE_DIR}" \ "${DESTINATION}" -"${PROJECT_ROOT}/.venv/bin/python" -m lightwatch.nas_sync_state \ +"${PROJECT_ROOT}/.venv/bin/python" -m src.lightwatch.nas_sync_state \ --database "${DATABASE_PATH}" \ --state "${SYNC_STATE}" \ --result success \ diff --git a/scripts/sync-version.ps1 b/scripts/sync-version.ps1 index 845e066..898be2a 100644 --- a/scripts/sync-version.ps1 +++ b/scripts/sync-version.ps1 @@ -47,6 +47,24 @@ finally { Pop-Location } +$uvCommand = Get-Command uv -ErrorAction SilentlyContinue +if ($null -eq $uvCommand) { + throw 'uv が見つかりません。uv をインストールしてから再実行してください。' +} + +Push-Location $ProjectRoot +try { + & $uvCommand.Path lock | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "uv lock に失敗しました。終了コード: $LASTEXITCODE" + } +} +finally { + Pop-Location +} + +Write-Host 'uv.lock synchronized.' + $venvPython = Join-Path $ProjectRoot '.venv\Scripts\python.exe' $pythonCommand = if (Test-Path $venvPython) { $venvPython } else { 'python' } & $pythonCommand (Join-Path $ProjectRoot 'scripts\verify-version.py') diff --git a/scripts/verify.ps1 b/scripts/verify.ps1 new file mode 100644 index 0000000..351ccbd --- /dev/null +++ b/scripts/verify.ps1 @@ -0,0 +1,74 @@ +param( + [switch]$SkipFrontend +) + +$ErrorActionPreference = 'Stop' +$ProjectRoot = Split-Path -Parent $PSScriptRoot + +function Invoke-VerificationStep { + param( + [Parameter(Mandatory)] + [string]$Name, + + [Parameter(Mandatory)] + [scriptblock]$Command + ) + + Write-Host "" + Write-Host "==> $Name" + + & $Command + + if ($LASTEXITCODE -ne 0) { + throw "$Name に失敗しました。終了コード: $LASTEXITCODE" + } +} + +Push-Location $ProjectRoot +try { + Invoke-VerificationStep "uv lock" { + uv lock --check + } + + Invoke-VerificationStep "Version consistency" { + uv run --locked python .\scripts\verify-version.py + } + + Invoke-VerificationStep "Ruff lint" { + uv run --locked ruff check . + } + + Invoke-VerificationStep "Ruff format" { + uv run --locked ruff format --check . + } + + Invoke-VerificationStep "pytest" { + uv run --locked pytest -q + } + + if (-not $SkipFrontend) { + Push-Location .\frontend + try { + Invoke-VerificationStep "Frontend lint" { + npm run lint + } + + Invoke-VerificationStep "Frontend build" { + npm run build + } + } + finally { + Pop-Location + } + } + + Invoke-VerificationStep "Git whitespace check" { + git diff --check + } + + Write-Host "" + Write-Host "All verification steps passed." +} +finally { + Pop-Location +} \ No newline at end of file diff --git a/src/lightwatch/analysis.py b/src/lightwatch/analysis.py index b2d2bfa..3338388 100644 --- a/src/lightwatch/analysis.py +++ b/src/lightwatch/analysis.py @@ -65,6 +65,7 @@ class LightingRoiRule: y2: int min_mean: float max_dark_ratio: float + required_for_bright: bool = True def __post_init__(self) -> None: if not self.name.strip(): @@ -276,7 +277,8 @@ def classify_lighting_with_rois( ) evaluations: list[str] = [] - all_rois_bright = True + required_rois_bright = True + required_roi_count = 0 for rule in roi_rules: metrics = roi_metrics.get(rule.name) if metrics is None: @@ -285,24 +287,36 @@ def classify_lighting_with_rois( metrics.mean_brightness >= rule.min_mean and metrics.dark_ratio <= rule.max_dark_ratio ) - all_rois_bright = all_rois_bright and matched + if rule.required_for_bright: + required_roi_count += 1 + required_rois_bright = required_rois_bright and matched evaluations.append( f"{rule.name}={'on' if matched else 'off'}" f"(mean={metrics.mean_brightness:.2f}/{rule.min_mean:.2f}," - f"dark={metrics.dark_ratio:.4f}/{rule.max_dark_ratio:.4f})" + f"dark={metrics.dark_ratio:.4f}/{rule.max_dark_ratio:.4f}," + f"required={'yes' if rule.required_for_bright else 'no'})" ) + if required_roi_count == 0: + raise ValueError("At least one lighting ROI must be required for BRIGHT.") + roi_summary = ", ".join(evaluations) - if all_rois_bright: + if required_rois_bright: return LightingClassification( state=LightingState.BRIGHT, - reason=f"all lighting ROIs matched: {roi_summary}; global: {global_summary}", + reason=( + "all required lighting ROIs matched: " + f"{roi_summary}; global: {global_summary}" + ), ) if _matches_partial(global_metrics, thresholds): return LightingClassification( state=LightingState.PARTIAL, - reason=f"one or more lighting ROIs did not match: {roi_summary}; global: {global_summary}", + reason=( + "one or more required lighting ROIs did not match: " + f"{roi_summary}; global: {global_summary}" + ), ) return LightingClassification( diff --git a/src/lightwatch/capture.py b/src/lightwatch/capture.py index e921664..498b3b2 100644 --- a/src/lightwatch/capture.py +++ b/src/lightwatch/capture.py @@ -4,7 +4,6 @@ import os import shutil import subprocess -import tomllib from datetime import datetime from pathlib import Path from typing import Any @@ -15,16 +14,12 @@ analyze_image_with_rois, classify_lighting_with_rois, ) +from .config import load_config from .database import get_latest_lighting_state, record_capture from .latest_observation import publish_latest_observation from .notification import notify_dark_transition -def load_config(path: Path) -> dict[str, Any]: - with path.open("rb") as file: - return tomllib.load(file) - - def build_lighting_thresholds(config: dict[str, Any]) -> LightingThresholds: lighting = config.get("lighting", {}) return LightingThresholds( @@ -54,6 +49,7 @@ def build_lighting_roi_rules(config: dict[str, Any]) -> tuple[LightingRoiRule, . y2=int(entry["y2"]), min_mean=float(entry["min_mean"]), max_dark_ratio=float(entry["max_dark_ratio"]), + required_for_bright=bool(entry.get("required_for_bright", True)), ) for entry in roi_entries ) @@ -127,7 +123,7 @@ def publish_image(temp_path: Path, output_path: Path) -> Path: def main() -> int: parser = argparse.ArgumentParser(description="Capture a lightwatch image.") - parser.add_argument("--config", type=Path, default=Path("config/default.toml")) + parser.add_argument("--config", type=Path, default=None) parser.add_argument("--dry-run", action="store_true") parser.add_argument("--force-save", action="store_true") args = parser.parse_args() diff --git a/src/lightwatch/cleanup.py b/src/lightwatch/cleanup.py index 91cddb5..5428c84 100644 --- a/src/lightwatch/cleanup.py +++ b/src/lightwatch/cleanup.py @@ -1,11 +1,12 @@ from __future__ import annotations import argparse +from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta from pathlib import Path -from .config import DEFAULT_CONFIG_PATH, get_operational_settings, load_config +from .config import get_operational_settings, load_config from .database import get_synced_capture_files_older_than PROJECT_ROOT = Path("/opt/lightwatch") @@ -72,10 +73,8 @@ def cleanup_local_images( reverse=True, ) for directory in directories: - try: + with suppress(OSError): directory.rmdir() - except OSError: - pass return CleanupResult( candidate_count=len(records), @@ -92,7 +91,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--config", type=Path, - default=DEFAULT_CONFIG_PATH, + default=None, help="Path to the TOML configuration file.", ) parser.add_argument( diff --git a/src/lightwatch/config.py b/src/lightwatch/config.py index 9577f6c..4b2f3c8 100644 --- a/src/lightwatch/config.py +++ b/src/lightwatch/config.py @@ -1,11 +1,13 @@ from __future__ import annotations +import os import tomllib from dataclasses import dataclass from pathlib import Path from typing import Any DEFAULT_CONFIG_PATH = Path("config/default.toml") +CONFIG_PATH_ENV_VAR = "LIGHTWATCH_CONFIG" DEFAULT_PROJECT_ROOT = Path("/opt/lightwatch") @@ -29,8 +31,30 @@ def destination(self) -> str: return f"{self.user}@{self.host}:{self.destination_dir.rstrip('/')}/" -def load_config(path: Path = DEFAULT_CONFIG_PATH) -> dict[str, Any]: - with path.open("rb") as file: +def resolve_config_path(path: Path | None = None) -> Path: + """Resolve the configuration path for the current process. + + An explicit path takes precedence over the environment variable. Relative + paths are resolved from the current working directory. + """ + configured = path + if configured is None: + environment_value = os.environ.get(CONFIG_PATH_ENV_VAR) + configured = ( + Path(environment_value) if environment_value else DEFAULT_CONFIG_PATH + ) + + resolved = configured.expanduser() + if not resolved.is_absolute(): + resolved = Path.cwd() / resolved + return resolved.resolve() + + +def load_config(path: Path | None = None) -> dict[str, Any]: + resolved = resolve_config_path(path) + if not resolved.is_file(): + raise FileNotFoundError(f"Configuration file not found: {resolved}") + with resolved.open("rb") as file: return tomllib.load(file) @@ -50,8 +74,8 @@ def get_operational_settings(config: dict[str, Any]) -> OperationalSettings: def get_nas_settings(config: dict[str, Any]) -> NasSettings: nas = config.get("nas", {}) settings = NasSettings( - user=str(nas.get("user", "backup")), - host=str(nas.get("host", "nas.local")), + user=str(nas.get("user", "pi")), + host=str(nas.get("host", "4bnas")), destination_dir=str( nas.get( "destination_dir", diff --git a/src/lightwatch/database.py b/src/lightwatch/database.py index d3c3cd4..6b63b52 100644 --- a/src/lightwatch/database.py +++ b/src/lightwatch/database.py @@ -1,14 +1,22 @@ from __future__ import annotations import sqlite3 +from collections.abc import Iterator +from contextlib import closing, contextmanager from dataclasses import dataclass from datetime import date, datetime, time, timedelta from pathlib import Path - DATABASE_SCHEMA_VERSION = 1 +@contextmanager +def _connect(database_path: Path) -> Iterator[sqlite3.Connection]: + """Open a SQLite connection and always close it after use.""" + with closing(sqlite3.connect(database_path)) as connection, connection: + yield connection + + _ADDITIONAL_CAPTURE_COLUMNS = { "mean_brightness": "REAL", "median_brightness": "INTEGER", @@ -81,7 +89,7 @@ def _create_capture_indexes(connection: sqlite3.Connection) -> None: def get_capture_index_names(database_path: Path) -> set[str]: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: rows = connection.execute( """ SELECT name @@ -98,14 +106,14 @@ def get_capture_index_names(database_path: Path) -> set[str]: def analyze_database(database_path: Path) -> None: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: connection.execute("ANALYZE") def initialize_database(database_path: Path) -> None: database_path.parent.mkdir(parents=True, exist_ok=True) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: connection.execute( """ CREATE TABLE IF NOT EXISTS captures ( @@ -150,7 +158,7 @@ def get_latest_lighting_state( ) -> str | None: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: row = connection.execute( """ SELECT lighting_state @@ -183,7 +191,7 @@ def record_capture( ) -> int: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: cursor = connection.execute( """ INSERT INTO captures ( @@ -254,7 +262,7 @@ def get_capture_by_id( ) -> LatestCapture | None: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: row = connection.execute( """ SELECT @@ -295,7 +303,7 @@ def get_latest_successful_capture( ) -> LatestCapture | None: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: row = connection.execute( """ SELECT @@ -334,7 +342,7 @@ def get_latest_successful_capture( def get_latest_saved_capture(database_path: Path) -> LatestCapture | None: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: row = connection.execute( """ SELECT captured_at, image_path, status, mean_brightness, @@ -371,7 +379,7 @@ def get_daily_capture_summary( date_start, date_end = _date_bounds(report_date.date()) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: count_row = connection.execute( """ SELECT @@ -433,7 +441,7 @@ def get_daily_capture_summary( def count_unsynced_successful_captures(database_path: Path) -> int: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: row = connection.execute( """ SELECT COUNT(*) @@ -460,7 +468,7 @@ def mark_existing_successful_captures_nas_synced( initialize_database(database_path) cutoff = captured_before_or_at.isoformat(timespec="seconds") - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: rows = connection.execute( """ SELECT id, image_path @@ -495,7 +503,7 @@ def get_synced_capture_files_older_than( initialize_database(database_path) cutoff = captured_before.isoformat(timespec="seconds") - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: rows = connection.execute( """ SELECT id, captured_at, image_path @@ -567,7 +575,7 @@ def get_reclassifiable_captures( ORDER BY captured_at ASC, id ASC """ - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: rows = connection.execute(query, parameters).fetchall() return [ @@ -594,7 +602,7 @@ def update_lighting_classifications( if not updates: return 0 - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: connection.executemany( """ UPDATE captures @@ -634,6 +642,7 @@ def list_capture_records( offset: int = 0, target_date: date | None = None, saved_only: bool = False, + lighting_state: str | None = None, ) -> list[WebCaptureRecord]: initialize_database(database_path) if limit < 1 or limit > 500: @@ -646,9 +655,12 @@ def list_capture_records( parameters.extend([start, end]) if saved_only: conditions.extend(["status = 'success'", "image_path IS NOT NULL"]) + if lighting_state is not None: + conditions.append("lighting_state = ?") + parameters.append(lighting_state) where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" parameters.extend([limit, offset]) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: rows = connection.execute( f""" SELECT id, captured_at, image_path, status, file_size, nas_synced, @@ -679,9 +691,36 @@ def list_capture_records( ] +def get_current_lighting_state_started_at(database_path: Path) -> str | None: + """Return when the current contiguous lighting state began.""" + initialize_database(database_path) + with _connect(database_path) as connection: + rows = connection.execute( + """ + SELECT captured_at, lighting_state + FROM captures + WHERE status = 'success' + AND lighting_state IS NOT NULL + ORDER BY captured_at DESC, id DESC + LIMIT 500 + """ + ).fetchall() + + if not rows: + return None + + current_state = rows[0][1] + started_at = rows[0][0] + for captured_at, state in rows[1:]: + if state != current_state: + break + started_at = captured_at + return str(started_at) + + def get_capture_record(database_path: Path, capture_id: int) -> WebCaptureRecord | None: initialize_database(database_path) - with sqlite3.connect(database_path) as connection: + with _connect(database_path) as connection: row = connection.execute( """SELECT id, captured_at, image_path, status, file_size, nas_synced, error_message, mean_brightness, median_brightness, dark_ratio, diff --git a/src/lightwatch/database_maintenance.py b/src/lightwatch/database_maintenance.py index 7ef58bb..bcf36a2 100644 --- a/src/lightwatch/database_maintenance.py +++ b/src/lightwatch/database_maintenance.py @@ -3,7 +3,8 @@ import argparse import sqlite3 from pathlib import Path -from .config import DEFAULT_CONFIG_PATH, load_config + +from .config import load_config from .database import ( DATABASE_SCHEMA_VERSION, analyze_database, @@ -54,7 +55,7 @@ } -def _database_path(config_path: Path) -> Path: +def _database_path(config_path: Path | None) -> Path: config = load_config(config_path) return Path(str(config["database"]["path"])) @@ -88,7 +89,7 @@ def _parse_args() -> argparse.Namespace: parser.add_argument( "--config", type=Path, - default=DEFAULT_CONFIG_PATH, + default=None, help="Path to the TOML configuration file.", ) parser.add_argument( diff --git a/src/lightwatch/health.py b/src/lightwatch/health.py index dca11ee..90dded7 100644 --- a/src/lightwatch/health.py +++ b/src/lightwatch/health.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Literal -from .config import DEFAULT_CONFIG_PATH, get_operational_settings, load_config +from .config import get_operational_settings, load_config from .database import ( count_unsynced_successful_captures, get_latest_successful_capture, @@ -256,7 +256,7 @@ def overall_status( return "healthy" -def run_health_check(config_path: Path = DEFAULT_CONFIG_PATH) -> HealthResult: +def run_health_check(config_path: Path | None = None) -> HealthResult: config = load_config(config_path) operations = get_operational_settings(config) database_path = Path(config["database"]["path"]) @@ -362,13 +362,11 @@ def exit_code(status: str) -> int: def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Check lightwatch operational health." - ) + parser = argparse.ArgumentParser(description="Check lightwatch operational health.") parser.add_argument( "--config", type=Path, - default=DEFAULT_CONFIG_PATH, + default=None, help="Path to the TOML configuration file.", ) parser.add_argument( diff --git a/src/lightwatch/nas_sync_state.py b/src/lightwatch/nas_sync_state.py index b54dfb1..e83fb88 100644 --- a/src/lightwatch/nas_sync_state.py +++ b/src/lightwatch/nas_sync_state.py @@ -2,7 +2,7 @@ import argparse import json -from dataclasses import dataclass, asdict +from dataclasses import asdict, dataclass from datetime import datetime from pathlib import Path from typing import Literal diff --git a/src/lightwatch/notification.py b/src/lightwatch/notification.py index 50b31ac..6dada23 100644 --- a/src/lightwatch/notification.py +++ b/src/lightwatch/notification.py @@ -69,9 +69,7 @@ def notify_dark_transition( def main() -> int: - parser = argparse.ArgumentParser( - description="Test lightwatch Slack notifications." - ) + parser = argparse.ArgumentParser(description="Test lightwatch Slack notifications.") parser.add_argument( "--previous-state", help="Previous lighting state.", diff --git a/src/lightwatch/report.py b/src/lightwatch/report.py index 375f87b..bd2f3b8 100644 --- a/src/lightwatch/report.py +++ b/src/lightwatch/report.py @@ -1,11 +1,11 @@ from __future__ import annotations import argparse -import tomllib from dataclasses import dataclass from datetime import datetime from pathlib import Path +from .config import load_config, resolve_config_path from .database import ( DailyCaptureSummary, LatestCapture, @@ -20,7 +20,6 @@ PROJECT_ROOT = Path("/opt/lightwatch") DATABASE_PATH = PROJECT_ROOT / "data" / "timelapse.db" -CONFIG_PATH = PROJECT_ROOT / "config" / "default.toml" DEFAULT_TEMP_DIR = Path("/dev/shm/lightwatch") @@ -30,12 +29,11 @@ class ScheduledReport: image_path: Path | None -def resolve_latest_observation_dir(config_path: Path = CONFIG_PATH) -> Path: +def resolve_latest_observation_dir(config_path: Path | None = None) -> Path: try: - with config_path.open("rb") as file: - config = tomllib.load(file) + config = load_config(config_path) temp_dir = Path(config["storage"].get("temp_dir", str(DEFAULT_TEMP_DIR))) - except (OSError, KeyError, TypeError, tomllib.TOMLDecodeError): + except (OSError, KeyError, TypeError): temp_dir = DEFAULT_TEMP_DIR return temp_dir / "latest-observation" @@ -184,6 +182,12 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Send the LightWatch scheduled Slack report." ) + parser.add_argument( + "--config", + type=Path, + default=None, + help="Path to the TOML configuration file.", + ) parser.add_argument( "--dry-run", action="store_true", @@ -194,7 +198,12 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() - report = build_scheduled_report() + config_path = resolve_config_path(args.config) + config = load_config(config_path) + report = build_scheduled_report( + Path(config["database"]["path"]), + latest_observation_dir=resolve_latest_observation_dir(config_path), + ) if report is None: print("Scheduled report skipped: no saved capture found.") diff --git a/src/lightwatch/sample_data.py b/src/lightwatch/sample_data.py new file mode 100644 index 0000000..12d3894 --- /dev/null +++ b/src/lightwatch/sample_data.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import argparse +import base64 +import shutil +import sqlite3 +from contextlib import closing +from dataclasses import dataclass +from datetime import date, datetime, time, timedelta +from pathlib import Path + +from .database import record_capture +from .nas_sync_state import NasSyncState, save_nas_sync_state + +DEFAULT_OUTPUT_DIR = Path(".local/web-preview") +_SAMPLE_JPEG = base64.b64decode( + "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPyF//9oADAMBAAIAAwAAABAf/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPxB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPxB//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxB//9k=" +) + + +@dataclass(frozen=True) +class SampleDataResult: + output_dir: Path + database_path: Path + image_dir: Path + capture_count: int + saved_image_count: int + + +def _reason(state: str, index: int) -> str: + values = { + "BRIGHT": (142.4, 139, 0.08, 0.34), + "PARTIAL": (91.6, 84, 0.31, 0.08), + "DIM": (58.3, 51, 0.62, 0.02), + "DARK": (19.8, 12, 0.91, 0.01), + }[state] + mean, median, dark_ratio, bright_ratio = values + prefix = { + "BRIGHT": "required lighting ROIs matched", + "PARTIAL": "partial lighting thresholds matched", + "DIM": "dim lighting range matched", + "DARK": "dark thresholds matched", + }[state] + return ( + f"{prefix}: left_ceiling(" + f"mean={mean - 8 + index % 3:.2f}, dark={dark_ratio:.4f}), " + f"center_ceiling(mean={mean + 3:.2f}, dark={max(dark_ratio - 0.03, 0):.4f}), " + f"right_ceiling(mean={mean + 11:.2f}, dark={max(dark_ratio - 0.05, 0):.4f}), " + f"global(mean={mean:.2f}, median={median}, dark={dark_ratio:.4f}, " + f"bright={bright_ratio:.4f}); sample observation for responsive detail display" + ) + + +def create_sample_data( + output_dir: Path = DEFAULT_OUTPUT_DIR, + *, + target_date: date | None = None, + force: bool = False, +) -> SampleDataResult: + output_dir = output_dir.resolve() + database_path = output_dir / "timelapse.db" + image_dir = output_dir / "images" + state_dir = output_dir / "state" + + if output_dir.exists(): + if not force: + raise FileExistsError( + f"Sample output already exists: {output_dir}. " + "Use --force to replace it." + ) + shutil.rmtree(output_dir) + + output_dir.mkdir(parents=True) + image_dir.mkdir() + state_dir.mkdir() + + sample_date = target_date or date.today() + states = ( + ["DARK"] * 4 + + ["DIM"] * 3 + + ["PARTIAL"] * 3 + + ["BRIGHT"] * 6 + + ["PARTIAL"] * 2 + + ["DIM"] * 2 + + ["DARK"] * 4 + ) + saved_indexes = {0, 4, 7, 10, 15, 18, 20, 23} + start = datetime.combine(sample_date, time(7, 0)) + + saved_count = 0 + for index, state in enumerate(states): + captured_at = start + timedelta(minutes=30 * index) + image_path = None + file_size = None + if index in saved_indexes: + image_path = image_dir / f"{captured_at:%Y%m%d_%H%M%S}_{state.lower()}.jpg" + image_path.write_bytes(_SAMPLE_JPEG) + file_size = image_path.stat().st_size + saved_count += 1 + + mean, median, dark_ratio, bright_ratio = { + "BRIGHT": (142.4, 139, 0.08, 0.34), + "PARTIAL": (91.6, 84, 0.31, 0.08), + "DIM": (58.3, 51, 0.62, 0.02), + "DARK": (19.8, 12, 0.91, 0.01), + }[state] + record_capture( + database_path, + captured_at=captured_at, + image_path=image_path, + status="success", + file_size=file_size, + lens_position=0.8, + mean_brightness=mean + (index % 3), + median_brightness=median, + dark_ratio=dark_ratio, + bright_ratio=bright_ratio, + lighting_state=state, + lighting_reason=_reason(state, index), + ) + + with closing(sqlite3.connect(database_path)) as connection, connection: + connection.execute("UPDATE captures SET nas_synced = 1") + connection.execute( + """ + UPDATE captures + SET nas_synced = 0 + WHERE id = ( + SELECT MAX(id) FROM captures WHERE image_path IS NOT NULL + ) + """ + ) + + latest_time = start + timedelta(minutes=30 * (len(states) - 1)) + save_nas_sync_state( + state_dir / "nas-sync-status.json", + NasSyncState( + result="success", + attempted_at=latest_time.isoformat(timespec="seconds"), + last_success_at=latest_time.isoformat(timespec="seconds"), + message="Local preview sample: no NAS connection was performed.", + synchronized_count=saved_count - 1, + ), + ) + + return SampleDataResult( + output_dir=output_dir, + database_path=database_path, + image_dir=image_dir, + capture_count=len(states), + saved_image_count=saved_count, + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create an isolated SQLite database for local Web UI preview." + ) + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT_DIR) + parser.add_argument("--date", type=date.fromisoformat) + parser.add_argument("--force", action="store_true") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + result = create_sample_data(args.output, target_date=args.date, force=args.force) + print(f"Sample data created: {result.output_dir}") + print(f"Database: {result.database_path}") + print(f"Images: {result.image_dir} ({result.saved_image_count})") + print(f"Captures: {result.capture_count}") + return 0 diff --git a/src/lightwatch/web.py b/src/lightwatch/web.py index 45b78e5..4d4348e 100644 --- a/src/lightwatch/web.py +++ b/src/lightwatch/web.py @@ -2,7 +2,7 @@ import subprocess from dataclasses import asdict -from datetime import date +from datetime import date, datetime from pathlib import Path from typing import Any from urllib.parse import quote @@ -11,10 +11,11 @@ from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles -from .config import DEFAULT_CONFIG_PATH, load_config +from .config import load_config, resolve_config_path from .database import ( count_unsynced_successful_captures, get_capture_record, + get_current_lighting_state_started_at, get_latest_saved_capture, get_latest_successful_capture, list_capture_records, @@ -32,6 +33,7 @@ "lightwatch-report.timer", "lightwatch-cleanup.timer", ) +ALLOWED_LIGHTING_STATES = {"BRIGHT", "PARTIAL", "DIM", "DARK"} def _payload(record: Any) -> dict[str, Any]: @@ -74,11 +76,34 @@ def _timer(unit: str) -> dict[str, Any]: } +def _timeline(records: list[Any], target_date: date) -> dict[str, Any]: + ordered = sorted(records, key=lambda item: (item.captured_at, item.capture_id)) + segments: list[dict[str, Any]] = [] + for record in ordered: + state = record.lighting_state or "UNKNOWN" + if not segments or segments[-1]["state"] != state: + segments.append( + { + "state": state, + "started_at": record.captured_at, + "ended_at": record.captured_at, + "samples": 1, + } + ) + else: + segments[-1]["ended_at"] = record.captured_at + segments[-1]["samples"] += 1 + return {"date": target_date.isoformat(), "segments": segments} + + def create_app( - *, config_path: Path = DEFAULT_CONFIG_PATH, frontend_dist: Path = DIST + *, config_path: Path | None = None, frontend_dist: Path = DIST ) -> FastAPI: + resolved_config_path = resolve_config_path(config_path) + load_config(resolved_config_path) + app = FastAPI(title="LightWatch", version=get_version()) - app.state.config_path = config_path + app.state.config_path = resolved_config_path def paths() -> tuple[Path, Path]: config = load_config(app.state.config_path) @@ -92,8 +117,16 @@ def status() -> dict[str, Any]: latest = get_latest_successful_capture(database) saved = get_latest_saved_capture(database) nas = load_nas_sync_state_result(state / "nas-sync-status.json").state + latest_record = ( + get_capture_record(database, latest.capture_id) + if latest and latest.capture_id is not None + else None + ) return { - "latest_observation": asdict(latest) if latest else None, + "version": get_version(), + "server_time": datetime.now().astimezone().isoformat(timespec="seconds"), + "state_started_at": get_current_lighting_state_started_at(database), + "latest_observation": _payload(latest_record) if latest_record else None, "latest_image": ( { **asdict(saved), @@ -180,24 +213,42 @@ def observations( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), date_value: date | None = Query(None, alias="date"), + lighting_state: str | None = Query(None), + saved_only: bool = Query(False), ) -> dict[str, Any]: + if lighting_state is not None: + lighting_state = lighting_state.upper() + if lighting_state not in ALLOWED_LIGHTING_STATES: + raise HTTPException(422, "Unsupported lighting state") database, _ = paths() return { "items": [ _payload(r) for r in list_capture_records( - database, limit=limit, offset=offset, target_date=date_value + database, + limit=limit, + offset=offset, + target_date=date_value, + saved_only=saved_only, + lighting_state=lighting_state, ) ] } + @app.get("/api/timeline") + def timeline(date_value: date | None = Query(None, alias="date")) -> dict[str, Any]: + target_date = date_value or date.today() + database, _ = paths() + records = list_capture_records(database, limit=500, target_date=target_date) + return _timeline(records, target_date) + @app.get("/api/health") def health() -> dict[str, Any]: return asdict(run_health_check(app.state.config_path)) @app.get("/api/system") def system() -> dict[str, Any]: - return {"timers": [_timer(unit) for unit in TIMERS]} + return {"version": get_version(), "timers": [_timer(unit) for unit in TIMERS]} if frontend_dist.is_dir(): app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="frontend") diff --git a/systemd/README.md b/systemd/README.md deleted file mode 100644 index 00e8416..0000000 --- a/systemd/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# systemd deployment - -The unit files in this directory are examples for a Raspberry Pi installation at `/opt/lightwatch`, running as user `pi`. - -## Before installation - -1. Install LightWatch under `/opt/lightwatch`. -2. Create the Python virtual environment and install `requirements.txt`. -3. Review `config/default.toml` or set `LIGHTWATCH_CONFIG` in `/etc/lightwatch.env`. -4. Configure passwordless SSH access to the NAS when NAS synchronization is enabled. -5. Build the frontend on a development machine and copy `frontend/dist/` to the Pi. -6. Confirm that `rpicam-still`, `rpicam-vid`, `rsync`, `sqlite3`, and `flock` are installed. - -## Install units - -```bash -cd /opt/lightwatch -sudo cp systemd/lightwatch-*.service systemd/lightwatch-*.timer /etc/systemd/system/ -sudo systemctl daemon-reload -``` - -Enable the timers and web service: - -```bash -sudo systemctl enable --now \ - lightwatch-capture.timer \ - lightwatch-nas-sync.timer \ - lightwatch-health.timer \ - lightwatch-report.timer \ - lightwatch-cleanup.timer \ - lightwatch-web.service -``` - -## Check status - -```bash -systemctl list-timers 'lightwatch-*' -systemctl status lightwatch-web.service -journalctl -u lightwatch-capture.service -n 100 --no-pager -``` - -## Manual execution - -```bash -sudo systemctl start lightwatch-capture.service -sudo systemctl start lightwatch-nas-sync.service -sudo systemctl start lightwatch-health.service -sudo systemctl start lightwatch-report.service -sudo systemctl start lightwatch-cleanup.service -``` - -## Remove units - -```bash -sudo systemctl disable --now \ - lightwatch-capture.timer \ - lightwatch-nas-sync.timer \ - lightwatch-health.timer \ - lightwatch-report.timer \ - lightwatch-cleanup.timer \ - lightwatch-web.service - -sudo rm -f /etc/systemd/system/lightwatch-*.service /etc/systemd/system/lightwatch-*.timer -sudo systemctl daemon-reload -``` - -The schedules are examples. Keep the capture timer interval consistent with `operations.capture_interval_minutes` in the configuration file. diff --git a/systemd/lightwatch-capture.service b/systemd/lightwatch-capture.service index 4f93b2c..1dfd800 100644 --- a/systemd/lightwatch-capture.service +++ b/systemd/lightwatch-capture.service @@ -1,5 +1,5 @@ [Unit] -Description=LightWatch Capture +Description=Business Timelapse Capture After=network-online.target Wants=network-online.target @@ -8,8 +8,8 @@ Type=oneshot User=pi Group=pi WorkingDirectory=/opt/lightwatch -EnvironmentFile=-/etc/lightwatch.env -ExecStart=/bin/bash /opt/lightwatch/scripts/run-with-camera-lock.sh /opt/lightwatch/.venv/bin/python -m lightwatch.capture +EnvironmentFile=/etc/lightwatch.env +ExecStart=/bin/bash /opt/lightwatch/scripts/run-with-camera-lock.sh /opt/lightwatch/.venv/bin/python -m src.lightwatch.capture [Install] WantedBy=multi-user.target diff --git a/systemd/lightwatch-cleanup.service b/systemd/lightwatch-cleanup.service index 5101086..3a411d3 100644 --- a/systemd/lightwatch-cleanup.service +++ b/systemd/lightwatch-cleanup.service @@ -1,5 +1,5 @@ [Unit] -Description=LightWatch Local Image Cleanup +Description=Business Timelapse Local Image Cleanup After=lightwatch-nas-sync.service [Service] @@ -7,7 +7,8 @@ Type=oneshot User=pi Group=pi WorkingDirectory=/opt/lightwatch -ExecStart=/opt/lightwatch/.venv/bin/python -m lightwatch.cleanup +EnvironmentFile=/etc/lightwatch.env +ExecStart=/opt/lightwatch/.venv/bin/python -m src.lightwatch.cleanup [Install] WantedBy=multi-user.target diff --git a/systemd/lightwatch-health.service b/systemd/lightwatch-health.service index c4a79d8..878868a 100644 --- a/systemd/lightwatch-health.service +++ b/systemd/lightwatch-health.service @@ -1,5 +1,5 @@ [Unit] -Description=LightWatch Health Check +Description=Business Timelapse Health Check After=local-fs.target network-online.target Wants=network-online.target @@ -8,8 +8,8 @@ Type=oneshot User=pi Group=pi WorkingDirectory=/opt/lightwatch -EnvironmentFile=-/etc/lightwatch.env -ExecStart=/opt/lightwatch/.venv/bin/python -m lightwatch.health +EnvironmentFile=/etc/lightwatch.env +ExecStart=/opt/lightwatch/.venv/bin/python -m src.lightwatch.health [Install] WantedBy=multi-user.target diff --git a/systemd/lightwatch-nas-sync.service b/systemd/lightwatch-nas-sync.service index 62e915c..c1482ea 100644 --- a/systemd/lightwatch-nas-sync.service +++ b/systemd/lightwatch-nas-sync.service @@ -1,5 +1,5 @@ [Unit] -Description=LightWatch NAS Sync +Description=Business Timelapse NAS Sync Wants=network-online.target After=network-online.target @@ -9,6 +9,7 @@ User=pi Group=pi Environment=HOME=/home/pi WorkingDirectory=/opt/lightwatch +EnvironmentFile=/etc/lightwatch.env ExecStart=/bin/bash /opt/lightwatch/scripts/sync-to-nas.sh TimeoutStartSec=10min diff --git a/systemd/lightwatch-report.service b/systemd/lightwatch-report.service index 01939b8..765fd2b 100644 --- a/systemd/lightwatch-report.service +++ b/systemd/lightwatch-report.service @@ -1,5 +1,5 @@ [Unit] -Description=LightWatch Scheduled Slack Report +Description=Business Timelapse Scheduled Slack Report After=network-online.target Wants=network-online.target @@ -8,8 +8,8 @@ Type=oneshot User=pi Group=pi WorkingDirectory=/opt/lightwatch -EnvironmentFile=-/etc/lightwatch.env -ExecStart=/opt/lightwatch/.venv/bin/python -m lightwatch.report +EnvironmentFile=/etc/lightwatch.env +ExecStart=/opt/lightwatch/.venv/bin/python -m src.lightwatch.report [Install] WantedBy=multi-user.target diff --git a/systemd/lightwatch-web.service b/systemd/lightwatch-web.service index 91824c4..efc0805 100644 --- a/systemd/lightwatch-web.service +++ b/systemd/lightwatch-web.service @@ -1,5 +1,5 @@ [Unit] -Description=LightWatch Web UI +Description=Business Timelapse Web UI After=network-online.target local-fs.target Wants=network-online.target @@ -8,8 +8,8 @@ Type=simple User=pi Group=pi WorkingDirectory=/opt/lightwatch -EnvironmentFile=-/etc/lightwatch.env -ExecStart=/opt/lightwatch/.venv/bin/python -m uvicorn lightwatch.web:app --host 0.0.0.0 --port 8000 +EnvironmentFile=/etc/lightwatch.env +ExecStart=/opt/lightwatch/.venv/bin/python -m uvicorn src.lightwatch.web:app --host 0.0.0.0 --port 8000 Restart=on-failure RestartSec=5 diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 957536b..5bf2e36 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -3,7 +3,7 @@ import pytest from PIL import Image -from lightwatch.analysis import analyze_image +from src.lightwatch.analysis import analyze_image def test_analyze_image(tmp_path: Path) -> None: @@ -36,7 +36,7 @@ def test_analyze_image_rejects_invalid_thresholds(tmp_path: Path) -> None: def test_analyze_image_with_rois(tmp_path: Path) -> None: from PIL import ImageDraw - from lightwatch.analysis import LightingRoiRule, analyze_image_with_rois + from src.lightwatch.analysis import LightingRoiRule, analyze_image_with_rois image_path = tmp_path / "roi.jpg" image = Image.new("L", (30, 10), 100) diff --git a/tests/test_capture_retention.py b/tests/test_capture_retention.py index 361df05..fbb63ce 100644 --- a/tests/test_capture_retention.py +++ b/tests/test_capture_retention.py @@ -1,8 +1,8 @@ from datetime import datetime from pathlib import Path -from lightwatch.capture import publish_image, should_retain_capture -from lightwatch.latest_observation import ( +from src.lightwatch.capture import publish_image, should_retain_capture +from src.lightwatch.latest_observation import ( publish_latest_observation, read_latest_observation, ) diff --git a/tests/test_cleanup.py b/tests/test_cleanup.py index cf465d6..bf815ce 100644 --- a/tests/test_cleanup.py +++ b/tests/test_cleanup.py @@ -2,8 +2,8 @@ from datetime import datetime from pathlib import Path -from lightwatch.cleanup import cleanup_local_images -from lightwatch.database import record_capture +from src.lightwatch.cleanup import cleanup_local_images +from src.lightwatch.database import record_capture def add_capture( diff --git a/tests/test_config.py b/tests/test_config.py index b3ab777..1dc1828 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,8 +1,9 @@ +import re from pathlib import Path import pytest -from lightwatch.config import ( +from src.lightwatch.config import ( get_nas_settings, get_operational_settings, load_config, @@ -50,3 +51,48 @@ def test_invalid_operational_values_are_rejected() -> None: def test_relative_nas_destination_is_rejected() -> None: with pytest.raises(ValueError): get_nas_settings({"nas": {"destination_dir": "relative/path"}}) + + +def test_resolve_config_path_uses_environment_variable( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from src.lightwatch.config import CONFIG_PATH_ENV_VAR, resolve_config_path + + config_path = tmp_path / "local.toml" + monkeypatch.setenv(CONFIG_PATH_ENV_VAR, str(config_path)) + + assert resolve_config_path() == config_path.resolve() + + +def test_explicit_config_path_takes_precedence_over_environment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from src.lightwatch.config import CONFIG_PATH_ENV_VAR, resolve_config_path + + explicit = tmp_path / "explicit.toml" + monkeypatch.setenv(CONFIG_PATH_ENV_VAR, str(tmp_path / "environment.toml")) + + assert resolve_config_path(explicit) == explicit.resolve() + + +def test_load_config_reports_missing_resolved_path(tmp_path: Path) -> None: + missing = tmp_path / "missing.toml" + + with pytest.raises(FileNotFoundError, match=re.escape(str(missing))): + load_config(missing) + + +def test_load_config_uses_environment_variable( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from src.lightwatch.config import CONFIG_PATH_ENV_VAR + + config_path = tmp_path / "production.toml" + config_path.write_text( + "[operations]\ncapture_interval_minutes = 9\n", encoding="utf-8" + ) + monkeypatch.setenv(CONFIG_PATH_ENV_VAR, str(config_path)) + + config = load_config() + + assert config["operations"]["capture_interval_minutes"] == 9 diff --git a/tests/test_database.py b/tests/test_database.py index eeb1583..6652df6 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -2,7 +2,7 @@ from datetime import datetime from pathlib import Path -from lightwatch.database import get_capture_by_id, record_capture +from src.lightwatch.database import get_capture_by_id, record_capture def test_record_capture_with_brightness_metrics(tmp_path: Path) -> None: diff --git a/tests/test_database_indexes.py b/tests/test_database_indexes.py index c416473..ff29c95 100644 --- a/tests/test_database_indexes.py +++ b/tests/test_database_indexes.py @@ -4,7 +4,7 @@ from datetime import datetime from pathlib import Path -from lightwatch.database import ( +from src.lightwatch.database import ( DATABASE_SCHEMA_VERSION, get_capture_index_names, get_daily_capture_summary, diff --git a/tests/test_database_lighting.py b/tests/test_database_lighting.py index 4a7b926..b9c7368 100644 --- a/tests/test_database_lighting.py +++ b/tests/test_database_lighting.py @@ -2,7 +2,7 @@ from datetime import datetime from pathlib import Path -from lightwatch.database import ( +from src.lightwatch.database import ( initialize_database, record_capture, ) diff --git a/tests/test_database_transition.py b/tests/test_database_transition.py index 0840801..8dc5a48 100644 --- a/tests/test_database_transition.py +++ b/tests/test_database_transition.py @@ -1,7 +1,7 @@ from datetime import datetime from pathlib import Path -from lightwatch.database import ( +from src.lightwatch.database import ( get_latest_lighting_state, record_capture, ) diff --git a/tests/test_health.py b/tests/test_health.py index f1a7f17..f1f6a0d 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -1,5 +1,5 @@ def test_build_health_summary_formats_nas_failure_in_japanese() -> None: - from lightwatch.health import ( + from src.lightwatch.health import ( HealthResult, NasSyncCheck, StorageCheck, @@ -32,7 +32,7 @@ def test_build_health_summary_formats_nas_failure_in_japanese() -> None: def test_build_health_summary_formats_nas_success() -> None: - from lightwatch.health import ( + from src.lightwatch.health import ( HealthResult, NasSyncCheck, StorageCheck, @@ -65,8 +65,8 @@ def test_build_health_summary_formats_nas_success() -> None: def test_check_capture_uses_latest_database_observation(tmp_path) -> None: from datetime import datetime - from lightwatch.database import record_capture - from lightwatch.health import check_capture + from src.lightwatch.database import record_capture + from src.lightwatch.health import check_capture database_path = tmp_path / "timelapse.db" record_capture( @@ -91,8 +91,8 @@ def test_check_capture_uses_latest_database_observation(tmp_path) -> None: def test_check_capture_warns_when_five_minute_capture_is_stale(tmp_path) -> None: from datetime import datetime - from lightwatch.database import record_capture - from lightwatch.health import check_capture + from src.lightwatch.database import record_capture + from src.lightwatch.health import check_capture database_path = tmp_path / "timelapse.db" record_capture( @@ -114,7 +114,7 @@ def test_check_capture_warns_when_five_minute_capture_is_stale(tmp_path) -> None def test_capture_age_thresholds_follow_capture_interval() -> None: - from lightwatch.health import capture_age_thresholds + from src.lightwatch.health import capture_age_thresholds assert capture_age_thresholds(5) == (12, 25) assert capture_age_thresholds(15) == (32, 75) @@ -123,8 +123,8 @@ def test_capture_age_thresholds_follow_capture_interval() -> None: def test_check_capture_uses_configured_capture_interval(tmp_path) -> None: from datetime import datetime - from lightwatch.database import record_capture - from lightwatch.health import check_capture + from src.lightwatch.database import record_capture + from src.lightwatch.health import check_capture database_path = tmp_path / "timelapse.db" record_capture( diff --git a/tests/test_health_notification.py b/tests/test_health_notification.py index bb47c33..fbcf6e2 100644 --- a/tests/test_health_notification.py +++ b/tests/test_health_notification.py @@ -1,7 +1,7 @@ import json from pathlib import Path -from lightwatch.health_notification import ( +from src.lightwatch.health_notification import ( build_health_message, decide_health_notification, load_previous_health_status, diff --git a/tests/test_lighting_classification.py b/tests/test_lighting_classification.py index 2867c73..06e3f17 100644 --- a/tests/test_lighting_classification.py +++ b/tests/test_lighting_classification.py @@ -1,6 +1,6 @@ import pytest -from lightwatch.analysis import ( +from src.lightwatch.analysis import ( ImageMetrics, LightingState, LightingThresholds, @@ -155,7 +155,7 @@ def test_classify_observed_partial_lighting_as_partial( def test_classify_bright_when_all_ceiling_rois_match( thresholds: LightingThresholds, ) -> None: - from lightwatch.analysis import LightingRoiRule, classify_lighting_with_rois + from src.lightwatch.analysis import LightingRoiRule, classify_lighting_with_rois rules = ( LightingRoiRule("left", 0, 0, 10, 10, 125.0, 0.10), @@ -172,13 +172,51 @@ def test_classify_bright_when_all_ceiling_rois_match( result = classify_lighting_with_rois(global_metrics, roi_metrics, thresholds, rules) assert result.state is LightingState.BRIGHT - assert "all lighting ROIs matched" in result.reason + assert "all required lighting ROIs matched" in result.reason + + +def test_classify_left_lighting_off_as_bright_when_left_is_optional( + thresholds: LightingThresholds, +) -> None: + from src.lightwatch.analysis import LightingRoiRule, classify_lighting_with_rois + + rules = ( + LightingRoiRule("left", 0, 0, 10, 10, 125.0, 0.10, required_for_bright=False), + LightingRoiRule("center", 10, 0, 20, 10, 125.0, 0.10), + LightingRoiRule("right", 20, 0, 30, 10, 180.0, 0.10), + ) + global_metrics = ImageMetrics(108.0, 119, 0.28, 0.078) + roi_metrics = { + "left": ImageMetrics(117.0, 120, 0.17, 0.08), + "center": ImageMetrics(140.0, 145, 0.03, 0.05), + "right": ImageMetrics(192.0, 200, 0.00, 0.30), + } + + result = classify_lighting_with_rois(global_metrics, roi_metrics, thresholds, rules) + + assert result.state is LightingState.BRIGHT + assert "left=off" in result.reason + assert "required=no" in result.reason + + +def test_classify_rejects_no_required_bright_rois( + thresholds: LightingThresholds, +) -> None: + from src.lightwatch.analysis import LightingRoiRule, classify_lighting_with_rois + + rules = ( + LightingRoiRule("left", 0, 0, 10, 10, 125.0, 0.10, required_for_bright=False), + ) + metrics = ImageMetrics(108.0, 119, 0.28, 0.078) + + with pytest.raises(ValueError, match="At least one lighting ROI"): + classify_lighting_with_rois(metrics, {"left": metrics}, thresholds, rules) def test_classify_right_side_lighting_off_as_partial( thresholds: LightingThresholds, ) -> None: - from lightwatch.analysis import LightingRoiRule, classify_lighting_with_rois + from src.lightwatch.analysis import LightingRoiRule, classify_lighting_with_rois rules = ( LightingRoiRule("left", 0, 0, 10, 10, 125.0, 0.10), @@ -201,7 +239,7 @@ def test_classify_right_side_lighting_off_as_partial( def test_classify_left_back_lighting_off_as_partial( thresholds: LightingThresholds, ) -> None: - from lightwatch.analysis import LightingRoiRule, classify_lighting_with_rois + from src.lightwatch.analysis import LightingRoiRule, classify_lighting_with_rois rules = ( LightingRoiRule("left", 0, 0, 10, 10, 125.0, 0.10), @@ -222,8 +260,43 @@ def test_classify_left_back_lighting_off_as_partial( assert "center=off" in result.reason +def test_build_lighting_roi_rules_reads_required_for_bright() -> None: + from src.lightwatch.capture import build_lighting_roi_rules + + config = { + "lighting": { + "rois": [ + { + "name": "left", + "x1": 0, + "y1": 0, + "x2": 10, + "y2": 10, + "min_mean": 125.0, + "max_dark_ratio": 0.10, + "required_for_bright": False, + }, + { + "name": "center", + "x1": 10, + "y1": 0, + "x2": 20, + "y2": 10, + "min_mean": 125.0, + "max_dark_ratio": 0.10, + }, + ] + } + } + + rules = build_lighting_roi_rules(config) + + assert rules[0].required_for_bright is False + assert rules[1].required_for_bright is True + + def test_roi_rule_rejects_invalid_bounds() -> None: - from lightwatch.analysis import LightingRoiRule + from src.lightwatch.analysis import LightingRoiRule with pytest.raises(ValueError): LightingRoiRule("invalid", 10, 0, 10, 10, 100.0, 0.1) diff --git a/tests/test_nas_sync_monitoring.py b/tests/test_nas_sync_monitoring.py index cb69e7d..dae86a7 100644 --- a/tests/test_nas_sync_monitoring.py +++ b/tests/test_nas_sync_monitoring.py @@ -1,12 +1,12 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path -from lightwatch.database import ( +from src.lightwatch.database import ( count_unsynced_successful_captures, record_capture, ) -from lightwatch.health import check_nas_sync -from lightwatch.nas_sync_state import ( +from src.lightwatch.health import check_nas_sync +from src.lightwatch.nas_sync_state import ( load_nas_sync_state, record_nas_sync_result, ) @@ -36,7 +36,7 @@ def test_count_unsynced_successful_captures(tmp_path: Path) -> None: def test_record_failure_preserves_last_success(tmp_path: Path) -> None: state_path = tmp_path / "nas-sync-status.json" - now = datetime.now(timezone.utc).astimezone() + now = datetime.now(UTC).astimezone() record_nas_sync_result(state_path, "success", attempted_at=now) record_nas_sync_result( state_path, @@ -54,7 +54,7 @@ def test_record_failure_preserves_last_success(tmp_path: Path) -> None: def test_failed_sync_is_error(tmp_path: Path) -> None: database_path = tmp_path / "timelapse.db" state_path = tmp_path / "nas-sync-status.json" - now = datetime.now(timezone.utc).astimezone() + now = datetime.now(UTC).astimezone() record_nas_sync_result( state_path, "failure", attempted_at=now, message="rsync exit code 23" ) @@ -69,7 +69,7 @@ def test_failed_sync_is_error(tmp_path: Path) -> None: def test_multiple_unsynced_captures_are_warning(tmp_path: Path) -> None: database_path = tmp_path / "timelapse.db" state_path = tmp_path / "nas-sync-status.json" - now = datetime.now(timezone.utc).astimezone() + now = datetime.now(UTC).astimezone() add_capture(database_path) record_capture( database_path, @@ -91,7 +91,7 @@ def test_multiple_unsynced_captures_are_warning(tmp_path: Path) -> None: def test_stale_last_attempt_is_warning(tmp_path: Path) -> None: database_path = tmp_path / "timelapse.db" state_path = tmp_path / "nas-sync-status.json" - now = datetime(2026, 7, 19, 20, 0, tzinfo=timezone.utc).astimezone() + now = datetime(2026, 7, 19, 20, 0, tzinfo=UTC).astimezone() record_nas_sync_result( state_path, "success", @@ -113,7 +113,7 @@ def test_stale_last_attempt_is_warning(tmp_path: Path) -> None: def test_stale_last_attempt_is_error(tmp_path: Path) -> None: database_path = tmp_path / "timelapse.db" state_path = tmp_path / "nas-sync-status.json" - now = datetime(2026, 7, 19, 20, 0, tzinfo=timezone.utc).astimezone() + now = datetime(2026, 7, 19, 20, 0, tzinfo=UTC).astimezone() record_nas_sync_result( state_path, "success", @@ -135,7 +135,7 @@ def test_stale_last_attempt_is_error(tmp_path: Path) -> None: def test_stale_last_success_is_error(tmp_path: Path) -> None: database_path = tmp_path / "timelapse.db" state_path = tmp_path / "nas-sync-status.json" - now = datetime(2026, 7, 19, 20, 0, tzinfo=timezone.utc).astimezone() + now = datetime(2026, 7, 19, 20, 0, tzinfo=UTC).astimezone() state_path.write_text( """{ "result": "success", @@ -163,7 +163,7 @@ def test_stale_last_success_is_error(tmp_path: Path) -> None: def test_missing_state_file_is_error(tmp_path: Path) -> None: result = check_nas_sync( tmp_path / "marker", - now=datetime.now(timezone.utc).astimezone(), + now=datetime.now(UTC).astimezone(), database_path=tmp_path / "timelapse.db", state_path=tmp_path / "missing.json", ) @@ -180,7 +180,7 @@ def test_invalid_state_file_is_error(tmp_path: Path) -> None: result = check_nas_sync( tmp_path / "marker", - now=datetime.now(timezone.utc).astimezone(), + now=datetime.now(UTC).astimezone(), database_path=tmp_path / "timelapse.db", state_path=state_path, ) diff --git a/tests/test_nas_sync_state.py b/tests/test_nas_sync_state.py index 2090938..a607ead 100644 --- a/tests/test_nas_sync_state.py +++ b/tests/test_nas_sync_state.py @@ -2,7 +2,7 @@ from datetime import datetime from pathlib import Path -from lightwatch.database import ( +from src.lightwatch.database import ( mark_existing_successful_captures_nas_synced, record_capture, ) diff --git a/tests/test_notification.py b/tests/test_notification.py index c1b6f53..ac9b89c 100644 --- a/tests/test_notification.py +++ b/tests/test_notification.py @@ -1,12 +1,12 @@ from datetime import datetime from pathlib import Path -from lightwatch.notification import ( +from src.lightwatch.notification import ( build_dark_message, notify_dark_transition, send_slack_message, ) -from lightwatch.slack_client import SlackResult +from src.lightwatch.slack_client import SlackResult def test_build_dark_message_uses_internal_state_labels() -> None: @@ -51,7 +51,7 @@ def fake_send(message: str, path: Path, **kwargs: object) -> SlackResult: return SlackResult(sent=True, message="sent") monkeypatch.setattr( - "lightwatch.notification.send_slack_image_message", + "src.lightwatch.notification.send_slack_image_message", fake_send, ) diff --git a/tests/test_reclassify_lighting.py b/tests/test_reclassify_lighting.py index 0dbd91a..4dbcfab 100644 --- a/tests/test_reclassify_lighting.py +++ b/tests/test_reclassify_lighting.py @@ -2,9 +2,9 @@ from datetime import date, datetime from pathlib import Path -from lightwatch.analysis import LightingState, LightingThresholds -from lightwatch.database import record_capture -from lightwatch.reclassify import reclassify_lighting_history +from src.lightwatch.analysis import LightingState, LightingThresholds +from src.lightwatch.database import record_capture +from src.lightwatch.reclassify import reclassify_lighting_history def thresholds() -> LightingThresholds: diff --git a/tests/test_reclassify_lighting_cli.py b/tests/test_reclassify_lighting_cli.py index 2ec6aab..9ca0eb5 100644 --- a/tests/test_reclassify_lighting_cli.py +++ b/tests/test_reclassify_lighting_cli.py @@ -4,8 +4,7 @@ from datetime import datetime from pathlib import Path -from lightwatch.database import record_capture - +from src.lightwatch.database import record_capture PROJECT_ROOT = Path(__file__).resolve().parents[1] SCRIPT_PATH = PROJECT_ROOT / "scripts" / "reclassify_lighting.py" diff --git a/tests/test_report.py b/tests/test_report.py index df51680..3ed990e 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -1,23 +1,23 @@ from datetime import datetime from pathlib import Path -from lightwatch.database import ( +from src.lightwatch.database import ( DailyCaptureSummary, LatestCapture, get_daily_capture_summary, get_latest_successful_capture, record_capture, ) -from lightwatch.health import ( +from src.lightwatch.health import ( HealthResult, NasSyncCheck, StorageCheck, TimeCheck, ) -from lightwatch.latest_observation import publish_latest_observation -from lightwatch.report import ( - build_scheduled_report, +from src.lightwatch.latest_observation import publish_latest_observation +from src.lightwatch.report import ( build_report_message, + build_scheduled_report, format_brightness, format_capture_time, format_ratio, @@ -174,7 +174,7 @@ def test_scheduled_report_uses_latest_observation_snapshot( ) monkeypatch.setattr( - "lightwatch.report.run_health_check", + "src.lightwatch.report.run_health_check", build_healthy_result, ) @@ -213,7 +213,7 @@ def test_scheduled_report_falls_back_to_latest_saved_capture( ) monkeypatch.setattr( - "lightwatch.report.run_health_check", + "src.lightwatch.report.run_health_check", build_healthy_result, ) diff --git a/tests/test_sample_data.py b/tests/test_sample_data.py new file mode 100644 index 0000000..8d1b52d --- /dev/null +++ b/tests/test_sample_data.py @@ -0,0 +1,61 @@ +from datetime import date +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from src.lightwatch.sample_data import create_sample_data +from src.lightwatch.web import create_app + + +def _write_preview_config(path: Path, output_dir: Path) -> None: + path.write_text( + f'''[storage]\ndata_dir="{(output_dir / "images").as_posix()}"\ntemp_dir="{(output_dir / "tmp").as_posix()}"\nstate_dir="{(output_dir / "state").as_posix()}"\n[database]\npath="{(output_dir / "timelapse.db").as_posix()}"\n''', + encoding="utf-8", + ) + + +def test_create_sample_data_supports_web_preview(tmp_path: Path) -> None: + output_dir = tmp_path / "preview" + result = create_sample_data(output_dir, target_date=date(2026, 7, 26), force=False) + config_path = tmp_path / "preview.toml" + _write_preview_config(config_path, output_dir) + + client = TestClient( + create_app(config_path=config_path, frontend_dist=tmp_path / "missing") + ) + observations = client.get("/api/observations?limit=100").json()["items"] + timeline = client.get("/api/timeline?date=2026-07-26").json()["segments"] + status = client.get("/api/status").json() + + assert result.capture_count == 24 + assert result.saved_image_count == 8 + assert {item["lighting_state"] for item in observations} == { + "BRIGHT", + "PARTIAL", + "DIM", + "DARK", + } + assert any(item["image_url"] is None for item in observations) + assert any(item["image_url"] is not None for item in observations) + assert any(len(item["lighting_reason"]) > 200 for item in observations) + assert {segment["state"] for segment in timeline} == { + "BRIGHT", + "PARTIAL", + "DIM", + "DARK", + } + assert status["latest_image"] is not None + assert status["unsynced_count"] == 1 + assert status["nas_sync"]["result"] == "success" + + +def test_create_sample_data_requires_force_to_replace(tmp_path: Path) -> None: + output_dir = tmp_path / "preview" + create_sample_data(output_dir, target_date=date(2026, 7, 26)) + + with pytest.raises(FileExistsError, match="--force"): + create_sample_data(output_dir, target_date=date(2026, 7, 26)) + + result = create_sample_data(output_dir, target_date=date(2026, 7, 26), force=True) + assert result.database_path.is_file() diff --git a/tests/test_slack_file.py b/tests/test_slack_file.py index e966235..3e02003 100644 --- a/tests/test_slack_file.py +++ b/tests/test_slack_file.py @@ -1,6 +1,6 @@ from pathlib import Path -from lightwatch.slack_file import send_slack_image_message +from src.lightwatch.slack_file import send_slack_image_message def test_missing_credentials_are_safe( @@ -27,7 +27,7 @@ def test_missing_image_uses_text_fallback( monkeypatch, ) -> None: monkeypatch.setattr( - "lightwatch.slack_file.send_slack_message", + "src.lightwatch.slack_file.send_slack_message", lambda *args, **kwargs: type( "Result", (), @@ -57,11 +57,11 @@ def test_upload_failure_uses_text_fallback( image_path.write_bytes(b"test") monkeypatch.setattr( - "lightwatch.slack_file._get_upload_url", + "src.lightwatch.slack_file._get_upload_url", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("upload URL error")), ) monkeypatch.setattr( - "lightwatch.slack_file.send_slack_message", + "src.lightwatch.slack_file.send_slack_message", lambda *args, **kwargs: type( "Result", (), diff --git a/tests/test_transition.py b/tests/test_transition.py index 033cf7c..6037b78 100644 --- a/tests/test_transition.py +++ b/tests/test_transition.py @@ -1,4 +1,4 @@ -from lightwatch.transition import should_notify_dark_transition +from src.lightwatch.transition import should_notify_dark_transition def test_notify_when_partial_changes_to_dark() -> None: diff --git a/tests/test_version.py b/tests/test_version.py index 6f2745a..2e24e7d 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,7 +2,7 @@ import pytest -from lightwatch.version import get_version +from src.lightwatch.version import get_version def test_get_version_reads_and_strips_value(tmp_path: Path) -> None: diff --git a/tests/test_web.py b/tests/test_web.py index 370f1a5..dfd9dfa 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,9 +1,11 @@ from datetime import datetime from pathlib import Path + from fastapi.testclient import TestClient -from lightwatch.database import record_capture -from lightwatch.version import get_version -from lightwatch.web import create_app + +from src.lightwatch.database import record_capture +from src.lightwatch.version import get_version +from src.lightwatch.web import create_app def config_file(tmp_path: Path) -> Path: @@ -63,3 +65,97 @@ def test_latest_image_returns_404_when_no_saved_image(tmp_path: Path) -> None: assert client.get("/api/status").json()["latest_image"] is None assert client.get("/api/images/latest").status_code == 404 + + +def test_status_exposes_version_state_duration_and_reason(tmp_path: Path) -> None: + config = config_file(tmp_path) + db = tmp_path / "data/timelapse.db" + record_capture( + db, + captured_at=datetime(2026, 7, 20, 11, 55), + image_path=None, + status="success", + file_size=None, + lens_position=0.8, + lighting_state="DARK", + lighting_reason="dark thresholds matched", + mean_brightness=20, + ) + record_capture( + db, + captured_at=datetime(2026, 7, 20, 12, 0), + image_path=None, + status="success", + file_size=None, + lens_position=0.8, + lighting_state="BRIGHT", + lighting_reason="required lighting ROIs matched", + mean_brightness=120, + ) + record_capture( + db, + captured_at=datetime(2026, 7, 20, 12, 5), + image_path=None, + status="success", + file_size=None, + lens_position=0.8, + lighting_state="BRIGHT", + lighting_reason="required lighting ROIs matched", + mean_brightness=121, + ) + + client = TestClient( + create_app(config_path=config, frontend_dist=tmp_path / "missing") + ) + payload = client.get("/api/status").json() + + assert payload["version"] == get_version() + assert payload["state_started_at"] == "2026-07-20T12:00:00" + assert payload["latest_observation"]["lighting_reason"] == ( + "required lighting ROIs matched" + ) + assert payload["server_time"] + + +def test_observation_filters_and_timeline(tmp_path: Path) -> None: + config = config_file(tmp_path) + db = tmp_path / "data/timelapse.db" + image = tmp_path / "data/images/saved.jpg" + image.write_bytes(b"jpeg") + for minute, state, image_path in ( + (0, "BRIGHT", image), + (5, "BRIGHT", None), + (10, "PARTIAL", None), + (15, "DARK", None), + ): + record_capture( + db, + captured_at=datetime(2026, 7, 20, 12, minute), + image_path=image_path, + status="success", + file_size=4 if image_path else None, + lens_position=0.8, + lighting_state=state, + lighting_reason=f"{state} reason", + mean_brightness=100, + ) + + client = TestClient( + create_app(config_path=config, frontend_dist=tmp_path / "missing") + ) + bright = client.get("/api/observations?lighting_state=BRIGHT&limit=20").json()[ + "items" + ] + saved = client.get("/api/observations?saved_only=true&limit=20").json()["items"] + timeline = client.get("/api/timeline?date=2026-07-20").json() + + assert len(bright) == 2 + assert {item["lighting_state"] for item in bright} == {"BRIGHT"} + assert len(saved) == 1 + assert [segment["state"] for segment in timeline["segments"]] == [ + "BRIGHT", + "PARTIAL", + "DARK", + ] + assert timeline["segments"][0]["samples"] == 2 + assert client.get("/api/observations?lighting_state=INVALID").status_code == 422 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fb4de38 --- /dev/null +++ b/uv.lock @@ -0,0 +1,656 @@ +version = 1 +revision = 3 +requires-python = ">=3.11, <3.14" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "lightwatch" +version = "1.1.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi" }, + { name = "pillow" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "httpx2" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.115,<1.0" }, + { name = "pillow", specifier = ">=11.0,<13.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.34,<1.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx2", specifier = ">=2.0,<3.0" }, + { name = "pytest", specifier = ">=9.0,<10.0" }, + { name = "ruff", specifier = ">=0.12,<1.0" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.140.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/fb/fd7671137d9fa3df1d93a2f5111eb982709201724b29f211e4beb2d58688/fastapi-0.140.0.tar.gz", hash = "sha256:f338951b82fd74ca8f843163aec43ea1a1ce84d515415a50fa98fa25572a5544", size = 420968, upload-time = "2026-07-24T21:16:41.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/76/6d9e25ad88da9d3ff744bcdbec4736e38c2288611d43f673a5d9bfa27c07/fastapi-0.140.0-py3-none-any.whl", hash = "sha256:e951c0a0d9540bf5d9a2a9e078fd415da2ab7e312d435139e7d9e2e7fe9f0b23", size = 130863, upload-time = "2026-07-24T21:16:42.89Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore2" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/a8/20ed1ed79cbc2ecdf5301c0968ab7c85547212e2a7bd126ddd2d986e206e/httpcore2-2.9.1.tar.gz", hash = "sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2", size = 67089, upload-time = "2026-07-24T09:21:03.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fb/46c52b781975c335a2bcf1072c7bbc007cbdc8d674217f5ee1daba2c848b/httpcore2-2.9.1-py3-none-any.whl", hash = "sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26", size = 82809, upload-time = "2026-07-24T09:21:01.178Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, +] + +[[package]] +name = "httpx2" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/14/38128fbafd7e0ed41d874df6c9a653d47c2d111cfe59e2b4ac95161b4abd/httpx2-2.9.1.tar.gz", hash = "sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a", size = 95458, upload-time = "2026-07-24T09:21:04.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/b8/cfd91c4ab9134d386d48f0b6ac662ff3d4be6efdee59ee1c67ebc3c0487c/httpx2-2.9.1-py3-none-any.whl", hash = "sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a", size = 91191, upload-time = "2026-07-24T09:21:02.6Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + +[[package]] +name = "websockets" +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" }, + { url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, +]