From 74ca4b38ebdf6964a9ea157aca7b4e7603664207 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Thu, 13 Nov 2025 15:38:58 -0600 Subject: [PATCH] Initial commit: GSPro Remote MVP - Phase 1 complete --- .editorconfig | 47 + .gitignore | 61 + GSPro App.png | Bin 0 -> 152744 bytes PRD.md | 229 + README.md | 212 + Recommended kickoff plan.md | 191 + SETUP.md | 259 + backend/README.md | 167 + backend/app/__init__.py | 13 + backend/app/api/__init__.py | 7 + backend/app/api/actions.py | 232 + backend/app/api/config.py | 348 ++ backend/app/api/system.py | 409 ++ backend/app/api/vision.py | 345 ++ backend/app/core/__init__.py | 21 + backend/app/core/config.py | 193 + backend/app/core/input_ctrl.py | 350 ++ backend/app/core/mdns.py | 335 ++ backend/app/core/screen.py | 370 ++ backend/app/main.py | 115 + backend/pyproject.toml | 76 + backend/requirements.txt | 27 + frontend/index.html | 182 + frontend/package-lock.json | 4813 ++++++++++++++++++ frontend/package.json | 36 + frontend/postcss.config.js | 6 + frontend/public/manifest.json | 60 + frontend/src/App.tsx | 131 + frontend/src/api/client.ts | 308 ++ frontend/src/api/system.ts | 123 + frontend/src/components/AimPad.tsx | 281 + frontend/src/components/ClubIndicator.tsx | 168 + frontend/src/components/ConnectionStatus.tsx | 125 + frontend/src/components/ErrorBoundary.tsx | 106 + frontend/src/components/MapPanel.tsx | 284 ++ frontend/src/components/QuickActions.tsx | 205 + frontend/src/components/ShotOptions.tsx | 130 + frontend/src/components/StatBar.tsx | 164 + frontend/src/components/TeeControls.tsx | 110 + frontend/src/index.css | 361 ++ frontend/src/main.tsx | 23 + frontend/src/pages/DynamicGolfUI.tsx | 202 + frontend/src/stores/appStore.ts | 229 + frontend/tailwind.config.js | 131 + frontend/tsconfig.json | 38 + frontend/tsconfig.node.json | 11 + frontend/vite.config.ts | 47 + scripts/dev.ps1 | 193 + start.bat | 94 + start.py | 250 + 50 files changed, 12818 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 GSPro App.png create mode 100644 PRD.md create mode 100644 README.md create mode 100644 Recommended kickoff plan.md create mode 100644 SETUP.md create mode 100644 backend/README.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/actions.py create mode 100644 backend/app/api/config.py create mode 100644 backend/app/api/system.py create mode 100644 backend/app/api/vision.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/input_ctrl.py create mode 100644 backend/app/core/mdns.py create mode 100644 backend/app/core/screen.py create mode 100644 backend/app/main.py create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements.txt create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/manifest.json create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/system.ts create mode 100644 frontend/src/components/AimPad.tsx create mode 100644 frontend/src/components/ClubIndicator.tsx create mode 100644 frontend/src/components/ConnectionStatus.tsx create mode 100644 frontend/src/components/ErrorBoundary.tsx create mode 100644 frontend/src/components/MapPanel.tsx create mode 100644 frontend/src/components/QuickActions.tsx create mode 100644 frontend/src/components/ShotOptions.tsx create mode 100644 frontend/src/components/StatBar.tsx create mode 100644 frontend/src/components/TeeControls.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/DynamicGolfUI.tsx create mode 100644 frontend/src/stores/appStore.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 scripts/dev.ps1 create mode 100644 start.bat create mode 100644 start.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4f71e19 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,47 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Universal settings +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space + +# Python files +[*.py] +indent_size = 4 +max_line_length = 120 + +# TypeScript, JavaScript, JSX, TSX files +[*.{ts,tsx,js,jsx}] +indent_size = 2 +max_line_length = 100 + +# JSON, YAML files +[*.{json,yml,yaml}] +indent_size = 2 + +# HTML, CSS, SCSS files +[*.{html,css,scss}] +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false +max_line_length = 120 + +# PowerShell scripts +[*.{ps1,psm1}] +end_of_line = crlf + +# Windows batch files +[*.{bat,cmd}] +end_of_line = crlf + +# Makefiles +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a2239b --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +.venv/ +ENV/ +env.bak/ +venv.bak/ +*.egg-info/ +dist/ +build/ +*.egg +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.hypothesis/ +*.log + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +dist/ +dist-ssr/ +*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Environment +.env +.env.local +.env.production +.env.development + +# Build outputs +backend/ui/ +*.exe +*.spec +*.msi + +# Config (keep templates, ignore actual) +config.json +!config.template.json + +# OS +Thumbs.db +Desktop.ini diff --git a/GSPro App.png b/GSPro App.png new file mode 100644 index 0000000000000000000000000000000000000000..95031ecc559985c75c0b149b425d6a09a3c18288 GIT binary patch literal 152744 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

;*&Y@O$*Lf-MuW% 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 literal 0 HcmV?d00001 diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..2d5c4ca --- /dev/null +++ b/PRD.md @@ -0,0 +1,229 @@ +# Product Requirements Document (PRD) + +**Product:** GSPro Remote +**Version:** 0.1.0 (Restart Foundation) +**Author:** \[You] +**Date:** \[Today’s date] + +--- + +## 1. Executive Summary + +GSPro Remote is a companion application for **GSPro golf simulator users** running on Windows. It allows golfers to control GSPro from a secondary device (tablet, phone, or PC) via a web-based interface. + +The app’s purpose is to: + +* Simplify control of GSPro without needing a physical keyboard nearby. +* Provide intuitive UI for common simulator functions (aim, club selection, mulligans, tee box navigation, shot options, map interaction). +* Enhance immersion and ease-of-use for golf enthusiasts with GSPro setups. + +Future versions may incorporate **computer vision (OCR/marker detection)** to auto-detect and track GSPro UI elements, reducing manual setup and enabling more advanced automation. + +--- + +## 2. Goals + +1. **Deliver a clean, intuitive remote-control interface** optimized for tablets/phones. +2. **Support all GSPro keyboard shortcuts** via touch-friendly buttons. +3. **Provide low-latency streaming of the GSPro map/mini-map** (\~200ms or less). +4. **Persist user settings/configuration** locally on the GSPro PC between sessions. +5. **Make installation simple** for non-technical users (one installer, auto-start server). +6. **Prepare for future OCR-based automation**, but keep it out of MVP. + +--- + +## 3. Non-Goals + +* Multiplayer sync or cloud-hosted services. (There is zero value in a any cloud related services) +* Mobile native apps (PWA installable from browser is sufficient). +* Full-fledged vision debugging UI for end-users (that stays an advanced/dev option). + +--- + +## 4. Target Audience + +* **Primary:** GSPro simulator owners with a launch monitor, seeking more convenient in-round controls. +* **Secondary:** Enthusiasts who like to customize sim setups (may use advanced OCR features later). + +Users are assumed to be **non-technical golfers**, not developers. Installation and updates must be straightforward. + +--- + +## 5. Personas & Use Cases + +### Persona A: “Weekend Golfer” + +* Wants to control GSPro from a tablet while standing on the mat. +* Needs quick access to Aim, Mulligan, Club Selection, Tee Box controls. +* Doesn’t want to touch a keyboard during play. + +--- + +## 6. Functional Requirements + +### 6.1 Core Features (MVP) + +* **UI Controls** + + * Directional Pad for Aim (Up/Down/Left/Right/Reset). + * Club Indicator + Detection (manual at first, OCR planned later). + * Mulligan button. + * Tee box navigation (previous/next tee). + * Map panel with expand/collapse. (should stream the map from the GSPro UI) + +* **Backend Integration** + + * WebSocket streaming of GSPro map region. + * API endpoints to send keypresses mapped to GSPro shortcuts. + * Config persistence (JSON stored on host PC). + * mDNS discovery (`gsproapp.local`) for easy access. + +* **Performance** + + * Streaming latency under 200ms (720p @ 30fps acceptable). + * Smooth interaction (key commands executed <100ms). + +### 6.2 Advanced / V2 Features + +* **OCR & Region Detection** + + * EasyOCR/Tesseract integration to auto-detect club, map, shot info. + * Auto-configuration of regions (user doesn’t manually define). +* **Custom Monitor Tasks** + + * Background monitoring of regions for changes (e.g., “detect when putting mode enabled”). +* **Marker Detection** + + * Visual template matching for advanced element tracking. +* **Extended Controls** + + * Support for additional GSPro shortcuts (e.g., free look, flyover, lighting). + +--- + +## 7. Technical Architecture + +* **Frontend:** React + TypeScript + + * `DynamicGolfUI` = Main consumer UI (touch-friendly, polished). + * `VisionDashboard` = Developer/advanced-only UI (OCR testing, markers). + +* **Backend:** Python + FastAPI + + * `/api/actions/key` → Trigger GSPro keypresses. + * `/api/vision/ws/stream` → WebSocket for video streaming. + * `/api/vision/ocr`, `/api/vision/markers`, `/api/vision/regions` → Vision endpoints (advanced). + +* **System Design** + + * Runs locally on Windows (FastAPI server). + * UI served at `http://gsproapp.local:5005/ui`. + * Input simulated via `pydirectinput` to control GSPro. + * Screen captured via `mss` + OpenCV. + +--- + +## 8. User Experience + +* **Primary UI (DynamicGolfUI)** + Matches "GSPro App.png" in project root directory: + + * Top: App name/version, Mulligan button. + * Left: Club indicator. + * Center: Aim directional pad. + * Right: Map stream (expandable). + * Bottom: Shot Options + Tee controls. + +* **Advanced UI (VisionDashboard)** + Hidden behind advanced toggle. Used by devs to: + + * Test OCR engines. + * Define/capture regions. + * Add/verify markers. + * Monitor tasks. + +--- + +## 9. Success Metrics + +* MVP launch: + + * Users can control 90% of their round without a keyboard. + * Map streaming latency <200ms. + * 1-click installer on Windows. +* V2: + + * Automatic detection of club + map area with >90% reliability. + * Config persistence across restarts without user intervention. + +--- + +## 10. Milestones + +* **Phase 1 (MVP / Public Beta)** + + * Polish DynamicGolfUI. + * Ensure all GSPro shortcuts are covered. + * Stable WebSocket streaming. + * Windows installer packaging. + +* **Phase 2 (Vision + OCR)** + + * Add OCR-based auto-detection. + * Background monitor tasks. + * VisionDashboard improvements. + +* **Phase 3 (Subscription-ready)** + + * Licensing/subscription integration. + * Polished UX/UI redesign (visual depth, animations, better icons). + * Documentation & onboarding guide. + +--- + +## 11. Risks / Open Questions + +* Will OCR performance be good enough at different resolutions (1080p vs 4K)? +* Will average users struggle with network/firewall setup for accessing the web UI? +* Packaging FastAPI + dependencies into an easy Windows installer (PyInstaller vs. MSI vs. Docker)? + +--- + +## 12. Feature Mapping Matrix + +| **UI Control** | **GSPro Shortcut** | **API Endpoint** | **Notes** | +| ----------------------------------------- | ------------------------------- | ----------------------------------------- | ---------------------------------------------- | +| **Reset** | `A` | `POST /api/actions/key` | Reset | +| **Aim Pad (Up/Down/Left/Right)** | Arrow keys | `POST /api/actions/keydown` + `/keyup` | aim adjust = arrow keys, needs “hold” support | +| **Club Selection (Up/Down)** | `U` (club up), `K` (club down) | `POST /api/actions/key` | Shown as club indicator in UI | +| **Mulligan** | `Ctrl+M` | `POST /api/actions/key` | Toggle mulligan | +| **Shot Options (Normal/Punch/Flop/Chip)** | `'` (apostrophe) + context? | `POST /api/actions/key` | Might need mapping to GSPro UI if not explicit | +| **Tee Box (Left/Right)** | `C` (tee left), `V` (tee right) | `POST /api/actions/key` | Navigates tee position | +| **Map Expand/Collapse** | `S` | `POST /api/actions/key` | Toggles map size | +| **Map Zoom (In/Out)** | `Q` (zoom +), `W` (zoom -) | `POST /api/actions/key` | Could be UI slider later | +| **Map Click (Expanded)** | Mouse click | `POST /api/actions/key` or WS click event | Normalized coordinates mapped to screen | +| **Scorecard** | `T` | `POST /api/actions/key` | Toggle scorecard | +| **Range Finder** | `R` | `POST /api/actions/key` | Launch rangefinder | +| **Heat Map** | `Y` | `POST /api/actions/key` | Toggle heatmap overlay | +| **Putt Toggle** | `U` | `POST /api/actions/key` | Enable/disable putting mode | +| **Pin Indicator** | `P` | `POST /api/actions/key` | Show/hide pin indicator | +| **Flyover** | `O` | `POST /api/actions/key` | Hole preview | +| **Tracer Clear** | `F1` | `POST /api/actions/key` | Clear ball tracer | +| **Aim Point** | `F3` | `POST /api/actions/key` | Toggle aim point view | +| **Free Look** | `F5` | `POST /api/actions/key` | Unlock camera | +| **Console Short/Tall** | `F8` / `F9` | `POST /api/actions/key` | Open GSPro dev consoles | +| **Fullscreen** | `F11` | `POST /api/actions/key` | Toggle fullscreen | +| **Camera Go To Ball** | `5` | `POST /api/actions/key` | Jump to ball cam | +| **Camera Fly To Ball** | `6` | `POST /api/actions/key` | Fly cam to ball | +| **Go To Ball (practice)** | `8` | `POST /api/actions/key` | Only in practice mode | +| **Previous/Next Hole** | `9` / `0` | `POST /api/actions/key` | Only in practice mode | +| **Sound On/Off** | `+` / `-` | `POST /api/actions/key` | Volume control | +| **FPS Display** | `F` | `POST /api/actions/key` | Show/hide FPS | +| **Green Grid** | `G` | `POST /api/actions/key` | Show/hide green putting grid | +| **Lighting** | `L` | `POST /api/actions/key` | Adjust lighting | +| **Shot Camera** | `J` | `POST /api/actions/key` | Switch to shot cam | +| **UI Hide/Show** | `H` | `POST /api/actions/key` | Toggle GSPro UI | +| **3D Grass Toggle** | `Z` | `POST /api/actions/key` | Show/hide grass detail | +| **Switch Handedness** | `N` | `POST /api/actions/key` | Left/right handed player | +| **Shadow Intensity** | `<` / `>` | `POST /api/actions/key` | Adjust shadows | +| **Fast Forward Ball Roll** | Hold key (space?) | `POST /api/actions/keydown` + `/keyup` | Needs “hold” support | \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a055ed4 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# GSPro Remote + +A companion application for GSPro golf simulator that allows remote control from any device on your network. + +## 🎯 Overview + +GSPro Remote is a web-based remote control application for GSPro golf simulator running on Windows. Control GSPro from your tablet, phone, or another PC without needing a keyboard nearby. + +### Key Features + +- **Touch-Friendly Interface** - Optimized for tablets and phones +- **All GSPro Shortcuts** - Full keyboard shortcut support via touch controls +- **Live Map Streaming** - Real-time streaming of the GSPro mini-map +- **mDNS Discovery** - Access via `gsproapp.local` on your network +- **Persistent Settings** - Configuration saved between sessions +- **Zero Cloud Dependencies** - Everything runs locally on your network + +## 🚀 Quick Start + +### Prerequisites + +- Windows PC running GSPro +- Python 3.11+ +- Node.js 20+ +- GSPro golf simulator + +### Installation + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/gspro-remote.git +cd gspro-remote +``` + +2. Run the development setup: +```powershell +.\scripts\dev.ps1 +``` + +This will: +- Install all backend dependencies +- Install all frontend dependencies +- Start both development servers +- Open the UI at http://localhost:5173 + +### Access Points + +- **Frontend UI**: http://localhost:5173 +- **Backend API**: http://localhost:5005 +- **API Documentation**: http://localhost:5005/api/docs +- **mDNS Access**: http://gsproapp.local:5005 + +## 📁 Project Structure + +``` +gspro-remote/ +├── backend/ # Python FastAPI backend +│ ├── app/ +│ │ ├── api/ # API endpoints +│ │ │ ├── actions.py # Keyboard control +│ │ │ ├── vision.py # Screen streaming +│ │ │ ├── config.py # Configuration +│ │ │ └── system.py # System utilities +│ │ ├── core/ # Core functionality +│ │ │ ├── config.py # Config management +│ │ │ ├── input_ctrl.py # Windows input +│ │ │ ├── screen.py # Screen capture +│ │ │ └── mdns.py # mDNS service +│ │ └── main.py # FastAPI app +│ └── pyproject.toml # Python dependencies +│ +├── frontend/ # React TypeScript frontend +│ ├── src/ +│ │ ├── pages/ # Page components +│ │ ├── components/ # UI components +│ │ ├── api/ # API client +│ │ ├── stores/ # State management +│ │ └── App.tsx # Main app +│ └── package.json # Node dependencies +│ +├── scripts/ # Development scripts +│ └── dev.ps1 # Windows dev script +│ +└── PRD.md # Product requirements +``` + +## 🎮 Features + +### Phase 1 (MVP) - Current +- ✅ Directional pad for aim control +- ✅ Club selection (up/down) +- ✅ Mulligan button +- ✅ Tee box navigation +- ✅ Map panel with streaming +- ✅ All GSPro keyboard shortcuts +- ✅ WebSocket-based streaming +- ✅ Configuration persistence +- ✅ mDNS service discovery + +### Phase 2 (Planned) +- ⏳ OCR-based auto-detection +- ⏳ Visual marker tracking +- ⏳ Background monitoring +- ⏳ Advanced automation + +### Phase 3 (Future) +- ⏳ Enhanced UI/UX +- ⏳ Subscription features +- ⏳ Extended documentation + +## 🛠️ Development + +### Backend Development + +```bash +cd backend +python -m venv .venv +.venv\Scripts\activate +pip install -e ".[dev]" +uvicorn app.main:app --reload --host 0.0.0.0 --port 5005 +``` + +### Frontend Development + +```bash +cd frontend +npm install +npm run dev +``` + +### Running Tests + +Backend: +```bash +cd backend +pytest +``` + +Frontend: +```bash +cd frontend +npm test +``` + +## 📖 API Documentation + +The backend provides a comprehensive REST API with WebSocket support for streaming. + +### Key Endpoints + +- `POST /api/actions/key` - Send keyboard input to GSPro +- `WS /api/vision/ws/stream` - WebSocket for map streaming +- `GET /api/config` - Get current configuration +- `GET /api/system/health` - Health check + +Full API documentation available at http://localhost:5005/api/docs when running. + +## 🔧 Configuration + +Configuration is stored at `%LOCALAPPDATA%\GSPro Remote\config.json` + +### Default Configuration + +```json +{ + "server": { + "host": "0.0.0.0", + "port": 5005, + "mdns_enabled": true + }, + "capture": { + "fps": 30, + "quality": 85, + "resolution": "720p" + }, + "gspro": { + "window_title": "GSPro", + "auto_focus": true + } +} +``` + +## 🐛 Troubleshooting + +### GSPro Window Not Found +- Ensure GSPro is running +- Check window title in configuration +- Run backend as administrator if needed + +### Connection Issues +- Verify both frontend and backend are running +- Check Windows firewall settings +- Ensure devices are on same network + +### Port Already in Use +- Check if another instance is running +- Change port in configuration +- Use `netstat -an | findstr :5005` to check + +## 📝 License + +MIT License - See LICENSE file for details + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## 📞 Support + +For issues and questions: +- Create an issue on GitHub +- Check existing issues for solutions \ No newline at end of file diff --git a/Recommended kickoff plan.md b/Recommended kickoff plan.md new file mode 100644 index 0000000..cd0705c --- /dev/null +++ b/Recommended kickoff plan.md @@ -0,0 +1,191 @@ +# Recommended kickoff plan + +## 0) Toolchain (lock these down) + +* **Python** 3.11 (for wheels availability + perf) +* **Node** 20.19+ and **npm** 10+ +* **UV** for deps +* **Playwright** (later for UI smoke tests) +* **Git** repo from day 1 + +## 1) Make a tiny, opinionated repo scaffold + +Create a new repo (you can copy/paste this tree into your README so the LLM follows it): + +``` +gspro-remote/ + backend/ + app/ + __init__.py + main.py # FastAPI app factory + router mounts + api/ + __init__.py + actions.py # /api/actions/* + vision.py # /api/vision/* (keep but V2-gated) + core/ + config.py # AppConfig + load/save + input_ctrl.py # pydirectinput helpers + screen.py # mss capture utils (non-vision bits) + pkg.json # (optional) for uvicorn dev script via npm - or just use Makefile + pyproject.toml + README.md + frontend/ + src/ + main.tsx + App.tsx + pages/DynamicGolfUI.tsx + components/ + AimPad.tsx + StatBar.tsx + MapPanel.tsx + styles/ + index.html + package.json + vite.config.ts + README.md + scripts/ + dev.ps1 # start both servers on Windows + .editorconfig + .gitignore + README.md +``` + + +## 2) Bootstrap bare bones projects + +### Backend (FastAPI) + +```bash +cd backend +uv venv && uv pip install fastapi uvicorn pydantic-settings pydirectinput pywin32 mss opencv-python pillow zeroconf +# Or with pip: +# python -m venv .venv && .venv\Scripts\activate && pip install fastapi uvicorn pydirectinput pywin32 mss opencv-python pillow zeroconf +``` + +`app/main.py` (minimal) + +```python +from fastapi import FastAPI +from .api.actions import router as actions_router + +def create_app() -> FastAPI: + app = FastAPI(title="GSPro Remote", version="0.1.0") + app.include_router(actions_router, prefix="/api/actions", tags=["actions"]) + return app + +app = create_app() +``` + +`app/api/actions.py` (just enough to prove the vertical slice) + +```python +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from ..core.input_ctrl import press_keys, focus_window + +router = APIRouter() + +class KeyReq(BaseModel): + keys: str + +@router.post("/key") +def post_key(req: KeyReq): + if not focus_window("GSPro"): + raise HTTPException(409, "GSPro window not found/active") + press_keys(req.keys) + return {"ok": True} +``` + +Run: + +```bash +uvicorn app.main:app --reload --host 0.0.0.0 --port 5005 +``` + +### Frontend (Vite + React + TS) + +```bash +cd ../frontend +npm create vite@latest . -- --template react-ts +npm i +npm run dev +``` + +Add a `.env` in frontend later if you want a configurable API base URL. + +## 3) Migrate your existing code **incrementally** + +You already have solid pieces. Bring them in slice by slice: + +* Move `config.py` → `backend/app/core/config.py` (keep AppConfig + persistence). +* Move `input_ctrl.py` → `backend/app/core/input_ctrl.py` (unchanged). +* Create `backend/app/api/vision.py` and paste **only** the streaming endpoint you’ll use first (WebSocket or SSE). Keep OCR endpoints behind a `VISION_ENABLED` flag for V2. +* Defer `screen_watch.py`, `capture.py`, `streaming.py` until the “MapPanel” slice (below). It’s okay if V1 ships with **no** OCR. + +## 4) Implement one **vertical slice** end-to-end (MVP proof) + +Start with the **Aim Pad + Mulligan** because it touches everything: + +* Frontend: + + * `components/AimPad.tsx`: buttons call `POST /api/actions/key` with `"left"`, `"right"`, `"up"`, `"down"`, and `"a"` (Reset). + * Add Mulligan button calling `"ctrl+m"`. + +* Backend: + + * The `post_key` route already exists. + * Make sure `focus_window("GSPro")` works on your machine. + +* Test on Windows: you should see GSPro react from the tablet/phone. + +Now you have a working remote! + +## 5) Add the **MapPanel** as the second slice + +* Backend: + + * Introduce a simple `/api/vision/ws/stream` that returns a **downscaled JPEG** buffer of a fixed region; reuse your `mss` capture and JPEG base64 helpers. Keep the code minimal. +* Frontend: + + * `components/MapPanel.tsx` opens a WS to `/api/vision/ws/stream`, paints frames onto a ``, supports expand/collapse (no click mapping yet). +* Aim for **720p @ 30fps, \~75–85 JPEG quality**. Measure latency, then optimize resize **before** JPEG encode. + +## 6) Wire the rest of the “Essentials” + +* Tee left/right (`c`/`v`) +* Scorecard (`t`) and Range finder (`r`) as secondary buttons under a kebab menu +* Stat row at bottom (hard-coded 0.0° up/right for now—wire later) + +## 7) Keep OCR as **V2** but leave hooks + +* Add a feature flag `VISION_ENABLED = False` in `app/core/config.py`. +* Keep `vision.py` imported but routes gated: if disabled, return 404 with a friendly message. +* This lets you merge OCR work later without reshaping the app. + +## 8) Dev ergonomics on Windows + +Create `scripts/dev.ps1` to run both servers: + +```powershell +Start-Job { Set-Location backend; uvicorn app.main:app --reload --port 5005 } +Start-Sleep -Seconds 1 +Start-Process powershell -ArgumentList 'cd frontend; npm run dev' +``` + +## 9) Packaging (when MVP is stable) + +* **Backend**: PyInstaller or Nuitka into a single EXE, then wrap with **Inno Setup** to produce an installer that: + + * Installs the EXE + config files to `C:\ProgramData\GSPro Remote\` + * Creates a Start Menu entry and an **auto-start shortcut** for the Windows user + * Opens firewall rule for your port +* **Frontend**: `npm run build` → copy `dist/` into `backend/ui/` and serve using FastAPI `StaticFiles`. (You already had `build-ui.py`; keep that idea.) + +## 10) How to use the LLM effectively (so it helps, not hurts) + +* **Give it the scaffold** and the **PRD** (and the Section-12 matrix). +* Ask for **one file at a time**: “Implement `components/AimPad.tsx` against `/api/actions/key`” with explicit props and return types. +* Paste back **real compiler/server errors** verbatim; ask it to fix **only those**. +* Freeze the public contracts early (API request/response shapes). LLMs drift if the interface is fuzzy. + +--- \ No newline at end of file diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..3cdcb70 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,259 @@ +# GSPro Remote - Setup & Run Instructions + +## 🚀 Quick Setup Guide + +Follow these steps to get GSPro Remote running on your network and accessible from your phone/tablet. + +## Prerequisites + +1. **Windows PC** with GSPro installed +2. **Python 3.11+** installed ([Download Python](https://www.python.org/downloads/)) +3. **Node.js 20+** installed ([Download Node.js](https://nodejs.org/)) +4. **Git** (optional, for cloning the repository) + +## Step 1: Initial Setup + +### Option A: Using PowerShell Script (Recommended) + +1. Open PowerShell as Administrator +2. Navigate to the project directory: + ```powershell + cd "path\to\GSPro Remote" + ``` +3. Run the development script: + ```powershell + .\scripts\dev.ps1 + ``` + +This script will: +- Check all prerequisites +- Install Python dependencies +- Install Node.js dependencies +- Start both backend and frontend servers +- Open browser windows with the application + +### Option B: Manual Setup + +#### Backend Setup: +```bash +cd backend +python -m venv .venv +.venv\Scripts\activate +pip install -e . +``` + +#### Frontend Setup: +```bash +cd frontend +npm install +``` + +## Step 2: Running the Application + +### Start Backend Server: +```bash +cd backend +.venv\Scripts\activate +python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 5005 +``` + +The backend will be available at: +- API: `http://localhost:5005` +- API Docs: `http://localhost:5005/api/docs` + +### Start Frontend Server: +```bash +cd frontend +npm run dev -- --host +``` + +The frontend will be available at: +- Local: `http://localhost:5173` +- Network: `http://[YOUR-IP]:5173` + +## Step 3: Access from Your Phone/Tablet + +### Find Your PC's IP Address + +1. Open Command Prompt or PowerShell +2. Run: `ipconfig` +3. Look for your IPv4 Address (e.g., `192.168.1.100`) + +### Connect from Mobile Device + +1. **Ensure your phone/tablet is on the same WiFi network as your PC** + +2. Open a web browser on your mobile device + +3. Navigate to one of these URLs: + - `http://[YOUR-PC-IP]:5173` (e.g., `http://192.168.1.100:5173`) + - `http://gsproapp.local:5005/ui` (if mDNS is working) + +### Install as Mobile App (PWA) + +**On iPhone/iPad:** +1. Open Safari (must be Safari) +2. Navigate to the app URL +3. Tap the Share button +4. Scroll down and tap "Add to Home Screen" +5. Name it "GSPro Remote" and tap Add + +**On Android:** +1. Open Chrome +2. Navigate to the app URL +3. Tap the menu (3 dots) +4. Tap "Add to Home screen" or "Install app" +5. Follow the prompts + +## Step 4: Configure Windows Firewall + +If you can't connect from your mobile device, you may need to allow the app through Windows Firewall: + +1. Open Windows Defender Firewall +2. Click "Allow an app or feature through Windows Defender Firewall" +3. Click "Change settings" then "Allow another app..." +4. Browse and add: + - Python.exe (from your Python installation) + - Node.exe (from your Node.js installation) +5. Check both Private and Public networks +6. Click OK + +### Alternative: Create Firewall Rules via PowerShell (Admin) +```powershell +# Allow backend port +New-NetFirewallRule -DisplayName "GSPro Remote Backend" -Direction Inbound -Protocol TCP -LocalPort 5005 -Action Allow + +# Allow frontend port +New-NetFirewallRule -DisplayName "GSPro Remote Frontend" -Direction Inbound -Protocol TCP -LocalPort 5173 -Action Allow +``` + +## Step 5: Start Using GSPro Remote + +1. **Start GSPro** on your PC +2. **Open GSPro Remote** on your mobile device +3. The app should show "Connected" status +4. You can now control GSPro from your mobile device! + +## 🎮 Using the App + +### Main Controls: +- **Aim Pad**: Hold arrows to adjust aim continuously +- **Center Button**: Press to reset aim +- **Club Selection**: Tap up/down to change clubs +- **Mulligan**: Toggle mulligan mode +- **Tee Box**: Move left/right on the tee +- **Map**: Start streaming to see live mini-map +- **Shot Options**: Select different shot types +- **Quick Actions** (+ button): Access additional controls + +### Gestures: +- **Hold** directional buttons for continuous movement +- **Tap** for single adjustments +- **Expand Map**: Tap expand button for fullscreen map + +## 🔧 Troubleshooting + +### Can't connect from phone +- ✅ Check both devices are on the same WiFi network +- ✅ Check Windows Firewall settings +- ✅ Try using IP address instead of hostname +- ✅ Disable VPN if active +- ✅ Make sure backend shows "Server running on 0.0.0.0:5005" + +### GSPro not responding to commands +- ✅ Ensure GSPro is running and focused +- ✅ Check GSPro window title matches config (default: "GSPro") +- ✅ Try running backend as Administrator +- ✅ Check backend console for errors + +### Map streaming not working +- ✅ Click "Start Streaming" button in map panel +- ✅ Check backend console for WebSocket connection +- ✅ Verify GSPro is on the main game screen +- ✅ Adjust map region in settings if needed + +### Poor performance on mobile +- ✅ Reduce stream quality in settings +- ✅ Lower FPS to 15-20 +- ✅ Use 5GHz WiFi if available +- ✅ Move closer to WiFi router + +## 📱 Mobile-Specific Tips + +### For Best Experience: +1. **Lock screen orientation** to prevent rotation while playing +2. **Enable "Add to Home Screen"** for full-screen experience +3. **Disable browser gestures** if they interfere +4. **Use landscape mode** on tablets for optimal layout +5. **Keep screen awake** during gameplay + +### Battery Optimization: +- Lower stream quality to "Low" or "Medium" +- Reduce FPS to 15-20 +- Disable haptic feedback if not needed +- Dim screen brightness when possible + +## 🔒 Security Notes + +- The app only works on your local network +- No data is sent to external servers +- All communication is between your PC and mobile device +- Consider using a dedicated WiFi network for gaming + +## 🆘 Getting Help + +### Check Status: +1. Backend health: `http://localhost:5005/api/system/health` +2. API docs: `http://localhost:5005/api/docs` +3. System info: `http://localhost:5005/api/system/info` +4. Diagnostics: `http://localhost:5005/api/system/diagnostics` + +### View Logs: +- Backend logs appear in the terminal where you started the server +- Frontend logs appear in the browser console (F12) + +### Common Commands: +```bash +# Check if ports are in use +netstat -an | findstr :5005 +netstat -an | findstr :5173 + +# Check Python version +python --version + +# Check Node version +node --version + +# Check network interfaces +ipconfig /all + +# Test backend directly +curl http://localhost:5005/api/system/health +``` + +## 🎯 Production Deployment + +For permanent installation: + +1. **Build the frontend**: + ```bash + cd frontend + npm run build + ``` + +2. **Configure backend to serve UI**: + - Frontend will be built to `backend/ui` + - Access complete app at `http://gsproapp.local:5005` + +3. **Create Windows Service** (optional): + - Use NSSM or similar to run backend as service + - Auto-starts with Windows + - Runs in background + +4. **Configure auto-start**: + - Add to Windows startup folder + - Or use Task Scheduler + +--- + +**Enjoy controlling GSPro from anywhere in your simulator room!** 🏌️‍♂️ \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..d02b57c --- /dev/null +++ b/backend/README.md @@ -0,0 +1,167 @@ +# GSPro Remote Backend + +FastAPI-based backend service for GSPro Remote, providing keyboard control and screen streaming capabilities for GSPro golf simulator. + +## Features + +- **Keyboard Control API**: Send keyboard shortcuts to GSPro +- **Screen Streaming**: WebSocket-based map region streaming +- **Configuration Management**: Persistent settings storage +- **mDNS Discovery**: Auto-discoverable at `gsproapp.local` +- **Windows Integration**: Native Windows input simulation + +## Requirements + +- Python 3.11+ +- Windows OS (for GSPro integration) +- GSPro running on the same machine + +## Installation + +### Using pip + +```bash +python -m venv .venv +.venv\Scripts\activate +pip install -e . +``` + +### Using UV (recommended) + +```bash +uv venv +uv pip install -e . +``` + +## Development Setup + +1. Install development dependencies: +```bash +pip install -e ".[dev]" +``` + +2. Run the development server: +```bash +uvicorn app.main:app --reload --host 0.0.0.0 --port 5005 +``` + +## API Structure + +``` +/api/actions/ + POST /key - Send single key press + POST /keydown - Hold key down + POST /keyup - Release key + POST /combo - Send key combination + +/api/config/ + GET / - Get current configuration + PUT / - Update configuration + POST /save - Persist to disk + +/api/vision/ + WS /ws/stream - WebSocket map streaming + GET /regions - Get defined screen regions + POST /capture - Capture screen region + +/api/system/ + GET /health - Health check + GET /info - System information +``` + +## Configuration + +Configuration is stored in `%LOCALAPPDATA%\GSPro Remote\config.json` + +Default configuration: +```json +{ + "server": { + "host": "0.0.0.0", + "port": 5005, + "mdns_enabled": true + }, + "capture": { + "fps": 30, + "quality": 85, + "resolution": "720p" + }, + "gspro": { + "window_title": "GSPro", + "auto_focus": true + } +} +``` + +## Project Structure + +``` +backend/ + app/ + __init__.py + main.py # FastAPI application + api/ + actions.py # Keyboard control endpoints + config.py # Configuration endpoints + vision.py # Screen capture/streaming + system.py # System utilities + core/ + config.py # Configuration management + input_ctrl.py # Windows input simulation + screen.py # Screen capture utilities + mdns.py # mDNS service registration + models/ + requests.py # Pydantic request models + responses.py # Pydantic response models + tests/ + test_*.py # Unit tests + pyproject.toml # Project dependencies +``` + +## Testing + +Run tests with pytest: +```bash +pytest +``` + +With coverage: +```bash +pytest --cov=app --cov-report=html +``` + +## Building for Distribution + +Build standalone executable: +```bash +pip install ".[build]" +python -m PyInstaller --onefile --name gspro-remote app/main.py +``` + +## Environment Variables + +- `GSPRO_REMOTE_PORT`: Override default port (5005) +- `GSPRO_REMOTE_HOST`: Override default host (0.0.0.0) +- `GSPRO_REMOTE_CONFIG_PATH`: Override config location +- `GSPRO_REMOTE_DEBUG`: Enable debug logging + +## Troubleshooting + +### GSPro window not found +- Ensure GSPro is running +- Check window title matches configuration +- Run as administrator if permission issues + +### Port already in use +- Check if another instance is running +- Change port in configuration +- Use `netstat -an | findstr :5005` to check + +### mDNS not working +- Check Windows firewall settings +- Ensure Bonjour service is running +- Try accessing directly via IP instead + +## License + +MIT License - See parent LICENSE file for details \ No newline at end of file diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..4859a84 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,13 @@ +""" +GSPro Remote Backend Application + +A FastAPI-based backend service for controlling GSPro golf simulator +via keyboard shortcuts and providing screen streaming capabilities. +""" + +__version__ = "0.1.0" +__author__ = "GSPro Remote Team" + +from .main import app + +__all__ = ["app", "__version__"] diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..673bb72 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1,7 @@ +""" +API module for GSPro Remote backend. +""" + +from . import actions, config, system, vision + +__all__ = ["actions", "config", "system", "vision"] diff --git a/backend/app/api/actions.py b/backend/app/api/actions.py new file mode 100644 index 0000000..6033b46 --- /dev/null +++ b/backend/app/api/actions.py @@ -0,0 +1,232 @@ +""" +Actions API for sending keyboard inputs to GSPro. +""" + +import asyncio +import logging +from typing import Optional + +from fastapi import APIRouter, HTTPException, BackgroundTasks +from pydantic import BaseModel, Field + +from ..core.input_ctrl import press_key, press_keys, key_down, key_up, focus_window, is_gspro_running +from ..core.config import get_config + +logger = logging.getLogger(__name__) +router = APIRouter() + + +class KeyPressRequest(BaseModel): + """Request model for single key press.""" + + key: str = Field(..., description="Key or key combination to press (e.g., 'a', 'ctrl+m')") + delay: Optional[float] = Field(0.0, description="Delay in seconds before pressing the key") + + +class KeyHoldRequest(BaseModel): + """Request model for key hold operations.""" + + key: str = Field(..., description="Key to hold or release") + duration: Optional[float] = Field(None, description="Duration to hold the key (seconds)") + + +class KeySequenceRequest(BaseModel): + """Request model for key sequence.""" + + keys: list[str] = Field(..., description="List of keys to press in sequence") + interval: float = Field(0.1, description="Interval between key presses (seconds)") + + +class ActionResponse(BaseModel): + """Response model for action endpoints.""" + + success: bool + message: str + key: Optional[str] = None + + +@router.post("/key", response_model=ActionResponse) +async def send_key(request: KeyPressRequest): + """ + Send a single key press or key combination to GSPro. + + Supports: + - Single keys: 'a', 'space', 'enter' + - Combinations: 'ctrl+m', 'shift+tab' + - Function keys: 'f1', 'f11' + - Arrow keys: 'up', 'down', 'left', 'right' + """ + config = get_config() + + # Check if GSPro is running + if not is_gspro_running(): + raise HTTPException(status_code=409, detail="GSPro is not running or window not found") + + # Focus GSPro window if auto-focus is enabled + if config.gspro.auto_focus: + if not focus_window(config.gspro.window_title): + logger.warning(f"Could not focus window: {config.gspro.window_title}") + + try: + # Apply delay if specified + if request.delay > 0: + await asyncio.sleep(request.delay) + + # Send the key press + if "+" in request.key: + # Handle key combination + press_keys(request.key) + else: + # Handle single key + press_key(request.key) + + logger.info(f"Sent key press: {request.key}") + return ActionResponse(success=True, message=f"Key '{request.key}' pressed successfully", key=request.key) + except Exception as e: + logger.error(f"Failed to send key press: {e}") + raise HTTPException(status_code=500, detail=f"Failed to send key press: {str(e)}") + + +@router.post("/keydown", response_model=ActionResponse) +async def send_key_down(request: KeyHoldRequest, background_tasks: BackgroundTasks): + """ + Press and hold a key down. + + If duration is specified, the key will be automatically released after that time. + Otherwise, you must call /keyup to release it. + """ + config = get_config() + + if not is_gspro_running(): + raise HTTPException(status_code=409, detail="GSPro is not running or window not found") + + if config.gspro.auto_focus: + focus_window(config.gspro.window_title) + + try: + key_down(request.key) + logger.info(f"Key down: {request.key}") + + # If duration is specified, schedule key release + if request.duration: + + async def release_key(): + await asyncio.sleep(request.duration) + key_up(request.key) + logger.info(f"Key released after {request.duration}s: {request.key}") + + background_tasks.add_task(release_key) + + return ActionResponse( + success=True, message=f"Key '{request.key}' held for {request.duration}s", key=request.key + ) + else: + return ActionResponse( + success=True, message=f"Key '{request.key}' pressed down (call /keyup to release)", key=request.key + ) + except Exception as e: + logger.error(f"Failed to hold key down: {e}") + raise HTTPException(status_code=500, detail=f"Failed to hold key down: {str(e)}") + + +@router.post("/keyup", response_model=ActionResponse) +async def send_key_up(request: KeyHoldRequest): + """ + Release a held key. + """ + config = get_config() + + if not is_gspro_running(): + raise HTTPException(status_code=409, detail="GSPro is not running or window not found") + + if config.gspro.auto_focus: + focus_window(config.gspro.window_title) + + try: + key_up(request.key) + logger.info(f"Key up: {request.key}") + + return ActionResponse(success=True, message=f"Key '{request.key}' released", key=request.key) + except Exception as e: + logger.error(f"Failed to release key: {e}") + raise HTTPException(status_code=500, detail=f"Failed to release key: {str(e)}") + + +@router.post("/sequence", response_model=ActionResponse) +async def send_key_sequence(request: KeySequenceRequest): + """ + Send a sequence of key presses with specified interval between them. + """ + config = get_config() + + if not is_gspro_running(): + raise HTTPException(status_code=409, detail="GSPro is not running or window not found") + + if config.gspro.auto_focus: + focus_window(config.gspro.window_title) + + try: + for i, key in enumerate(request.keys): + if "+" in key: + press_keys(key) + else: + press_key(key) + + # Add interval between keys (except after last one) + if i < len(request.keys) - 1: + await asyncio.sleep(request.interval) + + logger.info(f"Sent key sequence: {request.keys}") + return ActionResponse( + success=True, message=f"Sent {len(request.keys)} key presses", key=", ".join(request.keys) + ) + except Exception as e: + logger.error(f"Failed to send key sequence: {e}") + raise HTTPException(status_code=500, detail=f"Failed to send key sequence: {str(e)}") + + +@router.get("/shortcuts") +async def get_shortcuts(): + """ + Get a list of all available GSPro keyboard shortcuts. + """ + shortcuts = { + "aim": {"up": "up", "down": "down", "left": "left", "right": "right", "reset": "a"}, + "club": {"up": "u", "down": "k"}, + "shot": {"mulligan": "ctrl+m", "options": "'", "putt_toggle": "u"}, + "tee": {"left": "c", "right": "v"}, + "view": { + "map_toggle": "s", + "map_zoom_in": "q", + "map_zoom_out": "w", + "scorecard": "t", + "range_finder": "r", + "heat_map": "y", + "pin_indicator": "p", + "flyover": "o", + "free_look": "f5", + "aim_point": "f3", + "green_grid": "g", + "ui_toggle": "h", + }, + "camera": {"go_to_ball": "5", "fly_to_ball": "6", "shot_camera": "j"}, + "practice": {"go_to_ball": "8", "previous_hole": "9", "next_hole": "0"}, + "system": { + "fullscreen": "f11", + "fps_display": "f", + "console_short": "f8", + "console_tall": "f9", + "tracer_clear": "f1", + }, + "settings": { + "sound_on": "+", + "sound_off": "-", + "lighting": "l", + "3d_grass": "z", + "switch_hand": "n", + "shadow_increase": ">", + "shadow_decrease": "<", + }, + } + + return {"shortcuts": shortcuts, "total": sum(len(category) for category in shortcuts.values())} diff --git a/backend/app/api/config.py b/backend/app/api/config.py new file mode 100644 index 0000000..7ac1997 --- /dev/null +++ b/backend/app/api/config.py @@ -0,0 +1,348 @@ +""" +Configuration API for managing GSPro Remote settings. +""" + +import logging +from typing import Any, Dict, Optional + +from fastapi import APIRouter, BackgroundTasks, HTTPException, Query +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +from ..core.config import get_config, reset_config + +logger = logging.getLogger(__name__) +router = APIRouter() + + +class ConfigUpdateRequest(BaseModel): + """Request model for configuration updates.""" + + server: Optional[Dict[str, Any]] = Field(None, description="Server configuration") + capture: Optional[Dict[str, Any]] = Field(None, description="Capture configuration") + gspro: Optional[Dict[str, Any]] = Field(None, description="GSPro configuration") + vision: Optional[Dict[str, Any]] = Field(None, description="Vision configuration") + debug: Optional[bool] = Field(None, description="Debug mode") + + +class ConfigResponse(BaseModel): + """Response model for configuration endpoints.""" + + success: bool + message: str + config: Optional[Dict[str, Any]] = None + + +class CaptureConfigUpdate(BaseModel): + """Request model for capture configuration updates.""" + + fps: Optional[int] = Field(None, ge=1, le=60) + quality: Optional[int] = Field(None, ge=1, le=100) + resolution: Optional[str] = None + region_x: Optional[int] = Field(None, ge=0) + region_y: Optional[int] = Field(None, ge=0) + region_width: Optional[int] = Field(None, gt=0) + region_height: Optional[int] = Field(None, gt=0) + + +class ServerConfigUpdate(BaseModel): + """Request model for server configuration updates.""" + + host: Optional[str] = None + port: Optional[int] = None + mdns_enabled: Optional[bool] = None + + +class GSProConfigUpdate(BaseModel): + """Request model for GSPro configuration updates.""" + + window_title: Optional[str] = None + auto_focus: Optional[bool] = None + key_delay: Optional[float] = Field(None, ge=0, le=1) + + +class VisionConfigUpdate(BaseModel): + """Request model for vision configuration updates.""" + + enabled: Optional[bool] = None + ocr_engine: Optional[str] = None + confidence_threshold: Optional[float] = Field(None, ge=0, le=1) + + +@router.get("/", response_model=Dict[str, Any]) +async def get_configuration(): + """ + Get the current application configuration. + + Returns all configuration sections including server, capture, gspro, and vision settings. + """ + config = get_config() + return config.to_dict() + + +@router.put("/", response_model=ConfigResponse) +async def update_configuration(request: ConfigUpdateRequest): + """ + Update application configuration. + + Only provided fields will be updated. Nested configuration objects are merged. + """ + config = get_config() + + try: + update_dict = request.model_dump(exclude_none=True) + + if not update_dict: + return ConfigResponse(success=False, message="No configuration changes provided") + + # Update configuration + config.update(**update_dict) + + logger.info(f"Configuration updated: {list(update_dict.keys())}") + + return ConfigResponse(success=True, message="Configuration updated successfully", config=config.to_dict()) + + except Exception as e: + logger.error(f"Failed to update configuration: {e}") + raise HTTPException(status_code=500, detail=f"Failed to update configuration: {str(e)}") + + +@router.post("/save", response_model=ConfigResponse) +async def save_configuration(): + """ + Save the current configuration to disk. + + This ensures changes persist across application restarts. + """ + config = get_config() + + try: + config.save() + + return ConfigResponse( + success=True, message=f"Configuration saved to {config.config_path}", config=config.to_dict() + ) + + except Exception as e: + logger.error(f"Failed to save configuration: {e}") + raise HTTPException(status_code=500, detail=f"Failed to save configuration: {str(e)}") + + +@router.post("/reset", response_model=ConfigResponse) +async def reset_configuration(): + """ + Reset configuration to default values. + + This will overwrite any custom settings with the application defaults. + """ + config = get_config() + + try: + config.reset() + + logger.info("Configuration reset to defaults") + + return ConfigResponse(success=True, message="Configuration reset to defaults", config=config.to_dict()) + + except Exception as e: + logger.error(f"Failed to reset configuration: {e}") + raise HTTPException(status_code=500, detail=f"Failed to reset configuration: {str(e)}") + + +@router.get("/server") +async def get_server_config(): + """Get server-specific configuration.""" + config = get_config() + return config.server.model_dump() + + +@router.put("/server") +async def update_server_config(request: ServerConfigUpdate): + """ + Update server configuration. + + Note: Changing host or port requires application restart to take effect. + """ + config = get_config() + + updates = request.model_dump(exclude_none=True) + + if not updates: + return {"success": False, "message": "No updates provided"} + + try: + config.update(server=updates) + + return { + "success": True, + "message": "Server configuration updated (restart required for host/port changes)", + "config": config.server.model_dump(), + } + + except Exception as e: + logger.error(f"Failed to update server config: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/capture") +async def get_capture_config(): + """Get capture/streaming configuration.""" + config = get_config() + return config.capture.model_dump() + + +@router.put("/capture") +async def update_capture_config(request: CaptureConfigUpdate): + """Update capture/streaming configuration.""" + config = get_config() + + updates = request.model_dump(exclude_none=True) + + if not updates: + return {"success": False, "message": "No updates provided"} + + try: + config.update(capture=updates) + + return {"success": True, "message": "Capture configuration updated", "config": config.capture.model_dump()} + + except Exception as e: + logger.error(f"Failed to update capture config: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/gspro") +async def get_gspro_config(): + """Get GSPro-specific configuration.""" + config = get_config() + return config.gspro.model_dump() + + +@router.put("/gspro") +async def update_gspro_config(request: GSProConfigUpdate): + """Update GSPro-specific configuration.""" + config = get_config() + + updates = request.model_dump(exclude_none=True) + + if not updates: + return {"success": False, "message": "No updates provided"} + + try: + config.update(gspro=updates) + + return {"success": True, "message": "GSPro configuration updated", "config": config.gspro.model_dump()} + + except Exception as e: + logger.error(f"Failed to update GSPro config: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/vision") +async def get_vision_config(): + """Get vision/OCR configuration (V2 features).""" + config = get_config() + return {**config.vision.model_dump(), "note": "Vision features are planned for V2"} + + +@router.put("/vision") +async def update_vision_config(request: VisionConfigUpdate): + """ + Update vision/OCR configuration. + + Note: These features are planned for V2 and not yet implemented. + """ + config = get_config() + + updates = request.model_dump(exclude_none=True) + + if not updates: + return {"success": False, "message": "No updates provided"} + + # Validate OCR engine if provided + if "ocr_engine" in updates and updates["ocr_engine"] not in ["easyocr", "tesseract"]: + raise HTTPException(status_code=400, detail="Invalid OCR engine. Must be 'easyocr' or 'tesseract'") + + try: + config.update(vision=updates) + + return { + "success": True, + "message": "Vision configuration updated (V2 features)", + "config": config.vision.model_dump(), + } + + except Exception as e: + logger.error(f"Failed to update vision config: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/reload", response_model=ConfigResponse) +async def reload_configuration(): + """ + Reload configuration from disk. + + This discards any unsaved changes and reloads from the configuration file. + """ + try: + # Reset the global config instance + reset_config() + + # Get new config (will load from disk) + config = get_config() + + logger.info("Configuration reloaded from disk") + + return ConfigResponse( + success=True, message=f"Configuration reloaded from {config.config_path}", config=config.to_dict() + ) + + except Exception as e: + logger.error(f"Failed to reload configuration: {e}") + raise HTTPException(status_code=500, detail=f"Failed to reload configuration: {str(e)}") + + +@router.get("/path") +async def get_config_path(): + """Get the configuration file path.""" + config = get_config() + return {"path": str(config.config_path), "exists": config.config_path.exists() if config.config_path else False} + + +@router.get("/validate") +async def validate_configuration(): + """ + Validate the current configuration. + + Checks for common issues and required settings. + """ + config = get_config() + + issues = [] + warnings = [] + + # Check server configuration + if config.server.port < 1024: + warnings.append("Server port is below 1024, may require administrator privileges") + + # Check capture configuration + if config.capture.fps > 30: + warnings.append("High FPS may impact performance") + + if config.capture.quality < 50: + warnings.append("Low JPEG quality may result in poor image clarity") + + # Check GSPro configuration + if not config.gspro.window_title: + issues.append("GSPro window title is not set") + + # Check vision configuration + if config.vision.enabled: + warnings.append("Vision features are enabled but not yet implemented (V2)") + + return { + "valid": len(issues) == 0, + "issues": issues, + "warnings": warnings, + "summary": f"{len(issues)} issues, {len(warnings)} warnings", + } diff --git a/backend/app/api/system.py b/backend/app/api/system.py new file mode 100644 index 0000000..7c8fa9d --- /dev/null +++ b/backend/app/api/system.py @@ -0,0 +1,409 @@ +""" +System API for health checks, system information, and diagnostics. +""" + +import logging +import platform +import psutil +import os +from typing import Dict, Any, Optional +from datetime import datetime, timedelta +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from ..core.config import get_config +from ..core.input_ctrl import is_gspro_running, get_gspro_process_info + +logger = logging.getLogger(__name__) +router = APIRouter() + +# Track application start time +APP_START_TIME = datetime.now() + + +@router.get("/health") +async def health_check(): + """ + Health check endpoint for monitoring. + + Returns basic health status and service availability. + """ + config = get_config() + + # Check GSPro status + gspro_running = is_gspro_running(config.gspro.window_title) + + # Calculate uptime + uptime = datetime.now() - APP_START_TIME + + return { + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "uptime_seconds": int(uptime.total_seconds()), + "services": { + "api": "running", + "gspro": "connected" if gspro_running else "disconnected", + "mdns": "enabled" if config.server.mdns_enabled else "disabled", + }, + } + + +@router.get("/info") +async def system_info(): + """ + Get detailed system information. + + Returns information about the host system, Python environment, and application. + """ + config = get_config() + + # Get system information + system_info = { + "platform": { + "system": platform.system(), + "release": platform.release(), + "version": platform.version(), + "machine": platform.machine(), + "processor": platform.processor(), + "python_version": platform.python_version(), + }, + "hardware": { + "cpu_count": psutil.cpu_count(), + "cpu_percent": psutil.cpu_percent(interval=1), + "memory": { + "total": psutil.virtual_memory().total, + "available": psutil.virtual_memory().available, + "percent": psutil.virtual_memory().percent, + "used": psutil.virtual_memory().used, + }, + "disk": { + "total": psutil.disk_usage("/").total, + "used": psutil.disk_usage("/").used, + "free": psutil.disk_usage("/").free, + "percent": psutil.disk_usage("/").percent, + }, + }, + "network": { + "hostname": platform.node(), + "interfaces": _get_network_interfaces(), + }, + "application": { + "version": "0.1.0", + "config_path": str(config.config_path), + "debug_mode": config.debug, + "uptime": str(datetime.now() - APP_START_TIME), + "server": { + "host": config.server.host, + "port": config.server.port, + "url": f"http://{config.server.host}:{config.server.port}", + "mdns_url": f"http://gsproapp.local:{config.server.port}" if config.server.mdns_enabled else None, + }, + }, + } + + return system_info + + +@router.get("/gspro/status") +async def gspro_status(): + """ + Get GSPro application status. + + Returns information about the GSPro process if it's running. + """ + config = get_config() + + is_running = is_gspro_running(config.gspro.window_title) + process_info = get_gspro_process_info() if is_running else None + + return { + "running": is_running, + "window_title": config.gspro.window_title, + "process": process_info, + "auto_focus": config.gspro.auto_focus, + "key_delay": config.gspro.key_delay, + } + + +@router.get("/gspro/find") +async def find_gspro_window(): + """ + Search for GSPro windows. + + Helps users identify the correct window title for configuration. + """ + try: + import win32gui + + def enum_window_callback(hwnd, windows): + if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): + window_text = win32gui.GetWindowText(hwnd) + if window_text and len(window_text) > 0: + # Look for windows that might be GSPro + if any(keyword in window_text.lower() for keyword in ["gspro", "golf", "simulator"]): + windows.append( + {"title": window_text, "hwnd": hwnd, "suggested": "gspro" in window_text.lower()} + ) + return True + + windows = [] + win32gui.EnumWindows(enum_window_callback, windows) + + return { + "found": len(windows) > 0, + "windows": windows, + "message": "Found potential GSPro windows" if windows else "No GSPro windows found", + } + + except Exception as e: + logger.error(f"Failed to search for GSPro windows: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/metrics") +async def get_metrics(): + """ + Get application metrics and performance statistics. + """ + config = get_config() + + # Get current resource usage + process = psutil.Process() + + metrics = { + "timestamp": datetime.now().isoformat(), + "uptime_seconds": int((datetime.now() - APP_START_TIME).total_seconds()), + "resources": { + "cpu_percent": process.cpu_percent(), + "memory_mb": process.memory_info().rss / 1024 / 1024, + "memory_percent": process.memory_percent(), + "threads": process.num_threads(), + "open_files": len(process.open_files()) if hasattr(process, "open_files") else 0, + "connections": len(process.connections()) if hasattr(process, "connections") else 0, + }, + "system": { + "cpu_percent": psutil.cpu_percent(interval=0.5), + "memory_percent": psutil.virtual_memory().percent, + "disk_io": psutil.disk_io_counters()._asdict() if psutil.disk_io_counters() else {}, + "network_io": psutil.net_io_counters()._asdict() if psutil.net_io_counters() else {}, + }, + } + + return metrics + + +@router.get("/logs") +async def get_logs(lines: int = 100): + """ + Get recent application logs. + + Args: + lines: Number of log lines to return (max 1000) + """ + # This is a placeholder - in production, you'd read from actual log files + return { + "message": "Log retrieval not yet implemented", + "lines_requested": min(lines, 1000), + "log_level": logging.getLevelName(logger.getEffectiveLevel()), + } + + +@router.post("/restart") +async def restart_application(): + """ + Restart the application. + + Note: This endpoint requires proper process management setup. + """ + # This would typically signal the process manager to restart + # For now, just return a message + return { + "success": False, + "message": "Application restart requires process manager setup. Please restart manually.", + } + + +@router.get("/dependencies") +async def check_dependencies(): + """ + Check if all required dependencies are installed and accessible. + """ + dependencies = { + "required": {}, + "optional": {}, + } + + # Check required dependencies + required_modules = [ + "fastapi", + "uvicorn", + "pydantic", + "pydirectinput", + "mss", + "PIL", + "cv2", + "numpy", + "win32gui", + "psutil", + "zeroconf", + ] + + for module_name in required_modules: + try: + module = __import__(module_name) + version = getattr(module, "__version__", "unknown") + dependencies["required"][module_name] = {"installed": True, "version": version} + except ImportError: + dependencies["required"][module_name] = {"installed": False, "version": None} + + # Check optional dependencies (V2 features) + optional_modules = [ + "easyocr", + "pytesseract", + ] + + for module_name in optional_modules: + try: + module = __import__(module_name) + version = getattr(module, "__version__", "unknown") + dependencies["optional"][module_name] = {"installed": True, "version": version} + except ImportError: + dependencies["optional"][module_name] = {"installed": False, "version": None} + + # Check if all required dependencies are installed + all_installed = all(dep["installed"] for dep in dependencies["required"].values()) + + return { + "all_required_installed": all_installed, + "dependencies": dependencies, + "message": "All required dependencies installed" if all_installed else "Some required dependencies are missing", + } + + +@router.get("/diagnostics") +async def run_diagnostics(): + """ + Run comprehensive system diagnostics. + + Checks various aspects of the system and application configuration. + """ + config = get_config() + diagnostics = {"timestamp": datetime.now().isoformat(), "checks": []} + + # Check 1: GSPro connectivity + gspro_running = is_gspro_running(config.gspro.window_title) + diagnostics["checks"].append( + { + "name": "GSPro Connectivity", + "status": "pass" if gspro_running else "fail", + "message": "GSPro is running and accessible" if gspro_running else "GSPro window not found", + } + ) + + # Check 2: Network accessibility + try: + import socket + + socket.create_connection(("8.8.8.8", 53), timeout=3) + network_ok = True + network_msg = "Network connection is working" + except: + network_ok = False + network_msg = "No internet connection detected" + + diagnostics["checks"].append( + { + "name": "Network Connectivity", + "status": "pass" if network_ok else "warning", + "message": network_msg, + } + ) + + # Check 3: Configuration validity + config_valid = config.config_path and config.config_path.exists() + diagnostics["checks"].append( + { + "name": "Configuration File", + "status": "pass" if config_valid else "warning", + "message": f"Configuration file exists at {config.config_path}" + if config_valid + else "Configuration file not found", + } + ) + + # Check 4: Available disk space + disk_usage = psutil.disk_usage("/") + disk_ok = disk_usage.percent < 90 + diagnostics["checks"].append( + { + "name": "Disk Space", + "status": "pass" if disk_ok else "warning", + "message": f"Disk usage at {disk_usage.percent:.1f}%" + + (" - Consider freeing space" if not disk_ok else ""), + } + ) + + # Check 5: Memory availability + memory = psutil.virtual_memory() + memory_ok = memory.percent < 90 + diagnostics["checks"].append( + { + "name": "Memory Availability", + "status": "pass" if memory_ok else "warning", + "message": f"Memory usage at {memory.percent:.1f}%" + + (" - High memory usage detected" if not memory_ok else ""), + } + ) + + # Check 6: Python version + import sys + + python_ok = sys.version_info >= (3, 11) + diagnostics["checks"].append( + { + "name": "Python Version", + "status": "pass" if python_ok else "warning", + "message": f"Python {platform.python_version()}" + + (" - Consider upgrading to 3.11+" if not python_ok else ""), + } + ) + + # Calculate overall status + statuses = [check["status"] for check in diagnostics["checks"]] + if "fail" in statuses: + overall = "fail" + elif "warning" in statuses: + overall = "warning" + else: + overall = "pass" + + diagnostics["overall_status"] = overall + diagnostics["summary"] = { + "passed": sum(1 for s in statuses if s == "pass"), + "warnings": sum(1 for s in statuses if s == "warning"), + "failures": sum(1 for s in statuses if s == "fail"), + } + + return diagnostics + + +def _get_network_interfaces(): + """Helper function to get network interface information.""" + interfaces = [] + + try: + for interface, addrs in psutil.net_if_addrs().items(): + for addr in addrs: + if addr.family == socket.AF_INET: # IPv4 + interfaces.append( + { + "name": interface, + "address": addr.address, + "netmask": addr.netmask, + "broadcast": addr.broadcast, + } + ) + except Exception as e: + logger.warning(f"Could not get network interfaces: {e}") + + return interfaces diff --git a/backend/app/api/vision.py b/backend/app/api/vision.py new file mode 100644 index 0000000..e4b7929 --- /dev/null +++ b/backend/app/api/vision.py @@ -0,0 +1,345 @@ +""" +Vision API for screen capture and streaming. +Currently implements WebSocket streaming for the map panel. +OCR and advanced vision features are gated behind configuration flags for V2. +""" + +import asyncio +import json +import logging +from typing import Optional, Dict, Any +from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException, Depends +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +from ..core.config import get_config +from ..core.screen import get_screen_capture, capture_region + +logger = logging.getLogger(__name__) +router = APIRouter() + + +class StreamConfig(BaseModel): + """Configuration for video streaming.""" + + fps: int = Field(30, ge=1, le=60, description="Frames per second") + quality: int = Field(85, ge=1, le=100, description="JPEG quality") + resolution: str = Field("720p", description="Stream resolution preset") + region_x: int = Field(0, ge=0, description="Region X coordinate") + region_y: int = Field(0, ge=0, description="Region Y coordinate") + region_width: int = Field(640, ge=1, description="Region width") + region_height: int = Field(480, ge=1, description="Region height") + + +class RegionDefinition(BaseModel): + """Definition of a screen region for capture.""" + + name: str = Field(..., description="Region name") + x: int = Field(..., ge=0, description="X coordinate") + y: int = Field(..., ge=0, description="Y coordinate") + width: int = Field(..., gt=0, description="Width") + height: int = Field(..., gt=0, description="Height") + description: Optional[str] = Field(None, description="Region description") + + +class CaptureRequest(BaseModel): + """Request to capture a screen region.""" + + x: int = Field(0, ge=0, description="X coordinate") + y: int = Field(0, ge=0, description="Y coordinate") + width: int = Field(640, gt=0, description="Width") + height: int = Field(480, gt=0, description="Height") + format: str = Field("base64", description="Output format (base64 or raw)") + quality: int = Field(85, ge=1, le=100, description="JPEG quality") + + +class StreamManager: + """Manages WebSocket streaming sessions.""" + + def __init__(self): + self.active_streams: Dict[str, WebSocket] = {} + self.stream_configs: Dict[str, StreamConfig] = {} + + async def add_stream(self, client_id: str, websocket: WebSocket, config: StreamConfig): + """Add a new streaming session.""" + await websocket.accept() + self.active_streams[client_id] = websocket + self.stream_configs[client_id] = config + logger.info(f"Stream started for client {client_id}") + + async def remove_stream(self, client_id: str): + """Remove a streaming session.""" + if client_id in self.active_streams: + del self.active_streams[client_id] + del self.stream_configs[client_id] + logger.info(f"Stream stopped for client {client_id}") + + async def stream_frame(self, client_id: str, frame_data: str): + """Send a frame to a specific client.""" + if client_id in self.active_streams: + websocket = self.active_streams[client_id] + try: + await websocket.send_text(frame_data) + except Exception as e: + logger.error(f"Failed to send frame to client {client_id}: {e}") + await self.remove_stream(client_id) + + +# Global stream manager +stream_manager = StreamManager() + + +@router.websocket("/ws/stream") +async def stream_video(websocket: WebSocket): + """ + WebSocket endpoint for streaming video of a screen region. + + The client should send a JSON message with stream configuration: + { + "action": "start", + "config": { + "fps": 30, + "quality": 85, + "resolution": "720p", + "region_x": 0, + "region_y": 0, + "region_width": 640, + "region_height": 480 + } + } + """ + config = get_config() + client_id = id(websocket) + + try: + # Accept the WebSocket connection + await websocket.accept() + logger.info(f"WebSocket connected: client {client_id}") + + # Wait for initial configuration message + data = await websocket.receive_text() + message = json.loads(data) + + if message.get("action") != "start": + await websocket.send_json({"error": "First message must be start action"}) + await websocket.close() + return + + # Parse stream configuration + stream_config = StreamConfig(**message.get("config", {})) + + # Override with server config if needed + stream_config.fps = min(stream_config.fps, config.capture.fps) + stream_config.quality = config.capture.quality + + # Add to stream manager + await stream_manager.add_stream(str(client_id), websocket, stream_config) + + # Send confirmation + await websocket.send_json({"type": "config", "data": stream_config.model_dump()}) + + # Get screen capture instance + capture = get_screen_capture() + + # Calculate frame interval + frame_interval = 1.0 / stream_config.fps + + # Streaming loop + while True: + try: + # Capture the region + image = capture.capture_region( + stream_config.region_x, + stream_config.region_y, + stream_config.region_width, + stream_config.region_height, + ) + + # Resize if needed + if stream_config.resolution != "native": + target_width, target_height = capture.get_resolution_preset(stream_config.resolution) + # Only resize if different from capture size + if target_width != stream_config.region_width or target_height != stream_config.region_height: + image = capture.resize_image(image, width=target_width) + + # Convert to base64 + base64_image = capture.image_to_base64(image, quality=stream_config.quality) + + # Send frame + frame_data = json.dumps( + {"type": "frame", "data": base64_image, "timestamp": asyncio.get_event_loop().time()} + ) + + await websocket.send_text(frame_data) + + # Wait for next frame + await asyncio.sleep(frame_interval) + + except WebSocketDisconnect: + logger.info(f"Client {client_id} disconnected") + break + except Exception as e: + logger.error(f"Error in stream loop for client {client_id}: {e}") + await websocket.send_json({"type": "error", "message": str(e)}) + break + + except Exception as e: + logger.error(f"WebSocket error for client {client_id}: {e}") + finally: + await stream_manager.remove_stream(str(client_id)) + logger.info(f"WebSocket closed: client {client_id}") + + +@router.post("/capture", response_model=Dict[str, Any]) +async def capture_screen_region(request: CaptureRequest): + """ + Capture a single frame from a screen region. + + This is useful for testing or getting a single snapshot. + """ + try: + capture = get_screen_capture() + + # Capture the region + image = capture.capture_region(request.x, request.y, request.width, request.height) + + if request.format == "base64": + # Convert to base64 + base64_image = capture.image_to_base64(image, quality=request.quality) + + return { + "success": True, + "format": "base64", + "data": base64_image, + "width": request.width, + "height": request.height, + } + else: + return {"success": False, "error": f"Unsupported format: {request.format}"} + + except Exception as e: + logger.error(f"Failed to capture region: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/regions") +async def get_regions(): + """ + Get predefined screen regions for capture. + + Returns common regions like map area, club indicator, etc. + """ + config = get_config() + + # Predefined regions for GSPro UI elements + regions = { + "map": { + "name": "Map Panel", + "x": config.capture.region_x, + "y": config.capture.region_y, + "width": config.capture.region_width, + "height": config.capture.region_height, + "description": "GSPro mini-map or expanded map view", + }, + "club": { + "name": "Club Indicator", + "x": 50, + "y": 200, + "width": 200, + "height": 100, + "description": "Current club selection display", + }, + "shot_info": { + "name": "Shot Information", + "x": 50, + "y": 50, + "width": 300, + "height": 150, + "description": "Shot distance and trajectory information", + }, + "scorecard": { + "name": "Scorecard", + "x": 400, + "y": 100, + "width": 800, + "height": 600, + "description": "Scorecard overlay when visible", + }, + } + + return {"regions": regions, "total": len(regions)} + + +@router.post("/regions/{region_name}") +async def update_region(region_name: str, region: RegionDefinition): + """ + Update or create a screen region definition. + + This allows users to define custom regions for their setup. + """ + config = get_config() + + if region_name == "map": + # Update the map region in config + config.capture.region_x = region.x + config.capture.region_y = region.y + config.capture.region_width = region.width + config.capture.region_height = region.height + config.save() + + return {"success": True, "message": f"Region '{region_name}' updated", "region": region.model_dump()} + else: + # For now, only map region is persisted + # V2 will add support for custom regions + return {"success": False, "message": "Custom regions not yet supported (V2 feature)"} + + +# OCR endpoints - gated behind vision config flag +@router.post("/ocr") +async def perform_ocr(request: CaptureRequest): + """ + Perform OCR on a screen region (V2 feature). + + This endpoint is only available when vision features are enabled. + """ + config = get_config() + + if not config.vision.enabled: + raise HTTPException(status_code=403, detail="Vision features are not enabled. This is a V2 feature.") + + # OCR implementation will go here in V2 + return {"success": False, "message": "OCR features coming in V2", "vision_enabled": config.vision.enabled} + + +@router.get("/markers") +async def get_markers(): + """ + Get visual markers for template matching (V2 feature). + + This endpoint is only available when vision features are enabled. + """ + config = get_config() + + if not config.vision.enabled: + raise HTTPException(status_code=403, detail="Vision features are not enabled. This is a V2 feature.") + + # Marker management will go here in V2 + return {"markers": [], "message": "Marker features coming in V2", "vision_enabled": config.vision.enabled} + + +@router.get("/status") +async def get_vision_status(): + """Get the status of vision features.""" + config = get_config() + + return { + "streaming_enabled": True, + "ocr_enabled": config.vision.enabled, + "markers_enabled": config.vision.enabled, + "active_streams": len(stream_manager.active_streams), + "capture_config": { + "fps": config.capture.fps, + "quality": config.capture.quality, + "resolution": config.capture.resolution, + }, + } diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..6eaef36 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1,21 @@ +""" +Core modules for GSPro Remote backend. +""" + +from .config import AppConfig, get_config +from .input_ctrl import press_key, press_keys, key_down, key_up, focus_window, is_gspro_running +from .screen import capture_screen, get_screen_size, capture_region + +__all__ = [ + "AppConfig", + "get_config", + "press_key", + "press_keys", + "key_down", + "key_up", + "focus_window", + "is_gspro_running", + "capture_screen", + "get_screen_size", + "capture_region", +] diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..34dd4b3 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,193 @@ +""" +Configuration management for GSPro Remote. +""" + +import json +import logging +from pathlib import Path +from typing import Optional +from functools import lru_cache + +from pydantic import BaseModel, Field +from pydantic_settings import BaseSettings + +logger = logging.getLogger(__name__) + + +class ServerConfig(BaseModel): + """Server configuration settings.""" + + host: str = Field("0.0.0.0", description="Server host address") + port: int = Field(5005, description="Server port") + mdns_enabled: bool = Field(True, description="Enable mDNS service discovery") + + +class CaptureConfig(BaseModel): + """Screen capture configuration settings.""" + + fps: int = Field(30, description="Frames per second for streaming") + quality: int = Field(85, description="JPEG quality (0-100)") + resolution: str = Field("720p", description="Stream resolution") + region_x: int = Field(0, description="Map region X coordinate") + region_y: int = Field(0, description="Map region Y coordinate") + region_width: int = Field(640, description="Map region width") + region_height: int = Field(480, description="Map region height") + + +class GSProConfig(BaseModel): + """GSPro application configuration settings.""" + + window_title: str = Field("GSPro", description="GSPro window title") + auto_focus: bool = Field(True, description="Auto-focus GSPro window before sending keys") + key_delay: float = Field(0.05, description="Default delay between key presses (seconds)") + + +class VisionConfig(BaseModel): + """Computer vision configuration settings (for V2 features).""" + + enabled: bool = Field(False, description="Enable vision features") + ocr_engine: str = Field("easyocr", description="OCR engine to use (easyocr or tesseract)") + confidence_threshold: float = Field(0.7, description="Minimum confidence for OCR detection") + + +class AppConfig(BaseSettings): + """Main application configuration.""" + + server: ServerConfig = Field(default_factory=ServerConfig) + capture: CaptureConfig = Field(default_factory=CaptureConfig) + gspro: GSProConfig = Field(default_factory=GSProConfig) + vision: VisionConfig = Field(default_factory=VisionConfig) + + config_path: Optional[Path] = None + debug: bool = Field(False, description="Enable debug mode") + + class Config: + env_prefix = "GSPRO_REMOTE_" + env_nested_delimiter = "__" + case_sensitive = False + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if self.config_path is None: + self.config_path = self._get_default_config_path() + self.load() + + @staticmethod + def _get_default_config_path() -> Path: + """Get the default configuration file path.""" + import os + + if os.name == "nt": # Windows + base_path = Path(os.environ.get("LOCALAPPDATA", "")) + if not base_path: + base_path = Path.home() / "AppData" / "Local" + else: # Unix-like + base_path = Path.home() / ".config" + + config_dir = base_path / "GSPro Remote" + config_dir.mkdir(parents=True, exist_ok=True) + return config_dir / "config.json" + + def load(self) -> None: + """Load configuration from file.""" + if self.config_path and self.config_path.exists(): + try: + with open(self.config_path, "r") as f: + data = json.load(f) + + # Update configuration with loaded data + if "server" in data: + self.server = ServerConfig(**data["server"]) + if "capture" in data: + self.capture = CaptureConfig(**data["capture"]) + if "gspro" in data: + self.gspro = GSProConfig(**data["gspro"]) + if "vision" in data: + self.vision = VisionConfig(**data["vision"]) + if "debug" in data: + self.debug = data["debug"] + + logger.info(f"Configuration loaded from {self.config_path}") + except Exception as e: + logger.warning(f"Failed to load configuration: {e}") + self.save() # Save default configuration + else: + # Create default configuration file + self.save() + logger.info(f"Created default configuration at {self.config_path}") + + def save(self) -> None: + """Save configuration to file.""" + if self.config_path: + try: + self.config_path.parent.mkdir(parents=True, exist_ok=True) + + data = { + "server": self.server.model_dump(), + "capture": self.capture.model_dump(), + "gspro": self.gspro.model_dump(), + "vision": self.vision.model_dump(), + "debug": self.debug, + } + + with open(self.config_path, "w") as f: + json.dump(data, f, indent=2) + + logger.info(f"Configuration saved to {self.config_path}") + except Exception as e: + logger.error(f"Failed to save configuration: {e}") + + def update(self, **kwargs) -> None: + """Update configuration with new values.""" + for key, value in kwargs.items(): + if hasattr(self, key): + if isinstance(value, dict): + # Update nested configuration + current = getattr(self, key) + if isinstance(current, BaseModel): + for sub_key, sub_value in value.items(): + if hasattr(current, sub_key): + setattr(current, sub_key, sub_value) + else: + setattr(self, key, value) + self.save() + + def reset(self) -> None: + """Reset configuration to defaults.""" + self.server = ServerConfig() + self.capture = CaptureConfig() + self.gspro = GSProConfig() + self.vision = VisionConfig() + self.debug = False + self.save() + + def to_dict(self) -> dict: + """Convert configuration to dictionary.""" + return { + "server": self.server.model_dump(), + "capture": self.capture.model_dump(), + "gspro": self.gspro.model_dump(), + "vision": self.vision.model_dump(), + "debug": self.debug, + "config_path": str(self.config_path) if self.config_path else None, + } + + +# Global configuration instance +_config: Optional[AppConfig] = None + + +@lru_cache(maxsize=1) +def get_config() -> AppConfig: + """Get the global configuration instance.""" + global _config + if _config is None: + _config = AppConfig() + return _config + + +def reset_config() -> None: + """Reset the global configuration instance.""" + global _config + _config = None + get_config.cache_clear() diff --git a/backend/app/core/input_ctrl.py b/backend/app/core/input_ctrl.py new file mode 100644 index 0000000..c234e12 --- /dev/null +++ b/backend/app/core/input_ctrl.py @@ -0,0 +1,350 @@ +""" +Windows input control module for simulating keyboard inputs to GSPro. +""" + +import logging +import time +from typing import Optional, List + +try: + import pydirectinput + import win32gui + import win32con + import win32process + import psutil +except ImportError as e: + raise ImportError(f"Required Windows dependencies not installed: {e}") + +logger = logging.getLogger(__name__) + +# Configure pydirectinput +pydirectinput.PAUSE = 0.01 # Reduce default pause between actions + + +def is_gspro_running(window_title: str = "GSPro") -> bool: + """ + Check if GSPro is running by looking for its window. + + Args: + window_title: The window title to search for + + Returns: + True if GSPro window is found, False otherwise + """ + + def enum_window_callback(hwnd, windows): + if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): + window_text = win32gui.GetWindowText(hwnd) + if window_title.lower() in window_text.lower(): + windows.append(hwnd) + return True + + windows = [] + win32gui.EnumWindows(enum_window_callback, windows) + return len(windows) > 0 + + +def find_gspro_window(window_title: str = "GSPro") -> Optional[int]: + """ + Find the GSPro window handle. + + Args: + window_title: The window title to search for + + Returns: + Window handle if found, None otherwise + """ + + def enum_window_callback(hwnd, result): + window_text = win32gui.GetWindowText(hwnd) + if window_title.lower() in window_text.lower(): + result.append(hwnd) + return True + + result = [] + win32gui.EnumWindows(enum_window_callback, result) + + if result: + return result[0] + return None + + +def focus_window(window_title: str = "GSPro") -> bool: + """ + Focus the GSPro window to ensure it receives keyboard input. + + Args: + window_title: The window title to focus + + Returns: + True if window was focused successfully, False otherwise + """ + try: + hwnd = find_gspro_window(window_title) + if hwnd: + # Restore window if minimized + if win32gui.IsIconic(hwnd): + win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) + + # Set foreground window + win32gui.SetForegroundWindow(hwnd) + + # Small delay to ensure window is focused + time.sleep(0.1) + + logger.debug(f"Focused window: {window_title}") + return True + else: + logger.warning(f"Window not found: {window_title}") + return False + except Exception as e: + logger.error(f"Failed to focus window: {e}") + return False + + +def press_key(key: str, interval: float = 0.0) -> None: + """ + Simulate a single key press. + + Args: + key: The key to press (e.g., 'a', 'space', 'f1', 'up') + interval: Time to wait after pressing the key + """ + try: + # Normalize key name for pydirectinput + key_normalized = key.lower().strip() + + # Handle special key mappings + key_mappings = { + "ctrl": "ctrl", + "control": "ctrl", + "alt": "alt", + "shift": "shift", + "tab": "tab", + "space": "space", + "enter": "enter", + "return": "enter", + "escape": "esc", + "esc": "esc", + "backspace": "backspace", + "delete": "delete", + "del": "delete", + "insert": "insert", + "ins": "insert", + "home": "home", + "end": "end", + "pageup": "pageup", + "pagedown": "pagedown", + "up": "up", + "down": "down", + "left": "left", + "right": "right", + "plus": "+", + "minus": "-", + "apostrophe": "'", + "quote": "'", + } + + # Map key if needed + key_to_press = key_mappings.get(key_normalized, key_normalized) + + # Press the key + pydirectinput.press(key_to_press) + + if interval > 0: + time.sleep(interval) + + logger.debug(f"Pressed key: {key}") + except Exception as e: + logger.error(f"Failed to press key '{key}': {e}") + raise + + +def press_keys(keys: str, interval: float = 0.0) -> None: + """ + Simulate a key combination or sequence. + + Args: + keys: Key combination string (e.g., 'ctrl+m', 'shift+tab') + interval: Time to wait after pressing the keys + """ + try: + # Check if it's a key combination + if "+" in keys: + # Split into modifiers and key + parts = keys.lower().split("+") + modifiers = [] + main_key = parts[-1] + + # Identify modifiers + for part in parts[:-1]: + if part in ["ctrl", "control"]: + modifiers.append("ctrl") + elif part in ["alt"]: + modifiers.append("alt") + elif part in ["shift"]: + modifiers.append("shift") + elif part in ["win", "windows", "cmd", "command"]: + modifiers.append("win") + + # Press combination using hotkey + if modifiers: + hotkey_parts = modifiers + [main_key] + pydirectinput.hotkey(*hotkey_parts) + else: + press_key(main_key) + else: + # Single key press + press_key(keys) + + if interval > 0: + time.sleep(interval) + + logger.debug(f"Pressed keys: {keys}") + except Exception as e: + logger.error(f"Failed to press keys '{keys}': {e}") + raise + + +def key_down(key: str) -> None: + """ + Hold a key down. + + Args: + key: The key to hold down + """ + try: + key_normalized = key.lower().strip() + pydirectinput.keyDown(key_normalized) + logger.debug(f"Key down: {key}") + except Exception as e: + logger.error(f"Failed to hold key down '{key}': {e}") + raise + + +def key_up(key: str) -> None: + """ + Release a held key. + + Args: + key: The key to release + """ + try: + key_normalized = key.lower().strip() + pydirectinput.keyUp(key_normalized) + logger.debug(f"Key up: {key}") + except Exception as e: + logger.error(f"Failed to release key '{key}': {e}") + raise + + +def type_text(text: str, interval: float = 0.0) -> None: + """ + Type a string of text. + + Args: + text: The text to type + interval: Time between each character + """ + try: + pydirectinput.typewrite(text, interval=interval) + logger.debug(f"Typed text: {text[:20]}...") + except Exception as e: + logger.error(f"Failed to type text: {e}") + raise + + +def mouse_click(x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> None: + """ + Simulate a mouse click. + + Args: + x: X coordinate (None for current position) + y: Y coordinate (None for current position) + button: Mouse button ('left', 'right', 'middle') + """ + try: + if x is not None and y is not None: + pydirectinput.click(x, y, button=button) + logger.debug(f"Mouse click at ({x}, {y}) with {button} button") + else: + pydirectinput.click(button=button) + logger.debug(f"Mouse click with {button} button") + except Exception as e: + logger.error(f"Failed to perform mouse click: {e}") + raise + + +def mouse_move(x: int, y: int, duration: float = 0.0) -> None: + """ + Move the mouse cursor. + + Args: + x: Target X coordinate + y: Target Y coordinate + duration: Time to take for the movement + """ + try: + if duration > 0: + pydirectinput.moveTo(x, y, duration=duration) + else: + pydirectinput.moveTo(x, y) + logger.debug(f"Mouse moved to ({x}, {y})") + except Exception as e: + logger.error(f"Failed to move mouse: {e}") + raise + + +def get_gspro_process_info() -> Optional[dict]: + """ + Get information about the GSPro process if it's running. + + Returns: + Dictionary with process info or None if not found + """ + try: + for proc in psutil.process_iter(["pid", "name", "cpu_percent", "memory_info"]): + if "gspro" in proc.info["name"].lower(): + return { + "pid": proc.info["pid"], + "name": proc.info["name"], + "cpu_percent": proc.info["cpu_percent"], + "memory_mb": proc.info["memory_info"].rss / 1024 / 1024 if proc.info["memory_info"] else 0, + } + except Exception as e: + logger.error(f"Failed to get GSPro process info: {e}") + return None + + +# Test function for development +def test_input_control(): + """Test function to verify input control is working.""" + print("Testing input control...") + + # Check if GSPro is running + if is_gspro_running(): + print("✓ GSPro is running") + + # Try to focus the window + if focus_window(): + print("✓ GSPro window focused") + else: + print("✗ Could not focus GSPro window") + else: + print("✗ GSPro is not running") + print("Please start GSPro and try again") + return + + # Get process info + info = get_gspro_process_info() + if info: + print( + f"✓ GSPro process found: PID={info['pid']}, CPU={info['cpu_percent']:.1f}%, Memory={info['memory_mb']:.1f}MB" + ) + + print("\nInput control test complete!") + + +if __name__ == "__main__": + # Run test when module is executed directly + test_input_control() diff --git a/backend/app/core/mdns.py b/backend/app/core/mdns.py new file mode 100644 index 0000000..a46e338 --- /dev/null +++ b/backend/app/core/mdns.py @@ -0,0 +1,335 @@ +""" +mDNS service registration for GSPro Remote. +Allows the application to be discovered on the local network. +""" + +import logging +import socket +import threading +from typing import Optional, Dict, Any + +try: + from zeroconf import ServiceInfo, Zeroconf, IPVersion +except ImportError as e: + raise ImportError(f"Zeroconf library not installed: {e}") + +logger = logging.getLogger(__name__) + + +class MDNSService: + """ + Manages mDNS/Bonjour service registration for network discovery. + """ + + def __init__( + self, + name: str = "gsproapp", + port: int = 5005, + service_type: str = "_http._tcp.local.", + properties: Optional[Dict[str, Any]] = None, + ): + """ + Initialize mDNS service. + + Args: + name: Service name (will be accessible as {name}.local) + port: Port number the service is running on + service_type: mDNS service type + properties: Additional service properties + """ + self.name = name + self.port = port + self.service_type = service_type + self.properties = properties or {} + + self.zeroconf: Optional[Zeroconf] = None + self.service_info: Optional[ServiceInfo] = None + self.is_running = False + self._lock = threading.Lock() + + def _get_local_ip(self) -> str: + """ + Get the local IP address of the machine. + + Returns: + Local IP address as string + """ + try: + # Create a socket to determine the local IP + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + # Connect to a public DNS server to determine local interface + s.connect(("8.8.8.8", 80)) + return s.getsockname()[0] + except Exception: + # Fallback to localhost if can't determine + return "127.0.0.1" + + def _create_service_info(self) -> ServiceInfo: + """ + Create the ServiceInfo object for registration. + + Returns: + Configured ServiceInfo object + """ + local_ip = self._get_local_ip() + hostname = socket.gethostname() + + # Create fully qualified service name + service_name = f"{self.name}.{self.service_type}" + + # Add default properties + default_properties = { + "version": "0.1.0", + "platform": "windows", + "api": "rest", + "ui": "web", + } + + # Merge with custom properties + all_properties = {**default_properties, **self.properties} + + # Convert properties to bytes + properties_bytes = {} + for key, value in all_properties.items(): + if isinstance(value, str): + properties_bytes[key] = value.encode("utf-8") + elif isinstance(value, bytes): + properties_bytes[key] = value + else: + properties_bytes[key] = str(value).encode("utf-8") + + # Create service info + service_info = ServiceInfo( + type_=self.service_type, + name=service_name, + addresses=[socket.inet_aton(local_ip)], + port=self.port, + properties=properties_bytes, + server=f"{hostname}.local.", + ) + + return service_info + + def start(self) -> bool: + """ + Start the mDNS service registration. + + Returns: + True if service started successfully, False otherwise + """ + with self._lock: + if self.is_running: + logger.warning("mDNS service is already running") + return True + + try: + # Create Zeroconf instance + self.zeroconf = Zeroconf(ip_version=IPVersion.V4Only) + + # Create and register service + self.service_info = self._create_service_info() + self.zeroconf.register_service(self.service_info) + + self.is_running = True + + logger.info(f"mDNS service registered: {self.name}.local:{self.port} (type: {self.service_type})") + + return True + + except Exception as e: + logger.error(f"Failed to start mDNS service: {e}") + self.cleanup() + return False + + def stop(self) -> None: + """Stop the mDNS service registration.""" + with self._lock: + if not self.is_running: + return + + self.cleanup() + self.is_running = False + logger.info("mDNS service stopped") + + def cleanup(self) -> None: + """Clean up mDNS resources.""" + try: + if self.zeroconf and self.service_info: + self.zeroconf.unregister_service(self.service_info) + + if self.zeroconf: + self.zeroconf.close() + self.zeroconf = None + + self.service_info = None + + except Exception as e: + logger.error(f"Error during mDNS cleanup: {e}") + + def update_properties(self, properties: Dict[str, Any]) -> bool: + """ + Update service properties. + + Args: + properties: New properties to set + + Returns: + True if properties updated successfully, False otherwise + """ + with self._lock: + if not self.is_running: + logger.warning("Cannot update properties: service is not running") + return False + + try: + self.properties.update(properties) + + # Recreate and re-register service with new properties + if self.zeroconf and self.service_info: + self.zeroconf.unregister_service(self.service_info) + self.service_info = self._create_service_info() + self.zeroconf.register_service(self.service_info) + + logger.info("mDNS service properties updated") + return True + + except Exception as e: + logger.error(f"Failed to update mDNS properties: {e}") + + return False + + def get_url(self) -> str: + """ + Get the URL for accessing the service. + + Returns: + Service URL + """ + return f"http://{self.name}.local:{self.port}" + + def __enter__(self): + """Context manager entry.""" + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.stop() + + +class MDNSBrowser: + """ + Browse for mDNS services on the network. + Useful for discovering other GSPro Remote instances. + """ + + def __init__(self, service_type: str = "_http._tcp.local."): + """ + Initialize mDNS browser. + + Args: + service_type: Type of services to browse for + """ + self.service_type = service_type + self.services: Dict[str, Dict[str, Any]] = {} + self.zeroconf: Optional[Zeroconf] = None + + def browse(self, timeout: float = 5.0) -> Dict[str, Dict[str, Any]]: + """ + Browse for services on the network. + + Args: + timeout: Time to wait for services (seconds) + + Returns: + Dictionary of discovered services + """ + try: + from zeroconf import ServiceBrowser, ServiceListener + import time + + class Listener(ServiceListener): + def __init__(self, browser): + self.browser = browser + + def add_service(self, zeroconf, service_type, name): + info = zeroconf.get_service_info(service_type, name) + if info: + self.browser.services[name] = { + "name": name, + "address": socket.inet_ntoa(info.addresses[0]) if info.addresses else None, + "port": info.port, + "properties": info.properties, + } + + def remove_service(self, zeroconf, service_type, name): + self.browser.services.pop(name, None) + + def update_service(self, zeroconf, service_type, name): + pass + + self.zeroconf = Zeroconf(ip_version=IPVersion.V4Only) + listener = Listener(self) + browser = ServiceBrowser(self.zeroconf, self.service_type, listener) + + # Wait for services to be discovered + time.sleep(timeout) + + browser.cancel() + self.zeroconf.close() + + return self.services + + except Exception as e: + logger.error(f"Failed to browse for services: {e}") + return {} + + +# Test function for development +def test_mdns_service(): + """Test mDNS service registration.""" + import time + + print("Testing mDNS service registration...") + + # Test service registration + service = MDNSService( + name="gsproapp-test", + port=5005, + properties={"test": "true", "instance": "development"}, + ) + + if service.start(): + print(f"✓ mDNS service started: {service.get_url()}") + print(f" You should be able to access it at: http://gsproapp-test.local:5005") + + # Keep service running for 10 seconds + print(" Service will run for 10 seconds...") + time.sleep(10) + + # Test property update + if service.update_properties({"status": "running", "uptime": "10s"}): + print("✓ Properties updated successfully") + + service.stop() + print("✓ mDNS service stopped") + else: + print("✗ Failed to start mDNS service") + + # Test service browsing + print("\nBrowsing for HTTP services on the network...") + browser = MDNSBrowser() + services = browser.browse(timeout=3.0) + + if services: + print(f"Found {len(services)} services:") + for name, info in services.items(): + print(f" - {name}: {info['address']}:{info['port']}") + else: + print("No services found") + + print("\nmDNS test complete!") + + +if __name__ == "__main__": + test_mdns_service() diff --git a/backend/app/core/screen.py b/backend/app/core/screen.py new file mode 100644 index 0000000..a63500f --- /dev/null +++ b/backend/app/core/screen.py @@ -0,0 +1,370 @@ +""" +Screen capture utilities for GSPro Remote. +""" + +import logging +from typing import Optional, Tuple, Dict, Any +from io import BytesIO +import base64 + +try: + import mss + import mss.tools + from PIL import Image + import cv2 + import numpy as np +except ImportError as e: + raise ImportError(f"Required screen capture dependencies not installed: {e}") + +logger = logging.getLogger(__name__) + + +class ScreenCapture: + """Manages screen capture operations.""" + + def __init__(self): + """Initialize screen capture manager.""" + self.sct = mss.mss() + self._monitor_info = None + + def get_monitors(self) -> list[dict]: + """ + Get information about all available monitors. + + Returns: + List of monitor information dictionaries + """ + monitors = [] + for i, monitor in enumerate(self.sct.monitors): + monitors.append( + { + "index": i, + "left": monitor["left"], + "top": monitor["top"], + "width": monitor["width"], + "height": monitor["height"], + "is_primary": i == 0, # Index 0 is combined virtual screen + } + ) + return monitors + + def get_primary_monitor(self) -> dict: + """ + Get the primary monitor information. + + Returns: + Primary monitor information + """ + # Index 1 is typically the primary monitor in mss + return self.sct.monitors[1] if len(self.sct.monitors) > 1 else self.sct.monitors[0] + + def capture_screen(self, monitor_index: int = 1) -> np.ndarray: + """ + Capture the entire screen. + + Args: + monitor_index: Index of the monitor to capture (0 for all, 1 for primary) + + Returns: + Captured screen as numpy array (BGR format) + """ + try: + monitor = self.sct.monitors[monitor_index] + screenshot = self.sct.grab(monitor) + + # Convert to numpy array (BGR format for OpenCV compatibility) + img = np.array(screenshot) + img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR) + + return img + except Exception as e: + logger.error(f"Failed to capture screen: {e}") + raise + + def capture_region(self, x: int, y: int, width: int, height: int, monitor_index: int = 1) -> np.ndarray: + """ + Capture a specific region of the screen. + + Args: + x: X coordinate of the region (relative to monitor) + y: Y coordinate of the region (relative to monitor) + width: Width of the region + height: Height of the region + monitor_index: Index of the monitor to capture from + + Returns: + Captured region as numpy array (BGR format) + """ + try: + monitor = self.sct.monitors[monitor_index] + + # Define region to capture + region = { + "left": monitor["left"] + x, + "top": monitor["top"] + y, + "width": width, + "height": height, + } + + screenshot = self.sct.grab(region) + + # Convert to numpy array + img = np.array(screenshot) + img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR) + + return img + except Exception as e: + logger.error(f"Failed to capture region: {e}") + raise + + def capture_window(self, window_title: str) -> Optional[np.ndarray]: + """ + Capture a specific window by title. + + Args: + window_title: Title of the window to capture + + Returns: + Captured window as numpy array or None if window not found + """ + try: + import win32gui + + def enum_window_callback(hwnd, windows): + if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): + window_text = win32gui.GetWindowText(hwnd) + if window_title.lower() in window_text.lower(): + windows.append(hwnd) + return True + + windows = [] + win32gui.EnumWindows(enum_window_callback, windows) + + if not windows: + logger.warning(f"Window not found: {window_title}") + return None + + # Get window rectangle + hwnd = windows[0] + rect = win32gui.GetWindowRect(hwnd) + x, y, x2, y2 = rect + width = x2 - x + height = y2 - y + + # Capture the window region + return self.capture_region(x, y, width, height, monitor_index=0) + + except Exception as e: + logger.error(f"Failed to capture window: {e}") + return None + + def image_to_base64(self, image: np.ndarray, quality: int = 85, format: str = "JPEG") -> str: + """ + Convert an image array to base64 string. + + Args: + image: Image as numpy array (BGR format) + quality: JPEG quality (1-100) + format: Image format (JPEG, PNG) + + Returns: + Base64 encoded image string + """ + try: + # Convert BGR to RGB for PIL + rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + pil_image = Image.fromarray(rgb_image) + + # Save to bytes + buffer = BytesIO() + if format.upper() == "JPEG": + pil_image.save(buffer, format=format, quality=quality, optimize=True) + else: + pil_image.save(buffer, format=format) + + # Encode to base64 + buffer.seek(0) + base64_string = base64.b64encode(buffer.getvalue()).decode("utf-8") + + return base64_string + except Exception as e: + logger.error(f"Failed to convert image to base64: {e}") + raise + + def resize_image(self, image: np.ndarray, width: Optional[int] = None, height: Optional[int] = None) -> np.ndarray: + """ + Resize an image while maintaining aspect ratio. + + Args: + image: Image as numpy array + width: Target width (None to calculate from height) + height: Target height (None to calculate from width) + + Returns: + Resized image as numpy array + """ + try: + h, w = image.shape[:2] + + if width and not height: + # Calculate height maintaining aspect ratio + height = int(h * (width / w)) + elif height and not width: + # Calculate width maintaining aspect ratio + width = int(w * (height / h)) + elif not width and not height: + # No resize needed + return image + + return cv2.resize(image, (width, height), interpolation=cv2.INTER_AREA) + except Exception as e: + logger.error(f"Failed to resize image: {e}") + raise + + def get_resolution_preset(self, preset: str) -> Tuple[int, int]: + """ + Get width and height for a resolution preset. + + Args: + preset: Resolution preset (e.g., '720p', '1080p', '480p') + + Returns: + Tuple of (width, height) + """ + presets = { + "480p": (854, 480), + "540p": (960, 540), + "720p": (1280, 720), + "900p": (1600, 900), + "1080p": (1920, 1080), + "1440p": (2560, 1440), + "2160p": (3840, 2160), + "4k": (3840, 2160), + } + + return presets.get(preset.lower(), (1280, 720)) + + def close(self): + """Close the screen capture resources.""" + if hasattr(self, "sct"): + self.sct.close() + + +# Global screen capture instance +_screen_capture: Optional[ScreenCapture] = None + + +def get_screen_capture() -> ScreenCapture: + """ + Get the global screen capture instance. + + Returns: + ScreenCapture instance + """ + global _screen_capture + if _screen_capture is None: + _screen_capture = ScreenCapture() + return _screen_capture + + +def capture_screen(monitor_index: int = 1) -> np.ndarray: + """ + Capture the entire screen. + + Args: + monitor_index: Index of the monitor to capture + + Returns: + Captured screen as numpy array + """ + return get_screen_capture().capture_screen(monitor_index) + + +def capture_region(x: int, y: int, width: int, height: int) -> np.ndarray: + """ + Capture a specific region of the screen. + + Args: + x: X coordinate of the region + y: Y coordinate of the region + width: Width of the region + height: Height of the region + + Returns: + Captured region as numpy array + """ + return get_screen_capture().capture_region(x, y, width, height) + + +def get_screen_size() -> Tuple[int, int]: + """ + Get the primary screen size. + + Returns: + Tuple of (width, height) + """ + monitor = get_screen_capture().get_primary_monitor() + return monitor["width"], monitor["height"] + + +def capture_gspro_window(window_title: str = "GSPro") -> Optional[np.ndarray]: + """ + Capture the GSPro window. + + Args: + window_title: GSPro window title + + Returns: + Captured window as numpy array or None if not found + """ + return get_screen_capture().capture_window(window_title) + + +# Test function for development +def test_screen_capture(): + """Test screen capture functionality.""" + print("Testing screen capture...") + + capture = ScreenCapture() + + # Get monitor information + monitors = capture.get_monitors() + print(f"Found {len(monitors)} monitors:") + for monitor in monitors: + print( + f" Monitor {monitor['index']}: {monitor['width']}x{monitor['height']} at ({monitor['left']}, {monitor['top']})" + ) + + # Capture primary screen + try: + screen = capture.capture_screen() + print(f"✓ Captured primary screen: {screen.shape}") + except Exception as e: + print(f"✗ Failed to capture screen: {e}") + + # Test region capture + try: + region = capture.capture_region(100, 100, 640, 480) + print(f"✓ Captured region: {region.shape}") + except Exception as e: + print(f"✗ Failed to capture region: {e}") + + # Test image to base64 conversion + try: + base64_str = capture.image_to_base64(region) + print(f"✓ Converted to base64: {len(base64_str)} chars") + except Exception as e: + print(f"✗ Failed to convert to base64: {e}") + + # Test resolution presets + presets = ["480p", "720p", "1080p"] + for preset in presets: + width, height = capture.get_resolution_preset(preset) + print(f" {preset}: {width}x{height}") + + capture.close() + print("\nScreen capture test complete!") + + +if __name__ == "__main__": + test_screen_capture() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..0549ae6 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,115 @@ +""" +Main FastAPI application for GSPro Remote backend. +""" + +import logging +from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles + +from .api import actions, config, system, vision +from .core.config import AppConfig, get_config +from .core.mdns import MDNSService + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Application lifespan manager for startup/shutdown tasks. + """ + # Startup + logger.info("Starting GSPro Remote backend v0.1.0") + + # Load configuration + config = get_config() + logger.info(f"Configuration loaded from {config.config_path}") + + # Start mDNS service if enabled + mdns_service = None + if config.server.mdns_enabled: + try: + mdns_service = MDNSService(name="gsproapp", port=config.server.port, service_type="_http._tcp.local.") + mdns_service.start() + logger.info(f"mDNS service started: gsproapp.local:{config.server.port}") + except Exception as e: + logger.warning(f"Failed to start mDNS service: {e}") + + yield + + # Shutdown + logger.info("Shutting down GSPro Remote backend") + + # Stop mDNS service + if mdns_service: + mdns_service.stop() + logger.info("mDNS service stopped") + + # Save configuration + config.save() + logger.info("Configuration saved") + + +def create_app() -> FastAPI: + """ + Create and configure the FastAPI application. + """ + app = FastAPI( + title="GSPro Remote", + version="0.1.0", + description="Remote control API for GSPro golf simulator", + docs_url="/api/docs", + redoc_url="/api/redoc", + openapi_url="/api/openapi.json", + lifespan=lifespan, + ) + + # Configure CORS + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, specify actual origins + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Mount API routers + app.include_router(actions.router, prefix="/api/actions", tags=["Actions"]) + app.include_router(config.router, prefix="/api/config", tags=["Configuration"]) + app.include_router(vision.router, prefix="/api/vision", tags=["Vision"]) + app.include_router(system.router, prefix="/api/system", tags=["System"]) + + # Serve frontend UI if built + ui_path = Path(__file__).parent.parent / "ui" + if ui_path.exists(): + app.mount("/ui", StaticFiles(directory=str(ui_path), html=True), name="ui") + logger.info(f"Serving UI from {ui_path}") + + # Root redirect + @app.get("/") + async def root(): + return { + "name": "GSPro Remote", + "version": "0.1.0", + "status": "running", + "ui": "/ui" if ui_path.exists() else None, + "docs": "/api/docs", + } + + return app + + +# Create the application instance +app = create_app() + +if __name__ == "__main__": + import uvicorn + + config = get_config() + uvicorn.run("app.main:app", host=config.server.host, port=config.server.port, reload=True, log_level="info") diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..1a0d864 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,76 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "gspro-remote" +version = "0.1.0" +description = "Remote control application for GSPro golf simulator" +readme = "README.md" +requires-python = ">=3.11" +license = {text = "MIT"} +authors = [ + {name = "GSPro Remote Team"}, +] +dependencies = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "pydirectinput>=1.0.4", + "pywin32>=306", + "mss>=9.0.1", + "opencv-python>=4.8.0", + "pillow>=10.1.0", + "zeroconf>=0.120.0", + "websockets>=12.0", + "python-multipart>=0.0.6", + "aiofiles>=23.2.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.0", + "pytest-asyncio>=0.21.0", + "pytest-cov>=4.1.0", + "black>=23.11.0", + "ruff>=0.1.0", + "mypy>=1.7.0", + "httpx>=0.25.0", +] +vision = [ + "easyocr>=1.7.0", + "pytesseract>=0.3.10", + "numpy>=1.24.0", +] +build = [ + "pyinstaller>=6.0.0", + "nuitka>=1.8.0", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["app*"] +exclude = ["tests*"] + +[tool.black] +line-length = 120 +target-version = ['py311'] + +[tool.ruff] +line-length = 120 +select = ["E", "F", "I", "N", "W"] +ignore = ["E501"] +target-version = "py311" + +[tool.mypy] +python_version = "3.11" +warn_return_any = true +warn_unused_configs = true +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +addopts = "-v --cov=app --cov-report=html --cov-report=term" diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..9235e4b --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,27 @@ +# Core dependencies +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 + +# Windows input control +pydirectinput>=1.0.4 +pywin32>=306; sys_platform == 'win32' + +# Screen capture and image processing +mss>=9.0.1 +opencv-python>=4.8.0 +pillow>=10.1.0 + +# Networking and service discovery +zeroconf>=0.120.0 +websockets>=12.0 +python-multipart>=0.0.6 +aiofiles>=23.2.1 + +# System utilities +psutil>=5.9.0 +numpy>=1.24.0 + +# HTTP client for testing +httpx>=0.25.0 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..1dbbe54 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + GSPro Remote + + + + + +

+
+ +

GSPro Remote

+

Connecting to GSPro...

+
+
+
+ + +
+ + + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..3fd6a6b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4813 @@ +{ + "name": "gspro-remote-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gspro-remote-frontend", + "version": "0.1.0", + "dependencies": { + "axios": "^1.6.2", + "clsx": "^2.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", + "react-icons": "^4.12.0", + "zustand": "^4.4.7" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.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/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/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.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.27.tgz", + "integrity": "sha512-2CXFpkjVnY2FT+B6GrSYxzYf65BJWEqz5tIRHCvNsZZ2F3CmsCB37h8SpYgKG7y9C4YAeTipIPWG7EmFmhAeXA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "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.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.250", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz", + "integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "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": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "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==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "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/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a5dd15c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "gspro-remote-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "axios": "^1.6.2", + "clsx": "^2.0.0", + "react-icons": "^4.12.0", + "zustand": "^4.4.7", + "react-hot-toast": "^2.4.1" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..2855478 --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,60 @@ +{ + "name": "GSPro Remote", + "short_name": "GSPro Remote", + "description": "Remote control application for GSPro golf simulator", + "version": "0.1.0", + "manifest_version": 3, + "start_url": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#0f172a", + "theme_color": "#2D5016", + "categories": ["sports", "games", "utilities"], + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "shortcuts": [ + { + "name": "Quick Launch", + "short_name": "Launch", + "description": "Quick launch GSPro Remote", + "url": "/", + "icons": [{ "src": "/icon-192.png", "sizes": "192x192" }] + } + ], + "screenshots": [ + { + "src": "/screenshot1.png", + "type": "image/png", + "sizes": "1280x720", + "label": "Main control interface" + }, + { + "src": "/screenshot2.png", + "type": "image/png", + "sizes": "1280x720", + "label": "Map view with streaming" + } + ], + "related_applications": [], + "prefer_related_applications": false, + "scope": "/", + "launch_handler": { + "client_mode": "navigate-existing" + }, + "display_override": ["window-controls-overlay", "standalone", "minimal-ui"], + "edge_side_panel": { + "preferred_width": 480 + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..e480271 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState } from 'react' +import { Toaster } from 'react-hot-toast' +import DynamicGolfUI from './pages/DynamicGolfUI' +import ConnectionStatus from './components/ConnectionStatus' +import ErrorBoundary from './components/ErrorBoundary' +import { useStore } from './stores/appStore' +import { checkBackendConnection } from './api/system' + +function App() { + const [isConnected, setIsConnected] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const { setConnectionStatus } = useStore() + + useEffect(() => { + // Check backend connection on mount + const checkConnection = async () => { + try { + const result = await checkBackendConnection() + setIsConnected(result) + setConnectionStatus(result) + } catch (error) { + console.error('Failed to connect to backend:', error) + setIsConnected(false) + setConnectionStatus(false) + } finally { + setIsLoading(false) + } + } + + checkConnection() + + // Set up periodic connection check + const interval = setInterval(checkConnection, 5000) + + return () => clearInterval(interval) + }, [setConnectionStatus]) + + if (isLoading) { + return ( +
+
+
+

Connecting to GSPro Remote...

+
+
+ ) + } + + if (!isConnected) { + return ( +
+
+
+
+ + + +
+

Connection Error

+

+ Unable to connect to the GSPro Remote backend server. +

+
+

Please ensure:

+
    +
  • • The backend server is running
  • +
  • • You're on the same network
  • +
  • • Port 5005 is accessible
  • +
+
+ +
+
+
+ ) + } + + return ( + +
+ {/* Connection Status Bar */} + + + {/* Main UI */} + + + {/* Toast Notifications */} + +
+
+ ) +} + +export default App diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..f86b708 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,308 @@ +import axios, { AxiosInstance, AxiosError } from "axios"; +import toast from "react-hot-toast"; + +// Determine API base URL based on current location +const getApiBaseUrl = () => { + // If we're running on localhost (development), use localhost backend + if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") { + return "http://localhost:5005"; + } + + // If we're accessing from a network IP, use the same IP for backend + // This assumes backend is running on same machine as frontend + const protocol = window.location.protocol; + const hostname = window.location.hostname; + return `${protocol}//${hostname}:5005`; +}; + +const API_BASE_URL = import.meta.env.VITE_API_URL || getApiBaseUrl(); + +// Log the API URL for debugging +console.log("API Base URL:", API_BASE_URL); + +// Create axios instance with default config +const apiClient: AxiosInstance = axios.create({ + baseURL: API_BASE_URL, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + +// Request interceptor +apiClient.interceptors.request.use( + (config) => { + // Add any auth tokens or request modifications here + return config; + }, + (error) => { + return Promise.reject(error); + }, +); + +// Response interceptor +apiClient.interceptors.response.use( + (response) => { + return response; + }, + (error: AxiosError) => { + // Handle common errors + if (error.code === "ECONNABORTED") { + toast.error("Request timeout - please try again"); + } else if (error.code === "ERR_NETWORK") { + toast.error("Cannot connect to backend - check if server is running"); + } else if (error.response) { + // Server responded with error + const message = (error.response.data as any)?.detail || "An error occurred"; + + if (error.response.status === 409) { + // GSPro not running + toast.error("GSPro is not running or window not found"); + } else if (error.response.status >= 500) { + toast.error(`Server error: ${message}`); + } + } else if (error.request) { + // No response from server + console.error("Backend connection failed:", error.message); + toast.error("Cannot connect to backend server on " + API_BASE_URL); + } + + return Promise.reject(error); + }, +); + +// Actions API +export const actionsAPI = { + sendKey: async (key: string, delay?: number) => { + const response = await apiClient.post("/api/actions/key", { key, delay }); + return response.data; + }, + + sendKeyDown: async (key: string, duration?: number) => { + const response = await apiClient.post("/api/actions/keydown", { key, duration }); + return response.data; + }, + + sendKeyUp: async (key: string) => { + const response = await apiClient.post("/api/actions/keyup", { key }); + return response.data; + }, + + sendKeySequence: async (keys: string[], interval?: number) => { + const response = await apiClient.post("/api/actions/sequence", { keys, interval }); + return response.data; + }, + + getShortcuts: async () => { + const response = await apiClient.get("/api/actions/shortcuts"); + return response.data; + }, +}; + +// Config API +export const configAPI = { + getConfig: async () => { + const response = await apiClient.get("/api/config"); + return response.data; + }, + + updateConfig: async (config: any) => { + const response = await apiClient.put("/api/config", config); + return response.data; + }, + + saveConfig: async () => { + const response = await apiClient.post("/api/config/save"); + return response.data; + }, + + resetConfig: async () => { + const response = await apiClient.post("/api/config/reset"); + return response.data; + }, +}; + +// Vision API +export const visionAPI = { + captureRegion: async (x: number, y: number, width: number, height: number) => { + const response = await apiClient.post("/api/vision/capture", { + x, + y, + width, + height, + format: "base64", + quality: 85, + }); + return response.data; + }, + + getRegions: async () => { + const response = await apiClient.get("/api/vision/regions"); + return response.data; + }, + + updateRegion: async (name: string, region: any) => { + const response = await apiClient.post(`/api/vision/regions/${name}`, region); + return response.data; + }, + + getStatus: async () => { + const response = await apiClient.get("/api/vision/status"); + return response.data; + }, +}; + +// System API +export const systemAPI = { + getHealth: async () => { + const response = await apiClient.get("/api/system/health"); + return response.data; + }, + + getInfo: async () => { + const response = await apiClient.get("/api/system/info"); + return response.data; + }, + + getGSProStatus: async () => { + const response = await apiClient.get("/api/system/gspro/status"); + return response.data; + }, + + findGSProWindow: async () => { + const response = await apiClient.get("/api/system/gspro/find"); + return response.data; + }, + + getMetrics: async () => { + const response = await apiClient.get("/api/system/metrics"); + return response.data; + }, + + runDiagnostics: async () => { + const response = await apiClient.get("/api/system/diagnostics"); + return response.data; + }, +}; + +// WebSocket connection for streaming +export class StreamingClient { + private ws: WebSocket | null = null; + private reconnectAttempts = 0; + private maxReconnectAttempts = 5; + private reconnectDelay = 1000; + + constructor( + private onFrame: (data: any) => void, + private onError?: (error: any) => void, + private onConnect?: () => void, + private onDisconnect?: () => void, + ) {} + + connect(config: any = {}) { + // Use same logic for WebSocket URL + let wsUrl: string; + if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") { + wsUrl = "ws://localhost:5005"; + } else { + // Use the same hostname but with ws:// protocol + wsUrl = `ws://${window.location.hostname}:5005`; + } + + console.log("WebSocket URL:", wsUrl); + this.ws = new WebSocket(`${wsUrl}/api/vision/ws/stream`); + + this.ws.onopen = () => { + console.log("WebSocket connected"); + this.reconnectAttempts = 0; + + // Send configuration + this.ws?.send( + JSON.stringify({ + action: "start", + config: { + fps: config.fps || 30, + quality: config.quality || 85, + resolution: config.resolution || "720p", + region_x: config.region_x || 0, + region_y: config.region_y || 0, + region_width: config.region_width || 640, + region_height: config.region_height || 480, + }, + }), + ); + + this.onConnect?.(); + }; + + this.ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + if (data.type === "frame") { + this.onFrame(data); + } else if (data.type === "error") { + console.error("Stream error:", data.message); + this.onError?.(data.message); + } else if (data.type === "config") { + console.log("Stream config:", data.data); + } + } catch (error) { + console.error("Failed to parse WebSocket message:", error); + } + }; + + this.ws.onerror = (error) => { + console.error("WebSocket error:", error); + this.onError?.(error); + }; + + this.ws.onclose = () => { + console.log("WebSocket disconnected"); + this.onDisconnect?.(); + + // Attempt reconnection + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + console.log(`Reconnecting... (attempt ${this.reconnectAttempts})`); + setTimeout(() => this.connect(config), this.reconnectDelay * this.reconnectAttempts); + } + }; + } + + disconnect() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + updateConfig(config: any) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send( + JSON.stringify({ + action: "update", + config, + }), + ); + } + } +} + +// Export a function to manually check backend connection +export async function checkBackendConnection(): Promise { + try { + await systemAPI.getHealth(); + return true; + } catch (error) { + console.error("Backend connection check failed:", error); + return false; + } +} + +// Export the current API base URL for debugging +export function getCurrentApiUrl(): string { + return API_BASE_URL; +} + +export default apiClient; diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts new file mode 100644 index 0000000..7db9ca6 --- /dev/null +++ b/frontend/src/api/system.ts @@ -0,0 +1,123 @@ +import { systemAPI } from './client' + +export interface SystemHealth { + status: string + timestamp: string + uptime_seconds: number + services: { + api: string + gspro: string + mdns: string + } +} + +export interface GSProStatus { + running: boolean + window_title: string + process: { + pid: number + name: string + cpu_percent: number + memory_mb: number + } | null + auto_focus: boolean + key_delay: number +} + +let connectionCheckPromise: Promise | null = null + +export async function checkBackendConnection(): Promise { + // Prevent multiple simultaneous connection checks + if (connectionCheckPromise) { + return connectionCheckPromise + } + + connectionCheckPromise = systemAPI.getHealth() + .then(() => true) + .catch(() => false) + .finally(() => { + connectionCheckPromise = null + }) + + return connectionCheckPromise +} + +export async function checkGSProStatus(): Promise { + try { + const status = await systemAPI.getGSProStatus() + return status + } catch (error) { + console.error('Failed to get GSPro status:', error) + return null + } +} + +export async function getSystemHealth(): Promise { + try { + const health = await systemAPI.getHealth() + return health + } catch (error) { + console.error('Failed to get system health:', error) + return null + } +} + +export async function findGSProWindows(): Promise { + try { + const result = await systemAPI.findGSProWindow() + return result.windows || [] + } catch (error) { + console.error('Failed to find GSPro windows:', error) + return [] + } +} + +export async function runSystemDiagnostics(): Promise { + try { + const diagnostics = await systemAPI.runDiagnostics() + return diagnostics + } catch (error) { + console.error('Failed to run diagnostics:', error) + return null + } +} + +export function formatUptime(seconds: number): string { + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const secs = seconds % 60 + + if (hours > 0) { + return `${hours}h ${minutes}m` + } else if (minutes > 0) { + return `${minutes}m ${secs}s` + } else { + return `${secs}s` + } +} + +export function getConnectionStatusColor(status: string): string { + switch (status) { + case 'connected': + return 'text-green-500' + case 'disconnected': + return 'text-red-500' + case 'connecting': + return 'text-yellow-500' + default: + return 'text-gray-500' + } +} + +export function getConnectionStatusIcon(status: string): string { + switch (status) { + case 'connected': + return '●' + case 'disconnected': + return '○' + case 'connecting': + return '◐' + default: + return '◯' + } +} diff --git a/frontend/src/components/AimPad.tsx b/frontend/src/components/AimPad.tsx new file mode 100644 index 0000000..cc164a5 --- /dev/null +++ b/frontend/src/components/AimPad.tsx @@ -0,0 +1,281 @@ +import React, { useState, useCallback, useEffect } from 'react' +import { actionsAPI } from '../api/client' +import { useStore } from '../stores/appStore' +import toast from 'react-hot-toast' + +interface AimPadProps { + size?: 'small' | 'medium' | 'large' +} + +const AimPad: React.FC = ({ size = 'large' }) => { + const { settings, setAimDirection } = useStore() + const [activeDirection, setActiveDirection] = useState(null) + const [isHolding, setIsHolding] = useState(false) + + // Size configurations + const sizeClasses = { + small: 'w-32 h-32', + medium: 'w-48 h-48', + large: 'w-64 h-64', + } + + const buttonSizeClasses = { + small: 'text-xl', + medium: 'text-2xl', + large: 'text-3xl', + } + + // Handle key press/release + const handleKeyAction = useCallback(async (key: string, action: 'down' | 'up') => { + try { + if (action === 'down') { + await actionsAPI.sendKeyDown(key) + setIsHolding(true) + + // Haptic feedback on mobile + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + } else { + await actionsAPI.sendKeyUp(key) + setIsHolding(false) + } + } catch (error) { + console.error(`Failed to send key ${action}:`, error) + if (action === 'down') { + toast.error('Failed to send command') + } + } + }, [settings.hapticFeedback]) + + // Handle single press + const handlePress = useCallback(async (key: string, direction?: string) => { + try { + await actionsAPI.sendKey(key) + + if (direction) { + // Flash the button + setActiveDirection(direction) + setTimeout(() => setActiveDirection(null), 150) + + // Update aim direction in store + if (direction === 'up') setAimDirection({ x: 0, y: 1 }) + else if (direction === 'down') setAimDirection({ x: 0, y: -1 }) + else if (direction === 'left') setAimDirection({ x: -1, y: 0 }) + else if (direction === 'right') setAimDirection({ x: 1, y: 0 }) + else if (direction === 'center') setAimDirection({ x: 0, y: 0 }) + } + + // Haptic feedback + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + } catch (error) { + console.error('Failed to send key:', error) + } + }, [settings.hapticFeedback, setAimDirection]) + + // Touch handlers for hold functionality + const handleTouchStart = (key: string, direction: string) => { + setActiveDirection(direction) + handleKeyAction(key, 'down') + } + + const handleTouchEnd = (key: string) => { + setActiveDirection(null) + if (isHolding) { + handleKeyAction(key, 'up') + } + } + + // Mouse handlers for desktop + const handleMouseDown = (key: string, direction: string) => { + setActiveDirection(direction) + handleKeyAction(key, 'down') + } + + const handleMouseUp = (key: string) => { + setActiveDirection(null) + if (isHolding) { + handleKeyAction(key, 'up') + } + } + + const handleMouseLeave = (key: string) => { + if (isHolding) { + setActiveDirection(null) + handleKeyAction(key, 'up') + } + } + + // Cleanup on unmount + useEffect(() => { + return () => { + if (isHolding) { + // Clean up any held keys + ['up', 'down', 'left', 'right'].forEach(key => { + actionsAPI.sendKeyUp(key).catch(() => {}) + }) + } + } + }, [isHolding]) + + return ( +
+ {/* D-Pad Container */} +
+ {/* Background circle */} +
+ + {/* Center lines */} +
+
+ + {/* Up Button */} + + + {/* Down Button */} + + + {/* Left Button */} + + + {/* Right Button */} + + + {/* Center Reset Button */} + +
+ + {/* Label */} +
+ Aim Control +
+ + {/* Hold indicator */} + {isHolding && ( +
+ Holding... +
+ )} +
+ ) +} + +export default AimPad diff --git a/frontend/src/components/ClubIndicator.tsx b/frontend/src/components/ClubIndicator.tsx new file mode 100644 index 0000000..61e5409 --- /dev/null +++ b/frontend/src/components/ClubIndicator.tsx @@ -0,0 +1,168 @@ +import React from 'react' +import { useStore } from '../stores/appStore' +import { actionsAPI } from '../api/client' +import toast from 'react-hot-toast' + +interface ClubIndicatorProps { + compact?: boolean +} + +const ClubIndicator: React.FC = ({ compact = false }) => { + const { club, nextClub, previousClub, settings } = useStore() + + const handleClubChange = async (direction: 'up' | 'down') => { + try { + const key = direction === 'up' ? 'u' : 'k' + await actionsAPI.sendKey(key) + + if (direction === 'up') { + previousClub() + } else { + nextClub() + } + + // Haptic feedback + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + } catch (error) { + toast.error('Failed to change club') + } + } + + const getClubIcon = (clubName: string) => { + // Return appropriate icon based on club type + if (clubName.includes('Driver')) return '🏌️' + if (clubName.includes('Wood')) return '🪵' + if (clubName.includes('Hybrid')) return '⚡' + if (clubName.includes('Iron')) return '🔧' + if (clubName.includes('Wedge') || clubName.includes('W')) return '⛳' + if (clubName.includes('Putter')) return '🎯' + return '🏌️' + } + + const getClubDistance = (clubName: string) => { + // Approximate distances for each club + const distances: { [key: string]: string } = { + 'Driver': '250-280y', + '3 Wood': '215-240y', + '5 Wood': '200-220y', + '3 Hybrid': '190-210y', + '4 Iron': '180-200y', + '5 Iron': '170-190y', + '6 Iron': '160-180y', + '7 Iron': '150-170y', + '8 Iron': '140-160y', + '9 Iron': '130-150y', + 'PW': '120-140y', + 'GW': '100-120y', + 'SW': '80-100y', + 'LW': '60-80y', + 'Putter': 'Green', + } + return distances[clubName] || '---' + } + + if (compact) { + // Compact version for mobile + return ( +
+
+ + +
+
{club.current}
+
{getClubDistance(club.current)}
+
+ + +
+
+ ) + } + + // Full version for desktop/tablet + return ( +
+ {/* Header */} +
+

Club Selection

+
+ + {/* Current Club Display */} +
+
+
{getClubIcon(club.current)}
+
{club.current}
+
{getClubDistance(club.current)}
+
+ + {/* Club Change Buttons */} +
+ + + +
+ + {/* Club List Preview */} +
+
Club Bag
+
+ {[ + club.index > 0 ? `↑ ${['Driver', '3 Wood', '5 Wood', '3 Hybrid', '4 Iron', '5 Iron', '6 Iron', '7 Iron', '8 Iron', '9 Iron', 'PW', 'GW', 'SW', 'LW', 'Putter'][club.index - 1]}` : '', + `→ ${club.current}`, + club.index < 14 ? `↓ ${['Driver', '3 Wood', '5 Wood', '3 Hybrid', '4 Iron', '5 Iron', '6 Iron', '7 Iron', '8 Iron', '9 Iron', 'PW', 'GW', 'SW', 'LW', 'Putter'][club.index + 1]}` : '', + ].filter(Boolean).map((text, idx) => ( +
+ {text} +
+ ))} +
+
+
+
+ ) +} + +export default ClubIndicator diff --git a/frontend/src/components/ConnectionStatus.tsx b/frontend/src/components/ConnectionStatus.tsx new file mode 100644 index 0000000..1042d1d --- /dev/null +++ b/frontend/src/components/ConnectionStatus.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from 'react' +import { useStore } from '../stores/appStore' +import { getSystemHealth, formatUptime } from '../api/system' + +const ConnectionStatus: React.FC = () => { + const { connectionStatus, gsproStatus } = useStore() + const [health, setHealth] = useState(null) + const [isVisible, setIsVisible] = useState(true) + + useEffect(() => { + const fetchHealth = async () => { + const healthData = await getSystemHealth() + if (healthData) { + setHealth(healthData) + } + } + + fetchHealth() + const interval = setInterval(fetchHealth, 10000) // Update every 10 seconds + + return () => clearInterval(interval) + }, []) + + useEffect(() => { + // Auto-hide the status bar after 5 seconds if everything is connected + if (connectionStatus === 'connected' && gsproStatus === 'running') { + const timer = setTimeout(() => setIsVisible(false), 5000) + return () => clearTimeout(timer) + } else { + setIsVisible(true) + } + }, [connectionStatus, gsproStatus]) + + const getStatusColor = () => { + if (connectionStatus !== 'connected') return 'bg-red-600' + if (gsproStatus !== 'running') return 'bg-yellow-600' + return 'bg-green-600' + } + + const getStatusText = () => { + if (connectionStatus !== 'connected') return 'Backend Disconnected' + if (gsproStatus !== 'running') return 'GSPro Not Running' + return 'Connected' + } + + if (!isVisible && connectionStatus === 'connected' && gsproStatus === 'running') { + // Show a minimal indicator when hidden + return ( +
setIsVisible(true)} + > +
+
+
+
+ ) + } + + return ( +
+
+
+
+
+ {/* Status indicator */} +
+
+ {getStatusText()} +
+ + {/* Backend status */} + {connectionStatus === 'connected' && ( + <> +
+ Backend: + localhost:5005 +
+ + {/* Uptime */} + {health && ( +
+ Uptime: + {formatUptime(health.uptime_seconds)} +
+ )} + + )} +
+ + {/* Actions */} +
+ {/* GSPro status */} + {connectionStatus === 'connected' && ( +
+ + + + + {gsproStatus === 'running' ? 'GSPro Ready' : 'GSPro Not Found'} +
+ )} + + {/* Close button for mobile */} + +
+
+
+
+
+ ) +} + +export default ConnectionStatus diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..852b5fd --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,106 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react' + +interface Props { + children: ReactNode +} + +interface State { + hasError: boolean + error: Error | null + errorInfo: ErrorInfo | null +} + +class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + errorInfo: null, + } + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error, errorInfo: null } + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo) + this.setState({ + error, + errorInfo, + }) + } + + private handleReset = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + }) + window.location.reload() + } + + public render() { + if (this.state.hasError) { + return ( +
+
+
+
+ + + +
+
+

Something went wrong

+

An unexpected error occurred

+
+
+ + {this.state.error && ( +
+

{this.state.error.toString()}

+
+ )} + + {process.env.NODE_ENV === 'development' && this.state.errorInfo && ( +
+ + Show details + +
+                  {this.state.errorInfo.componentStack}
+                
+
+ )} + +
+ + +
+
+
+ ) + } + + return this.props.children + } +} + +export default ErrorBoundary diff --git a/frontend/src/components/MapPanel.tsx b/frontend/src/components/MapPanel.tsx new file mode 100644 index 0000000..aa903f0 --- /dev/null +++ b/frontend/src/components/MapPanel.tsx @@ -0,0 +1,284 @@ +import React, { useEffect, useRef, useState } from 'react' +import { useStore } from '../stores/appStore' +import { StreamingClient } from '../api/client' +import { actionsAPI } from '../api/client' +import toast from 'react-hot-toast' + +interface MapPanelProps { + compact?: boolean +} + +const MapPanel: React.FC = ({ compact = false }) => { + const { mapConfig, toggleMapExpanded, setMapStreaming, settings } = useStore() + const [isConnected, setIsConnected] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const canvasRef = useRef(null) + const streamClientRef = useRef(null) + const animationFrameRef = useRef() + + useEffect(() => { + // Initialize streaming client + streamClientRef.current = new StreamingClient( + (data) => { + // Handle incoming frame + if (data.data && canvasRef.current) { + const img = new Image() + img.onload = () => { + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext('2d') + if (!ctx) return + + // Clear and draw new frame + ctx.clearRect(0, 0, canvas.width, canvas.height) + ctx.drawImage(img, 0, 0, canvas.width, canvas.height) + } + img.src = `data:image/jpeg;base64,${data.data}` + } + }, + (error) => { + console.error('Stream error:', error) + toast.error('Stream connection lost') + setIsConnected(false) + setMapStreaming(false) + }, + () => { + console.log('Stream connected') + setIsConnected(true) + setMapStreaming(true) + setIsLoading(false) + }, + () => { + console.log('Stream disconnected') + setIsConnected(false) + setMapStreaming(false) + } + ) + + return () => { + // Cleanup + streamClientRef.current?.disconnect() + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + } + }, [setMapStreaming]) + + const handleStartStream = () => { + setIsLoading(true) + const config = { + fps: settings.streamFPS, + quality: settings.streamQuality === 'high' ? 90 : settings.streamQuality === 'medium' ? 75 : 60, + resolution: settings.streamQuality === 'high' ? '1080p' : settings.streamQuality === 'medium' ? '720p' : '480p', + region_x: mapConfig.x, + region_y: mapConfig.y, + region_width: mapConfig.width, + region_height: mapConfig.height, + } + streamClientRef.current?.connect(config) + } + + const handleStopStream = () => { + streamClientRef.current?.disconnect() + } + + const handleToggleMap = async () => { + try { + await actionsAPI.sendKey('s') + toggleMapExpanded() + + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + } catch (error) { + toast.error('Failed to toggle map') + } + } + + const handleZoom = async (direction: 'in' | 'out') => { + try { + const key = direction === 'in' ? 'q' : 'w' + await actionsAPI.sendKey(key) + + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + } catch (error) { + toast.error('Failed to zoom map') + } + } + + if (compact) { + // Compact version for mobile + return ( +
+
+ Map + +
+ +
+ {!isConnected ? ( +
+ +
+ ) : ( + <> + + + + )} +
+
+ ) + } + + // Full version for desktop/tablet + return ( +
+ {/* Header */} +
+

Course Map

+
+ {isConnected && ( + <> + + + + )} + +
+
+ + {/* Map Display */} +
+ {!isConnected ? ( +
+
+ + + +

Map Stream

+

View the GSPro course map in real-time

+ +
+
+ ) : ( + <> + + + {/* Stream Controls Overlay */} +
+
+
+ Live +
+ +
+ + {/* Map Click Overlay (when expanded) */} + {mapConfig.expanded && ( +
+

Click on the map to set target location

+
+ )} + + )} +
+ + {/* Stream Info */} + {isConnected && !mapConfig.expanded && ( +
+
+ Quality: {settings.streamQuality} + FPS: {settings.streamFPS} + Resolution: {settings.streamQuality === 'high' ? '1080p' : settings.streamQuality === 'medium' ? '720p' : '480p'} +
+
+ )} +
+ ) +} + +export default MapPanel diff --git a/frontend/src/components/QuickActions.tsx b/frontend/src/components/QuickActions.tsx new file mode 100644 index 0000000..675e7dc --- /dev/null +++ b/frontend/src/components/QuickActions.tsx @@ -0,0 +1,205 @@ +import React, { useState } from 'react' +import { actionsAPI } from '../api/client' +import toast from 'react-hot-toast' +import { useStore } from '../stores/appStore' + +interface QuickActionsProps { + mobile?: boolean +} + +const QuickActions: React.FC = ({ mobile = false }) => { + const [isOpen, setIsOpen] = useState(false) + const { settings } = useStore() + + const actions = [ + { + id: 'scorecard', + label: 'Scorecard', + icon: '📊', + key: 't', + description: 'Toggle scorecard', + }, + { + id: 'rangefinder', + label: 'Range Finder', + icon: '🎯', + key: 'r', + description: 'Launch range finder', + }, + { + id: 'heatmap', + label: 'Heat Map', + icon: '🗺️', + key: 'y', + description: 'Toggle heat map', + }, + { + id: 'flyover', + label: 'Flyover', + icon: '🚁', + key: 'o', + description: 'Hole preview', + }, + { + id: 'pin', + label: 'Pin Indicator', + icon: '🚩', + key: 'p', + description: 'Show/hide pin', + }, + { + id: 'freelook', + label: 'Free Look', + icon: '👀', + key: 'f5', + description: 'Unlock camera', + }, + { + id: 'aimpoint', + label: 'Aim Point', + icon: '🎪', + key: 'f3', + description: 'Toggle aim point', + }, + { + id: 'tracerclear', + label: 'Clear Tracer', + icon: '💨', + key: 'f1', + description: 'Clear ball tracer', + }, + ] + + const handleAction = async (action: typeof actions[0]) => { + try { + await actionsAPI.sendKey(action.key) + toast.success(action.description) + setIsOpen(false) + + // Haptic feedback + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + } catch (error) { + toast.error(`Failed to execute ${action.label}`) + } + } + + const handleToggle = () => { + setIsOpen(!isOpen) + + // Haptic feedback + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + } + + if (mobile) { + // Mobile version - bottom sheet style + return ( + <> + {/* Floating Action Button */} + + + {/* Bottom Sheet */} +
+
+
+

Quick Actions

+
+ {actions.map((action) => ( + + ))} +
+
+
+ + {/* Backdrop */} + {isOpen && ( +
setIsOpen(false)} + /> + )} + + ) + } + + // Desktop version - floating menu + return ( +
+ {/* Action Menu */} +
+
+
+ {actions.map((action) => ( + + ))} +
+
+
+ + {/* Floating Action Button */} + +
+ ) +} + +export default QuickActions diff --git a/frontend/src/components/ShotOptions.tsx b/frontend/src/components/ShotOptions.tsx new file mode 100644 index 0000000..f4bfa2e --- /dev/null +++ b/frontend/src/components/ShotOptions.tsx @@ -0,0 +1,130 @@ +import React from 'react' +import { useStore } from '../stores/appStore' +import { actionsAPI } from '../api/client' +import toast from 'react-hot-toast' + +interface ShotOptionsProps { + compact?: boolean +} + +const ShotOptions: React.FC = ({ compact = false }) => { + const { shotMode, setShotMode, settings } = useStore() + + const shotTypes = [ + { id: 'normal', label: 'Normal', icon: '⚫', key: null }, + { id: 'punch', label: 'Punch', icon: '👊', key: "'" }, + { id: 'flop', label: 'Flop', icon: '🎯', key: "'" }, + { id: 'chip', label: 'Chip', icon: '⛳', key: "'" }, + ] + + const handleShotChange = async (mode: 'normal' | 'punch' | 'flop' | 'chip') => { + try { + // Send key if needed (shot options typically cycle with apostrophe key) + if (mode !== 'normal' && mode !== shotMode) { + await actionsAPI.sendKey("'") + } + + setShotMode(mode) + + // Haptic feedback + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + + toast.success(`Shot mode: ${mode}`) + } catch (error) { + toast.error('Failed to change shot mode') + } + } + + if (compact) { + // Compact horizontal layout for mobile + return ( +
+
Shot Type
+
+ {shotTypes.map((shot) => ( + + ))} +
+
+ ) + } + + // Full version for desktop/tablet + return ( +
+

Shot Options

+ +
+ {shotTypes.map((shot) => ( + + ))} +
+ + {/* Additional shot controls */} +
+
+ + + +
+
+
+ ) +} + +export default ShotOptions diff --git a/frontend/src/components/StatBar.tsx b/frontend/src/components/StatBar.tsx new file mode 100644 index 0000000..179df82 --- /dev/null +++ b/frontend/src/components/StatBar.tsx @@ -0,0 +1,164 @@ +import React, { useEffect, useState } from 'react' +import { systemAPI } from '../api/client' +import { useStore } from '../stores/appStore' + +interface StatBarProps { + compact?: boolean +} + +interface Stats { + windSpeed: number + windDirection: string + distance: number + elevation: number + lie: string +} + +const StatBar: React.FC = ({ compact = false }) => { + const { club, shotMode } = useStore() + const [stats, setStats] = useState({ + windSpeed: 0, + windDirection: 'N', + distance: 0, + elevation: 0, + lie: 'Fairway', + }) + + useEffect(() => { + // In a real implementation, these would come from the backend + // For now, using placeholder values + setStats({ + windSpeed: Math.floor(Math.random() * 20), + windDirection: ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][Math.floor(Math.random() * 8)], + distance: 150, + elevation: Math.floor(Math.random() * 20) - 10, + lie: 'Fairway', + }) + }, [club]) + + const getWindArrow = (direction: string) => { + const arrows: { [key: string]: string } = { + 'N': '↑', + 'NE': '↗', + 'E': '→', + 'SE': '↘', + 'S': '↓', + 'SW': '↙', + 'W': '←', + 'NW': '↖', + } + return arrows[direction] || '↑' + } + + const getLieColor = (lie: string) => { + switch (lie.toLowerCase()) { + case 'fairway': + return 'text-green-400' + case 'rough': + return 'text-yellow-400' + case 'sand': + return 'text-orange-400' + case 'water': + return 'text-blue-400' + default: + return 'text-gray-400' + } + } + + if (compact) { + // Compact version for mobile + return ( +
+
+
+ Wind: + {stats.windSpeed}mph + {getWindArrow(stats.windDirection)} +
+ +
+ Dist: + {stats.distance}y +
+ +
+ Elev: + 0 ? 'text-red-400' : stats.elevation < 0 ? 'text-blue-400' : 'text-gray-400'}`}> + {stats.elevation > 0 ? '+' : ''}{stats.elevation}ft + +
+ +
+ Lie: + {stats.lie} +
+
+
+ ) + } + + // Full version for desktop/tablet + return ( +
+
+ {/* Wind */} +
+
Wind
+
+ {getWindArrow(stats.windDirection)} +
+
{stats.windSpeed}
+
mph
+
+
+
+ + {/* Distance to Pin */} +
+
To Pin
+
{stats.distance}
+
yards
+
+ + {/* Elevation */} +
+
Elevation
+
0 ? 'text-red-400' : stats.elevation < 0 ? 'text-blue-400' : 'text-gray-400'}`}> + {stats.elevation > 0 ? '+' : ''}{stats.elevation} +
+
feet
+
+ + {/* Lie */} +
+
Lie
+
{stats.lie}
+
{shotMode} shot
+
+ + {/* Club */} +
+
Club
+
{club.current}
+
Selected
+
+
+ + {/* Additional info bar */} +
+
+
+ Hole: 1 • Par 4 + Stroke: 1 +
+
+ Round Time: 0:00 + Score: E +
+
+
+
+ ) +} + +export default StatBar diff --git a/frontend/src/components/TeeControls.tsx b/frontend/src/components/TeeControls.tsx new file mode 100644 index 0000000..7c82937 --- /dev/null +++ b/frontend/src/components/TeeControls.tsx @@ -0,0 +1,110 @@ +import React from 'react' +import { actionsAPI } from '../api/client' +import { useStore } from '../stores/appStore' +import toast from 'react-hot-toast' + +interface TeeControlsProps { + compact?: boolean +} + +const TeeControls: React.FC = ({ compact = false }) => { + const { settings } = useStore() + + const handleTeeMove = async (direction: 'left' | 'right') => { + try { + const key = direction === 'left' ? 'c' : 'v' + await actionsAPI.sendKey(key) + + // Haptic feedback + if (settings.hapticFeedback && 'vibrate' in navigator) { + navigator.vibrate(10) + } + + toast.success(`Tee moved ${direction}`) + } catch (error) { + toast.error('Failed to move tee') + } + } + + if (compact) { + // Compact version for mobile + return ( +
+
Tee Box
+
+ + +
+
+
+ + +
+
+ ) + } + + // Full version for desktop/tablet + return ( +
+

Tee Position

+ +
+ + +
+
+ + + +
+
+
+ + +
+ +
+

Adjust your starting position on the tee box

+
+
+ ) +} + +export default TeeControls diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..9644434 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,361 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Custom global styles */ +@layer base { + /* Set default font and colors */ + html { + @apply font-sans antialiased; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + /* Custom scrollbar styles */ + ::-webkit-scrollbar { + @apply w-2 h-2; + } + + ::-webkit-scrollbar-track { + @apply bg-gray-800 rounded-full; + } + + ::-webkit-scrollbar-thumb { + @apply bg-gray-600 rounded-full hover:bg-gray-500; + } + + /* Firefox scrollbar */ + * { + scrollbar-width: thin; + scrollbar-color: #4b5563 #1f2937; + } + + /* Viewport height fix for mobile */ + .h-screen-safe { + height: 100vh; + height: calc(var(--vh, 1vh) * 100); + } + + /* Prevent text selection on interactive elements */ + button, + .btn, + .interactive { + @apply select-none; + -webkit-tap-highlight-color: transparent; + } + + /* Focus styles */ + .focus-ring { + @apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-gray-900; + } +} + +@layer components { + /* Button base styles */ + .btn { + @apply inline-flex items-center justify-center px-4 py-2 font-medium rounded-lg transition-all duration-200 focus-ring disabled:opacity-50 disabled:cursor-not-allowed; + } + + /* Button variants */ + .btn-primary { + @apply btn bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800; + } + + .btn-secondary { + @apply btn bg-secondary-600 text-white hover:bg-secondary-700 active:bg-secondary-800; + } + + .btn-success { + @apply btn bg-success-600 text-white hover:bg-success-700 active:bg-success-800; + } + + .btn-danger { + @apply btn bg-error-600 text-white hover:bg-error-700 active:bg-error-800; + } + + .btn-ghost { + @apply btn bg-transparent hover:bg-gray-800 active:bg-gray-700; + } + + .btn-outline { + @apply btn border-2 border-gray-600 hover:bg-gray-800 active:bg-gray-700; + } + + /* Icon button */ + .btn-icon { + @apply inline-flex items-center justify-center p-2 rounded-lg transition-all duration-200 focus-ring hover:bg-gray-800 active:bg-gray-700; + } + + /* Card component */ + .card { + @apply bg-gray-800 rounded-xl border border-gray-700 shadow-lg; + } + + .card-body { + @apply p-4; + } + + /* Input styles */ + .input { + @apply block w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500; + } + + /* Label styles */ + .label { + @apply block text-sm font-medium text-gray-300 mb-1; + } + + /* Directional pad button */ + .dpad-btn { + @apply flex items-center justify-center bg-gray-700 hover:bg-gray-600 active:bg-primary-600 transition-all duration-150 select-none cursor-pointer; + } + + /* Status indicator */ + .status-dot { + @apply inline-block w-2 h-2 rounded-full; + } + + .status-dot.online { + @apply bg-success-500 animate-pulse; + } + + .status-dot.offline { + @apply bg-gray-500; + } + + .status-dot.error { + @apply bg-error-500; + } + + /* Golf-specific styles */ + .golf-green { + @apply bg-gradient-to-br from-green-700 to-green-800; + } + + .golf-fairway { + @apply bg-gradient-to-br from-green-600 to-green-700; + } + + .golf-sand { + @apply bg-gradient-to-br from-yellow-600 to-yellow-700; + } + + .golf-water { + @apply bg-gradient-to-br from-blue-600 to-blue-700; + } + + /* Aim pad grid */ + .aim-grid { + @apply grid grid-cols-3 gap-1 w-48 h-48; + } + + /* Map panel */ + .map-panel { + @apply relative bg-black rounded-lg overflow-hidden; + } + + .map-panel.expanded { + @apply fixed inset-4 z-50 md:inset-8 lg:inset-16; + } + + /* Stat display */ + .stat-item { + @apply flex flex-col items-center p-2 bg-gray-800 rounded-lg; + } + + .stat-value { + @apply text-xl font-bold text-primary-400; + } + + .stat-label { + @apply text-xs text-gray-400 uppercase tracking-wider; + } + + /* Toggle switch */ + .toggle { + @apply relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-gray-900; + } + + .toggle.checked { + @apply bg-primary-600; + } + + .toggle:not(.checked) { + @apply bg-gray-700; + } + + .toggle-thumb { + @apply inline-block h-4 w-4 transform rounded-full bg-white transition-transform; + } + + .toggle.checked .toggle-thumb { + @apply translate-x-6; + } + + .toggle:not(.checked) .toggle-thumb { + @apply translate-x-1; + } +} + +@layer utilities { + /* Touch action utilities */ + .touch-none { + touch-action: none; + } + + .touch-pan-x { + touch-action: pan-x; + } + + .touch-pan-y { + touch-action: pan-y; + } + + /* Prevent double-tap zoom on mobile */ + .no-tap-zoom { + touch-action: manipulation; + } + + /* Glass morphism effect */ + .glass { + @apply bg-gray-900 bg-opacity-60 backdrop-blur-lg; + } + + /* Gradient text */ + .gradient-text { + @apply bg-gradient-to-r from-primary-400 to-success-400 bg-clip-text text-transparent; + } + + /* Loading animation */ + @keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } + } + + .shimmer { + animation: shimmer 2s linear infinite; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.1) 50%, + rgba(255, 255, 255, 0) 100% + ); + background-size: 1000px 100%; + } + + /* Fade animations */ + .fade-in { + animation: fadeIn 0.3s ease-in-out; + } + + .fade-out { + animation: fadeOut 0.3s ease-in-out; + } + + /* Slide animations */ + .slide-in-left { + animation: slideInLeft 0.3s ease-out; + } + + .slide-in-right { + animation: slideInRight 0.3s ease-out; + } + + .slide-in-up { + animation: slideInUp 0.3s ease-out; + } + + .slide-in-down { + animation: slideInDown 0.3s ease-out; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + @keyframes slideInLeft { + from { + transform: translateX(-100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes slideInUp { + from { + transform: translateY(100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + @keyframes slideInDown { + from { + transform: translateY(-100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + /* Responsive text utilities */ + .text-responsive { + @apply text-sm sm:text-base lg:text-lg; + } + + .heading-responsive { + @apply text-2xl sm:text-3xl lg:text-4xl; + } + + /* Custom grid utilities */ + .grid-center { + @apply grid place-items-center; + } + + /* Aspect ratio utilities */ + .aspect-map { + aspect-ratio: 4/3; + } + + /* Overlay utilities */ + .overlay { + @apply fixed inset-0 bg-black bg-opacity-50 z-40; + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..5617cad --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +// Create root element +const rootElement = document.getElementById('root') +if (!rootElement) { + throw new Error('Failed to find the root element') +} + +// Create React root +const root = ReactDOM.createRoot(rootElement) + +// Render the app +root.render( + + + +) + +// Signal that the app is ready (removes loading screen) +window.dispatchEvent(new Event('app-ready')) diff --git a/frontend/src/pages/DynamicGolfUI.tsx b/frontend/src/pages/DynamicGolfUI.tsx new file mode 100644 index 0000000..cbee9be --- /dev/null +++ b/frontend/src/pages/DynamicGolfUI.tsx @@ -0,0 +1,202 @@ +import React, { useEffect, useState } from 'react' +import { useStore } from '../stores/appStore' +import AimPad from '../components/AimPad' +import ClubIndicator from '../components/ClubIndicator' +import MapPanel from '../components/MapPanel' +import ShotOptions from '../components/ShotOptions' +import TeeControls from '../components/TeeControls' +import StatBar from '../components/StatBar' +import QuickActions from '../components/QuickActions' +import { actionsAPI, systemAPI } from '../api/client' +import toast from 'react-hot-toast' + +const DynamicGolfUI: React.FC = () => { + const { + club, + mapConfig, + shotMode, + mulliganActive, + setGSProStatus, + toggleMulligan, + toggleMapExpanded, + } = useStore() + + const [isLandscape, setIsLandscape] = useState( + window.innerWidth > window.innerHeight + ) + + useEffect(() => { + // Check GSPro status + const checkGSPro = async () => { + try { + const status = await systemAPI.getGSProStatus() + setGSProStatus(status.running ? 'running' : 'not_running') + } catch (error) { + setGSProStatus('unknown') + } + } + + checkGSPro() + const interval = setInterval(checkGSPro, 5000) + + return () => clearInterval(interval) + }, [setGSProStatus]) + + useEffect(() => { + // Handle orientation change + const handleResize = () => { + setIsLandscape(window.innerWidth > window.innerHeight) + } + + window.addEventListener('resize', handleResize) + window.addEventListener('orientationchange', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + window.removeEventListener('orientationchange', handleResize) + } + }, []) + + const handleMulligan = async () => { + try { + await actionsAPI.sendKey('ctrl+m') + toggleMulligan() + toast.success(mulliganActive ? 'Mulligan disabled' : 'Mulligan enabled') + } catch (error) { + toast.error('Failed to toggle mulligan') + } + } + + const handleReset = async () => { + try { + await actionsAPI.sendKey('a') + toast.success('Reset') + } catch (error) { + toast.error('Failed to reset') + } + } + + // Render different layouts based on orientation + if (isLandscape) { + // Landscape layout (tablets, desktop) + return ( +
+
+ {/* Header */} +
+
+

GSPro Remote

+ v0.1.0 +
+
+ + +
+
+ + {/* Main content */} +
+ {/* Left panel - Club */} +
+ +
+ + {/* Center - Aim Pad */} +
+ +
+ +
+
+ + {/* Right panel - Map */} +
+ +
+ +
+
+
+ + {/* Bottom bar */} +
+ +
+
+ + {/* Quick actions floating button */} + +
+ ) + } else { + // Portrait layout (phones) + return ( +
+
+ {/* Header - Compact */} +
+

GSPro Remote

+ +
+ + {/* Main content area */} +
+ {/* Top row - Club and Map */} +
+
+ +
+
+ +
+
+ + {/* Center - Aim Pad */} +
+ +
+ + {/* Bottom controls */} +
+ + +
+
+ + {/* Bottom stats */} +
+ +
+
+ + {/* Quick actions for mobile */} + +
+ ) + } +} + +export default DynamicGolfUI diff --git a/frontend/src/stores/appStore.ts b/frontend/src/stores/appStore.ts new file mode 100644 index 0000000..b74bd08 --- /dev/null +++ b/frontend/src/stores/appStore.ts @@ -0,0 +1,229 @@ +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +export interface ClubInfo { + current: string + index: number +} + +export interface MapConfig { + expanded: boolean + zoom: number + x: number + y: number + width: number + height: number +} + +export interface AppSettings { + hapticFeedback: boolean + soundEffects: boolean + streamQuality: 'low' | 'medium' | 'high' + streamFPS: number + showStats: boolean + compactMode: boolean +} + +export interface AppState { + // Connection status + isConnected: boolean + connectionStatus: 'connected' | 'disconnected' | 'connecting' + gsproStatus: 'running' | 'not_running' | 'unknown' + + // UI state + currentView: 'main' | 'settings' | 'advanced' + isLoading: boolean + + // Golf state + club: ClubInfo + aimDirection: { x: number; y: number } + mulliganActive: boolean + shotMode: 'normal' | 'punch' | 'flop' | 'chip' + + // Map state + mapConfig: MapConfig + isMapStreaming: boolean + + // Settings + settings: AppSettings + + // Actions + setConnectionStatus: (connected: boolean) => void + setGSProStatus: (status: 'running' | 'not_running' | 'unknown') => void + setCurrentView: (view: 'main' | 'settings' | 'advanced') => void + setLoading: (loading: boolean) => void + + // Golf actions + setClub: (club: ClubInfo) => void + nextClub: () => void + previousClub: () => void + setAimDirection: (direction: { x: number; y: number }) => void + toggleMulligan: () => void + setShotMode: (mode: 'normal' | 'punch' | 'flop' | 'chip') => void + + // Map actions + toggleMapExpanded: () => void + setMapConfig: (config: Partial) => void + setMapStreaming: (streaming: boolean) => void + + // Settings actions + updateSettings: (settings: Partial) => void + resetSettings: () => void +} + +const CLUBS = [ + 'Driver', + '3 Wood', + '5 Wood', + '3 Hybrid', + '4 Iron', + '5 Iron', + '6 Iron', + '7 Iron', + '8 Iron', + '9 Iron', + 'PW', + 'GW', + 'SW', + 'LW', + 'Putter' +] + +const DEFAULT_SETTINGS: AppSettings = { + hapticFeedback: true, + soundEffects: false, + streamQuality: 'medium', + streamFPS: 30, + showStats: true, + compactMode: false, +} + +const DEFAULT_MAP_CONFIG: MapConfig = { + expanded: false, + zoom: 1, + x: 0, + y: 0, + width: 640, + height: 480, +} + +export const useStore = create()( + devtools( + persist( + (set, get) => ({ + // Initial state + isConnected: false, + connectionStatus: 'disconnected', + gsproStatus: 'unknown', + currentView: 'main', + isLoading: false, + + club: { + current: 'Driver', + index: 0, + }, + aimDirection: { x: 0, y: 0 }, + mulliganActive: false, + shotMode: 'normal', + + mapConfig: DEFAULT_MAP_CONFIG, + isMapStreaming: false, + + settings: DEFAULT_SETTINGS, + + // Connection actions + setConnectionStatus: (connected) => + set({ + isConnected: connected, + connectionStatus: connected ? 'connected' : 'disconnected', + }), + + setGSProStatus: (status) => set({ gsproStatus: status }), + + // UI actions + setCurrentView: (view) => set({ currentView: view }), + setLoading: (loading) => set({ isLoading: loading }), + + // Golf actions + setClub: (club) => set({ club }), + + nextClub: () => { + const state = get() + const nextIndex = (state.club.index + 1) % CLUBS.length + set({ + club: { + current: CLUBS[nextIndex], + index: nextIndex, + }, + }) + }, + + previousClub: () => { + const state = get() + const prevIndex = state.club.index === 0 ? CLUBS.length - 1 : state.club.index - 1 + set({ + club: { + current: CLUBS[prevIndex], + index: prevIndex, + }, + }) + }, + + setAimDirection: (direction) => set({ aimDirection: direction }), + + toggleMulligan: () => + set((state) => ({ mulliganActive: !state.mulliganActive })), + + setShotMode: (mode) => set({ shotMode: mode }), + + // Map actions + toggleMapExpanded: () => + set((state) => ({ + mapConfig: { + ...state.mapConfig, + expanded: !state.mapConfig.expanded, + }, + })), + + setMapConfig: (config) => + set((state) => ({ + mapConfig: { + ...state.mapConfig, + ...config, + }, + })), + + setMapStreaming: (streaming) => set({ isMapStreaming: streaming }), + + // Settings actions + updateSettings: (settings) => + set((state) => ({ + settings: { + ...state.settings, + ...settings, + }, + })), + + resetSettings: () => set({ settings: DEFAULT_SETTINGS }), + }), + { + name: 'gspro-remote-store', + partialize: (state) => ({ + settings: state.settings, + mapConfig: state.mapConfig, + }), + } + ) + ) +) + +// Selectors +export const selectIsConnected = (state: AppState) => state.isConnected +export const selectConnectionStatus = (state: AppState) => state.connectionStatus +export const selectGSProStatus = (state: AppState) => state.gsproStatus +export const selectCurrentView = (state: AppState) => state.currentView +export const selectClub = (state: AppState) => state.club +export const selectMapConfig = (state: AppState) => state.mapConfig +export const selectSettings = (state: AppState) => state.settings +export const selectIsMapExpanded = (state: AppState) => state.mapConfig.expanded +export const selectIsMapStreaming = (state: AppState) => state.isMapStreaming diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..18fdb46 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,131 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + }, + secondary: { + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', + }, + success: { + 50: '#f0fdf4', + 100: '#dcfce7', + 200: '#bbf7d0', + 300: '#86efac', + 400: '#4ade80', + 500: '#22c55e', + 600: '#16a34a', + 700: '#15803d', + 800: '#166534', + 900: '#14532d', + }, + warning: { + 50: '#fefce8', + 100: '#fef9c3', + 200: '#fef08a', + 300: '#fde047', + 400: '#facc15', + 500: '#eab308', + 600: '#ca8a04', + 700: '#a16207', + 800: '#854d0e', + 900: '#713f12', + }, + error: { + 50: '#fef2f2', + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + }, + golf: { + green: '#2D5016', + fairway: '#355E3B', + sand: '#C2B280', + water: '#4682B4', + rough: '#3A5F0B', + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'], + mono: ['JetBrains Mono', 'Consolas', 'Monaco', 'Courier New', 'monospace'], + }, + animation: { + 'fade-in': 'fadeIn 0.3s ease-in-out', + 'fade-out': 'fadeOut 0.3s ease-in-out', + 'slide-in': 'slideIn 0.3s ease-out', + 'slide-out': 'slideOut 0.3s ease-out', + 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', + 'bounce-slow': 'bounce 2s infinite', + }, + keyframes: { + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + fadeOut: { + '0%': { opacity: '1' }, + '100%': { opacity: '0' }, + }, + slideIn: { + '0%': { transform: 'translateX(-100%)' }, + '100%': { transform: 'translateX(0)' }, + }, + slideOut: { + '0%': { transform: 'translateX(0)' }, + '100%': { transform: 'translateX(100%)' }, + }, + }, + boxShadow: { + 'golf': '0 4px 6px -1px rgba(45, 80, 22, 0.1), 0 2px 4px -1px rgba(45, 80, 22, 0.06)', + 'golf-lg': '0 10px 15px -3px rgba(45, 80, 22, 0.1), 0 4px 6px -2px rgba(45, 80, 22, 0.05)', + }, + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + 'golf-pattern': "url('data:image/svg+xml,%3Csvg width=\"40\" height=\"40\" viewBox=\"0 0 40 40\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"%232D5016\" fill-opacity=\"0.03\"%3E%3Cpath d=\"M0 40L40 0H20L0 20M40 40V20L20 40\"%3E%3C/path%3E%3C/g%3E%3C/svg%3E')", + }, + spacing: { + '18': '4.5rem', + '88': '22rem', + '120': '30rem', + '128': '32rem', + '144': '36rem', + }, + screens: { + 'xs': '480px', + '3xl': '1920px', + }, + }, + }, + plugins: [], +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..f8c023b --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@components/*": ["src/components/*"], + "@pages/*": ["src/pages/*"], + "@hooks/*": ["src/hooks/*"], + "@utils/*": ["src/utils/*"], + "@api/*": ["src/api/*"], + "@stores/*": ["src/stores/*"], + "@types/*": ["src/types/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..9098ec9 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,47 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@components': path.resolve(__dirname, './src/components'), + '@pages': path.resolve(__dirname, './src/pages'), + '@hooks': path.resolve(__dirname, './src/hooks'), + '@utils': path.resolve(__dirname, './src/utils'), + '@api': path.resolve(__dirname, './src/api'), + '@stores': path.resolve(__dirname, './src/stores'), + '@types': path.resolve(__dirname, './src/types'), + }, + }, + server: { + port: 5173, + host: true, + proxy: { + '/api': { + target: 'http://localhost:5005', + changeOrigin: true, + }, + '/ws': { + target: 'ws://localhost:5005', + ws: true, + }, + }, + }, + build: { + outDir: '../backend/ui', + emptyOutDir: true, + sourcemap: true, + rollupOptions: { + output: { + manualChunks: { + 'react-vendor': ['react', 'react-dom'], + 'ui-vendor': ['react-icons', 'clsx', 'react-hot-toast'], + }, + }, + }, + }, +}) diff --git a/scripts/dev.ps1 b/scripts/dev.ps1 new file mode 100644 index 0000000..26e9ef3 --- /dev/null +++ b/scripts/dev.ps1 @@ -0,0 +1,193 @@ +# GSPro Remote Development Script +# Starts both backend and frontend servers for development + +Write-Host "GSPro Remote Development Environment" -ForegroundColor Green +Write-Host "=====================================" -ForegroundColor Green +Write-Host "" + +# Check if Python is installed +$pythonVersion = python --version 2>$null +if (-not $pythonVersion) { + Write-Host "ERROR: Python is not installed or not in PATH" -ForegroundColor Red + Write-Host "Please install Python 3.11+ from https://www.python.org/" -ForegroundColor Yellow + exit 1 +} +Write-Host "✓ Python found: $pythonVersion" -ForegroundColor Green + +# Check if Node.js is installed +$nodeVersion = node --version 2>$null +if (-not $nodeVersion) { + Write-Host "ERROR: Node.js is not installed or not in PATH" -ForegroundColor Red + Write-Host "Please install Node.js 20+ from https://nodejs.org/" -ForegroundColor Yellow + exit 1 +} +Write-Host "✓ Node.js found: $nodeVersion" -ForegroundColor Green + +# Check if npm is installed +$npmVersion = npm --version 2>$null +if (-not $npmVersion) { + Write-Host "ERROR: npm is not installed or not in PATH" -ForegroundColor Red + exit 1 +} +Write-Host "✓ npm found: $npmVersion" -ForegroundColor Green +Write-Host "" + +# Get the script directory (project root) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$projectRoot = Split-Path -Parent $scriptDir +$backendDir = Join-Path $projectRoot "backend" +$frontendDir = Join-Path $projectRoot "frontend" + +# Function to check if a port is in use +function Test-Port { + param($port) + $tcpClient = New-Object System.Net.Sockets.TcpClient + try { + $tcpClient.Connect("localhost", $port) + $tcpClient.Close() + return $true + } catch { + return $false + } +} + +# Check if backend port is already in use +if (Test-Port 5005) { + Write-Host "WARNING: Port 5005 is already in use. Backend server might already be running." -ForegroundColor Yellow + Write-Host "Do you want to continue anyway? (Y/N): " -NoNewline + $response = Read-Host + if ($response -ne "Y" -and $response -ne "y") { + exit 0 + } +} + +# Check if frontend port is already in use +if (Test-Port 5173) { + Write-Host "WARNING: Port 5173 is already in use. Frontend server might already be running." -ForegroundColor Yellow + Write-Host "Do you want to continue anyway? (Y/N): " -NoNewline + $response = Read-Host + if ($response -ne "Y" -and $response -ne "y") { + exit 0 + } +} + +# Setup backend +Write-Host "Setting up Backend..." -ForegroundColor Cyan +Set-Location $backendDir + +# Check if virtual environment exists +$venvPath = Join-Path $backendDir ".venv" +if (-not (Test-Path $venvPath)) { + Write-Host "Creating Python virtual environment..." -ForegroundColor Yellow + python -m venv .venv + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to create virtual environment" -ForegroundColor Red + exit 1 + } +} + +# Activate virtual environment and install dependencies +Write-Host "Installing backend dependencies..." -ForegroundColor Yellow +$activateScript = Join-Path $venvPath "Scripts\Activate.ps1" +if (Test-Path $activateScript) { + & $activateScript +} else { + Write-Host "ERROR: Virtual environment activation script not found" -ForegroundColor Red + exit 1 +} + +# Check if requirements are installed +$pipList = pip list 2>$null +if (-not ($pipList -like "*fastapi*")) { + Write-Host "Installing Python packages..." -ForegroundColor Yellow + pip install -e . 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to install backend dependencies" -ForegroundColor Red + Write-Host "Try running: pip install -e . manually in the backend directory" -ForegroundColor Yellow + exit 1 + } +} + +Write-Host "✓ Backend dependencies installed" -ForegroundColor Green + +# Setup frontend +Write-Host "" +Write-Host "Setting up Frontend..." -ForegroundColor Cyan +Set-Location $frontendDir + +# Check if node_modules exists +if (-not (Test-Path "node_modules")) { + Write-Host "Installing frontend dependencies..." -ForegroundColor Yellow + npm install 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to install frontend dependencies" -ForegroundColor Red + Write-Host "Try running: npm install manually in the frontend directory" -ForegroundColor Yellow + exit 1 + } +} + +Write-Host "✓ Frontend dependencies installed" -ForegroundColor Green +Write-Host "" + +# Start servers +Write-Host "Starting Development Servers..." -ForegroundColor Cyan +Write-Host "================================" -ForegroundColor Cyan +Write-Host "" + +# Start backend server in a new PowerShell window +Write-Host "Starting Backend Server on http://localhost:5005" -ForegroundColor Green +$backendScript = @" +Write-Host 'GSPro Remote Backend Server' -ForegroundColor Cyan +Write-Host '===========================' -ForegroundColor Cyan +Write-Host '' +Write-Host 'Server: http://localhost:5005' -ForegroundColor Yellow +Write-Host 'API Docs: http://localhost:5005/api/docs' -ForegroundColor Yellow +Write-Host 'mDNS: http://gsproapp.local:5005' -ForegroundColor Yellow +Write-Host '' +Set-Location '$backendDir' +& '$activateScript' +python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 5005 --log-level info +"@ + +$backendScriptPath = Join-Path $env:TEMP "gspro-backend.ps1" +$backendScript | Out-File -FilePath $backendScriptPath -Encoding UTF8 +Start-Process powershell -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $backendScriptPath + +# Give backend a moment to start +Start-Sleep -Seconds 2 + +# Start frontend server in a new PowerShell window +Write-Host "Starting Frontend Server on http://localhost:5173" -ForegroundColor Green +$frontendScript = @" +Write-Host 'GSPro Remote Frontend Server' -ForegroundColor Cyan +Write-Host '============================' -ForegroundColor Cyan +Write-Host '' +Write-Host 'UI: http://localhost:5173' -ForegroundColor Yellow +Write-Host '' +Set-Location '$frontendDir' +npm run dev +"@ + +$frontendScriptPath = Join-Path $env:TEMP "gspro-frontend.ps1" +$frontendScript | Out-File -FilePath $frontendScriptPath -Encoding UTF8 +Start-Process powershell -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $frontendScriptPath + +Write-Host "" +Write-Host "Development servers starting..." -ForegroundColor Green +Write-Host "" +Write-Host "Access Points:" -ForegroundColor Cyan +Write-Host " Frontend UI: http://localhost:5173" -ForegroundColor White +Write-Host " Backend API: http://localhost:5005" -ForegroundColor White +Write-Host " API Docs: http://localhost:5005/api/docs" -ForegroundColor White +Write-Host " mDNS Access: http://gsproapp.local:5005" -ForegroundColor White +Write-Host "" +Write-Host "Press any key to stop watching for changes..." -ForegroundColor Yellow + +# Wait for user input +$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") + +# The servers are running in separate windows, so they'll keep running +# This script can exit, and the user can close the server windows manually +Write-Host "" +Write-Host "Note: The servers are still running in separate windows." -ForegroundColor Yellow +Write-Host "Close those windows to stop the servers." -ForegroundColor Yellow diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..c77214b --- /dev/null +++ b/start.bat @@ -0,0 +1,94 @@ +@echo off +echo =============================================== +echo GSPro Remote - Starting Development Servers +echo =============================================== +echo. + +REM Check if Python is installed +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo ERROR: Python is not installed or not in PATH + echo Please install Python 3.11+ from https://www.python.org/ + pause + exit /b 1 +) + +REM Check if Node.js is installed +node --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo ERROR: Node.js is not installed or not in PATH + echo Please install Node.js 20+ from https://nodejs.org/ + pause + exit /b 1 +) + +REM Get the directory where this script is located +set SCRIPT_DIR=%~dp0 +set PROJECT_ROOT=%SCRIPT_DIR% +set BACKEND_DIR=%PROJECT_ROOT%backend +set FRONTEND_DIR=%PROJECT_ROOT%frontend + +echo Project Root: %PROJECT_ROOT% +echo. + +REM Check if backend virtual environment exists +if not exist "%BACKEND_DIR%\.venv" ( + echo Creating Python virtual environment... + cd /d "%BACKEND_DIR%" + python -m venv .venv +) + +REM Install backend dependencies +echo Checking backend dependencies... +cd /d "%BACKEND_DIR%" +call .venv\Scripts\activate.bat +pip show fastapi >nul 2>&1 +if %errorlevel% neq 0 ( + echo Installing backend dependencies... + pip install -e . +) + +REM Check if frontend node_modules exists +if not exist "%FRONTEND_DIR%\node_modules" ( + echo Installing frontend dependencies... + cd /d "%FRONTEND_DIR%" + npm install +) + +echo. +echo =============================================== +echo Starting Backend Server on port 5005... +echo =============================================== +cd /d "%BACKEND_DIR%" +start "GSPro Remote Backend" cmd /k ".venv\Scripts\activate.bat && python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 5005" + +REM Wait a moment for backend to start +timeout /t 3 /nobreak >nul + +echo. +echo =============================================== +echo Starting Frontend Server on port 5173... +echo =============================================== +cd /d "%FRONTEND_DIR%" +start "GSPro Remote Frontend" cmd /k "npm run dev -- --host" + +REM Wait for servers to initialize +timeout /t 3 /nobreak >nul + +echo. +echo =============================================== +echo Servers are starting up... +echo =============================================== +echo. +echo Access the application at: +echo Local: http://localhost:5173 +echo Network: http://YOUR-IP:5173 +echo Backend API: http://localhost:5005 +echo API Docs: http://localhost:5005/api/docs +echo. +echo To find your IP address, run: ipconfig +echo. +echo Both servers are running in separate windows. +echo Close those windows to stop the servers. +echo. +pause diff --git a/start.py b/start.py new file mode 100644 index 0000000..5343aad --- /dev/null +++ b/start.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" +GSPro Remote - Development Server Startup Script +Simple Python script to start both backend and frontend servers +""" + +import os +import platform +import subprocess +import sys +import time +import webbrowser +from pathlib import Path + + +# Colors for terminal output +class Colors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + + +def print_header(text): + print(f"\n{Colors.HEADER}{Colors.BOLD}{'=' * 50}{Colors.ENDC}") + print(f"{Colors.HEADER}{Colors.BOLD}{text}{Colors.ENDC}") + print(f"{Colors.HEADER}{Colors.BOLD}{'=' * 50}{Colors.ENDC}\n") + + +def print_success(text): + print(f"{Colors.OKGREEN}✓ {text}{Colors.ENDC}") + + +def print_error(text): + print(f"{Colors.FAIL}✗ {text}{Colors.ENDC}") + + +def print_info(text): + print(f"{Colors.OKCYAN}→ {text}{Colors.ENDC}") + + +def check_python(): + """Check if Python is installed and version is 3.11+""" + version = sys.version_info + if version.major < 3 or (version.major == 3 and version.minor < 11): + print_error(f"Python 3.11+ required, found {version.major}.{version.minor}") + return False + print_success(f"Python {version.major}.{version.minor} found") + return True + + +def check_node(): + """Check if Node.js is installed""" + try: + result = subprocess.run(["node", "--version"], capture_output=True, text=True) + if result.returncode == 0: + print_success(f"Node.js {result.stdout.strip()} found") + return True + except FileNotFoundError: + pass + print_error("Node.js not found. Please install from https://nodejs.org/") + return False + + +def check_npm(): + """Check if npm is installed""" + try: + result = subprocess.run(["npm", "--version"], capture_output=True, text=True) + if result.returncode == 0: + print_success(f"npm {result.stdout.strip()} found") + return True + except FileNotFoundError: + pass + print_error("npm not found") + return False + + +def setup_backend(backend_dir): + """Setup backend virtual environment and dependencies""" + venv_path = backend_dir / ".venv" + + # Create virtual environment if it doesn't exist + if not venv_path.exists(): + print_info("Creating Python virtual environment...") + subprocess.run([sys.executable, "-m", "venv", str(venv_path)], cwd=backend_dir) + + # Determine the activation script based on OS + if platform.system() == "Windows": + activate_script = venv_path / "Scripts" / "activate.bat" + python_exe = venv_path / "Scripts" / "python.exe" + pip_exe = venv_path / "Scripts" / "pip.exe" + else: + activate_script = venv_path / "bin" / "activate" + python_exe = venv_path / "bin" / "python" + pip_exe = venv_path / "bin" / "pip" + + # Check if dependencies are installed + result = subprocess.run( + [str(pip_exe), "show", "fastapi"], capture_output=True, cwd=backend_dir + ) + + if result.returncode != 0: + print_info("Installing backend dependencies...") + subprocess.run([str(pip_exe), "install", "-e", "."], cwd=backend_dir) + else: + print_success("Backend dependencies already installed") + + return python_exe + + +def setup_frontend(frontend_dir): + """Setup frontend dependencies""" + node_modules = frontend_dir / "node_modules" + + if not node_modules.exists(): + print_info("Installing frontend dependencies...") + subprocess.run(["npm", "install"], cwd=frontend_dir, shell=True) + else: + print_success("Frontend dependencies already installed") + + +def get_local_ip(): + """Get local IP address""" + import socket + + try: + # Connect to a public DNS server to get local IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except: + return "localhost" + + +def main(): + print_header("GSPro Remote - Development Server Startup") + + # Get project directories + script_dir = Path(__file__).parent.resolve() + backend_dir = script_dir / "backend" + frontend_dir = script_dir / "frontend" + + print(f"Project root: {script_dir}") + + # Check prerequisites + print_header("Checking Prerequisites") + if not check_python(): + sys.exit(1) + if not check_node(): + sys.exit(1) + if not check_npm(): + sys.exit(1) + + # Setup backend + print_header("Setting up Backend") + os.chdir(backend_dir) + python_exe = setup_backend(backend_dir) + + # Setup frontend + print_header("Setting up Frontend") + os.chdir(frontend_dir) + setup_frontend(frontend_dir) + + # Start backend server + print_header("Starting Backend Server") + backend_cmd = [ + str(python_exe), + "-m", + "uvicorn", + "app.main:app", + "--reload", + "--host", + "0.0.0.0", + "--port", + "5005", + "--log-level", + "info", + ] + + if platform.system() == "Windows": + backend_process = subprocess.Popen( + backend_cmd, cwd=backend_dir, creationflags=subprocess.CREATE_NEW_CONSOLE + ) + else: + backend_process = subprocess.Popen(backend_cmd, cwd=backend_dir) + + print_success("Backend server starting on http://localhost:5005") + + # Wait for backend to start + time.sleep(3) + + # Start frontend server + print_header("Starting Frontend Server") + frontend_cmd = "npm run dev -- --host" + + if platform.system() == "Windows": + frontend_process = subprocess.Popen( + frontend_cmd, + shell=True, + cwd=frontend_dir, + creationflags=subprocess.CREATE_NEW_CONSOLE, + ) + else: + frontend_process = subprocess.Popen(frontend_cmd, shell=True, cwd=frontend_dir) + + print_success("Frontend server starting on http://localhost:5173") + + # Get local IP + local_ip = get_local_ip() + + # Print access information + print_header("Servers Started Successfully!") + print(f"{Colors.OKGREEN}Access the application at:{Colors.ENDC}") + print(f" {Colors.BOLD}Local:{Colors.ENDC} http://localhost:5173") + print(f" {Colors.BOLD}Network:{Colors.ENDC} http://{local_ip}:5173") + print(f" {Colors.BOLD}Backend API:{Colors.ENDC} http://localhost:5005") + print(f" {Colors.BOLD}API Docs:{Colors.ENDC} http://localhost:5005/api/docs") + print(f" {Colors.BOLD}mDNS:{Colors.ENDC} http://gsproapp.local:5005") + + print(f"\n{Colors.WARNING}To access from your phone/tablet:{Colors.ENDC}") + print(f"1. Ensure your device is on the same WiFi network") + print(f"2. Open a browser and go to: http://{local_ip}:5173") + + print(f"\n{Colors.BOLD}Press Ctrl+C to stop all servers{Colors.ENDC}\n") + + # Keep script running and handle shutdown + try: + # Open browser after a short delay + time.sleep(2) + webbrowser.open("http://localhost:5173") + + # Wait for processes + backend_process.wait() + frontend_process.wait() + except KeyboardInterrupt: + print(f"\n{Colors.WARNING}Shutting down servers...{Colors.ENDC}") + backend_process.terminate() + frontend_process.terminate() + print_success("Servers stopped") + sys.exit(0) + + +if __name__ == "__main__": + main()