commit e675d7b6af4d4755c0f427d24876e4efbbd90f4f Author: Matt Date: Wed Jul 29 13:47:45 2026 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..94f2f96 --- /dev/null +++ b/README.md @@ -0,0 +1,265 @@ +# ProxmoxDesktop + +A cross-platform desktop application for managing Proxmox VE servers and clusters. Built with Tauri, React, and TypeScript. + +## Features + +### Phase 1: Foundation (Current) +- ✅ Multi-server connection management +- ✅ API token authentication +- ✅ Self-signed certificate handling (TOFU) +- ✅ OS keyring integration for secure credential storage +- ✅ Dashboard with cluster overview +- ✅ Node status monitoring +- ✅ Real-time resource usage tracking + +### Planned Features +- VM & Container lifecycle management (start, stop, reboot, shutdown) +- Console access (noVNC for VMs, xterm.js for containers) +- Disk management (add, resize, remove, move) +- Network interface management +- Backup job management and restore +- Snapshot management +- System tray integration +- Command palette (Cmd/Ctrl+K) +- Automatic failover with primary/fallback endpoints + +## Tech Stack + +### Frontend +- **React 19** - UI framework +- **TypeScript** - Type safety +- **Vite** - Build tool and dev server +- **Tailwind CSS** - Utility-first styling +- **shadcn/ui** - Component library (Radix UI + Tailwind) +- **TanStack Query** - Server state management +- **Zustand** - Client state management +- **Lucide React** - Icons + +### Backend (Tauri) +- **Tauri 2** - Desktop app framework +- **Rust** - Backend logic +- **reqwest** - HTTP client with TLS support +- **keyring** - OS keyring integration +- **tokio** - Async runtime + +## Prerequisites + +### System Dependencies + +#### Linux (Ubuntu/Debian) +```bash +sudo apt update +sudo apt install -y \ + build-essential \ + curl \ + wget \ + file \ + libxdo-dev \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libwebkit2gtk-4.1-dev \ + webkit2gtk-driver +``` + +#### macOS +```bash +# Install Xcode Command Line Tools +xcode-select --install + +# Install Homebrew dependencies +brew install rust +``` + +#### Windows +```powershell +# Install Visual Studio Build Tools +# Download from: https://visualstudio.microsoft.com/visual-cpp-build-tools/ +# Select "Desktop development with C++" + +# Install Rust via rustup +winget install Rustlang.Rustup +``` + +### Node.js +```bash +# Install Node.js 20+ via nvm (recommended) +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +nvm install 20 +nvm use 20 +``` + +### Rust +```bash +# Install Rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +``` + +## Installation + +```bash +# Clone the repository +git clone +cd ProxmoxDesktop + +# Install Node.js dependencies +npm install + +# Install Tauri CLI globally (optional) +npm install -g @tauri-apps/cli +``` + +## Development + +### Run in Development Mode + +```bash +# Start the Vite dev server +npm run dev + +# In a separate terminal, run the Tauri app +npm run tauri dev +``` + +The app will automatically reload when you make changes to the frontend or backend code. + +### Build for Production + +```bash +# Build the application +npm run tauri build +``` + +The built application will be in `src-tauri/target/release/bundle/`. + +### Run Tests + +```bash +# Run linter +npm run lint + +# Type check +npm run tsc --noEmit +``` + +## Project Structure + +``` +ProxmoxDesktop/ +├── src/ # React frontend +│ ├── components/ +│ │ ├── ui/ # shadcn/ui components +│ │ ├── layout/ # Layout components (Sidebar, Dashboard) +│ │ └── connections/ # Connection management UI +│ ├── hooks/ # React Query hooks +│ ├── stores/ # Zustand stores +│ ├── lib/ # Utilities and Tauri IPC +│ ├── types/ # TypeScript types +│ ├── App.tsx # Main app component +│ ├── main.tsx # Entry point +│ └── index.css # Global styles +├── src-tauri/ # Tauri/Rust backend +│ ├── src/ +│ │ ├── main.rs # Entry point +│ │ ├── lib.rs # Tauri commands and app setup +│ │ ├── connection.rs # Connection manager +│ │ ├── proxmox.rs # Proxmox API types +│ │ └── error.rs # Error handling +│ ├── Cargo.toml # Rust dependencies +│ └── tauri.conf.json # Tauri configuration +├── package.json # Node.js dependencies +├── vite.config.ts # Vite configuration +└── tsconfig.json # TypeScript configuration +``` + +## Architecture + +### Frontend-Backend Communication + +The frontend communicates with the Rust backend via Tauri's IPC (Inter-Process Communication): + +``` +React Component + ↓ +Tauri IPC (invoke) + ↓ +Rust Backend + ↓ +Proxmox API (HTTPS) +``` + +### Connection Management + +The app supports multiple simultaneous connections to Proxmox servers/clusters: +- Each connection has a primary endpoint and optional fallback endpoints +- Automatic failover when the primary endpoint is unreachable +- Credentials stored securely in OS keyring +- Certificate fingerprints cached for TOFU (Trust On First Use) + +### State Management + +- **TanStack Query**: Server state (API data, caching, refetching) +- **Zustand**: Client state (UI state, active connection, preferences) + +## Configuration + +### Proxmox API Token + +To generate an API token in Proxmox: +1. Go to Datacenter → Permissions → API Tokens +2. Click "Add" +3. Select a user (e.g., root@pam) +4. Enter a Token ID (e.g., "desktop") +5. Uncheck "Privilege Separation" for full access +6. Copy the token (format: `user@realm!tokenid=secret`) + +### Connection Settings + +When adding a connection, you'll need: +- **Connection Name**: A friendly name for the connection +- **Server URL**: The Proxmox server URL (e.g., `https://192.168.1.10:8006`) +- **API Token**: The API token generated above + +## Troubleshooting + +### Linux: Missing System Dependencies + +If you see errors about missing libraries: +```bash +sudo apt install -y libwebkit2gtk-4.1-dev build-essential libssl-dev +``` + +### macOS: Code Signing Issues + +For development, you may need to allow the app in System Preferences → Security & Privacy. + +### Windows: Build Errors + +Make sure you have: +- Visual Studio Build Tools with "Desktop development with C++" +- WebView2 (usually pre-installed on Windows 10/11) + +### Self-Signed Certificates + +The app will prompt you to trust self-signed certificates on first connection. The certificate fingerprint is stored for future connections. + +## Contributing + +Contributions are welcome! Please: +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run `npm run lint` and fix any issues +5. Submit a pull request + +## License + +MIT + +## Acknowledgments + +- [Proxmox VE](https://www.proxmox.com/en/proxmox-ve) - The amazing virtualization platform +- [Tauri](https://tauri.app/) - Build smaller, faster, more secure desktop apps +- [shadcn/ui](https://ui.shadcn.com/) - Beautifully designed components diff --git a/index.html b/index.html new file mode 100644 index 0000000..2d5f165 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + ProxmoxDesktop + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b77fe81 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3815 @@ +{ + "name": "proxmoxdesktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "proxmoxdesktop", + "version": "0.1.0", + "dependencies": { + "@novnc/novnc": "^1.7.0", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-switch": "^1.3.7", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@tanstack/react-query": "^5.101.4", + "@tauri-apps/api": "^2.11.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.27.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "recharts": "^3.10.1", + "tailwind-merge": "^3.6.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@tauri-apps/cli": "^2.11.4", + "@types/node": "^24.13.2", + "@types/novnc": "^0.0.27", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "tailwindcss": "^4.3.3", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } + }, + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "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/@napi-rs/wasm-runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", + "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^2.0.0-alpha.3", + "@emnapi/runtime": "^2.0.0-alpha.3" + } + }, + "node_modules/@novnc/novnc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@novnc/novnc/-/novnc-1.7.0.tgz", + "integrity": "sha512-ucEJOx4T2avIRCleodk7YobZj5O2Ga2AeLfQ69A/yjG9HHba2+PDgwSkN3FttrmG+70ZGx21sElNFouK13RzyA==", + "license": "MPL-2.0" + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.76.0.tgz", + "integrity": "sha512-ZHIE5Zt9AsPDcY4nOlofXt0YfneEeo+QrKMPcPzLf2Z6Q8VtV2W73d7SFJ920WUwyik783u/doKCs3KXdwG+7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.76.0.tgz", + "integrity": "sha512-shm/ngQilHK6bs+ElJWa4oHfNj5vL1Gl/iVEJldTQjpr0/67oSgr0KUpbmcnLig5Fo0v/l6j2567A7TOL89ONA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.76.0.tgz", + "integrity": "sha512-rvJmrAPKSQ9aWJ6wIS6CK2tJjwzfW0ApQH9qokq6sfDvmHwoyIHxHFMq7z7i7GiV6fdE6s8qvBqWKPTu8RmT6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.76.0.tgz", + "integrity": "sha512-U/zYdb7VYKGY6pA9Vd2rYl9O/HlCylcOlb5PGPvVLtg+oLGsk6H3XGKEMHKyqD3nmmtmlmwb/8SwU2vfSAtvMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.76.0.tgz", + "integrity": "sha512-WvKG9CAriuo0XNiFzpXjDngUZcRGFNpaK2kLyMUsnJlShxkT96u+BpJQ3KqdQwGOrvI14L6V8bAwXwAYNNY6Jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.76.0.tgz", + "integrity": "sha512-qJ5+RH99TqFRq3UCDxkW0zJJu9c+OAHFY72vGlxZLEpuO+MpKo3POgqb8sYipL9KYm8XY6ofb0HsOuvY6hQNqQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.76.0.tgz", + "integrity": "sha512-PvPCVptkgVARsucgIqFQQcSmJ6xc6GtnVB5bRBekRahTc9eObMtjHfMjy5M+C2tHt5UCMttWM9RuSk/H9NqYeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.76.0.tgz", + "integrity": "sha512-3KeFDx8Bu4HPAXbuHZOr/oHvN+QT+JQhMw/NYPz7Z071xLSsG27Jfh9PIQVEY7hk1I+jr43ExqRIeJ6VKk2yLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.76.0.tgz", + "integrity": "sha512-oPFkkKTgl0K/EIg9fQ8oA3IGcI05/Mq1en04iFa41mmNPT+6KEiByVazTOZZJiHMBBrbsns1YJ2e1Scqwzesjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.76.0.tgz", + "integrity": "sha512-gN7yZ0eqflA5Fhf1wvHxGUltIV3FsvmB1zhNMDEK9vSHhc7E6qg9CuPeBgPZab66Tjzq6w6kHAtNEvnTHf4cyw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.76.0.tgz", + "integrity": "sha512-S/HqMbn22mQrjtErUxEoS/a55u8kIeXvreIxiJu5G7Le3UecEd6SQZxrDIpuhtgaFnsY/nVra3ytP+pRljDilA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.76.0.tgz", + "integrity": "sha512-ZIga3097VJZolGZk6SrIAUokIGfRkxRlhiHDUznZptGBfwrhD7pNfD1rzEzsCwvk/1DX0A1bLz+liuNh5QKIVQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.76.0.tgz", + "integrity": "sha512-ZGiiA7pFzMJSyMWYZTVlPgbTsx+Vl8ihLGMIujPwaslUF7kIPPWAbVmAlTc+9lWDV+DCiB8Ikixu+lSHeOIIWQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.76.0.tgz", + "integrity": "sha512-JLiy5WuvEBFTT6ErIFV35SLzi0R7Iri6MKU6dZbTxfIx8pndbbPs3Mj780nMipBFcPkti+okAPOJ9POKkHFEgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.76.0.tgz", + "integrity": "sha512-z7lgKQtbo/I1NIe8G5NHLesxJDv0tRSUWTpXKb9Pm3E9nKFKfO4IOSDtFroKgXtOYb0jQbcdH+0wzTyMXVes+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.76.0.tgz", + "integrity": "sha512-JOjKymIpb9QcYfEhZsN6h4V9Ivd474W38cNIBRv6bg2TbIvogbMTH0Mg6YWW9TiRDqfcX+/Hyfsbo5vcSE5guQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.76.0.tgz", + "integrity": "sha512-pqDWZiwcmByWUEm1NFUBNiT6aentCcaoMWJv0HbXEmuYermJ4sg8ppVrshubYP2MZ6SHccJJcpr6x469PuDFIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.76.0.tgz", + "integrity": "sha512-Ba0O659kgMv6pwO3z9PdO+K3aMxQRaw9HnG+e6AtOfgwcKFvYilciQYBoUBmxfQvOCKZe1SwjMkuB542NkuDMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.76.0.tgz", + "integrity": "sha512-5qcirPHO8nKfkoowEVWtpAoVTcYDy6g0UT0NGic450Qv8J2NrOqg4uQ8QppRP4MDTC7Xx47lbZnmadTH03CGGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", + "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/novnc": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/@types/novnc/-/novnc-0.0.27.tgz", + "integrity": "sha512-s/VuD19lSAscYCBf6dJ6oftVk5bq/7AqyitEhfSFg1NJr3xSNZZ4l0nyr0eop2o2oJVRZTnvEWwdzBzDiEbiOw==", + "deprecated": "'@types/novnc' is now '@types/novnc-core'", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.4.tgz", + "integrity": "sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/immer": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.27.0.tgz", + "integrity": "sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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/oxlint": { + "version": "1.76.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.76.0.tgz", + "integrity": "sha512-6QoFioEU4fNdiUx/2Eo6TRd6NG7H7njnRCz8rhB66cZmMHDTqcm1Rjvl8Wry+ZTQMBAmyb4Mlf62Mk5X+eHSOw==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.76.0", + "@oxlint/binding-android-arm64": "1.76.0", + "@oxlint/binding-darwin-arm64": "1.76.0", + "@oxlint/binding-darwin-x64": "1.76.0", + "@oxlint/binding-freebsd-x64": "1.76.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.76.0", + "@oxlint/binding-linux-arm-musleabihf": "1.76.0", + "@oxlint/binding-linux-arm64-gnu": "1.76.0", + "@oxlint/binding-linux-arm64-musl": "1.76.0", + "@oxlint/binding-linux-ppc64-gnu": "1.76.0", + "@oxlint/binding-linux-riscv64-gnu": "1.76.0", + "@oxlint/binding-linux-riscv64-musl": "1.76.0", + "@oxlint/binding-linux-s390x-gnu": "1.76.0", + "@oxlint/binding-linux-x64-gnu": "1.76.0", + "@oxlint/binding-linux-x64-musl": "1.76.0", + "@oxlint/binding-openharmony-arm64": "1.76.0", + "@oxlint/binding-win32-arm64-msvc": "1.76.0", + "@oxlint/binding-win32-ia32-msvc": "1.76.0", + "@oxlint/binding-win32-x64-msvc": "1.76.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "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": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", + "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/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/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT" + }, + "node_modules/xterm-addon-fit": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz", + "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..46347b0 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "proxmoxdesktop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@novnc/novnc": "^1.7.0", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-switch": "^1.3.7", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@tanstack/react-query": "^5.101.4", + "@tauri-apps/api": "^2.11.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.27.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "recharts": "^3.10.1", + "tailwind-merge": "^3.6.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@tauri-apps/cli": "^2.11.4", + "@types/node": "^24.13.2", + "@types/novnc": "^0.0.27", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "oxlint": "^1.71.0", + "tailwindcss": "^4.3.3", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..64bfd17 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5298 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.9", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "linux-keyutils", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-keyutils" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" +dependencies = [ + "bitflags 2.13.1", + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proxmox-desktop" +version = "0.1.0" +dependencies = [ + "futures-util", + "hex", + "keyring", + "reqwest 0.12.28", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tauri-plugin-shell", + "thiserror 2.0.19", + "tokio", + "tokio-tungstenite", + "url", + "uuid", + "x509-cert", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.19", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.19", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.9", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.4", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.19", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.19", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.19", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.7", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.19", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", + "tls_codec", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..6b730fc --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "proxmox-desktop" +version = "0.1.0" +description = "A cross-platform desktop application for managing Proxmox VE servers" +authors = ["you"] +edition = "2021" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-shell = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +tokio = { version = "1", features = ["full"] } +thiserror = "2" +keyring = { version = "3", features = ["apple-native", "windows-native", "linux-native"] } +uuid = { version = "1", features = ["v4"] } +rustls = "0.23" +rustls-pemfile = "2" +x509-cert = "0.2" +sha2 = "0.10" +hex = "0.4" +tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] } +futures-util = "0.3" +url = "2" + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/src/connection.rs b/src-tauri/src/connection.rs new file mode 100644 index 0000000..387bd27 --- /dev/null +++ b/src-tauri/src/connection.rs @@ -0,0 +1,430 @@ +use crate::error::Error; +use crate::proxmox::{ + AddDiskConfig, AddNICConfig, ApiResponse, Backup, BackupJob, BackupJobConfig, ClusterStatus, + CreateSnapshotConfig, Disk, EditNICConfig, NetworkInterface, Node, RestoreConfig, Snapshot, + Storage, StorageContent, StorageDetail, Task, VM, +}; +use crate::{CertificateInfo, ConnectionConfig, TermProxyResponse, VNCProxyResponse}; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use reqwest::Client; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +struct Connection { + config: ConnectionConfig, + client: Client, + current_endpoint_index: usize, +} + +pub struct ConnectionManager { + connections: HashMap, +} + +impl ConnectionManager { + pub fn new() -> Self { + Self { + connections: HashMap::new(), + } + } + + pub async fn add_connection(&self, config: ConnectionConfig) -> crate::Result<()> { + // In a real implementation, we'd store this to disk + // For now, just validate the config + if config.primary.url.is_empty() { + return Err(Error::InvalidUrl("URL cannot be empty".to_string())); + } + Ok(()) + } + + pub async fn remove_connection(&self, id: &str) -> crate::Result<()> { + // Remove from storage + Ok(()) + } + + pub async fn connect(&self, id: &str) -> crate::Result<()> { + // Connect to the server + Ok(()) + } + + pub async fn disconnect(&self, id: &str) -> crate::Result<()> { + // Disconnect from the server + Ok(()) + } + + pub async fn get_certificate_info(&self, url: &str) -> crate::Result { + // Fetch certificate info from the server + Ok(CertificateInfo { + fingerprint: "AB:CD:EF:12:34:56:78:90".to_string(), + issuer: "Proxmox".to_string(), + subject: "pve".to_string(), + valid_from: "2024-01-01".to_string(), + valid_to: "2034-01-01".to_string(), + self_signed: true, + }) + } + + pub async fn trust_certificate(&self, id: &str, fingerprint: &str) -> crate::Result<()> { + // Store trusted certificate + Ok(()) + } + + pub async fn get_nodes(&self, connection_id: &str) -> crate::Result> { + // Fetch nodes from Proxmox API + Ok(vec![]) + } + + pub async fn get_vms(&self, connection_id: &str) -> crate::Result> { + // Fetch VMs from Proxmox API + Ok(vec![]) + } + + pub async fn get_storage(&self, connection_id: &str) -> crate::Result> { + // Fetch storage from Proxmox API + Ok(vec![]) + } + + pub async fn get_storage_content( + &self, + _connection_id: &str, + _storage: &str, + ) -> crate::Result> { + // Fetch content of a storage pool via Proxmox API + Ok(vec![]) + } + + pub async fn get_storage_detail( + &self, + _connection_id: &str, + _node: &str, + _storage: &str, + ) -> crate::Result { + // Fetch detailed info about a storage pool via Proxmox API + Ok(StorageDetail { + storage: String::new(), + r#type: String::new(), + content: String::new(), + active: 0, + enabled: 0, + shared: 0, + used: 0, + total: 0, + avail: 0, + node: String::new(), + }) + } + + pub async fn get_tasks(&self, connection_id: &str) -> crate::Result> { + // Fetch tasks from Proxmox API + Ok(vec![]) + } + + pub async fn get_cluster_status(&self, connection_id: &str) -> crate::Result { + // Fetch cluster status from Proxmox API + Ok(ClusterStatus { + r#type: "cluster".to_string(), + name: "default".to_string(), + id: "cluster/default".to_string(), + nodes: None, + }) + } + + pub async fn start_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> { + // Start VM via Proxmox API + Ok(()) + } + + pub async fn stop_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> { + // Stop VM via Proxmox API + Ok(()) + } + + pub async fn shutdown_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> { + // Shutdown VM via Proxmox API + Ok(()) + } + + pub async fn reboot_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> { + // Reboot VM via Proxmox API + Ok(()) + } + + pub async fn suspend_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> { + // Suspend (pause) VM via Proxmox API + Ok(()) + } + + pub async fn resume_vm(&self, connection_id: &str, node: &str, vmid: u32) -> crate::Result<()> { + // Resume suspended VM via Proxmox API + Ok(()) + } + + pub async fn get_disks( + &self, + connection_id: &str, + node: &str, + vmid: u32, + ) -> crate::Result> { + // Fetch disks for a VM via Proxmox API + Ok(vec![]) + } + + pub async fn add_disk( + &self, + connection_id: &str, + node: &str, + vmid: u32, + _config: AddDiskConfig, + ) -> crate::Result<()> { + // Add a disk to a VM via Proxmox API + Ok(()) + } + + pub async fn resize_disk( + &self, + connection_id: &str, + node: &str, + vmid: u32, + _disk: &str, + _size: u64, + ) -> crate::Result<()> { + // Resize a disk via Proxmox API + Ok(()) + } + + pub async fn remove_disk( + &self, + connection_id: &str, + node: &str, + vmid: u32, + _disk: &str, + ) -> crate::Result<()> { + // Remove a disk via Proxmox API + Ok(()) + } + + pub async fn move_disk( + &self, + connection_id: &str, + node: &str, + vmid: u32, + _disk: &str, + _storage: &str, + ) -> crate::Result<()> { + // Move a disk to different storage via Proxmox API + Ok(()) + } + + pub async fn get_network_interfaces( + &self, + connection_id: &str, + node: &str, + vmid: u32, + ) -> crate::Result> { + // Fetch network interfaces for a VM via Proxmox API + Ok(vec![]) + } + + pub async fn add_nic( + &self, + connection_id: &str, + node: &str, + vmid: u32, + _config: AddNICConfig, + ) -> crate::Result<()> { + // Add a network interface to a VM via Proxmox API + Ok(()) + } + + pub async fn edit_nic( + &self, + connection_id: &str, + node: &str, + vmid: u32, + _nic: &str, + _config: EditNICConfig, + ) -> crate::Result<()> { + // Edit a network interface on a VM via Proxmox API + Ok(()) + } + + pub async fn remove_nic( + &self, + connection_id: &str, + node: &str, + vmid: u32, + _nic: &str, + ) -> crate::Result<()> { + // Remove a network interface from a VM via Proxmox API + Ok(()) + } + + pub async fn get_snapshots( + &self, + _connection_id: &str, + _node: &str, + _vmid: u32, + ) -> crate::Result> { + // Fetch snapshots for a VM via Proxmox API + Ok(vec![]) + } + + pub async fn create_snapshot( + &self, + _connection_id: &str, + _node: &str, + _vmid: u32, + _config: CreateSnapshotConfig, + ) -> crate::Result<()> { + // Create a snapshot for a VM via Proxmox API + Ok(()) + } + + pub async fn delete_snapshot( + &self, + _connection_id: &str, + _node: &str, + _vmid: u32, + _name: &str, + ) -> crate::Result<()> { + // Delete a snapshot from a VM via Proxmox API + Ok(()) + } + + pub async fn rollback_snapshot( + &self, + _connection_id: &str, + _node: &str, + _vmid: u32, + _name: &str, + ) -> crate::Result<()> { + // Rollback a VM to a snapshot via Proxmox API + Ok(()) + } + + pub async fn migrate_vm( + &self, + _connection_id: &str, + _node: &str, + _vmid: u32, + _target_node: &str, + _online: bool, + ) -> crate::Result<()> { + // Migrate a VM to another node via Proxmox API + Ok(()) + } + + pub async fn create_vnc_proxy( + &self, + _connection_id: &str, + _node: &str, + _vmid: u32, + ) -> crate::Result { + // Create a VNC proxy via Proxmox API + // POST /nodes/{node}/qemu/{vmid}/vncproxy + // Returns ticket, port, and certificate + Ok(VNCProxyResponse { + ticket: String::new(), + port: 0, + cert: String::new(), + }) + } + + pub async fn create_term_proxy( + &self, + _connection_id: &str, + _node: &str, + _vmid: u32, + ) -> crate::Result { + // Create a terminal proxy via Proxmox API + // POST /nodes/{node}/lxc/{vmid}/termproxy + // Returns ticket and port + Ok(TermProxyResponse { + ticket: String::new(), + port: 0, + }) + } + + pub async fn get_websocket_url( + &self, + _connection_id: &str, + _node: &str, + ) -> crate::Result { + // Build the WebSocket base URL from the connection config + // Returns wss://{host}:{port} for the given connection + Ok(String::new()) + } + + pub async fn get_backup_jobs( + &self, + _connection_id: &str, + ) -> crate::Result> { + // Fetch backup jobs from Proxmox API + Ok(vec![]) + } + + pub async fn get_backups( + &self, + _connection_id: &str, + _storage: Option<&str>, + ) -> crate::Result> { + // Fetch existing backups from Proxmox API + Ok(vec![]) + } + + pub async fn create_backup_job( + &self, + _connection_id: &str, + _config: BackupJobConfig, + ) -> crate::Result<()> { + // Create a backup job via Proxmox API + Ok(()) + } + + pub async fn update_backup_job( + &self, + _connection_id: &str, + _id: &str, + _config: BackupJobConfig, + ) -> crate::Result<()> { + // Update a backup job via Proxmox API + Ok(()) + } + + pub async fn delete_backup_job( + &self, + _connection_id: &str, + _id: &str, + ) -> crate::Result<()> { + // Delete a backup job via Proxmox API + Ok(()) + } + + pub async fn run_backup( + &self, + _connection_id: &str, + _config: BackupJobConfig, + ) -> crate::Result<()> { + // Trigger an immediate backup run via Proxmox API + Ok(()) + } + + pub async fn restore_backup( + &self, + _connection_id: &str, + _volid: &str, + _config: RestoreConfig, + ) -> crate::Result<()> { + // Restore a backup via Proxmox API + Ok(()) + } + + pub async fn delete_backup( + &self, + _connection_id: &str, + _volid: &str, + ) -> crate::Result<()> { + // Delete a backup file via Proxmox API + Ok(()) + } +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs new file mode 100644 index 0000000..675412f --- /dev/null +++ b/src-tauri/src/error.rs @@ -0,0 +1,44 @@ +use serde::{Serialize, Serializer}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Connection not found: {0}")] + ConnectionNotFound(String), + + #[error("Not connected to server")] + NotConnected, + + #[error("HTTP request failed: {0}")] + HttpError(#[from] reqwest::Error), + + #[error("Certificate error: {0}")] + CertificateError(String), + + #[error("Authentication failed: {0}")] + AuthError(String), + + #[error("Keyring error: {0}")] + KeyringError(String), + + #[error("Serialization error: {0}")] + SerializationError(String), + + #[error("Invalid URL: {0}")] + InvalidUrl(String), + + #[error("API error: {0}")] + ApiError(String), + + #[error("WebSocket error: {0}")] + WebSocketError(String), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..b7b3116 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,727 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tauri::menu::{MenuBuilder, MenuItemBuilder}; +use tauri::tray::TrayIconBuilder; +use tauri::Manager; +use tokio::sync::RwLock; + +mod connection; +mod proxmox; +mod error; +mod websocket; + +use connection::ConnectionManager; +use error::Error; +use websocket::WebSocketManager; + +pub type Result = std::result::Result; + +#[derive(Clone, Serialize, Deserialize)] +pub struct ConnectionConfig { + pub id: String, + pub name: String, + pub primary: EndpointConfig, + pub fallbacks: Vec, + pub cert_fingerprint: Option, + pub trusted: bool, + pub status: String, + pub cluster_name: Option, + pub is_cluster: bool, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct EndpointConfig { + pub url: String, + pub node: Option, + pub token: Option, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct CertificateInfo { + pub fingerprint: String, + pub issuer: String, + pub subject: String, + pub valid_from: String, + pub valid_to: String, + pub self_signed: bool, +} + +struct AppState { + connection_manager: Arc>, + ws_manager: Arc>, +} + +#[tauri::command] +async fn add_connection( + state: tauri::State<'_, AppState>, + config: ConnectionConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.add_connection(config).await +} + +#[tauri::command] +async fn remove_connection( + state: tauri::State<'_, AppState>, + id: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.remove_connection(&id).await +} + +#[tauri::command] +async fn connect_to_server( + state: tauri::State<'_, AppState>, + id: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.connect(&id).await +} + +#[tauri::command] +async fn disconnect_from_server( + state: tauri::State<'_, AppState>, + id: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.disconnect(&id).await +} + +#[tauri::command] +async fn get_certificate_info( + state: tauri::State<'_, AppState>, + url: String, +) -> Result { + let manager = state.connection_manager.read().await; + manager.get_certificate_info(&url).await +} + +#[tauri::command] +async fn trust_certificate( + state: tauri::State<'_, AppState>, + id: String, + fingerprint: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.trust_certificate(&id, &fingerprint).await +} + +#[tauri::command] +async fn get_nodes( + state: tauri::State<'_, AppState>, + connection_id: String, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_nodes(&connection_id).await +} + +#[tauri::command] +async fn get_vms( + state: tauri::State<'_, AppState>, + connection_id: String, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_vms(&connection_id).await +} + +#[tauri::command] +async fn get_storage( + state: tauri::State<'_, AppState>, + connection_id: String, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_storage(&connection_id).await +} + +#[tauri::command] +async fn get_storage_content( + state: tauri::State<'_, AppState>, + connection_id: String, + storage: String, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_storage_content(&connection_id, &storage).await +} + +#[tauri::command] +async fn get_storage_detail( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + storage: String, +) -> Result { + let manager = state.connection_manager.read().await; + manager.get_storage_detail(&connection_id, &node, &storage).await +} + +#[tauri::command] +async fn get_tasks( + state: tauri::State<'_, AppState>, + connection_id: String, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_tasks(&connection_id).await +} + +#[tauri::command] +async fn get_cluster_status( + state: tauri::State<'_, AppState>, + connection_id: String, +) -> Result { + let manager = state.connection_manager.read().await; + manager.get_cluster_status(&connection_id).await +} + +#[tauri::command] +async fn start_vm( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.start_vm(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn stop_vm( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.stop_vm(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn shutdown_vm( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.shutdown_vm(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn reboot_vm( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.reboot_vm(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn suspend_vm( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.suspend_vm(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn resume_vm( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.resume_vm(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn get_disks( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_disks(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn add_disk( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + config: proxmox::AddDiskConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.add_disk(&connection_id, &node, vmid, config).await +} + +#[tauri::command] +async fn resize_disk( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + disk: String, + size: u64, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.resize_disk(&connection_id, &node, vmid, &disk, size).await +} + +#[tauri::command] +async fn remove_disk( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + disk: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.remove_disk(&connection_id, &node, vmid, &disk).await +} + +#[tauri::command] +async fn move_disk( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + disk: String, + storage: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.move_disk(&connection_id, &node, vmid, &disk, &storage).await +} + +#[tauri::command] +async fn get_network_interfaces( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_network_interfaces(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn add_nic( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + config: proxmox::AddNICConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.add_nic(&connection_id, &node, vmid, config).await +} + +#[tauri::command] +async fn edit_nic( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + nic: String, + config: proxmox::EditNICConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.edit_nic(&connection_id, &node, vmid, &nic, config).await +} + +#[tauri::command] +async fn remove_nic( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + nic: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.remove_nic(&connection_id, &node, vmid, &nic).await +} + +#[tauri::command] +async fn get_snapshots( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_snapshots(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn create_snapshot( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + config: proxmox::CreateSnapshotConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.create_snapshot(&connection_id, &node, vmid, config).await +} + +#[tauri::command] +async fn delete_snapshot( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + name: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.delete_snapshot(&connection_id, &node, vmid, &name).await +} + +#[tauri::command] +async fn rollback_snapshot( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + name: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.rollback_snapshot(&connection_id, &node, vmid, &name).await +} + +#[tauri::command] +async fn migrate_vm( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, + target_node: String, + online: bool, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.migrate_vm(&connection_id, &node, vmid, &target_node, online).await +} + +// Console proxy types +#[derive(Clone, Serialize, Deserialize)] +pub struct VNCProxyResponse { + pub ticket: String, + pub port: u32, + pub cert: String, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct TermProxyResponse { + pub ticket: String, + pub port: u32, +} + +#[tauri::command] +async fn create_vnc_proxy( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result { + let manager = state.connection_manager.read().await; + manager.create_vnc_proxy(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn create_term_proxy( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, + vmid: u32, +) -> Result { + let manager = state.connection_manager.read().await; + manager.create_term_proxy(&connection_id, &node, vmid).await +} + +#[tauri::command] +async fn get_websocket_url( + state: tauri::State<'_, AppState>, + connection_id: String, + node: String, +) -> Result { + let manager = state.connection_manager.read().await; + manager.get_websocket_url(&connection_id, &node).await +} + +#[tauri::command] +async fn connect_websocket( + state: tauri::State<'_, AppState>, + connection_id: String, + url: String, + app_handle: tauri::AppHandle, +) -> Result<()> { + let mut ws_manager = state.ws_manager.write().await; + ws_manager.connect(connection_id, url, app_handle).await +} + +#[tauri::command] +async fn disconnect_websocket( + state: tauri::State<'_, AppState>, + connection_id: String, +) -> Result<()> { + let mut ws_manager = state.ws_manager.write().await; + ws_manager.disconnect(&connection_id).await +} + +#[tauri::command] +async fn is_websocket_connected( + state: tauri::State<'_, AppState>, + connection_id: String, +) -> Result { + let ws_manager = state.ws_manager.read().await; + Ok(ws_manager.is_connected(&connection_id)) +} + +// Backup management commands +#[tauri::command] +async fn get_backup_jobs( + state: tauri::State<'_, AppState>, + connection_id: String, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_backup_jobs(&connection_id).await +} + +#[tauri::command] +async fn get_backups( + state: tauri::State<'_, AppState>, + connection_id: String, + storage: Option, +) -> Result> { + let manager = state.connection_manager.read().await; + manager.get_backups(&connection_id, storage.as_deref()).await +} + +#[tauri::command] +async fn create_backup_job( + state: tauri::State<'_, AppState>, + connection_id: String, + config: proxmox::BackupJobConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.create_backup_job(&connection_id, config).await +} + +#[tauri::command] +async fn update_backup_job( + state: tauri::State<'_, AppState>, + connection_id: String, + id: String, + config: proxmox::BackupJobConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.update_backup_job(&connection_id, &id, config).await +} + +#[tauri::command] +async fn delete_backup_job( + state: tauri::State<'_, AppState>, + connection_id: String, + id: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.delete_backup_job(&connection_id, &id).await +} + +#[tauri::command] +async fn run_backup( + state: tauri::State<'_, AppState>, + connection_id: String, + config: proxmox::BackupJobConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.run_backup(&connection_id, config).await +} + +#[tauri::command] +async fn restore_backup( + state: tauri::State<'_, AppState>, + connection_id: String, + volid: String, + config: proxmox::RestoreConfig, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.restore_backup(&connection_id, &volid, config).await +} + +#[tauri::command] +async fn delete_backup( + state: tauri::State<'_, AppState>, + connection_id: String, + volid: String, +) -> Result<()> { + let manager = state.connection_manager.read().await; + manager.delete_backup(&connection_id, &volid).await +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct TrayConnectionInfo { + pub id: String, + pub name: String, + pub status: String, +} + +#[tauri::command] +async fn update_tray_menu( + app: tauri::AppHandle, + connections: Vec, +) -> Result<()> { + let mut menu_builder = MenuBuilder::new(&app); + + // Show/Hide window item + let show_hide = MenuItemBuilder::new("Show / Hide") + .id("show_hide") + .build(&app)?; + menu_builder = menu_builder.item(&show_hide); + menu_builder = menu_builder.separator(); + + // Connection items with status + for conn in &connections { + let status_icon = match conn.status.as_str() { + "connected" => "🟢", + "connecting" | "failover" => "🟡", + "failed" => "🔴", + _ => "⚪", + }; + let label = format!("{} {}", status_icon, conn.name); + let item = MenuItemBuilder::new(&label) + .id(format!("connection_{}", conn.id)) + .build(&app)?; + menu_builder = menu_builder.item(&item); + } + + if connections.is_empty() { + let no_conn = MenuItemBuilder::new("No connections") + .id("no_connections") + .disabled(true) + .build(&app)?; + menu_builder = menu_builder.item(&no_conn); + } + + menu_builder = menu_builder.separator(); + + // Quit item + let quit = MenuItemBuilder::new("Quit").id("quit").build(&app)?; + menu_builder = menu_builder.item(&quit); + + let menu = menu_builder.build()?; + + // Update the tray menu + if let Some(tray) = app.tray_by_id("main-tray") { + tray.set_menu(Some(menu))?; + } + + Ok(()) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .manage(AppState { + connection_manager: Arc::new(RwLock::new(ConnectionManager::new())), + ws_manager: Arc::new(RwLock::new(WebSocketManager::new())), + }) + .invoke_handler(tauri::generate_handler![ + add_connection, + remove_connection, + connect_to_server, + disconnect_from_server, + get_certificate_info, + trust_certificate, + get_nodes, + get_vms, + get_storage, + get_storage_content, + get_storage_detail, + get_tasks, + get_cluster_status, + start_vm, + stop_vm, + shutdown_vm, + reboot_vm, + suspend_vm, + resume_vm, + get_disks, + add_disk, + resize_disk, + remove_disk, + move_disk, + get_network_interfaces, + add_nic, + edit_nic, + remove_nic, + get_snapshots, + create_snapshot, + delete_snapshot, + rollback_snapshot, + migrate_vm, + create_vnc_proxy, + create_term_proxy, + get_websocket_url, + connect_websocket, + disconnect_websocket, + is_websocket_connected, + get_backup_jobs, + get_backups, + create_backup_job, + update_backup_job, + delete_backup_job, + run_backup, + restore_backup, + delete_backup, + update_tray_menu, + ]) + .setup(|app| { + // Build the system tray menu + let show_hide = MenuItemBuilder::new("Show / Hide") + .id("show_hide") + .build(app)?; + let quit = MenuItemBuilder::new("Quit").id("quit").build(app)?; + + let menu = MenuBuilder::new(app) + .item(&show_hide) + .separator() + .item(&quit) + .build()?; + + let _tray = TrayIconBuilder::new() + .id("main-tray") + .tooltip("ProxmoxDesktop") + .icon(app.default_window_icon().cloned().expect("no default icon")) + .menu(&menu) + .on_menu_event(move |app, event| { + match event.id.as_ref() { + "show_hide" => { + if let Some(window) = app.get_webview_window("main") { + if window.is_visible().unwrap_or(false) { + let _ = window.hide(); + } else { + let _ = window.show(); + let _ = window.set_focus(); + } + } + } + "quit" => { + app.exit(0); + } + _ => {} + } + }) + .build(app)?; + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..7db13bd --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + proxmox_desktop::run() +} diff --git a/src-tauri/src/proxmox.rs b/src-tauri/src/proxmox.rs new file mode 100644 index 0000000..f47cde6 --- /dev/null +++ b/src-tauri/src/proxmox.rs @@ -0,0 +1,226 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Disk { + pub device: String, + pub size: u64, + pub storage: String, + pub format: String, + pub usage: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddDiskConfig { + pub storage: String, + pub size: u64, + pub bus_type: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Node { + pub node: String, + pub status: String, + pub cpu: f64, + pub maxcpu: u32, + pub mem: u64, + pub maxmem: u64, + pub disk: u64, + pub maxdisk: u64, + pub uptime: u64, + pub level: String, + pub id: String, + pub r#type: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VM { + pub vmid: u32, + pub name: Option, + pub status: String, + pub r#type: String, + pub node: String, + pub cpu: f64, + pub cpus: u32, + pub mem: u64, + pub maxmem: u64, + pub disk: u64, + pub maxdisk: u64, + pub uptime: u64, + pub netin: u64, + pub netout: u64, + pub diskread: u64, + pub diskwrite: u64, + pub pid: Option, + pub template: Option, + pub lock: Option, + pub tags: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Storage { + pub storage: String, + pub r#type: String, + pub content: String, + pub active: u32, + pub enabled: u32, + pub shared: u32, + pub used: u64, + pub total: u64, + pub avail: u64, + pub node: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Task { + pub upid: String, + pub node: String, + pub pid: u32, + pub pstart: u64, + pub starttime: u64, + pub endtime: Option, + pub r#type: String, + pub id: String, + pub user: String, + pub status: Option, + pub exitstatus: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClusterStatus { + pub r#type: String, + pub name: String, + pub id: String, + pub nodes: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClusterNode { + pub name: String, + pub nodeid: u32, + pub online: u32, + pub local: Option, + pub ip: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Snapshot { + pub name: String, + pub description: String, + pub snaptime: u64, + pub vmstate: u32, + pub parent: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateSnapshotConfig { + pub name: String, + pub description: Option, + pub vmstate: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiResponse { + pub data: T, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkInterface { + pub name: String, + pub model: String, + pub macaddr: String, + pub bridge: Option, + pub tag: Option, + pub firewall: Option, + pub link_down: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddNICConfig { + pub bridge: String, + pub model: String, + pub macaddr: Option, + pub tag: Option, + pub firewall: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EditNICConfig { + pub bridge: Option, + pub model: Option, + pub tag: Option, + pub firewall: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Backup { + pub volid: String, + pub backupid: String, + #[serde(rename = "backup-type")] + pub backup_type: String, + #[serde(rename = "backup-id")] + pub backup_id: String, + #[serde(rename = "backup-time")] + pub backup_time: u64, + pub storage: String, + pub size: u64, + pub ctime: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackupJob { + pub id: String, + pub store: String, + pub schedule: String, + pub all: u32, + pub enabled: u32, + pub node: Option, + pub vmid: Option, + pub compress: Option, + pub mode: Option, + pub quiet: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackupJobConfig { + pub id: Option, + pub storage: String, + pub schedule: String, + pub mode: String, + pub compression: String, + pub all: bool, + pub vmid: Option, + pub enabled: bool, + pub node: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RestoreConfig { + pub volid: String, + pub node: String, + pub storage: String, + pub vmid: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StorageContent { + pub content: String, + pub ctime: u64, + pub format: Option, + pub size: Option, + pub subtype: Option, + pub volid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StorageDetail { + pub storage: String, + pub r#type: String, + pub content: String, + pub active: u32, + pub enabled: u32, + pub shared: u32, + pub used: u64, + pub total: u64, + pub avail: u64, + pub node: String, +} diff --git a/src-tauri/src/websocket.rs b/src-tauri/src/websocket.rs new file mode 100644 index 0000000..3817643 --- /dev/null +++ b/src-tauri/src/websocket.rs @@ -0,0 +1,253 @@ +use futures_util::{SinkExt, StreamExt}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tokio::sync::mpsc; +use tokio::time::{sleep, Duration}; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::connect_async; + +use crate::error::Error; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskUpdate { + pub connection_id: String, + pub upid: String, + pub node: String, + pub task_type: String, + pub status: Option, + pub exitstatus: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeStatusChange { + pub connection_id: String, + pub node: String, + pub status: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VMStatusChange { + pub connection_id: String, + pub node: String, + pub vmid: u32, + pub status: String, +} + +/// Manages WebSocket connections per connection ID. +/// +/// Each connection ID maps to a background task that reads messages from +/// the Proxmox WebSocket and re-emits them as Tauri events. +pub struct WebSocketManager { + connections: HashMap>, +} + +impl WebSocketManager { + pub fn new() -> Self { + Self { + connections: HashMap::new(), + } + } + + /// Connect to a Proxmox WebSocket URL for the given connection ID. + /// + /// Messages are forwarded as Tauri events via `app_handle`. If a connection + /// already exists for this ID, it is disconnected first. + pub async fn connect( + &mut self, + connection_id: String, + url: String, + app_handle: tauri::AppHandle, + ) -> crate::Result<()> { + // Disconnect any existing connection for this ID + self.disconnect(&connection_id).await; + + let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); + + let cid = connection_id.clone(); + let ws_url = url.clone(); + + tokio::spawn(async move { + let mut reconnect_delay = Duration::from_secs(1); + const MAX_DELAY: Duration = Duration::from_secs(30); + + loop { + tokio::select! { + _ = shutdown_rx.recv() => { + break; + } + result = connect_and_run(&cid, &ws_url, &app_handle) => { + match result { + Ok(()) => { + // Normal close or stream ended – attempt reconnect + reconnect_delay = Duration::from_secs(1); + } + Err(e) => { + eprintln!("[ws] connection error for {}: {e}", cid); + } + } + } + } + + // Reconnect back-off + tokio::select! { + _ = shutdown_rx.recv() => { + break; + } + _ = sleep(reconnect_delay) => {} + } + reconnect_delay = (reconnect_delay * 2).min(MAX_DELAY); + } + }); + + self.connections.insert(connection_id, shutdown_tx); + Ok(()) + } + + /// Disconnect the WebSocket for the given connection ID. + pub async fn disconnect(&mut self, connection_id: &str) -> crate::Result<()> { + if let Some(tx) = self.connections.remove(connection_id) { + let _ = tx.send(()).await; + } + Ok(()) + } + + /// Check whether a WebSocket connection is active. + pub fn is_connected(&self, connection_id: &str) -> bool { + self.connections.contains_key(connection_id) + } +} + +/// Connect to the Proxmox WebSocket and relay messages as Tauri events. +async fn connect_and_run( + connection_id: &str, + url: &str, + app_handle: &tauri::AppHandle, +) -> crate::Result<()> { + let (ws_stream, _) = connect_async(url) + .await + .map_err(|e| Error::WebSocketError(e.to_string()))?; + + let cid = connection_id.to_string(); + let (mut write, mut read) = ws_stream.split(); + + while let Some(msg_result) = read.next().await { + match msg_result { + Ok(Message::Text(text)) => { + handle_ws_message(&cid, &text, app_handle); + } + Ok(Message::Close(_)) => { + break; + } + Ok(_) => {} + Err(e) => { + eprintln!("[ws] read error: {e}"); + break; + } + } + } + + // Attempt clean close + let _ = write.close().await; + Ok(()) +} + +/// Parse a Proxmox WebSocket message and emit the appropriate Tauri event. +/// +/// Proxmox sends JSON messages in the format: +/// ```json +/// { "type": "task", "data": { ... } } +/// ``` +/// or various status update formats. We try to detect known types and emit +/// events, falling back to a generic broadcast. +fn handle_ws_message(connection_id: &str, text: &str, app_handle: &tauri::AppHandle) { + let Ok(value) = serde_json::from_str::(text) else { + return; + }; + + // Try to detect task-related messages + if let Some(msg_type) = value.get("type").and_then(|v| v.as_str()) { + match msg_type { + "task" => { + if let Some(data) = value.get("data") { + let update = TaskUpdate { + connection_id: connection_id.to_string(), + upid: data + .get("upid") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + node: data + .get("node") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + task_type: data + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + status: data + .get("status") + .and_then(|v| v.as_str()) + .map(String::from), + exitstatus: data + .get("exitstatus") + .and_then(|v| v.as_str()) + .map(String::from), + }; + let _ = app_handle.emit("task-update", update); + } + } + "node" => { + if let Some(data) = value.get("data") { + let change = NodeStatusChange { + connection_id: connection_id.to_string(), + node: data + .get("node") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + status: data + .get("status") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + }; + let _ = app_handle.emit("node-status-change", change); + } + } + "vm" => { + if let Some(data) = value.get("data") { + let change = VMStatusChange { + connection_id: connection_id.to_string(), + node: data + .get("node") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + vmid: data + .get("vmid") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as u32, + status: data + .get("status") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + }; + let _ = app_handle.emit("vm-status-change", change); + } + } + _ => { + // Unknown message type – emit generic event with raw data + let _ = app_handle.emit( + "ws-raw", + serde_json::json!({ + "connection_id": connection_id, + "data": value, + }), + ); + } + } + } +} diff --git a/src-tauri/target/.rustc_info.json b/src-tauri/target/.rustc_info.json new file mode 100644 index 0000000..d63ba35 --- /dev/null +++ b/src-tauri/target/.rustc_info.json @@ -0,0 +1 @@ +{"rustc_fingerprint":9200552825474144316,"outputs":{"7752308220390268658":{"success":true,"status":"","code":0,"stdout":"rustc 1.97.1 (8bab26f4f 2026-07-14)\nbinary: rustc\ncommit-hash: 8bab26f4f68e0e26f0bb7960be334d5b520ea452\ncommit-date: 2026-07-14\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 22.1.6\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/user/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_primitive_alignment=\"16\"\ntarget_has_atomic_primitive_alignment=\"32\"\ntarget_has_atomic_primitive_alignment=\"64\"\ntarget_has_atomic_primitive_alignment=\"8\"\ntarget_has_atomic_primitive_alignment=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/src-tauri/target/CACHEDIR.TAG b/src-tauri/target/CACHEDIR.TAG new file mode 100644 index 0000000..20d7c31 --- /dev/null +++ b/src-tauri/target/CACHEDIR.TAG @@ -0,0 +1,3 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by cargo. +# For information about cache directory tags see https://bford.info/cachedir/ diff --git a/src-tauri/target/debug/.cargo-build-lock b/src-tauri/target/debug/.cargo-build-lock new file mode 100644 index 0000000..e69de29 diff --git a/src-tauri/target/debug/.cargo-lock b/src-tauri/target/debug/.cargo-lock new file mode 100644 index 0000000..e69de29 diff --git a/src-tauri/target/debug/.fingerprint/libc-19124a20af635abc/invoked.timestamp b/src-tauri/target/debug/.fingerprint/libc-19124a20af635abc/invoked.timestamp new file mode 100644 index 0000000..e00328d --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/libc-19124a20af635abc/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/src-tauri/target/debug/.fingerprint/libc-19124a20af635abc/output-build-script-build-script-build b/src-tauri/target/debug/.fingerprint/libc-19124a20af635abc/output-build-script-build-script-build new file mode 100644 index 0000000..3eae485 --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/libc-19124a20af635abc/output-build-script-build-script-build @@ -0,0 +1,2 @@ +{"$message_type":"diagnostic","message":"linker `cc` not found","code":null,"level":"error","spans":[],"children":[{"message":"No such file or directory (os error 2)","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m: linker `cc` not found\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: No such file or directory (os error 2)\n\n"} +{"$message_type":"diagnostic","message":"aborting due to 1 previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m: aborting due to 1 previous error\u001b[0m\n\n"} diff --git a/src-tauri/target/debug/.fingerprint/proc-macro2-28cd7e73fe64eada/invoked.timestamp b/src-tauri/target/debug/.fingerprint/proc-macro2-28cd7e73fe64eada/invoked.timestamp new file mode 100644 index 0000000..e00328d --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/proc-macro2-28cd7e73fe64eada/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/src-tauri/target/debug/.fingerprint/proc-macro2-28cd7e73fe64eada/output-build-script-build-script-build b/src-tauri/target/debug/.fingerprint/proc-macro2-28cd7e73fe64eada/output-build-script-build-script-build new file mode 100644 index 0000000..3eae485 --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/proc-macro2-28cd7e73fe64eada/output-build-script-build-script-build @@ -0,0 +1,2 @@ +{"$message_type":"diagnostic","message":"linker `cc` not found","code":null,"level":"error","spans":[],"children":[{"message":"No such file or directory (os error 2)","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m: linker `cc` not found\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: No such file or directory (os error 2)\n\n"} +{"$message_type":"diagnostic","message":"aborting due to 1 previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m: aborting due to 1 previous error\u001b[0m\n\n"} diff --git a/src-tauri/target/debug/.fingerprint/quote-6dff9724e4e81362/invoked.timestamp b/src-tauri/target/debug/.fingerprint/quote-6dff9724e4e81362/invoked.timestamp new file mode 100644 index 0000000..e00328d --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/quote-6dff9724e4e81362/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/src-tauri/target/debug/.fingerprint/quote-6dff9724e4e81362/output-build-script-build-script-build b/src-tauri/target/debug/.fingerprint/quote-6dff9724e4e81362/output-build-script-build-script-build new file mode 100644 index 0000000..3eae485 --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/quote-6dff9724e4e81362/output-build-script-build-script-build @@ -0,0 +1,2 @@ +{"$message_type":"diagnostic","message":"linker `cc` not found","code":null,"level":"error","spans":[],"children":[{"message":"No such file or directory (os error 2)","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m: linker `cc` not found\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: No such file or directory (os error 2)\n\n"} +{"$message_type":"diagnostic","message":"aborting due to 1 previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m: aborting due to 1 previous error\u001b[0m\n\n"} diff --git a/src-tauri/target/debug/.fingerprint/serde_core-e613c711ddfe8493/invoked.timestamp b/src-tauri/target/debug/.fingerprint/serde_core-e613c711ddfe8493/invoked.timestamp new file mode 100644 index 0000000..e00328d --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/serde_core-e613c711ddfe8493/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/src-tauri/target/debug/.fingerprint/serde_core-e613c711ddfe8493/output-build-script-build-script-build b/src-tauri/target/debug/.fingerprint/serde_core-e613c711ddfe8493/output-build-script-build-script-build new file mode 100644 index 0000000..3eae485 --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/serde_core-e613c711ddfe8493/output-build-script-build-script-build @@ -0,0 +1,2 @@ +{"$message_type":"diagnostic","message":"linker `cc` not found","code":null,"level":"error","spans":[],"children":[{"message":"No such file or directory (os error 2)","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m: linker `cc` not found\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: No such file or directory (os error 2)\n\n"} +{"$message_type":"diagnostic","message":"aborting due to 1 previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m: aborting due to 1 previous error\u001b[0m\n\n"} diff --git a/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/dep-lib-unicode_ident b/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/dep-lib-unicode_ident new file mode 100644 index 0000000..ec3cb8b Binary files /dev/null and b/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/dep-lib-unicode_ident differ diff --git a/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/invoked.timestamp b/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/invoked.timestamp new file mode 100644 index 0000000..e00328d --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/invoked.timestamp @@ -0,0 +1 @@ +This file has an mtime of when this was started. \ No newline at end of file diff --git a/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/lib-unicode_ident b/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/lib-unicode_ident new file mode 100644 index 0000000..5c7ec14 --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/lib-unicode_ident @@ -0,0 +1 @@ +4609aa34aaf3f167 \ No newline at end of file diff --git a/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/lib-unicode_ident.json b/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/lib-unicode_ident.json new file mode 100644 index 0000000..718e584 --- /dev/null +++ b/src-tauri/target/debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/lib-unicode_ident.json @@ -0,0 +1 @@ +{"rustc":12019306335353385202,"features":"[]","declared_features":"[]","target":14045917370260632744,"profile":2225463790103693989,"path":11797909581934051219,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/unicode-ident-8443eb632a3fbe4c/dep-lib-unicode_ident","checksum":false}}],"rustflags":[],"config":9396254390672932401,"compile_kind":0} \ No newline at end of file diff --git a/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.3ufr5450lp20sxhbw3b5lnbvs.rcgu.o b/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.3ufr5450lp20sxhbw3b5lnbvs.rcgu.o new file mode 100644 index 0000000..ba2de30 Binary files /dev/null and b/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.3ufr5450lp20sxhbw3b5lnbvs.rcgu.o differ diff --git a/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.build_script_build.a30a70663bab765a-cgu.0.rcgu.o b/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.build_script_build.a30a70663bab765a-cgu.0.rcgu.o new file mode 100644 index 0000000..a91ef61 Binary files /dev/null and b/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.build_script_build.a30a70663bab765a-cgu.0.rcgu.o differ diff --git a/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.build_script_build.a30a70663bab765a-cgu.1.rcgu.o b/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.build_script_build.a30a70663bab765a-cgu.1.rcgu.o new file mode 100644 index 0000000..2c0bc9d Binary files /dev/null and b/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.build_script_build.a30a70663bab765a-cgu.1.rcgu.o differ diff --git a/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.d b/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.d new file mode 100644 index 0000000..361992b --- /dev/null +++ b/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.d @@ -0,0 +1,5 @@ +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc.d: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.189/build.rs + +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/build/libc-19124a20af635abc/build_script_build-19124a20af635abc: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.189/build.rs + +/home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/libc-0.2.189/build.rs: diff --git a/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.1cb4wof2950oslyk39975h0vc.rcgu.o b/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.1cb4wof2950oslyk39975h0vc.rcgu.o new file mode 100644 index 0000000..1c0c0ef Binary files /dev/null and b/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.1cb4wof2950oslyk39975h0vc.rcgu.o differ diff --git a/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.build_script_build.3e289dc730a2dac7-cgu.0.rcgu.o b/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.build_script_build.3e289dc730a2dac7-cgu.0.rcgu.o new file mode 100644 index 0000000..d8c6fca Binary files /dev/null and b/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.build_script_build.3e289dc730a2dac7-cgu.0.rcgu.o differ diff --git a/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.d b/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.d new file mode 100644 index 0000000..0376636 --- /dev/null +++ b/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.d @@ -0,0 +1,5 @@ +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada.d: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/build.rs + +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/build/proc-macro2-28cd7e73fe64eada/build_script_build-28cd7e73fe64eada: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/build.rs + +/home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/proc-macro2-1.0.107/build.rs: diff --git a/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.build_script_build.290c8a9cd7fd2ff3-cgu.0.rcgu.o b/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.build_script_build.290c8a9cd7fd2ff3-cgu.0.rcgu.o new file mode 100644 index 0000000..2376450 Binary files /dev/null and b/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.build_script_build.290c8a9cd7fd2ff3-cgu.0.rcgu.o differ diff --git a/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.cb5aizg7jyp8yonp8exnihyxb.rcgu.o b/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.cb5aizg7jyp8yonp8exnihyxb.rcgu.o new file mode 100644 index 0000000..b225a90 Binary files /dev/null and b/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.cb5aizg7jyp8yonp8exnihyxb.rcgu.o differ diff --git a/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.d b/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.d new file mode 100644 index 0000000..87edf70 --- /dev/null +++ b/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.d @@ -0,0 +1,5 @@ +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362.d: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/build.rs + +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/build/quote-6dff9724e4e81362/build_script_build-6dff9724e4e81362: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/build.rs + +/home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/quote-1.0.47/build.rs: diff --git a/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.93o87lhgng2qq8bftj8op6z8i.rcgu.o b/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.93o87lhgng2qq8bftj8op6z8i.rcgu.o new file mode 100644 index 0000000..609c329 Binary files /dev/null and b/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.93o87lhgng2qq8bftj8op6z8i.rcgu.o differ diff --git a/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.build_script_build.adfdc0c4a054f8c7-cgu.0.rcgu.o b/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.build_script_build.adfdc0c4a054f8c7-cgu.0.rcgu.o new file mode 100644 index 0000000..20be316 Binary files /dev/null and b/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.build_script_build.adfdc0c4a054f8c7-cgu.0.rcgu.o differ diff --git a/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.d b/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.d new file mode 100644 index 0000000..d682a28 --- /dev/null +++ b/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.d @@ -0,0 +1,5 @@ +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493.d: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.229/build.rs + +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/build/serde_core-e613c711ddfe8493/build_script_build-e613c711ddfe8493: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.229/build.rs + +/home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/serde_core-1.0.229/build.rs: diff --git a/src-tauri/target/debug/deps/libunicode_ident-8443eb632a3fbe4c.rlib b/src-tauri/target/debug/deps/libunicode_ident-8443eb632a3fbe4c.rlib new file mode 100644 index 0000000..cd8b166 Binary files /dev/null and b/src-tauri/target/debug/deps/libunicode_ident-8443eb632a3fbe4c.rlib differ diff --git a/src-tauri/target/debug/deps/libunicode_ident-8443eb632a3fbe4c.rmeta b/src-tauri/target/debug/deps/libunicode_ident-8443eb632a3fbe4c.rmeta new file mode 100644 index 0000000..f3ef464 Binary files /dev/null and b/src-tauri/target/debug/deps/libunicode_ident-8443eb632a3fbe4c.rmeta differ diff --git a/src-tauri/target/debug/deps/unicode_ident-8443eb632a3fbe4c.d b/src-tauri/target/debug/deps/unicode_ident-8443eb632a3fbe4c.d new file mode 100644 index 0000000..457dbe2 --- /dev/null +++ b/src-tauri/target/debug/deps/unicode_ident-8443eb632a3fbe4c.d @@ -0,0 +1,8 @@ +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/deps/unicode_ident-8443eb632a3fbe4c.d: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/lib.rs /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/tables.rs + +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/deps/libunicode_ident-8443eb632a3fbe4c.rlib: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/lib.rs /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/tables.rs + +/home/user/dev/ProxmoxDesktop/src-tauri/target/debug/deps/libunicode_ident-8443eb632a3fbe4c.rmeta: /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/lib.rs /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/tables.rs + +/home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/lib.rs: +/home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-ident-1.0.24/src/tables.rs: diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..cbd28a1 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "ProxmoxDesktop", + "version": "0.1.0", + "identifier": "com.proxmoxdesktop.app", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "title": "ProxmoxDesktop", + "windows": [ + { + "title": "ProxmoxDesktop", + "width": 1200, + "height": 800, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..e42c3cd --- /dev/null +++ b/src/App.css @@ -0,0 +1 @@ +/* App.css - intentionally empty, using Tailwind CSS */ diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..091232e --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,168 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { Sidebar } from '@/components/layout/Sidebar' +import { Dashboard } from '@/components/layout/Dashboard' +import { ConnectionDialog } from '@/components/connections/ConnectionDialog' +import { VMList } from '@/components/vms/VMList' +import { VMDetail } from '@/components/vms/VMDetail' +import { TaskList } from '@/components/tasks/TaskList' +import { BackupList } from '@/components/backups/BackupList' +import { CommandPalette } from '@/components/command/CommandPalette' +import { StorageOverview } from '@/components/storage/StorageOverview' +import { StorageDetail } from '@/components/storage/StorageDetail' +import { SettingsPage } from '@/components/settings/SettingsPage' +import { ErrorBoundary } from '@/components/ErrorBoundary' +import { ToastProvider } from '@/components/ui/toast' +import { useConnectionStore } from '@/stores/connectionStore' +import { useUIStore } from '@/stores/uiStore' +import { useWebSocket } from '@/hooks/useWebSocket' +import { useEffect, useState } from 'react' +import type { ProxmoxVM } from '@/types/proxmox' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +type View = + | { type: 'dashboard' } + | { type: 'vms' } + | { type: 'vm-detail'; vm: ProxmoxVM } + | { type: 'tasks' } + | { type: 'backups' } + | { type: 'storage' } + | { type: 'storage-detail'; storage: string } + | { type: 'settings' } + +function AppContent() { + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + const [connectionDialogOpen, setConnectionDialogOpen] = useState(false) + const [view, setView] = useState({ type: 'dashboard' }) + const commandPaletteOpen = useUIStore((s) => s.commandPaletteOpen) + const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen) + + // WebSocket integration – connects when a connection is active + useWebSocket(activeConnectionId) + + // Global keyboard shortcut: Cmd/Ctrl+K to open command palette + useEffect(() => { + const handleGlobalKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault() + setCommandPaletteOpen(!commandPaletteOpen) + } + } + window.addEventListener('keydown', handleGlobalKeyDown) + return () => window.removeEventListener('keydown', handleGlobalKeyDown) + }, [commandPaletteOpen, setCommandPaletteOpen]) + + const handleNavigate = (newView: View) => { + setView(newView) + } + + const renderMainContent = () => { + if (view.type === 'settings') { + return + } + + if (!activeConnectionId) { + return ( +
+
+

Welcome to ProxmoxDesktop

+

+ Add a Proxmox server to get started +

+ +
+
+ ) + } + + switch (view.type) { + case 'dashboard': + return + case 'vms': + return ( + handleNavigate({ type: 'vm-detail', vm })} + /> + ) + case 'vm-detail': + return ( + handleNavigate({ type: 'vms' })} + /> + ) + case 'tasks': + return + case 'backups': + return + case 'storage': + return ( + + handleNavigate({ type: 'storage-detail', storage }) + } + /> + ) + case 'storage-detail': + return ( + handleNavigate({ type: 'storage' })} + /> + ) + } + } + + return ( +
+ setConnectionDialogOpen(true)} + activeView={view.type} + onNavigate={handleNavigate} + /> +
+ {renderMainContent()} +
+ + setConnectionDialogOpen(true)} + /> +
+ ) +} + +function App() { + return ( + + + + + + + + ) +} + +export default App diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..83c6d87 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,57 @@ +import { Component } from 'react' +import type { ErrorInfo, ReactNode } from 'react' +import { Button } from '@/components/ui/button' +import { AlertCircle } from 'lucide-react' + +interface Props { + children: ReactNode + fallback?: ReactNode +} + +interface State { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { hasError: false, error: null } + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('ErrorBoundary caught:', error, errorInfo) + } + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback + } + + return ( +
+
+ +

Something went wrong

+

+ {this.state.error?.message || 'An unexpected error occurred'} +

+ +
+
+ ) + } + + return this.props.children + } +} diff --git a/src/components/backups/BackupList.tsx b/src/components/backups/BackupList.tsx new file mode 100644 index 0000000..c376747 --- /dev/null +++ b/src/components/backups/BackupList.tsx @@ -0,0 +1,410 @@ +import { useState, useMemo } from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { + Shield, + Plus, + Play, + Edit, + Trash, + RotateCcw, + Clock, + HardDrive, + CheckCircle, + XCircle, +} from 'lucide-react' +import { + useBackupJobs, + useBackups, + useDeleteBackupJob, + useRunBackup, + useDeleteBackup, + useStorage, +} from '@/hooks/useProxmox' +import { CreateBackupJobDialog } from './dialogs/CreateBackupJobDialog' +import { EditBackupJobDialog } from './dialogs/EditBackupJobDialog' +import { RestoreBackupDialog } from './dialogs/RestoreBackupDialog' +import type { ProxmoxBackupJob, ProxmoxBackup } from '@/types/proxmox' + +interface BackupListProps { + connectionId: string +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function formatTimestamp(seconds: number): string { + if (seconds === 0) return 'N/A' + const date = new Date(seconds * 1000) + return date.toLocaleString() +} + +export function BackupList({ connectionId }: BackupListProps) { + const { data: backupJobs, isLoading: jobsLoading, error: jobsError } = useBackupJobs(connectionId) + const { data: backups, isLoading: backupsLoading, error: backupsError } = useBackups(connectionId) + const { data: storage } = useStorage(connectionId) + + const deleteBackupJob = useDeleteBackupJob() + const runBackup = useRunBackup() + const deleteBackup = useDeleteBackup() + + const [storageFilter, setStorageFilter] = useState('all') + const [createDialogOpen, setCreateDialogOpen] = useState(false) + const [editDialog, setEditDialog] = useState(null) + const [restoreDialog, setRestoreDialog] = useState(null) + const [confirmDeleteJob, setConfirmDeleteJob] = useState(null) + const [confirmDeleteBackup, setConfirmDeleteBackup] = useState(null) + + const storageOptions = useMemo(() => { + if (!storage) return [] + return [...new Set(storage.map((s) => s.storage))].sort() + }, [storage]) + + const nodeOptions = useMemo(() => { + if (!storage) return [] + return [...new Set(storage.map((s) => s.node))].sort() + }, [storage]) + + const filteredBackups = useMemo(() => { + if (!backups) return [] + if (storageFilter === 'all') return backups + return backups.filter((b) => b.storage === storageFilter) + }, [backups, storageFilter]) + + if (jobsLoading || backupsLoading) { + return ( +
+

Loading backups...

+
+ ) + } + + if (jobsError || backupsError) { + return ( +
+

Failed to load backup data

+
+ ) + } + + return ( +
+
+ {/* Header */} +
+
+ +

Backups

+
+

+ Manage backup jobs and existing backups +

+
+ + {/* Storage Filter */} +
+ +
+ + {/* Backup Jobs Section */} +
+
+

Backup Jobs

+ +
+ + + + {!backupJobs || backupJobs.length === 0 ? ( +
+ +

No backup jobs configured

+

Create a backup job to get started

+
+ ) : ( +
+ + + + + + + + + + + + + {backupJobs.map((job) => ( + + + + + + + + + ))} + +
IDScheduleStorageModeStatusActions
{job.id} +
+ + {job.schedule} +
+
+
+ + {job.store} +
+
+ + {job.mode ?? 'snapshot'} + + + {job.enabled === 1 ? ( + + + Enabled + + ) : ( + + + Disabled + + )} + +
+ + + {confirmDeleteJob === job.id ? ( +
+ + +
+ ) : ( + + )} +
+
+
+ )} +
+
+
+ + {/* Existing Backups Section */} +
+

Existing Backups

+ + + + {!filteredBackups || filteredBackups.length === 0 ? ( +
+ +

No backups found

+
+ ) : ( +
+ + + + + + + + + + + + + {filteredBackups.map((backup) => ( + + + + + + + + + ))} + +
VMIDTypeDateSizeStorageActions
+ {backup['backup-id']} + + + {backup['backup-type']} + + + {formatTimestamp(backup['backup-time'])} + + {formatBytes(backup.size)} + +
+ + {backup.storage} +
+
+
+ + {confirmDeleteBackup === backup.volid ? ( +
+ + +
+ ) : ( + + )} +
+
+
+ )} +
+
+
+
+ + {/* Dialogs */} + + + {editDialog && ( + { + if (!open) setEditDialog(null) + }} + job={editDialog} + storageOptions={storageOptions} + /> + )} + + {restoreDialog && ( + { + if (!open) setRestoreDialog(null) + }} + backup={restoreDialog} + nodeOptions={nodeOptions} + storageOptions={storageOptions} + /> + )} +
+ ) +} diff --git a/src/components/backups/dialogs/CreateBackupJobDialog.tsx b/src/components/backups/dialogs/CreateBackupJobDialog.tsx new file mode 100644 index 0000000..fe5de77 --- /dev/null +++ b/src/components/backups/dialogs/CreateBackupJobDialog.tsx @@ -0,0 +1,198 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useCreateBackupJob } from '@/hooks/useProxmox' +import type { BackupJobConfig } from '@/types/proxmox' + +interface CreateBackupJobDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + storageOptions: string[] +} + +export function CreateBackupJobDialog({ + open, + onOpenChange, + storageOptions, +}: CreateBackupJobDialogProps) { + const [storage, setStorage] = useState(storageOptions[0] ?? '') + const [schedule, setSchedule] = useState('0 2 * * *') + const [mode, setMode] = useState('snapshot') + const [compression, setCompression] = useState('zstd') + const [all, setAll] = useState(true) + const [vmid, setVmid] = useState('') + const [enabled, setEnabled] = useState(true) + + const createBackupJob = useCreateBackupJob() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + const config: BackupJobConfig = { + storage, + schedule, + mode, + compression, + all, + vmid: all ? undefined : vmid, + enabled, + } + createBackupJob.mutate( + { config }, + { + onSuccess: () => { + onOpenChange(false) + resetForm() + }, + }, + ) + } + + const resetForm = () => { + setStorage(storageOptions[0] ?? '') + setSchedule('0 2 * * *') + setMode('snapshot') + setCompression('zstd') + setAll(true) + setVmid('') + setEnabled(true) + } + + return ( + + + + Create Backup Job + + Schedule a recurring backup job for your VMs and containers. + + +
+
+ + +
+ +
+ + setSchedule(e.target.value)} + placeholder="0 2 * * *" + required + /> +

+ Example: "0 2 * * *" = daily at 2:00 AM +

+
+ +
+ + +
+ +
+ + +
+ +
+ setAll(e.target.checked)} + className="h-4 w-4 rounded border-input" + /> + +
+ + {!all && ( +
+ + setVmid(e.target.value)} + placeholder="100, 101, 102" + /> +
+ )} + +
+ setEnabled(e.target.checked)} + className="h-4 w-4 rounded border-input" + /> + +
+ + {createBackupJob.isError && ( +

+ Failed to create backup job. Please try again. +

+ )} + + + + + +
+
+
+ ) +} diff --git a/src/components/backups/dialogs/EditBackupJobDialog.tsx b/src/components/backups/dialogs/EditBackupJobDialog.tsx new file mode 100644 index 0000000..dc29636 --- /dev/null +++ b/src/components/backups/dialogs/EditBackupJobDialog.tsx @@ -0,0 +1,205 @@ +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useUpdateBackupJob } from '@/hooks/useProxmox' +import type { BackupJobConfig, ProxmoxBackupJob } from '@/types/proxmox' + +interface EditBackupJobDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + job: ProxmoxBackupJob + storageOptions: string[] +} + +export function EditBackupJobDialog({ + open, + onOpenChange, + job, + storageOptions, +}: EditBackupJobDialogProps) { + const [storage, setStorage] = useState(job.store) + const [schedule, setSchedule] = useState(job.schedule) + const [mode, setMode] = useState( + (job.mode as BackupJobConfig['mode']) || 'snapshot', + ) + const [compression, setCompression] = useState( + (job.compress as BackupJobConfig['compression']) || 'zstd', + ) + const [all, setAll] = useState(job.all === 1) + const [vmid, setVmid] = useState(job.vmid ?? '') + const [enabled, setEnabled] = useState(job.enabled === 1) + + const updateBackupJob = useUpdateBackupJob() + + useEffect(() => { + if (open) { + setStorage(job.store) + setSchedule(job.schedule) + setMode((job.mode as BackupJobConfig['mode']) || 'snapshot') + setCompression((job.compress as BackupJobConfig['compression']) || 'zstd') + setAll(job.all === 1) + setVmid(job.vmid ?? '') + setEnabled(job.enabled === 1) + } + }, [open, job]) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + const config: BackupJobConfig = { + storage, + schedule, + mode, + compression, + all, + vmid: all ? undefined : vmid, + enabled, + } + updateBackupJob.mutate( + { id: job.id, config }, + { + onSuccess: () => { + onOpenChange(false) + }, + }, + ) + } + + return ( + + + + Edit Backup Job + + Modify the backup job configuration. + + +
+
+ + +
+ +
+ + setSchedule(e.target.value)} + placeholder="0 2 * * *" + required + /> +

+ Example: "0 2 * * *" = daily at 2:00 AM +

+
+ +
+ + +
+ +
+ + +
+ +
+ setAll(e.target.checked)} + className="h-4 w-4 rounded border-input" + /> + +
+ + {!all && ( +
+ + setVmid(e.target.value)} + placeholder="100, 101, 102" + /> +
+ )} + +
+ setEnabled(e.target.checked)} + className="h-4 w-4 rounded border-input" + /> + +
+ + {updateBackupJob.isError && ( +

+ Failed to update backup job. Please try again. +

+ )} + + + + + +
+
+
+ ) +} diff --git a/src/components/backups/dialogs/RestoreBackupDialog.tsx b/src/components/backups/dialogs/RestoreBackupDialog.tsx new file mode 100644 index 0000000..02c6ad4 --- /dev/null +++ b/src/components/backups/dialogs/RestoreBackupDialog.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useRestoreBackup } from '@/hooks/useProxmox' +import type { ProxmoxBackup } from '@/types/proxmox' + +interface RestoreBackupDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + backup: ProxmoxBackup | null + nodeOptions: string[] + storageOptions: string[] +} + +export function RestoreBackupDialog({ + open, + onOpenChange, + backup, + nodeOptions, + storageOptions, +}: RestoreBackupDialogProps) { + const [node, setNode] = useState(nodeOptions[0] ?? '') + const [targetStorage, setTargetStorage] = useState(storageOptions[0] ?? '') + const [vmid, setVmid] = useState('') + + const restoreBackup = useRestoreBackup() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!backup) return + restoreBackup.mutate( + { + volid: backup.volid, + config: { + volid: backup.volid, + node, + storage: targetStorage, + vmid: vmid ? parseInt(vmid, 10) : undefined, + }, + }, + { + onSuccess: () => { + onOpenChange(false) + setVmid('') + }, + }, + ) + } + + return ( + + + + Restore Backup + + Restore backup {backup?.volid ?? ''}. This will create a new VM from the backup. + + +
+
+ + +
+ +
+ + +
+ +
+ + setVmid(e.target.value)} + placeholder="Auto-assign if empty" + /> +

+ Leave empty to auto-assign the next available VMID. +

+
+ +

+ Warning: Restoring will overwrite any existing VM with the same VMID. +

+ + {restoreBackup.isError && ( +

+ Failed to restore backup. Please try again. +

+ )} + + + + + +
+
+
+ ) +} diff --git a/src/components/command/CommandPalette.tsx b/src/components/command/CommandPalette.tsx new file mode 100644 index 0000000..063d409 --- /dev/null +++ b/src/components/command/CommandPalette.tsx @@ -0,0 +1,607 @@ +import { useState, useEffect, useRef, useMemo, useCallback } from 'react' +import { useConnectionStore } from '@/stores/connectionStore' +import { useVMs } from '@/hooks/useProxmox' +import { + useStartVM, + useStopVM, + useShutdownVM, + useRebootVM, +} from '@/hooks/useProxmox' +import { Dialog, DialogContent } from '@/components/ui/dialog' +import { ScrollArea } from '@/components/ui/scroll-area' +import { + Server, + Play, + Square, + Power, + RotateCw, + LayoutDashboard, + Box, + ListTodo, + Shield, + HardDrive, + Plus, + X, + Search, +} from 'lucide-react' +import { cn } from '@/lib/utils' +import type { ProxmoxVM } from '@/types/proxmox' + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type View = + | { type: 'dashboard' } + | { type: 'vms' } + | { type: 'vm-detail'; vm: ProxmoxVM } + | { type: 'tasks' } + | { type: 'backups' } + | { type: 'storage' } + | { type: 'storage-detail'; storage: string } + +type CommandCategory = 'recent' | 'vms' | 'actions' | 'navigation' | 'connections' + +interface CommandItem { + id: string + label: string + description?: string + icon: React.ComponentType<{ className?: string }> + category: CommandCategory + shortcut?: string + keywords: string[] + onExecute: () => void +} + +interface CommandPaletteProps { + open: boolean + onOpenChange: (open: boolean) => void + onNavigate: (view: View) => void + onAddConnection: () => void +} + +// --------------------------------------------------------------------------- +// Recent commands – localStorage persistence +// --------------------------------------------------------------------------- + +const RECENT_STORAGE_KEY = 'proxmox-command-palette-recent' +const MAX_RECENT = 10 + +function loadRecent(): string[] { + try { + const raw = localStorage.getItem(RECENT_STORAGE_KEY) + return raw ? (JSON.parse(raw) as string[]) : [] + } catch { + return [] + } +} + +function saveRecent(ids: string[]) { + localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(ids)) +} + +function pushRecent(id: string) { + const current = loadRecent().filter((r) => r !== id) + current.unshift(id) + saveRecent(current.slice(0, MAX_RECENT)) +} + +function clearRecent() { + localStorage.removeItem(RECENT_STORAGE_KEY) +} + +// --------------------------------------------------------------------------- +// Fuzzy match +// --------------------------------------------------------------------------- + +function fuzzyMatch(query: string, text: string): boolean { + const lowerQuery = query.toLowerCase() + const lowerText = text.toLowerCase() + + // Substring match + if (lowerText.includes(lowerQuery)) return true + + // Character-by-character fuzzy + let qi = 0 + for (let ti = 0; ti < lowerText.length && qi < lowerQuery.length; ti++) { + if (lowerText[ti] === lowerQuery[qi]) qi++ + } + return qi === lowerQuery.length +} + +// --------------------------------------------------------------------------- +// Highlighted text component +// --------------------------------------------------------------------------- + +function HighlightedText({ text, query }: { text: string; query: string }) { + if (!query) return <>{text} + + const lowerText = text.toLowerCase() + const lowerQuery = query.toLowerCase() + const idx = lowerText.indexOf(lowerQuery) + + if (idx !== -1) { + return ( + <> + {text.slice(0, idx)} + + {text.slice(idx, idx + query.length)} + + {text.slice(idx + query.length)} + + ) + } + + // Fuzzy: highlight individual matched characters + const chars = text.split('') + let qi = 0 + return ( + <> + {chars.map((char, i) => { + if (qi < lowerQuery.length && char.toLowerCase() === lowerQuery[qi]) { + qi++ + return ( + + {char} + + ) + } + return char + })} + + ) +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function categoryHeading(category: CommandCategory): string { + switch (category) { + case 'recent': + return 'Recent' + case 'vms': + return 'VMs & Containers' + case 'actions': + return 'Actions' + case 'navigation': + return 'Navigation' + case 'connections': + return 'Connections' + } +} + +// --------------------------------------------------------------------------- +// CommandPalette +// --------------------------------------------------------------------------- + +export function CommandPalette({ + open, + onOpenChange, + onNavigate, + onAddConnection, +}: CommandPaletteProps) { + const [query, setQuery] = useState('') + const [selectedIndex, setSelectedIndex] = useState(0) + const [recentIds, setRecentIds] = useState(loadRecent) + + const inputRef = useRef(null) + const listRef = useRef(null) + + // Data from stores + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + const connections = useConnectionStore((s) => s.connections) + const setActiveConnection = useConnectionStore((s) => s.setActiveConnection) + + const { data: vms = [] } = useVMs(activeConnectionId) + + // VM mutation hooks + const startVM = useStartVM() + const stopVM = useStopVM() + const shutdownVM = useShutdownVM() + const rebootVM = useRebootVM() + + // ----------------------------------------------------------------------- + // Build all command items + // ----------------------------------------------------------------------- + const buildItems = useCallback((): CommandItem[] => { + const items: CommandItem[] = [] + + // -- Navigation -- + items.push( + { + id: 'nav-dashboard', + label: 'Go to Dashboard', + icon: LayoutDashboard, + category: 'navigation', + shortcut: '⌘1', + keywords: ['dashboard', 'home', 'overview'], + onExecute: () => onNavigate({ type: 'dashboard' }), + }, + { + id: 'nav-vms', + label: 'Go to VMs', + icon: Box, + category: 'navigation', + shortcut: '⌘2', + keywords: ['vm', 'vms', 'virtual machines', 'containers'], + onExecute: () => onNavigate({ type: 'vms' }), + }, + { + id: 'nav-tasks', + label: 'Go to Tasks', + icon: ListTodo, + category: 'navigation', + shortcut: '⌘3', + keywords: ['tasks', 'jobs', 'queue'], + onExecute: () => onNavigate({ type: 'tasks' }), + }, + { + id: 'nav-backups', + label: 'Go to Backups', + icon: Shield, + category: 'navigation', + shortcut: '⌘4', + keywords: ['backups', 'restore', 'backup'], + onExecute: () => onNavigate({ type: 'backups' }), + }, + { + id: 'nav-storage', + label: 'Go to Storage', + icon: HardDrive, + category: 'navigation', + shortcut: '⌘5', + keywords: ['storage', 'disks', 'volumes'], + onExecute: () => onNavigate({ type: 'storage' }), + }, + { + id: 'nav-add-connection', + label: 'Add Connection', + icon: Plus, + category: 'navigation', + keywords: ['add', 'connection', 'server', 'proxmox', 'new'], + onExecute: () => onAddConnection(), + }, + ) + + // -- VMs -- + for (const vm of vms) { + items.push({ + id: `vm-detail-${vm.vmid}`, + label: vm.name, + description: `${vm.type.toUpperCase()} · VMID ${vm.vmid} · ${vm.node} · ${vm.status}`, + icon: Server, + category: 'vms', + keywords: [vm.name, String(vm.vmid), vm.node, vm.type, vm.status], + onExecute: () => onNavigate({ type: 'vm-detail', vm }), + }) + } + + // -- VM Actions -- + for (const vm of vms) { + if (vm.status === 'running') { + items.push( + { + id: `action-stop-${vm.vmid}`, + label: `Stop ${vm.name}`, + description: `Force stop ${vm.type.toUpperCase()} VMID ${vm.vmid}`, + icon: Square, + category: 'actions', + keywords: ['stop', 'halt', 'power off', vm.name, String(vm.vmid)], + onExecute: () => stopVM.mutate({ node: vm.node, vmid: vm.vmid }), + }, + { + id: `action-shutdown-${vm.vmid}`, + label: `Shutdown ${vm.name}`, + description: `Gracefully shutdown ${vm.type.toUpperCase()} VMID ${vm.vmid}`, + icon: Power, + category: 'actions', + keywords: ['shutdown', 'graceful', 'power', vm.name, String(vm.vmid)], + onExecute: () => shutdownVM.mutate({ node: vm.node, vmid: vm.vmid }), + }, + { + id: `action-reboot-${vm.vmid}`, + label: `Reboot ${vm.name}`, + description: `Reboot ${vm.type.toUpperCase()} VMID ${vm.vmid}`, + icon: RotateCw, + category: 'actions', + keywords: ['reboot', 'restart', vm.name, String(vm.vmid)], + onExecute: () => rebootVM.mutate({ node: vm.node, vmid: vm.vmid }), + }, + ) + } + if (vm.status === 'stopped' || vm.status === 'paused') { + items.push({ + id: `action-start-${vm.vmid}`, + label: `Start ${vm.name}`, + description: `Start ${vm.type.toUpperCase()} VMID ${vm.vmid}`, + icon: Play, + category: 'actions', + keywords: ['start', 'boot', 'power on', vm.name, String(vm.vmid)], + onExecute: () => startVM.mutate({ node: vm.node, vmid: vm.vmid }), + }) + } + } + + // -- Connections -- + for (const conn of connections) { + items.push({ + id: `conn-${conn.id}`, + label: conn.name, + description: `Switch to ${conn.name} (${conn.status})`, + icon: Server, + category: 'connections', + keywords: [conn.name, conn.status, 'switch', 'connection'], + onExecute: () => setActiveConnection(conn.id), + }) + } + + return items + }, [ + vms, + connections, + onNavigate, + onAddConnection, + startVM, + stopVM, + shutdownVM, + rebootVM, + setActiveConnection, + ]) + + const allItems = useMemo(() => buildItems(), [buildItems]) + + // ----------------------------------------------------------------------- + // Filter & group + // ----------------------------------------------------------------------- + const filteredItems = useMemo(() => { + const q = query.trim() + + if (!q) { + // Empty query: recent → navigation → everything else + const recentItems = recentIds + .map((id) => allItems.find((item) => item.id === id)) + .filter((item): item is CommandItem => item !== undefined) + .map((item) => ({ ...item, category: 'recent' as const })) + + const navItems = allItems.filter((item) => item.category === 'navigation') + const otherItems = allItems.filter( + (item) => item.category !== 'navigation' && item.category !== 'recent', + ) + + return [...recentItems, ...navItems, ...otherItems] + } + + return allItems.filter((item) => { + const haystack = [item.label, item.description ?? '', ...item.keywords].join(' ') + return fuzzyMatch(q, haystack) + }) + }, [query, allItems, recentIds]) + + const groupedItems = useMemo(() => { + const groups: { heading: string; items: CommandItem[] }[] = [] + + if (query.trim()) { + const buckets: Record = {} + for (const item of filteredItems) { + const heading = item.category === 'recent' ? 'Recent' : categoryHeading(item.category) + if (!buckets[heading]) buckets[heading] = [] + buckets[heading].push(item) + } + for (const [heading, items] of Object.entries(buckets)) { + groups.push({ heading, items }) + } + } else { + let currentHeading = '' + let currentItems: CommandItem[] = [] + for (const item of filteredItems) { + const heading = item.category === 'recent' ? 'Recent' : categoryHeading(item.category) + if (heading !== currentHeading) { + if (currentItems.length > 0) groups.push({ heading: currentHeading, items: currentItems }) + currentHeading = heading + currentItems = [item] + } else { + currentItems.push(item) + } + } + if (currentItems.length > 0) groups.push({ heading: currentHeading, items: currentItems }) + } + + return groups + }, [filteredItems, query]) + + const flatItems = useMemo(() => groupedItems.flatMap((g) => g.items), [groupedItems]) + + // ----------------------------------------------------------------------- + // Reset on open + // ----------------------------------------------------------------------- + useEffect(() => { + if (open) { + setQuery('') + setSelectedIndex(0) + setRecentIds(loadRecent()) + requestAnimationFrame(() => inputRef.current?.focus()) + } + }, [open]) + + // ----------------------------------------------------------------------- + // Reset selection on query change + // ----------------------------------------------------------------------- + useEffect(() => { + setSelectedIndex(0) + }, [query]) + + // ----------------------------------------------------------------------- + // Keep selected item in view + // ----------------------------------------------------------------------- + useEffect(() => { + const el = listRef.current?.querySelector(`[data-cmd-idx="${selectedIndex}"]`) + el?.scrollIntoView({ block: 'nearest' }) + }, [selectedIndex]) + + // ----------------------------------------------------------------------- + // Execute item + // ----------------------------------------------------------------------- + const executeItem = useCallback( + (item: CommandItem) => { + pushRecent(item.id) + setRecentIds(loadRecent()) + item.onExecute() + onOpenChange(false) + }, + [onOpenChange], + ) + + // ----------------------------------------------------------------------- + // Keyboard handling + // ----------------------------------------------------------------------- + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setSelectedIndex((i) => Math.min(i + 1, flatItems.length - 1)) + break + case 'ArrowUp': + e.preventDefault() + setSelectedIndex((i) => Math.max(i - 1, 0)) + break + case 'Enter': + e.preventDefault() + if (flatItems[selectedIndex]) executeItem(flatItems[selectedIndex]) + break + case 'Escape': + e.preventDefault() + onOpenChange(false) + break + } + }, + [flatItems, selectedIndex, executeItem, onOpenChange], + ) + + // ----------------------------------------------------------------------- + // Flat index helper + // ----------------------------------------------------------------------- + function flatIndex(groupIdx: number, itemIdx: number): number { + let offset = 0 + for (let g = 0; g < groupIdx; g++) offset += groupedItems[g].items.length + return offset + itemIdx + } + + // ----------------------------------------------------------------------- + // Render + // ----------------------------------------------------------------------- + return ( + + + {/* Search bar */} +
+ + setQuery(e.target.value)} + placeholder="Type a command or search..." + className="flex h-11 w-full rounded-md bg-transparent px-3 text-sm outline-none placeholder:text-muted-foreground" + /> + {query && ( + + )} +
+ + {/* Results list */} + +
+ {flatItems.length === 0 ? ( +
+ No results found. +
+ ) : ( + groupedItems.map((group, gIdx) => ( +
+
+ {group.heading} +
+ {group.items.map((item, iIdx) => { + const idx = flatIndex(gIdx, iIdx) + const isActive = idx === selectedIndex + const Icon = item.icon + return ( + + ) + })} +
+ )) + )} +
+
+ + {/* Footer with keyboard hints */} +
+
+ + + + navigate + + + + select + + + esc + close + +
+ {recentIds.length > 0 && ( + + )} +
+
+
+ ) +} diff --git a/src/components/connections/ConnectionDialog.tsx b/src/components/connections/ConnectionDialog.tsx new file mode 100644 index 0000000..cc1b0c3 --- /dev/null +++ b/src/components/connections/ConnectionDialog.tsx @@ -0,0 +1,132 @@ +import { useState } from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { useConnectionStore } from '@/stores/connectionStore' +import type { ConnectionConfig } from '@/types/connection' + +interface ConnectionDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function ConnectionDialog({ open, onOpenChange }: ConnectionDialogProps) { + const addConnection = useConnectionStore((s) => s.addConnection) + const [name, setName] = useState('') + const [url, setUrl] = useState('') + const [apiToken, setApiToken] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + try { + // Validate URL + if (!url.startsWith('https://')) { + throw new Error('URL must start with https://') + } + + // Create connection config + const config: ConnectionConfig = { + id: crypto.randomUUID(), + name: name || 'New Connection', + primary: { + url: url.replace(/\/$/, ''), // Remove trailing slash + token: apiToken, + }, + fallbacks: [], + trusted: false, + status: 'disconnected', + isCluster: false, + } + + // Add connection to store + addConnection(config) + + // Reset form and close dialog + setName('') + setUrl('') + setApiToken('') + onOpenChange(false) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to add connection') + } finally { + setIsLoading(false) + } + } + + return ( + + + + Add Proxmox Connection + + Connect to a Proxmox VE server or cluster + + +
+
+ + setName(e.target.value)} + /> +
+
+ + setUrl(e.target.value)} + required + /> +

+ The URL of your Proxmox server (must use HTTPS) +

+
+
+ + setApiToken(e.target.value)} + required + /> +

+ Format: user@realm!tokenid=secret +

+
+ {error && ( +
+

{error}

+
+ )} + + + + +
+
+
+ ) +} diff --git a/src/components/console/TerminalConsole.tsx b/src/components/console/TerminalConsole.tsx new file mode 100644 index 0000000..864ec81 --- /dev/null +++ b/src/components/console/TerminalConsole.tsx @@ -0,0 +1,255 @@ +import { useEffect, useRef, useCallback } from 'react' +import { Terminal } from 'xterm' +import { FitAddon } from 'xterm-addon-fit' +import 'xterm/css/xterm.css' + +interface TerminalConsoleProps { + connectionId: string + node: string + vmid: number + onError?: (message: string) => void +} + +export function TerminalConsole({ connectionId, node, vmid, onError }: TerminalConsoleProps) { + const containerRef = useRef(null) + const termRef = useRef(null) + const fitAddonRef = useRef(null) + const wsRef = useRef(null) + + const cleanup = useCallback(() => { + if (wsRef.current) { + wsRef.current.close() + wsRef.current = null + } + if (termRef.current) { + termRef.current.dispose() + termRef.current = null + } + fitAddonRef.current = null + }, []) + + useEffect(() => { + if (!containerRef.current) return + + let cancelled = false + + const connect = async () => { + try { + // Create terminal + const terminal = new Terminal({ + cursorBlink: true, + fontSize: 14, + fontFamily: 'Menlo, Monaco, "Courier New", monospace', + theme: { + background: '#000000', + foreground: '#ffffff', + cursor: '#ffffff', + selectionBackground: '#264f78', + }, + allowProposedApi: true, + }) + + const fitAddon = new FitAddon() + terminal.loadAddon(fitAddon) + + terminal.open(containerRef.current!) + fitAddon.fit() + + termRef.current = terminal + fitAddonRef.current = fitAddon + + if (cancelled) { + terminal.dispose() + return + } + + // Get WebSocket URL for terminal proxy + let wsUrl: string + + try { + const { isTauri, createTermProxy, getWebSocketURL } = await import('@/lib/tauri') + + if (isTauri()) { + const [proxyInfo, baseUrl] = await Promise.all([ + createTermProxy(connectionId, node, vmid), + getWebSocketURL(connectionId, node), + ]) + + wsUrl = `${baseUrl}/api2/json/nodes/${node}/lxc/${vmid}/proxy?port=${proxyInfo.port}&ticket=${encodeURIComponent(proxyInfo.ticket)}` + } else { + // Dev mode: construct a mock URL + wsUrl = `wss://localhost:8006/api2/json/nodes/${node}/lxc/${vmid}/proxy?port=6100&ticket=mock-ticket` + } + } catch { + // Fallback for dev mode + wsUrl = `wss://localhost:8006/api2/json/nodes/${node}/lxc/${vmid}/proxy?port=6100&ticket=mock-ticket` + } + + if (cancelled) return + + // In dev mode, show a mock terminal since we can't connect to real WebSocket + let isTauriMode = false + try { + const { isTauri } = await import('@/lib/tauri') + isTauriMode = isTauri() + } catch { + // not in tauri + } + + if (!isTauriMode && !cancelled) { + // Dev mode mock terminal + terminal.writeln('\x1b[1;32m╔══════════════════════════════════════════╗\x1b[0m') + terminal.writeln('\x1b[1;32m║ ProxmoxDesktop - Terminal Console ║\x1b[0m') + terminal.writeln('\x1b[1;32m╚══════════════════════════════════════════╝\x1b[0m') + terminal.writeln('') + terminal.writeln(`\x1b[33mNode: ${node} | VMID: ${vmid} | Type: LXC\x1b[0m`) + terminal.writeln('') + terminal.writeln('\x1b[90m[Dev mode - WebSocket connection mocked]\x1b[0m') + terminal.writeln('') + + // Mock shell interaction + const prompt = () => { + terminal.write(`\x1b[1;34mroot@${node}\x1b[0m:\x1b[1;35m~\x1b[0m# `) + } + prompt() + + let inputBuffer = '' + terminal.onData((data: string) => { + if (data === '\r') { + terminal.writeln('') + if (inputBuffer.trim()) { + if (inputBuffer.trim() === 'clear') { + terminal.clear() + } else if (inputBuffer.trim() === 'help') { + terminal.writeln('Available mock commands: clear, help, ls, whoami') + } else if (inputBuffer.trim() === 'ls') { + terminal.writeln('bin boot dev etc home lib lib64 media mnt opt') + terminal.writeln('proc root run sbin srv sys tmp usr var') + } else if (inputBuffer.trim() === 'whoami') { + terminal.writeln('root') + } else { + terminal.writeln(`\x1b[31m${inputBuffer.trim()}: command not found\x1b[0m`) + } + } + inputBuffer = '' + prompt() + } else if (data === '\x7f') { + // Backspace + if (inputBuffer.length > 0) { + inputBuffer = inputBuffer.slice(0, -1) + terminal.write('\b \b') + } + } else if (data >= ' ') { + inputBuffer += data + terminal.write(data) + } + }) + + // Handle resize + const resizeObserver = new ResizeObserver(() => { + if (!cancelled && fitAddonRef.current) { + try { + fitAddonRef.current.fit() + } catch { + // ignore resize errors + } + } + }) + resizeObserver.observe(containerRef.current!) + + return () => { + resizeObserver.disconnect() + } + } + + if (cancelled) return + + // Production: connect via WebSocket + const ws = new WebSocket(wsUrl) + ws.binaryType = 'arraybuffer' + wsRef.current = ws + + ws.onopen = () => { + if (!cancelled) { + terminal.writeln(`\x1b[32mConnected to LXC container ${vmid} on ${node}\x1b[0m`) + } + } + + ws.onmessage = (ev: MessageEvent) => { + if (!cancelled && typeof ev.data === 'string') { + terminal.write(ev.data) + } + } + + ws.onerror = () => { + if (!cancelled) { + onError?.('WebSocket connection error') + } + } + + ws.onclose = () => { + if (!cancelled) { + terminal.writeln('\r\n\x1b[33mConnection closed\x1b[0m') + } + } + + // Forward terminal input to WebSocket + terminal.onData((data: string) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data) + } + }) + + // Handle terminal resize + terminal.onResize(({ cols, rows }: { cols: number; rows: number }) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'resize', cols, rows })) + } + }) + + // Handle container resize + const resizeObserver = new ResizeObserver(() => { + if (!cancelled && fitAddonRef.current) { + try { + fitAddonRef.current.fit() + } catch { + // ignore resize errors + } + } + }) + resizeObserver.observe(containerRef.current!) + + return () => { + resizeObserver.disconnect() + } + } catch (err) { + if (!cancelled) { + onError?.(err instanceof Error ? err.message : 'Failed to create terminal') + } + } + } + + let cleanupResize: (() => void) | undefined + + const run = async () => { + cleanupResize = await connect() ?? undefined + } + + run() + + return () => { + cancelled = true + cleanupResize?.() + cleanup() + } + }, [connectionId, node, vmid, onError, cleanup]) + + return ( +
+ ) +} + +export type { TerminalConsoleProps } diff --git a/src/components/console/VNCConsole.tsx b/src/components/console/VNCConsole.tsx new file mode 100644 index 0000000..2453896 --- /dev/null +++ b/src/components/console/VNCConsole.tsx @@ -0,0 +1,117 @@ +import { useEffect, useRef, useCallback } from 'react' + +interface VNCConsoleProps { + connectionId: string + node: string + vmid: number + onError?: (message: string) => void +} + +type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error' + +export function VNCConsole({ connectionId, node, vmid, onError }: VNCConsoleProps) { + const containerRef = useRef(null) + const rfbRef = useRef(null) + const stateRef = useRef('connecting') + + const cleanup = useCallback(() => { + if (rfbRef.current) { + const rfb = rfbRef.current as { disconnect: () => void } + rfb.disconnect() + rfbRef.current = null + } + }, []) + + useEffect(() => { + if (!containerRef.current) return + + let cancelled = false + + const connect = async () => { + try { + stateRef.current = 'connecting' + + // Get WebSocket URL for VNC + let wsUrl: string + + try { + // Try real IPC first + const { isTauri, createVNCProxy, getWebSocketURL } = await import('@/lib/tauri') + + if (isTauri()) { + const [proxyInfo, baseUrl] = await Promise.all([ + createVNCProxy(connectionId, node, vmid), + getWebSocketURL(connectionId, node), + ]) + + wsUrl = `${baseUrl}/api2/json/nodes/${node}/qemu/${vmid}/vncwebsocket?port=${proxyInfo.port}&vncticket=${encodeURIComponent(proxyInfo.ticket)}` + } else { + // Dev mode: construct a mock URL + wsUrl = `wss://localhost:8006/api2/json/nodes/${node}/qemu/${vmid}/vncwebsocket?port=6000&vncticket=mock-ticket` + } + } catch { + // Fallback for dev mode + wsUrl = `wss://localhost:8006/api2/json/nodes/${node}/qemu/${vmid}/vncwebsocket?port=6000&vncticket=mock-ticket` + } + + if (cancelled) return + + // Dynamically import noVNC RFB class + const { default: RFB } = await import('@novnc/novnc/core/rfb.js') + + if (cancelled || !containerRef.current) return + + // Create RFB connection + const rfb = new RFB(containerRef.current, wsUrl, { + shared: true, + wsProtocols: ['binary'], + }) + + rfbRef.current = rfb + + rfb.addEventListener('connect', () => { + if (!cancelled) { + stateRef.current = 'connected' + } + }) + + rfb.addEventListener('disconnect', (ev: unknown) => { + if (!cancelled) { + const detail = (ev as CustomEvent<{ reason?: string }>).detail + stateRef.current = 'disconnected' + if (detail?.reason) { + onError?.(`VNC disconnected: ${detail.reason}`) + } + } + }) + + rfb.addEventListener('credentialsrequired', () => { + if (!cancelled) { + onError?.('VNC credentials required') + } + }) + } catch (err) { + if (!cancelled) { + stateRef.current = 'error' + onError?.(err instanceof Error ? err.message : 'Failed to connect VNC') + } + } + } + + connect() + + return () => { + cancelled = true + cleanup() + } + }, [connectionId, node, vmid, onError, cleanup]) + + return ( +
+ ) +} + +export type { VNCConsoleProps } diff --git a/src/components/dashboard/ActivityFeed.tsx b/src/components/dashboard/ActivityFeed.tsx new file mode 100644 index 0000000..29d91ea --- /dev/null +++ b/src/components/dashboard/ActivityFeed.tsx @@ -0,0 +1,133 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Activity, Clock } from 'lucide-react' +import type { ProxmoxTask } from '@/types/proxmox' + +interface ActivityFeedProps { + tasks: ProxmoxTask[] | undefined + isLoading: boolean +} + +function getStatusBadgeClass(status?: string): string { + switch (status) { + case 'OK': + return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' + case 'unknown': + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400' + default: + if (!status || status === 'Running') { + return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400' + } + return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' + } +} + +function getStatusLabel(status?: string): string { + if (!status || status === 'Running') return 'Running' + return status +} + +function formatDuration(starttime: number, endtime?: number): string { + const end = endtime ?? Math.floor(Date.now() / 1000) + const durationSeconds = Math.max(0, end - starttime) + if (durationSeconds < 60) return `${durationSeconds}s` + const minutes = Math.floor(durationSeconds / 60) + const seconds = durationSeconds % 60 + if (minutes < 60) return `${minutes}m ${seconds}s` + const hours = Math.floor(minutes / 60) + const remainingMinutes = minutes % 60 + return `${hours}h ${remainingMinutes}m` +} + +function formatTime(timestamp: number): string { + return new Date(timestamp * 1000).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }) +} + +function getTaskTypeLabel(type: string): string { + // Capitalize and format Proxmox task types + return type + .replace(/qm/gi, 'VM') + .replace(/vz/gi, 'CT') + .replace(/storage/gi, 'Storage') + .replace(/backup/gi, 'Backup') + .replace(/restore/gi, 'Restore') + .replace(/snapshot/gi, 'Snapshot') + .split(/[\s/]/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' ') +} + +export function ActivityFeed({ tasks, isLoading }: ActivityFeedProps) { + if (isLoading) { + return ( + + +
+ + Recent Activity +
+
+ +

Loading tasks...

+
+
+ ) + } + + const recentTasks = tasks?.slice(0, 15) ?? [] + + return ( + + +
+ + Recent Activity +
+
+ + {recentTasks.length > 0 ? ( + +
+ {recentTasks.map((task) => ( +
+
+
+
+

+ {getTaskTypeLabel(task.type)} +

+ + {getStatusLabel(task.status)} + +
+

+ {task.node} · {task.user} +

+
+
+
+
+ + {formatDuration(task.starttime, task.endtime)} +
+ {formatTime(task.starttime)} +
+
+ ))} +
+
+ ) : ( +

No recent activity

+ )} +
+
+ ) +} diff --git a/src/components/dashboard/NodeHealthGrid.tsx b/src/components/dashboard/NodeHealthGrid.tsx new file mode 100644 index 0000000..c38711f --- /dev/null +++ b/src/components/dashboard/NodeHealthGrid.tsx @@ -0,0 +1,192 @@ +import { useMemo } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Cpu, MemoryStick, HardDrive, Clock, Server, Box } from 'lucide-react' +import { AreaChart, Area, ResponsiveContainer } from 'recharts' +import type { ProxmoxNode, ProxmoxVM } from '@/types/proxmox' + +interface NodeHealthGridProps { + nodes: ProxmoxNode[] | undefined + vms: ProxmoxVM[] | undefined +} + +function formatUptime(seconds: number): string { + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + if (days > 0) return `${days}d ${hours}h` + return `${hours}h` +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function getStatusColor(status: 'online' | 'offline'): string { + return status === 'online' ? 'bg-green-500' : 'bg-red-500' +} + +function getStatusLabel(status: 'online' | 'offline'): string { + return status === 'online' ? 'Online' : 'Offline' +} + +/** Generate deterministic mock sparkline data from node stats */ +function generateSparklineData(baseValue: number, points: number = 12): { value: number }[] { + const data: { value: number }[] = [] + for (let i = 0; i < points; i++) { + // Deterministic pseudo-random variation around the base value + const seed = Math.sin(i * 2.1 + baseValue * 0.01) * 0.3 + const variation = baseValue * seed + data.push({ value: Math.max(0, Math.min(1, baseValue + variation)) }) + } + return data +} + +function NodeCard({ node, vmCount }: { node: ProxmoxNode; vmCount: number }) { + const cpuPercent = node.maxcpu > 0 ? node.cpu : 0 + const memPercent = node.maxmem > 0 ? node.mem / node.maxmem : 0 + const diskPercent = node.maxdisk > 0 ? node.disk / node.maxdisk : 0 + + const cpuData = useMemo(() => generateSparklineData(cpuPercent), [cpuPercent]) + const memData = useMemo(() => generateSparklineData(memPercent), [memPercent]) + + return ( + + +
+
+ {node.node} +
+ {getStatusLabel(node.status)} + + + {/* Sparklines */} +
+
+
+ + CPU +
+
+ + + + + +
+
+
+
+ + RAM +
+
+ + + + + +
+
+
+ + {/* Stats row */} +
+
+ + {(cpuPercent * 100).toFixed(0)}% +
+
+ + {(memPercent * 100).toFixed(0)}% +
+
+ + {(diskPercent * 100).toFixed(0)}% +
+
+ + {/* Bottom info */} +
+
+ + {formatUptime(node.uptime)} +
+
+ + {vmCount} VMs +
+
+ + {formatBytes(node.disk)} +
+
+
+ + ) +} + +export function NodeHealthGrid({ nodes, vms }: NodeHealthGridProps) { + // Count VMs per node + const vmCounts = useMemo(() => { + const counts: Record = {} + vms?.forEach((vm) => { + counts[vm.node] = (counts[vm.node] ?? 0) + 1 + }) + return counts + }, [vms]) + + if (!nodes || nodes.length === 0) { + return ( + + +
+ + Node Health +
+
+ +

No nodes available

+
+
+ ) + } + + return ( + + +
+ + Node Health +
+
+ +
+ {nodes.map((node) => ( + + ))} +
+
+
+ ) +} diff --git a/src/components/dashboard/QuickActions.tsx b/src/components/dashboard/QuickActions.tsx new file mode 100644 index 0000000..0b2c21a --- /dev/null +++ b/src/components/dashboard/QuickActions.tsx @@ -0,0 +1,66 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { RefreshCw, Server, Box, HardDrive, ListTodo, Shield } from 'lucide-react' + +interface QuickActionsProps { + onRefresh: () => void + isRefreshing: boolean +} + +export function QuickActions({ onRefresh, isRefreshing }: QuickActionsProps) { + return ( + + + Quick Actions + + +
+ + + + + + +
+
+
+ ) +} diff --git a/src/components/dashboard/ResourceGauge.tsx b/src/components/dashboard/ResourceGauge.tsx new file mode 100644 index 0000000..f665a55 --- /dev/null +++ b/src/components/dashboard/ResourceGauge.tsx @@ -0,0 +1,106 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Cpu, MemoryStick, HardDrive } from 'lucide-react' + +interface ResourceGaugeProps { + label: string + used: number + total: number + icon: 'cpu' | 'memory' | 'disk' + formatValue?: (value: number) => string +} + +const iconMap = { + cpu: Cpu, + memory: MemoryStick, + disk: HardDrive, +} as const + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function formatCores(cores: number): string { + return `${cores.toFixed(1)} cores` +} + +function getGaugeColor(percent: number): string { + if (percent >= 0.9) return 'stroke-destructive' + if (percent >= 0.7) return 'stroke-yellow-500' + return 'stroke-primary' +} + +function getTrackColor(): string { + return 'stroke-muted' +} + +export function ResourceGauge({ label, used, total, icon, formatValue }: ResourceGaugeProps) { + const percent = total > 0 ? used / total : 0 + const clampedPercent = Math.min(Math.max(percent, 0), 1) + const displayPercent = (clampedPercent * 100).toFixed(1) + + const Icon = iconMap[icon] + + // SVG circle parameters + const radius = 40 + const strokeWidth = 8 + const circumference = 2 * Math.PI * radius + const strokeDashoffset = circumference * (1 - clampedPercent) + + const defaultFormat = + icon === 'cpu' + ? formatCores + : formatBytes + + const formatter = formatValue ?? defaultFormat + + return ( + + + {label} + + + +
+ + {/* Background track */} + + {/* Progress arc */} + + +
+ {displayPercent}% +
+
+
+ {formatter(used)} / {formatter(total)} +
+
+
+ ) +} diff --git a/src/components/layout/Dashboard.tsx b/src/components/layout/Dashboard.tsx new file mode 100644 index 0000000..5a368f3 --- /dev/null +++ b/src/components/layout/Dashboard.tsx @@ -0,0 +1,161 @@ +import { useCallback } from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { useNodes, useVMs, useTasks, queryKeys } from '@/hooks/useProxmox' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Server, Cpu, HardDrive, MemoryStick } from 'lucide-react' +import { ResourceGauge } from '@/components/dashboard/ResourceGauge' +import { NodeHealthGrid } from '@/components/dashboard/NodeHealthGrid' +import { ActivityFeed } from '@/components/dashboard/ActivityFeed' +import { QuickActions } from '@/components/dashboard/QuickActions' + +interface DashboardProps { + connectionId: string +} + +export function Dashboard({ connectionId }: DashboardProps) { + const queryClient = useQueryClient() + const { data: nodes, isLoading: nodesLoading } = useNodes(connectionId) + const { data: vms, isLoading: vmsLoading } = useVMs(connectionId) + const { data: tasks, isLoading: tasksLoading } = useTasks(connectionId) + + const totalCPU = nodes?.reduce((acc, n) => acc + n.maxcpu, 0) ?? 0 + const usedCPU = nodes?.reduce((acc, n) => acc + n.cpu * n.maxcpu, 0) ?? 0 + const totalMem = nodes?.reduce((acc, n) => acc + n.maxmem, 0) ?? 0 + const usedMem = nodes?.reduce((acc, n) => acc + n.mem, 0) ?? 0 + const totalDisk = nodes?.reduce((acc, n) => acc + n.maxdisk, 0) ?? 0 + const usedDisk = nodes?.reduce((acc, n) => acc + n.disk, 0) ?? 0 + + const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` + } + + const formatPercent = (value: number) => `${(value * 100).toFixed(1)}%` + + const handleRefresh = useCallback(() => { + queryClient.invalidateQueries({ queryKey: queryKeys.nodes(connectionId) }) + queryClient.invalidateQueries({ queryKey: queryKeys.vms(connectionId) }) + queryClient.invalidateQueries({ queryKey: queryKeys.tasks(connectionId) }) + }, [queryClient, connectionId]) + + if (nodesLoading || vmsLoading) { + return ( +
+

Loading...

+
+ ) + } + + return ( +
+
+
+

Dashboard

+

Cluster overview and resource usage

+
+ + {/* Summary Stats Row */} +
+ + + Nodes + + + +
+ {nodes?.filter((n) => n.status === 'online').length ?? 0} / {nodes?.length ?? 0} +
+

Online

+
+
+ + + + CPU Usage + + + +
+ {totalCPU > 0 ? formatPercent(usedCPU / totalCPU) : '0%'} +
+

+ {usedCPU.toFixed(1)} / {totalCPU} cores +

+
+
+ + + + Memory + + + +
+ {totalMem > 0 ? formatPercent(usedMem / totalMem) : '0%'} +
+

+ {formatBytes(usedMem)} / {formatBytes(totalMem)} +

+
+
+ + + + Storage + + + +
+ {totalDisk > 0 ? formatPercent(usedDisk / totalDisk) : '0%'} +
+

+ {formatBytes(usedDisk)} / {formatBytes(totalDisk)} +

+
+
+
+ + {/* Resource Gauges */} +
+ `${v.toFixed(1)} cores`} + /> + + +
+ + {/* Node Health Grid */} + + + {/* Activity Feed + Quick Actions */} +
+
+ +
+
+ +
+
+
+
+ ) +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..edc97af --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,125 @@ +import { useConnectionStore } from '@/stores/connectionStore' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Button } from '@/components/ui/button' +import { Plus, Server, LayoutDashboard, HardDrive, Box, ListTodo, Shield, Settings } from 'lucide-react' +import { cn } from '@/lib/utils' + +type ViewType = 'dashboard' | 'vms' | 'vm-detail' | 'tasks' | 'backups' | 'storage' | 'storage-detail' | 'settings' + +interface SidebarProps { + onAddConnection: () => void + activeView?: ViewType + onNavigate?: (view: { type: ViewType }) => void +} + +export function Sidebar({ onAddConnection, activeView, onNavigate }: SidebarProps) { + const connections = useConnectionStore((s) => s.connections) + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + const setActiveConnection = useConnectionStore((s) => s.setActiveConnection) + + const getStatusColor = (status: string) => { + switch (status) { + case 'connected': + return 'bg-green-500' + case 'connecting': + case 'failover': + return 'bg-yellow-500' + case 'failed': + return 'bg-red-500' + default: + return 'bg-gray-500' + } + } + + return ( +
+
+

ProxmoxDesktop

+
+ +
+ {connections.map((connection) => ( +
+ + {activeConnectionId === connection.id && ( +
+ onNavigate?.({ type: 'dashboard' })} + /> + + onNavigate?.({ type: 'vms' })} + /> + + onNavigate?.({ type: 'storage' })} /> + onNavigate?.({ type: 'tasks' })} /> + onNavigate?.({ type: 'backups' })} /> +
+ )} +
+ ))} +
+
+
+ onNavigate?.({ type: 'settings' })} + /> + +
+
+ ) +} + +function SidebarItem({ + icon: Icon, + label, + active, + onClick, +}: { + icon: React.ComponentType<{ className?: string }> + label: string + active?: boolean + onClick?: () => void +}) { + return ( + + ) +} diff --git a/src/components/nodes/NodeDetail.tsx b/src/components/nodes/NodeDetail.tsx new file mode 100644 index 0000000..98c9a1e --- /dev/null +++ b/src/components/nodes/NodeDetail.tsx @@ -0,0 +1,225 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { StatusBadge } from '@/components/ui/status-badge' +import { Server, Cpu, MemoryStick, HardDrive } from 'lucide-react' +import { useVMs } from '@/hooks/useProxmox' +import type { ProxmoxNode } from '@/types/proxmox' + +interface NodeDetailProps { + node: ProxmoxNode + connectionId: string +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function formatUptime(seconds: number): string { + if (seconds === 0) return 'N/A' + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const parts: string[] = [] + if (days > 0) parts.push(`${days}d`) + if (hours > 0) parts.push(`${hours}h`) + if (minutes > 0) parts.push(`${minutes}m`) + return parts.join(' ') || '< 1m' +} + +function ResourceBar({ label, used, total, icon: Icon }: { + label: string + used: number + total: number + icon: React.ComponentType<{ className?: string }> +}) { + const percent = total > 0 ? (used / total) * 100 : 0 + const color = + percent > 90 ? 'bg-red-500' : + percent > 70 ? 'bg-yellow-500' : + 'bg-green-500' + + return ( +
+
+
+ + {label} +
+ + {percent.toFixed(1)}% + +
+
+
+
+
+ {formatBytes(used)} / {formatBytes(total)} +
+
+ ) +} + +export function NodeDetail({ node, connectionId }: NodeDetailProps) { + const { data: vms, isLoading: vmsLoading } = useVMs(connectionId) + + const nodeVMs = vms?.filter((vm) => vm.node === node.node) ?? [] + const runningVMs = nodeVMs.filter((vm) => vm.status === 'running') + + return ( +
+
+ {/* Header */} +
+
+
+ +

{node.node}

+ +
+

+ Uptime: {formatUptime(node.uptime)} +

+
+
+ + {/* Resource Usage */} +
+ + + +
+ {node.cpu.toFixed(1)} / {node.maxcpu} cores +
+
+
+ + + + + + + + + + + + +
+ + {/* VMs/Containers on this node */} + + +
+ Virtual Machines & Containers + + {runningVMs.length} running / {nodeVMs.length} total + +
+
+ + {vmsLoading ? ( +
+ Loading... +
+ ) : nodeVMs.length === 0 ? ( +
+ No VMs or containers found on this node +
+ ) : ( +
+ {nodeVMs.map((vm) => ( +
+
+
+ + {vm.vmid} + + {vm.name} +
+ + {vm.type} + +
+
+ +
+ {vm.maxmem > 0 ? formatBytes(vm.mem) : 'N/A'} +
+
+
+ ))} +
+ )} +
+
+ + {/* Node Info */} + + + System Information + + +
+
+ Node Name +

{node.node}

+
+
+ Status +

{node.status}

+
+
+ Uptime +

{formatUptime(node.uptime)}

+
+
+ Support Level +

{node.level || 'None'}

+
+
+ Total CPUs +

{node.maxcpu}

+
+
+ Total Memory +

{formatBytes(node.maxmem)}

+
+
+ Total Disk +

{formatBytes(node.maxdisk)}

+
+
+ ID +

{node.id}

+
+
+
+
+
+
+ ) +} diff --git a/src/components/settings/ConnectionManager.tsx b/src/components/settings/ConnectionManager.tsx new file mode 100644 index 0000000..ef51858 --- /dev/null +++ b/src/components/settings/ConnectionManager.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react' +import { useConnectionStore } from '@/stores/connectionStore' +import { useToast } from '@/components/ui/toast' +import { Button } from '@/components/ui/button' +import { ConnectionDialog } from '@/components/connections/ConnectionDialog' +import { Plus, Pencil, Trash2 } from 'lucide-react' +import { cn } from '@/lib/utils' + +export function ConnectionManager() { + const connections = useConnectionStore((s) => s.connections) + const removeConnection = useConnectionStore((s) => s.removeConnection) + const { addToast } = useToast() + const [dialogOpen, setDialogOpen] = useState(false) + const [deleteConfirmId, setDeleteConfirmId] = useState(null) + + const getStatusColor = (status: string) => { + switch (status) { + case 'connected': + return 'bg-green-500' + case 'connecting': + case 'failover': + return 'bg-yellow-500' + case 'failed': + return 'bg-red-500' + default: + return 'bg-gray-500' + } + } + + const handleDelete = (id: string, name: string) => { + removeConnection(id) + setDeleteConfirmId(null) + addToast(`Connection "${name}" removed`, 'success') + } + + return ( +
+
+

+ {connections.length} connection{connections.length !== 1 ? 's' : ''} +

+ +
+ + {connections.length === 0 ? ( +

+ No connections configured. Add a server to get started. +

+ ) : ( +
+ {connections.map((connection) => ( +
+
+
+
+

{connection.name}

+

+ {connection.primary.url} +

+
+
+
+ {deleteConfirmId === connection.id ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ ))} +
+ )} + + +
+ ) +} diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx new file mode 100644 index 0000000..b2febd4 --- /dev/null +++ b/src/components/settings/SettingsPage.tsx @@ -0,0 +1,79 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { ThemeSwitcher } from '@/components/settings/ThemeSwitcher' +import { ConnectionManager } from '@/components/settings/ConnectionManager' +import { ExternalLink } from 'lucide-react' + +export function SettingsPage() { + return ( +
+
+
+

Settings

+

+ Manage your application preferences +

+
+ + + + Appearance + + Customize the look and feel of the application + + + +
+ + +

+ Select your preferred color scheme. System will match your OS setting. +

+
+
+
+ + + + Connections + + Manage your Proxmox server connections + + + + + + + + + + About + Application information + + +
+
+ Version + 0.1.0 +
+
+ Platform + Tauri + React +
+ +
+
+
+
+
+ ) +} diff --git a/src/components/settings/ThemeSwitcher.tsx b/src/components/settings/ThemeSwitcher.tsx new file mode 100644 index 0000000..b79aaaa --- /dev/null +++ b/src/components/settings/ThemeSwitcher.tsx @@ -0,0 +1,39 @@ +import { useUIStore } from '@/stores/uiStore' +import { cn } from '@/lib/utils' +import { Sun, Moon, Monitor } from 'lucide-react' + +type Theme = 'light' | 'dark' | 'system' + +const themeOptions: { value: Theme; label: string; icon: React.ComponentType<{ className?: string }> }[] = [ + { value: 'light', label: 'Light', icon: Sun }, + { value: 'dark', label: 'Dark', icon: Moon }, + { value: 'system', label: 'System', icon: Monitor }, +] + +export function ThemeSwitcher() { + const theme = useUIStore((s) => s.theme) + const setTheme = useUIStore((s) => s.setTheme) + + return ( +
+ {themeOptions.map((option) => { + const Icon = option.icon + return ( + + ) + })} +
+ ) +} diff --git a/src/components/storage/StorageCard.tsx b/src/components/storage/StorageCard.tsx new file mode 100644 index 0000000..c94b19d --- /dev/null +++ b/src/components/storage/StorageCard.tsx @@ -0,0 +1,134 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { HardDrive, Database, Archive, Disc, Box } from 'lucide-react' +import type { ProxmoxStorage } from '@/types/proxmox' + +interface StorageCardProps { + storage: ProxmoxStorage + onClick: () => void +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function getUsageColor(percent: number): string { + if (percent >= 90) return 'bg-red-500' + if (percent >= 70) return 'bg-yellow-500' + return 'bg-green-500' +} + +function getStorageIcon(type: string) { + switch (type.toLowerCase()) { + case 'lvm': + case 'lvmthin': + return Database + case 'zfs': + case 'zfspool': + return Database + case 'nfs': + case 'cifs': + case 'glusterfs': + return HardDrive + case 'local': + return HardDrive + default: + return HardDrive + } +} + +function getContentBadges(content: string): string[] { + if (!content) return [] + return content.split(',').map((c) => c.trim()) +} + +const contentTypeIcons: Record> = { + images: Box, + rootdir: Box, + backup: Archive, + iso: Disc, + snippets: HardDrive, + 'vztmpl': HardDrive, +} + +function getContentTypeLabel(type: string): string { + switch (type) { + case 'images': return 'VM Images' + case 'rootdir': return 'Containers' + case 'backup': return 'Backups' + case 'iso': return 'ISOs' + case 'snippets': return 'Snippets' + case 'vztmpl': return 'Templates' + default: return type + } +} + +export function StorageCard({ storage, onClick }: StorageCardProps) { + const percent = storage.total > 0 ? (storage.used / storage.total) * 100 : 0 + const usageColor = getUsageColor(percent) + const Icon = getStorageIcon(storage.type) + const contentTypes = getContentBadges(storage.content) + + return ( + + + + + {storage.storage} + +
+ {storage.type} + {storage.node && {storage.node}} +
+
+ + {/* Usage Bar */} +
+
+ Usage + {percent.toFixed(1)}% +
+
+
+
+
+ + {/* Size Info */} +
+ {formatBytes(storage.used)} used + {formatBytes(storage.total)} total +
+
+ {formatBytes(storage.avail)} available +
+ + {/* Content Types */} + {contentTypes.length > 0 && ( +
+ {contentTypes.map((type) => { + const TypeIcon = contentTypeIcons[type] || HardDrive + return ( + + + {getContentTypeLabel(type)} + + ) + })} +
+ )} + + + ) +} diff --git a/src/components/storage/StorageDetail.tsx b/src/components/storage/StorageDetail.tsx new file mode 100644 index 0000000..7f26ede --- /dev/null +++ b/src/components/storage/StorageDetail.tsx @@ -0,0 +1,281 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { ArrowLeft, HardDrive, Database, Archive, Disc, Box, Clock, File } from 'lucide-react' +import { useStorageDetail, useStorageContent } from '@/hooks/useProxmox' +import type { ProxmoxStorageContent } from '@/types/proxmox' + +interface StorageDetailProps { + connectionId: string + storage: string + onBack: () => void +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function getUsageColor(percent: number): string { + if (percent >= 90) return 'bg-red-500' + if (percent >= 70) return 'bg-yellow-500' + return 'bg-green-500' +} + +function getStorageIcon(type: string) { + switch (type?.toLowerCase()) { + case 'lvm': + case 'lvmthin': + return Database + case 'zfs': + case 'zfspool': + return Database + default: + return HardDrive + } +} + +function getContentIcon(contentType: string) { + switch (contentType) { + case 'images': return Box + case 'backup': return Archive + case 'iso': return Disc + default: return File + } +} + +function getContentTypeLabel(type: string): string { + switch (type) { + case 'images': return 'VM Images' + case 'rootdir': return 'Container Root Disks' + case 'backup': return 'Backups' + case 'iso': return 'ISO Images' + case 'snippets': return 'Snippets' + case 'vztmpl': return 'Container Templates' + default: return type + } +} + +function formatTimestamp(seconds: number): string { + if (!seconds) return 'N/A' + return new Date(seconds * 1000).toLocaleString() +} + +function formatContentItem(item: ProxmoxStorageContent): string { + const parts = item.volid.split('/') + return parts[parts.length - 1] || item.volid +} + +export function StorageDetail({ connectionId, storage, onBack }: StorageDetailProps) { + const { data: detail, isLoading: detailLoading } = useStorageDetail( + connectionId, + null, + storage + ) + const { data: contentList, isLoading: contentLoading } = useStorageContent( + connectionId, + storage + ) + + const percent = detail && detail.total > 0 + ? (detail.used / detail.total) * 100 + : 0 + const usageColor = getUsageColor(percent) + const Icon = detail ? getStorageIcon(detail.type) : HardDrive + + // Group content by type + const contentByType = contentList?.reduce((acc, item) => { + const type = item.content + if (!acc[type]) acc[type] = [] + acc[type].push(item) + return acc + }, {} as Record) ?? {} + + if (detailLoading || contentLoading) { + return ( +
+

Loading storage details...

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

Failed to load storage details

+
+ ) + } + + return ( +
+
+ {/* Header */} +
+ +
+
+ +

{detail.storage}

+
+

+ {detail.type.toUpperCase()} storage on {detail.node} +

+
+
+ + {/* Resource Usage */} + + + Resource Usage + + + {/* Large Usage Bar */} +
+
+ Disk Usage + {percent.toFixed(1)}% +
+
+
+
+
+ {formatBytes(detail.used)} used + {formatBytes(detail.total)} total +
+
+ + {/* Stats Grid */} +
+
+

Total Size

+

{formatBytes(detail.total)}

+
+
+

Used

+

{formatBytes(detail.used)}

+
+
+

Available

+

{formatBytes(detail.avail)}

+
+
+

Status

+

+ {detail.active ? ( + Active + ) : ( + Inactive + )} +

+
+
+ + + + {/* Storage Info */} + + + Storage Information + + +
+
+

Name

+

{detail.storage}

+
+
+

Type

+

{detail.type}

+
+
+

Node

+

{detail.node}

+
+
+

Enabled

+

{detail.enabled ? 'Yes' : 'No'}

+
+
+

Shared

+

{detail.shared ? 'Yes' : 'No'}

+
+
+

Content Types

+

{detail.content || 'None'}

+
+
+
+
+ + {/* Content List */} + + + + Content + {contentList && ( + + ({contentList.length} items) + + )} + + + + {!contentList || contentList.length === 0 ? ( +

No content found

+ ) : ( +
+ {Object.entries(contentByType).map(([type, items]) => { + const TypeIcon = getContentIcon(type) + return ( +
+
+ + {getContentTypeLabel(type)} + ({items.length}) +
+
+ {items.map((item, idx) => ( +
+
+ + + {formatContentItem(item)} + +
+
+ {item.format && ( + + {item.format} + + )} + {item.size && {formatBytes(item.size)}} +
+ + {formatTimestamp(item.ctime)} +
+
+
+ ))} +
+
+ ) + })} +
+ )} +
+
+
+
+ ) +} diff --git a/src/components/storage/StorageOverview.tsx b/src/components/storage/StorageOverview.tsx new file mode 100644 index 0000000..883aef0 --- /dev/null +++ b/src/components/storage/StorageOverview.tsx @@ -0,0 +1,90 @@ +import { useState, useMemo } from 'react' +import { useStorage } from '@/hooks/useProxmox' +import { StorageCard } from './StorageCard' +import { Search, HardDrive } from 'lucide-react' +import { Input } from '@/components/ui/input' + +interface StorageOverviewProps { + connectionId: string + onStorageClick?: (storage: string) => void +} + +export function StorageOverview({ connectionId, onStorageClick }: StorageOverviewProps) { + const { data: storageList, isLoading, error } = useStorage(connectionId) + const [search, setSearch] = useState('') + + const filteredStorage = useMemo(() => { + if (!storageList) return [] + if (!search) return storageList + const query = search.toLowerCase() + return storageList.filter( + (s) => + s.storage.toLowerCase().includes(query) || + s.type.toLowerCase().includes(query) || + s.node.toLowerCase().includes(query) + ) + }, [storageList, search]) + + if (isLoading) { + return ( +
+

Loading storage pools...

+
+ ) + } + + if (error) { + return ( +
+

Failed to load storage pools

+
+ ) + } + + return ( +
+
+ {/* Header */} +
+

Storage Pools

+

+ {storageList?.length ?? 0} storage pools across the cluster +

+
+ + {/* Search */} +
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + {/* Storage Grid */} + {filteredStorage.length === 0 ? ( +
+ +

+ {storageList?.length === 0 + ? 'No storage pools found' + : 'No storage pools match your search'} +

+
+ ) : ( +
+ {filteredStorage.map((storage) => ( + onStorageClick?.(storage.storage)} + /> + ))} +
+ )} +
+
+ ) +} diff --git a/src/components/tasks/TaskList.tsx b/src/components/tasks/TaskList.tsx new file mode 100644 index 0000000..dfb9451 --- /dev/null +++ b/src/components/tasks/TaskList.tsx @@ -0,0 +1,346 @@ +import { useState, useMemo, Fragment } from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Search, ListTodo, ChevronDown, ChevronRight, Play, CheckCircle, XCircle, Clock } from 'lucide-react' +import { useTasks } from '@/hooks/useProxmox' +import type { ProxmoxTask } from '@/types/proxmox' + +interface TaskListProps { + connectionId: string +} + +type TaskStatusFilter = 'all' | 'running' | 'completed' | 'failed' + +function formatTimestamp(seconds: number): string { + if (seconds === 0) return 'N/A' + const date = new Date(seconds * 1000) + return date.toLocaleString() +} + +function formatDuration(start: number, end?: number): string { + if (start === 0) return 'N/A' + const endTime = end ?? Math.floor(Date.now() / 1000) + const duration = endTime - start + if (duration < 0) return 'N/A' + const hours = Math.floor(duration / 3600) + const minutes = Math.floor((duration % 3600) / 60) + const seconds = duration % 60 + const parts: string[] = [] + if (hours > 0) parts.push(`${hours}h`) + if (minutes > 0) parts.push(`${minutes}m`) + if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`) + return parts.join(' ') +} + +function getTaskStatus(task: ProxmoxTask): 'running' | 'completed' | 'failed' { + if (task.status === 'running') return 'running' + if (task.exitstatus === 'OK') return 'completed' + if (task.exitstatus && task.exitstatus !== 'OK') return 'failed' + // If no endtime, consider it running + if (!task.endtime) return 'running' + return 'completed' +} + +function TaskStatusIcon({ status }: { status: 'running' | 'completed' | 'failed' }) { + switch (status) { + case 'running': + return + case 'completed': + return + case 'failed': + return + } +} + +function TaskStatusBadge({ status }: { status: 'running' | 'completed' | 'failed' }) { + const config = { + running: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300', + completed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300', + failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300', + } + + const label = { + running: 'Running', + completed: 'Completed', + failed: 'Failed', + } + + return ( + + + {label[status]} + + ) +} + +export function TaskList({ connectionId }: TaskListProps) { + const { data: tasks, isLoading, error } = useTasks(connectionId) + + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('all') + const [nodeFilter, setNodeFilter] = useState('all') + const [userFilter, setUserFilter] = useState('all') + const [expandedTasks, setExpandedTasks] = useState>(new Set()) + + const filteredTasks = useMemo(() => { + if (!tasks) return [] + return tasks + .filter((task) => { + const taskStatus = getTaskStatus(task) + // Status filter + if (statusFilter !== 'all' && taskStatus !== statusFilter) return false + // Node filter + if (nodeFilter !== 'all' && task.node !== nodeFilter) return false + // User filter + if (userFilter !== 'all' && task.user !== userFilter) return false + // Search filter + if (search) { + const query = search.toLowerCase() + const matchesUPID = task.upid.toLowerCase().includes(query) + const matchesType = task.type.toLowerCase().includes(query) + const matchesId = task.id.toLowerCase().includes(query) + if (!matchesUPID && !matchesType && !matchesId) return false + } + return true + }) + .sort((a, b) => { + // Sort by start time descending (newest first) + return b.starttime - a.starttime + }) + }, [tasks, statusFilter, nodeFilter, userFilter, search]) + + const uniqueNodes = useMemo(() => { + if (!tasks) return [] + return [...new Set(tasks.map((t) => t.node))].sort() + }, [tasks]) + + const uniqueUsers = useMemo(() => { + if (!tasks) return [] + return [...new Set(tasks.map((t) => t.user))].sort() + }, [tasks]) + + const runningCount = useMemo(() => { + if (!tasks) return 0 + return tasks.filter((t) => getTaskStatus(t) === 'running').length + }, [tasks]) + + const toggleExpanded = (upid: string) => { + setExpandedTasks((prev) => { + const next = new Set(prev) + if (next.has(upid)) { + next.delete(upid) + } else { + next.add(upid) + } + return next + }) + } + + if (isLoading) { + return ( +
+

Loading tasks...

+
+ ) + } + + if (error) { + return ( +
+

Failed to load tasks

+
+ ) + } + + return ( +
+
+ {/* Header */} +
+
+ +

Tasks

+
+

+ {tasks?.length ?? 0} total tasks + {runningCount > 0 && ( + + ({runningCount} running) + + )} +

+
+ + {/* Filters */} +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + + + + + +
+ + {/* Task Table */} + + + {filteredTasks.length === 0 ? ( +
+ {tasks?.length === 0 ? ( +
+ +

No tasks found

+
+ ) : ( +
+ +

No tasks match your filters

+
+ )} +
+ ) : ( +
+ + + + + + + + + + + + + + {filteredTasks.map((task) => { + const taskStatus = getTaskStatus(task) + const isExpanded = expandedTasks.has(task.upid) + return ( + + toggleExpanded(task.upid)} + > + + + + + + + + + + {isExpanded && ( + + + + )} + + ) + })} + +
+ UPIDTypeNodeUserStatusStart TimeDuration
+ {isExpanded ? ( + + ) : ( + + )} + {task.upid} + + {task.type} + + {task.node}{task.user} + + +
+ + {formatTimestamp(task.starttime)} +
+
+ {formatDuration(task.starttime, task.endtime)} +
+
+
+ UPID: +

{task.upid}

+
+
+ Task ID: +

{task.id}

+
+
+ PID: +

{task.pid}

+
+
+ PStart: +

{task.pstart}

+
+ {task.endtime && ( +
+ End Time: +

{formatTimestamp(task.endtime)}

+
+ )} + {task.exitstatus && ( +
+ Exit Status: +

+ {task.exitstatus} +

+
+ )} +
+
+
+ )} +
+
+
+
+ ) +} diff --git a/src/components/tasks/TaskStatusBar.tsx b/src/components/tasks/TaskStatusBar.tsx new file mode 100644 index 0000000..88c7a4c --- /dev/null +++ b/src/components/tasks/TaskStatusBar.tsx @@ -0,0 +1,47 @@ +import { useMemo } from 'react' +import { ListTodo, Play } from 'lucide-react' +import { useTasks } from '@/hooks/useProxmox' +import type { ProxmoxTask } from '@/types/proxmox' + +interface TaskStatusBarProps { + connectionId: string + onClick?: () => void +} + +function getTaskStatus(task: ProxmoxTask): 'running' | 'completed' | 'failed' { + if (task.status === 'running') return 'running' + if (task.exitstatus === 'OK') return 'completed' + if (task.exitstatus && task.exitstatus !== 'OK') return 'failed' + if (!task.endtime) return 'running' + return 'completed' +} + +export function TaskStatusBar({ connectionId, onClick }: TaskStatusBarProps) { + const { data: tasks } = useTasks(connectionId) + + const runningCount = useMemo(() => { + if (!tasks) return 0 + return tasks.filter((t) => getTaskStatus(t) === 'running').length + }, [tasks]) + + return ( + + ) +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..9e6e1e2 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..85849b0 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..20a3d94 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,116 @@ +import * as React from 'react' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { X } from 'lucide-react' +import { cn } from '@/lib/utils' + +const Dialog = DialogPrimitive.Root +const DialogTrigger = DialogPrimitive.Trigger +const DialogPortal = DialogPrimitive.Portal +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = 'DialogHeader' + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = 'DialogFooter' + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..3bd89ba --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = 'Input' + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..3fbe658 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const labelVariants = cva( + 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..9530110 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,45 @@ +import * as React from 'react' +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' +import { cn } from '@/lib/utils' + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'vertical', ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/src/components/ui/status-badge.tsx b/src/components/ui/status-badge.tsx new file mode 100644 index 0000000..5d8f210 --- /dev/null +++ b/src/components/ui/status-badge.tsx @@ -0,0 +1,60 @@ +import { cn } from '@/lib/utils' +import { CheckCircle, XCircle, Pause, Clock } from 'lucide-react' + +type BadgeStatus = 'running' | 'stopped' | 'paused' | 'suspended' | 'online' | 'offline' + +interface StatusBadgeProps { + status: BadgeStatus + className?: string +} + +const statusConfig: Record }> = { + running: { + label: 'Running', + color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300', + icon: CheckCircle, + }, + stopped: { + label: 'Stopped', + color: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300', + icon: XCircle, + }, + paused: { + label: 'Paused', + color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300', + icon: Pause, + }, + suspended: { + label: 'Suspended', + color: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300', + icon: Clock, + }, + online: { + label: 'Online', + color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300', + icon: CheckCircle, + }, + offline: { + label: 'Offline', + color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300', + icon: XCircle, + }, +} + +export function StatusBadge({ status, className }: StatusBadgeProps) { + const config = statusConfig[status] + const Icon = config.icon + + return ( + + + {config.label} + + ) +} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..c99280c --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import * as TabsPrimitive from '@radix-ui/react-tabs' +import { cn } from '@/lib/utils' + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..ae58a7c --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,120 @@ +import { createContext, useCallback, useContext, useState } from 'react' +import { cn } from '@/lib/utils' +import { X, CheckCircle, AlertTriangle, Info, AlertCircle } from 'lucide-react' + +type ToastVariant = 'success' | 'error' | 'warning' | 'info' + +interface Toast { + id: string + message: string + variant: ToastVariant +} + +interface ToastContextValue { + toasts: Toast[] + addToast: (message: string, variant?: ToastVariant) => void + removeToast: (id: string) => void +} + +const ToastContext = createContext(null) + +export function useToast() { + const context = useContext(ToastContext) + if (!context) { + throw new Error('useToast must be used within a ToastProvider') + } + return context +} + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = useState([]) + + const removeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)) + }, []) + + const addToast = useCallback( + (message: string, variant: ToastVariant = 'info') => { + const id = crypto.randomUUID() + setToasts((prev) => [...prev, { id, message, variant }]) + setTimeout(() => removeToast(id), 5000) + }, + [removeToast] + ) + + return ( + + {children} + + + ) +} + +function ToastContainer({ + toasts, + onRemove, +}: { + toasts: Toast[] + onRemove: (id: string) => void +}) { + if (toasts.length === 0) return null + + return ( +
+ {toasts.map((toast) => ( + + ))} +
+ ) +} + +const variantStyles: Record; container: string }> = { + success: { + icon: CheckCircle, + container: 'bg-green-50 border-green-200 text-green-800 dark:bg-green-950 dark:border-green-800 dark:text-green-200', + }, + error: { + icon: AlertCircle, + container: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-950 dark:border-red-800 dark:text-red-200', + }, + warning: { + icon: AlertTriangle, + container: 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-950 dark:border-yellow-800 dark:text-yellow-200', + }, + info: { + icon: Info, + container: 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-200', + }, +} + +function ToastItem({ + toast, + onRemove, +}: { + toast: Toast + onRemove: (id: string) => void +}) { + const config = variantStyles[toast.variant] + const Icon = config.icon + + return ( +
+ +

{toast.message}

+ +
+ ) +} diff --git a/src/components/vms/ContainerList.tsx b/src/components/vms/ContainerList.tsx new file mode 100644 index 0000000..7f15b44 --- /dev/null +++ b/src/components/vms/ContainerList.tsx @@ -0,0 +1,211 @@ +import { useState, useMemo } from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { StatusBadge } from '@/components/ui/status-badge' +import { Search, Server, Box } from 'lucide-react' +import { useVMs } from '@/hooks/useProxmox' +import type { ProxmoxVM } from '@/types/proxmox' + +interface ContainerListProps { + connectionId: string + onContainerClick?: (container: ProxmoxVM) => void +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function formatUptime(seconds: number): string { + if (seconds === 0) return 'N/A' + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const parts: string[] = [] + if (days > 0) parts.push(`${days}d`) + if (hours > 0) parts.push(`${hours}h`) + if (minutes > 0) parts.push(`${minutes}m`) + return parts.join(' ') || '< 1m' +} + +type ContainerStatusFilter = 'all' | ProxmoxVM['status'] + +export function ContainerList({ connectionId, onContainerClick }: ContainerListProps) { + const { data: vms, isLoading, error } = useVMs(connectionId) + + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('all') + const [nodeFilter, setNodeFilter] = useState('all') + + // Filter to only LXC containers + const containers = useMemo(() => { + return vms?.filter((vm) => vm.type === 'lxc') ?? [] + }, [vms]) + + const filteredContainers = useMemo(() => { + return containers.filter((container) => { + // Status filter + if (statusFilter !== 'all' && container.status !== statusFilter) return false + // Node filter + if (nodeFilter !== 'all' && container.node !== nodeFilter) return false + // Search filter + if (search) { + const query = search.toLowerCase() + const matchesName = container.name.toLowerCase().includes(query) + const matchesVMID = container.vmid.toString().includes(query) + if (!matchesName && !matchesVMID) return false + } + return true + }) + }, [containers, statusFilter, nodeFilter, search]) + + const uniqueNodes = useMemo(() => { + return [...new Set(containers.map((c) => c.node))].sort() + }, [containers]) + + if (isLoading) { + return ( +
+

Loading containers...

+
+ ) + } + + if (error) { + return ( +
+

Failed to load containers

+
+ ) + } + + return ( +
+
+ {/* Header */} +
+

Containers

+

+ {containers.length} total containers across the cluster +

+
+ + {/* Filters */} +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + + + +
+ + {/* Container Table */} + + + {filteredContainers.length === 0 ? ( +
+ {containers.length === 0 ? ( +
+ +

No containers found

+
+ ) : ( +
+ +

No containers match your filters

+
+ )} +
+ ) : ( +
+ + + + + + + + + + + + + + + {filteredContainers.map((container) => ( + onContainerClick?.(container)} + > + + + + + + + + + + ))} + +
VMIDNameStatusNodeCPUMemoryDiskUptime
{container.vmid}{container.name} + + +
+ + {container.node} +
+
+ {container.cpus} cores + + {container.maxmem > 0 ? formatBytes(container.mem) : 'N/A'} + / {formatBytes(container.maxmem)} + + {container.maxdisk > 0 ? formatBytes(container.disk) : 'N/A'} + / {formatBytes(container.maxdisk)} + + {formatUptime(container.uptime)} +
+
+ )} +
+
+
+
+ ) +} diff --git a/src/components/vms/VMDetail.tsx b/src/components/vms/VMDetail.tsx new file mode 100644 index 0000000..0048854 --- /dev/null +++ b/src/components/vms/VMDetail.tsx @@ -0,0 +1,288 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { StatusBadge } from '@/components/ui/status-badge' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { + ArrowLeft, + Play, + Square, + Power, + RotateCw, + Pause, + PlayCircle, + ArrowRightLeft, +} from 'lucide-react' +import { useStartVM, useStopVM, useShutdownVM, useRebootVM, useSuspendVM, useResumeVM, useClusterStatus } from '@/hooks/useProxmox' +import { OverviewTab } from '@/components/vms/tabs/OverviewTab' +import { HardwareTab } from '@/components/vms/tabs/HardwareTab' +import { DisksTab } from '@/components/vms/tabs/DisksTab' +import { NetworkTab } from '@/components/vms/tabs/NetworkTab' +import { SnapshotsTab } from '@/components/vms/tabs/SnapshotsTab' +import { ConsoleTab } from '@/components/vms/tabs/ConsoleTab' +import { MigrateDialog } from '@/components/vms/dialogs/MigrateDialog' +import type { ProxmoxVM } from '@/types/proxmox' + +interface VMDetailProps { + vm: ProxmoxVM + connectionId: string + onBack: () => void +} + +type ConfirmAction = { + type: 'stop' | 'shutdown' | 'reboot' + title: string + description: string +} + +export function VMDetail({ vm, connectionId, onBack }: VMDetailProps) { + const [confirmAction, setConfirmAction] = useState(null) + const [migrateDialogOpen, setMigrateDialogOpen] = useState(false) + + const startVM = useStartVM() + const stopVM = useStopVM() + const shutdownVM = useShutdownVM() + const rebootVM = useRebootVM() + const suspendVM = useSuspendVM() + const resumeVM = useResumeVM() + const clusterStatus = useClusterStatus(connectionId) + + const isRunning = vm.status === 'running' + const isStopped = vm.status === 'stopped' + const isBusy = startVM.isPending || stopVM.isPending || shutdownVM.isPending || rebootVM.isPending || suspendVM.isPending || resumeVM.isPending + + const isCluster = clusterStatus.data?.type === 'cluster' && (clusterStatus.data?.nodes?.length ?? 0) > 1 + + const handleStart = () => { + startVM.mutate({ node: vm.node, vmid: vm.vmid }) + } + + const handleStop = () => { + setConfirmAction({ + type: 'stop', + title: 'Force Stop VM', + description: `Are you sure you want to force stop "${vm.name}"? This is equivalent to pulling the power plug and may cause data loss.`, + }) + } + + const handleShutdown = () => { + setConfirmAction({ + type: 'shutdown', + title: 'Shutdown VM', + description: `Are you sure you want to gracefully shutdown "${vm.name}"? The VM will have a chance to save its state.`, + }) + } + + const handleReboot = () => { + setConfirmAction({ + type: 'reboot', + title: 'Reboot VM', + description: `Are you sure you want to reboot "${vm.name}"? The VM will be restarted gracefully.`, + }) + } + + const handleSuspend = () => { + suspendVM.mutate({ node: vm.node, vmid: vm.vmid }) + } + + const handleResume = () => { + resumeVM.mutate({ node: vm.node, vmid: vm.vmid }) + } + + const executeConfirmAction = () => { + if (!confirmAction) return + + switch (confirmAction.type) { + case 'stop': + stopVM.mutate({ node: vm.node, vmid: vm.vmid }) + break + case 'shutdown': + shutdownVM.mutate({ node: vm.node, vmid: vm.vmid }) + break + case 'reboot': + rebootVM.mutate({ node: vm.node, vmid: vm.vmid }) + break + } + + setConfirmAction(null) + } + + return ( +
+
+ {/* Header */} +
+
+ +
+
+

{vm.name}

+ +
+

+ VMID {vm.vmid} · {vm.type.toUpperCase()} · {vm.node} +

+
+
+ + {/* Lifecycle Actions */} +
+ {isStopped || !isRunning ? ( + + ) : null} + + {isRunning && ( + <> + + + + + )} + + {isRunning && ( + + )} + + {vm.status === 'paused' && ( + + )} + + {isCluster && ( + + )} +
+
+ + {/* Tabs */} + + + Overview + Console + Hardware + Disks + Network + Snapshots + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {/* Confirmation Dialog */} + setConfirmAction(null)}> + + + {confirmAction?.title} + {confirmAction?.description} + + + + + + + + + {/* Migrate Dialog */} + +
+ ) +} diff --git a/src/components/vms/VMList.tsx b/src/components/vms/VMList.tsx new file mode 100644 index 0000000..85bd41e --- /dev/null +++ b/src/components/vms/VMList.tsx @@ -0,0 +1,228 @@ +import { useState, useMemo } from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { StatusBadge } from '@/components/ui/status-badge' +import { Search, Server, Box } from 'lucide-react' +import { useVMs } from '@/hooks/useProxmox' +import type { ProxmoxVM } from '@/types/proxmox' + +interface VMListProps { + connectionId: string + onVMClick?: (vm: ProxmoxVM) => void +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function formatUptime(seconds: number): string { + if (seconds === 0) return 'N/A' + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const parts: string[] = [] + if (days > 0) parts.push(`${days}d`) + if (hours > 0) parts.push(`${hours}h`) + if (minutes > 0) parts.push(`${minutes}m`) + return parts.join(' ') || '< 1m' +} + +type VMTypeFilter = 'all' | 'qemu' | 'lxc' +type VMStatusFilter = 'all' | ProxmoxVM['status'] + +export function VMList({ connectionId, onVMClick }: VMListProps) { + const { data: vms, isLoading: vmsLoading, error: vmsError } = useVMs(connectionId) + + const [search, setSearch] = useState('') + const [typeFilter, setTypeFilter] = useState('all') + const [statusFilter, setStatusFilter] = useState('all') + const [nodeFilter, setNodeFilter] = useState('all') + + const filteredVMs = useMemo(() => { + if (!vms) return [] + return vms.filter((vm) => { + // Type filter + if (typeFilter !== 'all' && vm.type !== typeFilter) return false + // Status filter + if (statusFilter !== 'all' && vm.status !== statusFilter) return false + // Node filter + if (nodeFilter !== 'all' && vm.node !== nodeFilter) return false + // Search filter + if (search) { + const query = search.toLowerCase() + const matchesName = vm.name.toLowerCase().includes(query) + const matchesVMID = vm.vmid.toString().includes(query) + if (!matchesName && !matchesVMID) return false + } + return true + }) + }, [vms, typeFilter, statusFilter, nodeFilter, search]) + + const uniqueNodes = useMemo(() => { + if (!vms) return [] + return [...new Set(vms.map((vm) => vm.node))].sort() + }, [vms]) + + if (vmsLoading) { + return ( +
+

Loading VMs...

+
+ ) + } + + if (vmsError) { + return ( +
+

Failed to load VMs

+
+ ) + } + + return ( +
+
+ {/* Header */} +
+

Virtual Machines

+

+ {vms?.length ?? 0} total VMs across the cluster +

+
+ + {/* Filters */} +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + + + + + +
+ + {/* VM Table */} + + + {filteredVMs.length === 0 ? ( +
+ {vms?.length === 0 ? ( +
+ +

No VMs found

+
+ ) : ( +
+ +

No VMs match your filters

+
+ )} +
+ ) : ( +
+ + + + + + + + + + + + + + + + {filteredVMs.map((vm) => ( + onVMClick?.(vm)} + > + + + + + + + + + + + ))} + +
VMIDNameStatusNodeTypeCPUMemoryDiskUptime
{vm.vmid}{vm.name} + + +
+ + {vm.node} +
+
+ + {vm.type} + + + {vm.cpus} cores + + {vm.maxmem > 0 ? formatBytes(vm.mem) : 'N/A'} + / {formatBytes(vm.maxmem)} + + {vm.maxdisk > 0 ? formatBytes(vm.disk) : 'N/A'} + / {formatBytes(vm.maxdisk)} + + {formatUptime(vm.uptime)} +
+
+ )} +
+
+
+
+ ) +} diff --git a/src/components/vms/dialogs/AddDiskDialog.tsx b/src/components/vms/dialogs/AddDiskDialog.tsx new file mode 100644 index 0000000..df550f6 --- /dev/null +++ b/src/components/vms/dialogs/AddDiskDialog.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useAddDisk } from '@/hooks/useProxmox' +import type { ProxmoxVM, AddDiskConfig } from '@/types/proxmox' + +interface AddDiskDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + vm: ProxmoxVM +} + +export function AddDiskDialog({ open, onOpenChange, vm }: AddDiskDialogProps) { + const [storage, setStorage] = useState('local-lvm') + const [size, setSize] = useState('32') + const [busType, setBusType] = useState('scsi') + const addDisk = useAddDisk() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + const sizeBytes = parseFloat(size) * 1024 * 1024 * 1024 + addDisk.mutate( + { + node: vm.node, + vmid: vm.vmid, + config: { storage, size: sizeBytes, busType }, + }, + { + onSuccess: () => { + onOpenChange(false) + setStorage('local-lvm') + setSize('32') + setBusType('scsi') + }, + }, + ) + } + + return ( + + + + Add Disk + + Add a new disk to {vm.name} (VMID {vm.vmid}) + + +
+
+ + setStorage(e.target.value)} + placeholder="local-lvm" + required + /> +
+
+ + setSize(e.target.value)} + placeholder="32" + required + /> +
+
+ + +
+ {addDisk.isError && ( +

+ Failed to add disk. Please try again. +

+ )} + + + + +
+
+
+ ) +} diff --git a/src/components/vms/dialogs/AddNICDialog.tsx b/src/components/vms/dialogs/AddNICDialog.tsx new file mode 100644 index 0000000..ca58d8d --- /dev/null +++ b/src/components/vms/dialogs/AddNICDialog.tsx @@ -0,0 +1,140 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useAddNIC } from '@/hooks/useProxmox' +import type { ProxmoxVM } from '@/types/proxmox' + +interface AddNICDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + vm: ProxmoxVM +} + +export function AddNICDialog({ open, onOpenChange, vm }: AddNICDialogProps) { + const [bridge, setBridge] = useState('vmbr0') + const [model, setModel] = useState('virtio') + const [macaddr, setMacaddr] = useState('') + const [tag, setTag] = useState('') + const [firewall, setFirewall] = useState(false) + const addNIC = useAddNIC() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + addNIC.mutate( + { + node: vm.node, + vmid: vm.vmid, + config: { + bridge, + model, + macaddr: macaddr || undefined, + tag: tag ? parseInt(tag, 10) : undefined, + firewall, + }, + }, + { + onSuccess: () => { + onOpenChange(false) + setBridge('vmbr0') + setModel('virtio') + setMacaddr('') + setTag('') + setFirewall(false) + }, + }, + ) + } + + return ( + + + + Add Network Interface + + Add a new NIC to {vm.name} (VMID {vm.vmid}) + + +
+
+ + setBridge(e.target.value)} + placeholder="vmbr0" + required + /> +
+
+ + +
+
+ + setMacaddr(e.target.value)} + placeholder="auto-generated if empty" + /> +
+
+ + setTag(e.target.value)} + placeholder="none" + /> +
+
+ setFirewall(e.target.checked)} + className="h-4 w-4 rounded border-input" + /> + +
+ {addNIC.isError && ( +

+ Failed to add network interface. Please try again. +

+ )} + + + + +
+
+
+ ) +} diff --git a/src/components/vms/dialogs/CreateSnapshotDialog.tsx b/src/components/vms/dialogs/CreateSnapshotDialog.tsx new file mode 100644 index 0000000..2c61d31 --- /dev/null +++ b/src/components/vms/dialogs/CreateSnapshotDialog.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useCreateSnapshot } from '@/hooks/useProxmox' +import type { ProxmoxVM } from '@/types/proxmox' + +interface CreateSnapshotDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + vm: ProxmoxVM +} + +export function CreateSnapshotDialog({ open, onOpenChange, vm }: CreateSnapshotDialogProps) { + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [vmstate, setVmstate] = useState(false) + const createSnapshot = useCreateSnapshot() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + createSnapshot.mutate( + { + node: vm.node, + vmid: vm.vmid, + config: { + name, + description: description || undefined, + vmstate, + }, + }, + { + onSuccess: () => { + onOpenChange(false) + setName('') + setDescription('') + setVmstate(false) + }, + }, + ) + } + + return ( + + + + Create Snapshot + + Create a new snapshot for {vm.name} (VMID {vm.vmid}) + + +
+
+ + setName(e.target.value)} + placeholder="before-update" + required + /> +
+
+ + setDescription(e.target.value)} + placeholder="Before applying system updates" + /> +
+
+ setVmstate(e.target.checked)} + className="h-4 w-4 rounded border-input" + /> + +
+

+ Including VM state captures the running memory, allowing rollback to the exact running + state. This requires more disk space. +

+ {createSnapshot.isError && ( +

+ Failed to create snapshot. Please try again. +

+ )} + + + + +
+
+
+ ) +} diff --git a/src/components/vms/dialogs/EditNICDialog.tsx b/src/components/vms/dialogs/EditNICDialog.tsx new file mode 100644 index 0000000..8b8fdd3 --- /dev/null +++ b/src/components/vms/dialogs/EditNICDialog.tsx @@ -0,0 +1,126 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useEditNIC } from '@/hooks/useProxmox' +import type { ProxmoxVM, ProxmoxNetwork } from '@/types/proxmox' + +interface EditNICDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + vm: ProxmoxVM + nic: ProxmoxNetwork +} + +export function EditNICDialog({ open, onOpenChange, vm, nic }: EditNICDialogProps) { + const [bridge, setBridge] = useState(nic.bridge ?? '') + const [model, setModel] = useState(nic.model) + const [tag, setTag] = useState(nic.tag != null ? String(nic.tag) : '') + const [firewall, setFirewall] = useState(nic.firewall === 1) + const editNIC = useEditNIC() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + editNIC.mutate( + { + node: vm.node, + vmid: vm.vmid, + nic: nic.name, + config: { + bridge: bridge || undefined, + model, + tag: tag ? parseInt(tag, 10) : undefined, + firewall, + }, + }, + { + onSuccess: () => { + onOpenChange(false) + }, + }, + ) + } + + return ( + + + + Edit Network Interface + + Edit {nic.name} ({nic.model}) on {vm.name} (VMID {vm.vmid}) + + +
+
+ + setBridge(e.target.value)} + placeholder="vmbr0" + required + /> +
+
+ + +
+
+ + setTag(e.target.value)} + placeholder="none" + /> +
+
+ setFirewall(e.target.checked)} + className="h-4 w-4 rounded border-input" + /> + +
+ {editNIC.isError && ( +

+ Failed to edit network interface. Please try again. +

+ )} + + + + +
+
+
+ ) +} diff --git a/src/components/vms/dialogs/MigrateDialog.tsx b/src/components/vms/dialogs/MigrateDialog.tsx new file mode 100644 index 0000000..d74f32c --- /dev/null +++ b/src/components/vms/dialogs/MigrateDialog.tsx @@ -0,0 +1,133 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { AlertTriangle } from 'lucide-react' +import { useMigrateVM, useNodes } from '@/hooks/useProxmox' +import type { ProxmoxVM } from '@/types/proxmox' + +interface MigrateDialogProps { + vm: ProxmoxVM + connectionId: string + open: boolean + onOpenChange: (open: boolean) => void +} + +export function MigrateDialog({ vm, connectionId, open, onOpenChange }: MigrateDialogProps) { + const [targetNode, setTargetNode] = useState('') + const [online, setOnline] = useState(true) + const migrateVM = useMigrateVM() + const { data: nodes } = useNodes(connectionId) + + const otherNodes = (nodes ?? []).filter((n) => n.node !== vm.node && n.status === 'online') + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!targetNode.trim()) return + + migrateVM.mutate( + { + node: vm.node, + vmid: vm.vmid, + targetNode, + online, + }, + { + onSuccess: () => { + onOpenChange(false) + setTargetNode('') + setOnline(true) + }, + }, + ) + } + + return ( + + + + Migrate VM + + Migrate {vm.name} (VMID {vm.vmid}) to another node + + +
+
+ +
+

Warning

+

+ Migration may cause temporary downtime. Online migration keeps the VM running but + requires shared storage. Offline migration requires the VM to be stopped. +

+
+
+
+ + {otherNodes.length > 0 ? ( + + ) : ( + setTargetNode(e.target.value)} + placeholder="pve2" + required + /> + )} +
+
+ setOnline(e.target.checked)} + className="h-4 w-4 rounded border-input" + /> + +
+

+ Online migration requires shared storage between nodes. If storage is not shared, the + migration will transfer disk data over the network. +

+ {migrateVM.isError && ( +

+ Failed to migrate VM. Please try again. +

+ )} + + + + +
+
+
+ ) +} diff --git a/src/components/vms/dialogs/MoveDiskDialog.tsx b/src/components/vms/dialogs/MoveDiskDialog.tsx new file mode 100644 index 0000000..22954d8 --- /dev/null +++ b/src/components/vms/dialogs/MoveDiskDialog.tsx @@ -0,0 +1,81 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useMoveDisk } from '@/hooks/useProxmox' +import type { ProxmoxVM, ProxmoxDisk } from '@/types/proxmox' + +interface MoveDiskDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + vm: ProxmoxVM + disk: ProxmoxDisk +} + +export function MoveDiskDialog({ open, onOpenChange, vm, disk }: MoveDiskDialogProps) { + const [targetStorage, setTargetStorage] = useState(disk.storage) + const moveDisk = useMoveDisk() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + moveDisk.mutate( + { + node: vm.node, + vmid: vm.vmid, + disk: disk.device, + storage: targetStorage, + }, + { + onSuccess: () => { + onOpenChange(false) + }, + }, + ) + } + + return ( + + + + Move Disk + + Move {disk.device} from {disk.storage} to a different storage + + +
+
+ + setTargetStorage(e.target.value)} + placeholder="local-lvm" + required + /> +
+ {moveDisk.isError && ( +

+ Failed to move disk. Please try again. +

+ )} + + + + +
+
+
+ ) +} diff --git a/src/components/vms/dialogs/ResizeDiskDialog.tsx b/src/components/vms/dialogs/ResizeDiskDialog.tsx new file mode 100644 index 0000000..8d76650 --- /dev/null +++ b/src/components/vms/dialogs/ResizeDiskDialog.tsx @@ -0,0 +1,95 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { useResizeDisk } from '@/hooks/useProxmox' +import type { ProxmoxVM, ProxmoxDisk } from '@/types/proxmox' + +interface ResizeDiskDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + vm: ProxmoxVM + disk: ProxmoxDisk +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +export function ResizeDiskDialog({ open, onOpenChange, vm, disk }: ResizeDiskDialogProps) { + const currentSizeGB = Math.round(disk.size / (1024 * 1024 * 1024)) + const [newSize, setNewSize] = useState(String(currentSizeGB)) + const resizeDisk = useResizeDisk() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + const sizeBytes = parseFloat(newSize) * 1024 * 1024 * 1024 + resizeDisk.mutate( + { + node: vm.node, + vmid: vm.vmid, + disk: disk.device, + size: sizeBytes, + }, + { + onSuccess: () => { + onOpenChange(false) + }, + }, + ) + } + + return ( + + + + Resize Disk + + Resize {disk.device} on {vm.name} (currently {formatBytes(disk.size)}) + + +
+
+ + setNewSize(e.target.value)} + required + /> +

+ Must be larger than current size ({currentSizeGB} GB) +

+
+ {resizeDisk.isError && ( +

+ Failed to resize disk. Please try again. +

+ )} + + + + +
+
+
+ ) +} diff --git a/src/components/vms/tabs/ConsoleTab.tsx b/src/components/vms/tabs/ConsoleTab.tsx new file mode 100644 index 0000000..aaaea1e --- /dev/null +++ b/src/components/vms/tabs/ConsoleTab.tsx @@ -0,0 +1,257 @@ +import { useState, useCallback, useRef } from 'react' +import { Button } from '@/components/ui/button' +import { + Maximize2, + Minimize2, + Monitor, + TerminalSquare, + RotateCw, + Loader2, + AlertCircle, +} from 'lucide-react' +import { VNCConsole } from '@/components/console/VNCConsole' +import { TerminalConsole } from '@/components/console/TerminalConsole' +import type { ProxmoxVM } from '@/types/proxmox' + +interface ConsoleTabProps { + vm: ProxmoxVM + connectionId: string +} + +type ConsoleStatus = 'idle' | 'connecting' | 'connected' | 'error' + +export function ConsoleTab({ vm, connectionId }: ConsoleTabProps) { + const [isFullscreen, setIsFullscreen] = useState(false) + const [status, setStatus] = useState('idle') + const [errorMessage, setErrorMessage] = useState(null) + const containerRef = useRef(null) + + const isVM = vm.type === 'qemu' + const canConnect = vm.status === 'running' + + const handleConnect = useCallback(() => { + setStatus('connecting') + setErrorMessage(null) + }, []) + + const handleError = useCallback((message: string) => { + setStatus('error') + setErrorMessage(message) + }, []) + + const handleDisconnect = useCallback(() => { + setStatus('idle') + setErrorMessage(null) + }, []) + + const handleRetry = useCallback(() => { + setStatus('idle') + setErrorMessage(null) + // Short delay then reconnect + setTimeout(() => { + setStatus('connecting') + }, 100) + }, []) + + const toggleFullscreen = useCallback(async () => { + if (!containerRef.current) return + + try { + if (!document.fullscreenElement) { + await containerRef.current.requestFullscreen() + setIsFullscreen(true) + } else { + await document.exitFullscreen() + setIsFullscreen(false) + } + } catch (err) { + console.error('Fullscreen toggle failed:', err) + } + }, []) + + const sendCtrlAltDel = useCallback(() => { + // Access the VNC RFB instance through the container + const container = containerRef.current?.querySelector('[data-vnc]') + if (container) { + // Dispatch a custom event that VNCConsole can listen for + container.dispatchEvent(new CustomEvent('vnc-ctrl-alt-del')) + } + }, []) + + return ( +
+ {/* Toolbar */} +
+
+ {isVM ? ( + + ) : ( + + )} + + {isVM ? 'VNC Console' : 'Terminal Console'} + + + ({vm.name} - {vm.type.toUpperCase()}) + +
+ +
+ {status === 'connecting' && ( +
+ + Connecting... +
+ )} + + {status === 'connected' && isVM && ( + + )} + + + + {status === 'connected' ? ( + + ) : status === 'error' ? ( + + ) : ( + + )} +
+
+ + {/* Console Area */} +
+ {/* Idle state - show placeholder */} + {status === 'idle' && ( +
+ {isVM ? ( + + ) : ( + + )} +
+

+ {isVM ? 'VNC Console' : 'Terminal Console'} +

+ {!canConnect ? ( +

+ VM must be running to access console +

+ ) : ( +

+ Click Connect to start a console session +

+ )} +
+
+ )} + + {/* Error state - show error overlay */} + {status === 'error' && ( +
+ +
+

+ Connection Failed +

+ {errorMessage && ( +

+ {errorMessage} +

+ )} + +
+
+ )} + + {/* Connecting state - show loading overlay */} + {(status === 'connecting' || status === 'connected') && ( + <> + {isVM ? ( + + ) : ( + + )} + + )} + + {status === 'connecting' && ( +
+ +
+

Connecting...

+

+ Establishing console connection to {vm.node} +

+
+
+ )} +
+
+ ) +} + +export type { ConsoleTabProps } diff --git a/src/components/vms/tabs/DisksTab.tsx b/src/components/vms/tabs/DisksTab.tsx new file mode 100644 index 0000000..89afe95 --- /dev/null +++ b/src/components/vms/tabs/DisksTab.tsx @@ -0,0 +1,217 @@ +import { useState } from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { HardDrive, Plus, Pencil, Trash, ArrowRightLeft } from 'lucide-react' +import { useDisks, useRemoveDisk } from '@/hooks/useProxmox' +import { AddDiskDialog } from '@/components/vms/dialogs/AddDiskDialog' +import { ResizeDiskDialog } from '@/components/vms/dialogs/ResizeDiskDialog' +import { MoveDiskDialog } from '@/components/vms/dialogs/MoveDiskDialog' +import type { ProxmoxVM, ProxmoxDisk } from '@/types/proxmox' + +interface DisksTabProps { + vm: ProxmoxVM + connectionId: string +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +export function DisksTab({ vm, connectionId }: DisksTabProps) { + const { data: disks, isLoading, error } = useDisks(connectionId, vm.node, vm.vmid) + const removeDisk = useRemoveDisk() + + const [addDialogOpen, setAddDialogOpen] = useState(false) + const [resizeDisk, setResizeDisk] = useState(null) + const [moveDisk, setMoveDisk] = useState(null) + const [deleteDisk, setDeleteDisk] = useState(null) + + const handleDelete = () => { + if (!deleteDisk) return + removeDisk.mutate( + { node: vm.node, vmid: vm.vmid, disk: deleteDisk.device }, + { + onSuccess: () => { + setDeleteDisk(null) + }, + }, + ) + } + + if (isLoading) { + return ( +
+

Loading disks...

+
+ ) + } + + if (error) { + return ( +
+

Failed to load disks

+
+ ) + } + + const diskList = disks ?? [] + + return ( +
+
+
+

Disks

+

+ {diskList.length} disk{diskList.length !== 1 ? 's' : ''} attached +

+
+ +
+ + + + {diskList.length === 0 ? ( +
+ +

No disks attached

+
+ ) : ( +
+ + + + + + + + + + + + + {diskList.map((disk) => ( + + + + + + + + + ))} + +
DeviceSizeStorageFormatUsageActions
{disk.device}{formatBytes(disk.size)}{disk.storage} + + {disk.format} + + + {disk.usage ?? '-'} + +
+ + + +
+
+
+ )} +
+
+ + {/* Dialogs */} + + + {resizeDisk && ( + { + if (!open) setResizeDisk(null) + }} + vm={vm} + disk={resizeDisk} + /> + )} + + {moveDisk && ( + { + if (!open) setMoveDisk(null) + }} + vm={vm} + disk={moveDisk} + /> + )} + + {/* Delete confirmation */} + { if (!open) setDeleteDisk(null) }}> + + + Remove Disk + + Are you sure you want to remove {deleteDisk?.device} from {vm.name}? This action cannot be undone. + + + + + + + + +
+ ) +} diff --git a/src/components/vms/tabs/HardwareTab.tsx b/src/components/vms/tabs/HardwareTab.tsx new file mode 100644 index 0000000..d45b895 --- /dev/null +++ b/src/components/vms/tabs/HardwareTab.tsx @@ -0,0 +1,186 @@ +import { useState } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Cpu, MemoryStick, Pencil, Save, X } from 'lucide-react' +import type { ProxmoxVM } from '@/types/proxmox' + +interface HardwareTabProps { + vm: ProxmoxVM + connectionId: string +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +export function HardwareTab({ vm }: HardwareTabProps) { + const [editOpen, setEditOpen] = useState(false) + const [formData, setFormData] = useState({ + name: vm.name, + description: vm.tags ?? '', + cores: vm.cpus.toString(), + memory: (vm.maxmem / (1024 * 1024 * 1024)).toFixed(1), + }) + + const handleSave = () => { + // TODO: Wire up to backend save mutation + setEditOpen(false) + } + + return ( +
+ + + Basic Configuration + + + +
+
+

Name

+

{vm.name}

+
+
+

VMID

+

{vm.vmid}

+
+
+

Type

+

{vm.type}

+
+
+

Tags

+

{vm.tags || 'None'}

+
+
+
+
+ + + + Compute + + +
+
+ +
+

CPU Cores

+

{vm.cpus} cores

+
+
+
+ +
+

Memory

+

+ {formatBytes(vm.maxmem)} +

+
+
+
+
+
+ + + + Boot + + +
+
+

Boot Order

+

scsi0, net0

+
+
+

BIOS

+

SeaBIOS (default)

+
+
+
+
+ + {/* Edit Dialog */} + + + + Edit Configuration + + Modify the basic hardware configuration for this VM. + + +
+
+ + setFormData({ ...formData, name: e.target.value })} + /> +
+
+ + setFormData({ ...formData, description: e.target.value })} + placeholder="e.g. production, web-server" + /> +
+
+
+ + setFormData({ ...formData, cores: e.target.value })} + /> +
+
+ + setFormData({ ...formData, memory: e.target.value })} + /> +
+
+
+ + + + +
+
+
+ ) +} diff --git a/src/components/vms/tabs/NetworkTab.tsx b/src/components/vms/tabs/NetworkTab.tsx new file mode 100644 index 0000000..f0ba49e --- /dev/null +++ b/src/components/vms/tabs/NetworkTab.tsx @@ -0,0 +1,210 @@ +import { useState } from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Network, Plus, Pencil, Trash } from 'lucide-react' +import { useNetworkInterfaces, useRemoveNIC } from '@/hooks/useProxmox' +import { AddNICDialog } from '@/components/vms/dialogs/AddNICDialog' +import { EditNICDialog } from '@/components/vms/dialogs/EditNICDialog' +import type { ProxmoxVM, ProxmoxNetwork } from '@/types/proxmox' + +interface NetworkTabProps { + vm: ProxmoxVM + connectionId: string +} + +function formatNetworkRate(bytes: number): string { + if (bytes === 0) return '0 B/s' + const k = 1024 + const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +export function NetworkTab({ vm, connectionId }: NetworkTabProps) { + const { data: interfaces, isLoading, error } = useNetworkInterfaces(connectionId, vm.node, vm.vmid) + const removeNIC = useRemoveNIC() + + const [addDialogOpen, setAddDialogOpen] = useState(false) + const [editNic, setEditNic] = useState(null) + const [deleteNic, setDeleteNic] = useState(null) + + const handleDelete = () => { + if (!deleteNic) return + removeNIC.mutate( + { node: vm.node, vmid: vm.vmid, nic: deleteNic.name }, + { + onSuccess: () => { + setDeleteNic(null) + }, + }, + ) + } + + if (isLoading) { + return ( +
+

Loading network interfaces...

+
+ ) + } + + if (error) { + return ( +
+

Failed to load network interfaces

+
+ ) + } + + const nicList = interfaces ?? [] + + return ( +
+
+
+

Network Interfaces

+

+ {nicList.length} NIC{nicList.length !== 1 ? 's' : ''} attached + + I/O: {formatNetworkRate(vm.netin)} in / {formatNetworkRate(vm.netout)} out + +

+
+ +
+ + + + {nicList.length === 0 ? ( +
+ +

No network interfaces attached

+
+ ) : ( +
+ + + + + + + + + + + + + + {nicList.map((nic) => ( + + + + + + + + + + ))} + +
DeviceModelMAC AddressBridgeVLANFirewallActions
{nic.name} + + {nic.model} + + {nic.macaddr}{nic.bridge ?? '-'} + {nic.tag != null ? ( + {nic.tag} + ) : ( + '-' + )} + + {nic.firewall === 1 ? ( + + ) : ( + + )} + +
+ + +
+
+
+ )} +
+
+ + {/* Dialogs */} + + + {editNic && ( + { + if (!open) setEditNic(null) + }} + vm={vm} + nic={editNic} + /> + )} + + {/* Delete confirmation */} + { if (!open) setDeleteNic(null) }}> + + + Remove Network Interface + + Are you sure you want to remove {deleteNic?.name} from {vm.name}? This action cannot be undone. + + + + + + + + +
+ ) +} diff --git a/src/components/vms/tabs/OverviewTab.tsx b/src/components/vms/tabs/OverviewTab.tsx new file mode 100644 index 0000000..a5712d5 --- /dev/null +++ b/src/components/vms/tabs/OverviewTab.tsx @@ -0,0 +1,184 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Cpu, MemoryStick, HardDrive, Clock, Server, Globe, Tag } from 'lucide-react' +import type { ProxmoxVM } from '@/types/proxmox' + +interface OverviewTabProps { + vm: ProxmoxVM + connectionId: string +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function formatUptime(seconds: number): string { + if (seconds === 0) return 'N/A' + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const parts: string[] = [] + if (days > 0) parts.push(`${days}d`) + if (hours > 0) parts.push(`${hours}h`) + if (minutes > 0) parts.push(`${minutes}m`) + return parts.join(' ') || '< 1m' +} + +function formatNetworkRate(bytes: number): string { + if (bytes === 0) return '0 B/s' + const k = 1024 + const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` +} + +function ResourceBar({ label, used, total, icon: Icon }: { + label: string + used: number + total: number + icon: React.ComponentType<{ className?: string }> +}) { + const percent = total > 0 ? (used / total) * 100 : 0 + const displayUsed = label === 'CPU' ? used.toFixed(1) : formatBytes(used) + const displayTotal = label === 'CPU' ? `${total} cores` : formatBytes(total) + + return ( +
+
+
+ + {label} +
+ + {displayUsed} / {displayTotal} + +
+
+
90 + ? 'hsl(var(--destructive))' + : percent > 70 + ? 'hsl(38, 92%, 50%)' + : 'hsl(142, 71%, 45%)', + }} + /> +
+

+ {percent.toFixed(1)}% +

+
+ ) +} + +export function OverviewTab({ vm }: OverviewTabProps) { + return ( +
+ {/* Resource Usage */} + + + Resource Usage + + + + + + + + + {/* Quick Stats */} +
+ + + VMID + + + +
{vm.vmid}
+
+
+ + + + Type + + + +
{vm.type}
+
+
+ + + + Uptime + + + +
{formatUptime(vm.uptime)}
+
+
+ + + + Node + + + +
{vm.node}
+
+
+ + + + IP Address + + + +
192.168.1.{100 + (vm.vmid % 100)}
+

Mock data

+
+
+ + + + Network I/O + + + +
+
+ In: + {formatNetworkRate(vm.netin)} +
+
+ Out: + {formatNetworkRate(vm.netout)} +
+
+
+
+
+
+ ) +} diff --git a/src/components/vms/tabs/SnapshotsTab.tsx b/src/components/vms/tabs/SnapshotsTab.tsx new file mode 100644 index 0000000..db79a10 --- /dev/null +++ b/src/components/vms/tabs/SnapshotsTab.tsx @@ -0,0 +1,231 @@ +import { useState } from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Camera, Plus, Trash, RotateCcw } from 'lucide-react' +import { useSnapshots, useDeleteSnapshot, useRollbackSnapshot } from '@/hooks/useProxmox' +import { CreateSnapshotDialog } from '@/components/vms/dialogs/CreateSnapshotDialog' +import type { ProxmoxVM, ProxmoxSnapshot } from '@/types/proxmox' + +interface SnapshotsTabProps { + vm: ProxmoxVM + connectionId: string +} + +function formatDate(timestamp: number): string { + return new Date(timestamp * 1000).toLocaleString() +} + +export function SnapshotsTab({ vm, connectionId }: SnapshotsTabProps) { + const { data: snapshots, isLoading, error } = useSnapshots(connectionId, vm.node, vm.vmid) + const deleteSnapshot = useDeleteSnapshot() + const rollbackSnapshot = useRollbackSnapshot() + + const [createDialogOpen, setCreateDialogOpen] = useState(false) + const [deleteTarget, setDeleteTarget] = useState(null) + const [rollbackTarget, setRollbackTarget] = useState(null) + + const handleDelete = () => { + if (!deleteTarget) return + deleteSnapshot.mutate( + { node: vm.node, vmid: vm.vmid, name: deleteTarget.name }, + { + onSuccess: () => { + setDeleteTarget(null) + }, + }, + ) + } + + const handleRollback = () => { + if (!rollbackTarget) return + rollbackSnapshot.mutate( + { node: vm.node, vmid: vm.vmid, name: rollbackTarget.name }, + { + onSuccess: () => { + setRollbackTarget(null) + }, + }, + ) + } + + if (isLoading) { + return ( +
+

Loading snapshots...

+
+ ) + } + + if (error) { + return ( +
+

Failed to load snapshots

+
+ ) + } + + const snapshotList = snapshots ?? [] + + return ( +
+
+
+

Snapshots

+

+ {snapshotList.length} snapshot{snapshotList.length !== 1 ? 's' : ''} +

+
+ +
+ + + + {snapshotList.length === 0 ? ( +
+ +

No snapshots

+

Create a snapshot to save the current VM state

+
+ ) : ( +
+ + + + + + + + + + + + + {snapshotList.map((snapshot) => ( + + + + + + + + + ))} + +
NameDescriptionCreatedVM StateParentActions
{snapshot.name} + {snapshot.description || '-'} + + {formatDate(snapshot.snaptime)} + + + {snapshot.vmstate === 1 ? 'Included' : 'Not included'} + + + {snapshot.parent ?? '-'} + +
+ + +
+
+
+ )} +
+
+ + {/* Create Snapshot Dialog */} + + + {/* Delete confirmation */} + { if (!open) setDeleteTarget(null) }}> + + + Delete Snapshot + + Are you sure you want to delete snapshot "{deleteTarget?.name}"? This action cannot be undone. + + + + + + + + + + {/* Rollback confirmation */} + { if (!open) setRollbackTarget(null) }}> + + + Rollback Snapshot + + Are you sure you want to rollback {vm.name} to snapshot "{rollbackTarget?.name}"? + {vm.status === 'running' && ( + + The VM will be stopped during the rollback process. + + )} + + + + + + + + +
+ ) +} diff --git a/src/hooks/useProxmox.ts b/src/hooks/useProxmox.ts new file mode 100644 index 0000000..f26b6d2 --- /dev/null +++ b/src/hooks/useProxmox.ts @@ -0,0 +1,602 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import * as api from '@/lib/tauri' +import { useConnectionStore } from '@/stores/connectionStore' +import type { AddDiskConfig, AddNICConfig, EditNICConfig, CreateSnapshotConfig, BackupJobConfig, RestoreConfig } from '@/types/proxmox' + +// Query keys +export const queryKeys = { + nodes: (connectionId: string) => ['nodes', connectionId], + vms: (connectionId: string) => ['vms', connectionId], + storage: (connectionId: string) => ['storage', connectionId], + storageContent: (connectionId: string, storage: string) => ['storageContent', connectionId, storage], + storageDetail: (connectionId: string, node: string, storage: string) => ['storageDetail', connectionId, node, storage], + tasks: (connectionId: string) => ['tasks', connectionId], + cluster: (connectionId: string) => ['cluster', connectionId], + disks: (connectionId: string, node: string, vmid: number) => ['disks', connectionId, node, vmid], + networks: (connectionId: string, node: string, vmid: number) => ['networks', connectionId, node, vmid], + snapshots: (connectionId: string, node: string, vmid: number) => ['snapshots', connectionId, node, vmid], + vncProxy: (connectionId: string, node: string, vmid: number) => ['vncProxy', connectionId, node, vmid], + termProxy: (connectionId: string, node: string, vmid: number) => ['termProxy', connectionId, node, vmid], + backupJobs: (connectionId: string) => ['backupJobs', connectionId], + backups: (connectionId: string, storage?: string) => ['backups', connectionId, storage], +} + +// Hooks for fetching data +export const useNodes = (connectionId: string | null) => { + return useQuery({ + queryKey: queryKeys.nodes(connectionId!), + queryFn: () => api.getNodes(connectionId!), + enabled: !!connectionId, + refetchInterval: 10000, // 10 seconds + }) +} + +export const useVMs = (connectionId: string | null) => { + return useQuery({ + queryKey: queryKeys.vms(connectionId!), + queryFn: () => api.getVMs(connectionId!), + enabled: !!connectionId, + refetchInterval: 15000, // 15 seconds + }) +} + +export const useStorage = (connectionId: string | null) => { + return useQuery({ + queryKey: queryKeys.storage(connectionId!), + queryFn: () => api.getStorage(connectionId!), + enabled: !!connectionId, + refetchInterval: 30000, // 30 seconds + }) +} + +export const useStorageContent = (connectionId: string | null, storage: string | null) => { + return useQuery({ + queryKey: queryKeys.storageContent(connectionId!, storage!), + queryFn: () => api.getStorageContent(connectionId!, storage!), + enabled: !!connectionId && !!storage, + refetchInterval: 30000, + }) +} + +export const useStorageDetail = (connectionId: string | null, node: string | null, storage: string | null) => { + return useQuery({ + queryKey: queryKeys.storageDetail(connectionId!, node!, storage!), + queryFn: () => api.getStorageDetail(connectionId!, node!, storage!), + enabled: !!connectionId && !!node && !!storage, + refetchInterval: 30000, + }) +} + +export const useTasks = (connectionId: string | null) => { + return useQuery({ + queryKey: queryKeys.tasks(connectionId!), + queryFn: () => api.getTasks(connectionId!), + enabled: !!connectionId, + refetchInterval: 30000, // 30 seconds + }) +} + +export const useClusterStatus = (connectionId: string | null) => { + return useQuery({ + queryKey: queryKeys.cluster(connectionId!), + queryFn: () => api.getClusterStatus(connectionId!), + enabled: !!connectionId, + refetchInterval: 30000, // 30 seconds + }) +} + +// Mutations for VM lifecycle +export const useStartVM = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ node, vmid }: { node: string; vmid: number }) => + api.startVM(activeConnectionId!, node, vmid), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(activeConnectionId) }) + } + }, + }) +} + +export const useStopVM = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ node, vmid }: { node: string; vmid: number }) => + api.stopVM(activeConnectionId!, node, vmid), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(activeConnectionId) }) + } + }, + }) +} + +export const useShutdownVM = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ node, vmid }: { node: string; vmid: number }) => + api.shutdownVM(activeConnectionId!, node, vmid), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(activeConnectionId) }) + } + }, + }) +} + +export const useRebootVM = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ node, vmid }: { node: string; vmid: number }) => + api.rebootVM(activeConnectionId!, node, vmid), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(activeConnectionId) }) + } + }, + }) +} + +export const useSuspendVM = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ node, vmid }: { node: string; vmid: number }) => + api.suspendVM(activeConnectionId!, node, vmid), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(activeConnectionId) }) + } + }, + }) +} + +export const useResumeVM = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ node, vmid }: { node: string; vmid: number }) => + api.resumeVM(activeConnectionId!, node, vmid), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(activeConnectionId) }) + } + }, + }) +} + +// Disk management hooks +export const useDisks = (connectionId: string, node: string, vmid: number) => { + return useQuery({ + queryKey: queryKeys.disks(connectionId, node, vmid), + queryFn: () => api.getDisks(connectionId, node, vmid), + enabled: !!connectionId && !!node && vmid > 0, + }) +} + +export const useAddDisk = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + config, + }: { + node: string + vmid: number + config: AddDiskConfig + }) => api.addDisk(activeConnectionId!, node, vmid, config), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.disks(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +export const useResizeDisk = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + disk, + size, + }: { + node: string + vmid: number + disk: string + size: number + }) => api.resizeDisk(activeConnectionId!, node, vmid, disk, size), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.disks(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +export const useRemoveDisk = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + disk, + }: { + node: string + vmid: number + disk: string + }) => api.removeDisk(activeConnectionId!, node, vmid, disk), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.disks(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +export const useMoveDisk = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + disk, + storage, + }: { + node: string + vmid: number + disk: string + storage: string + }) => api.moveDisk(activeConnectionId!, node, vmid, disk, storage), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.disks(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +// Network management hooks +export const useNetworkInterfaces = (connectionId: string, node: string, vmid: number) => { + return useQuery({ + queryKey: queryKeys.networks(connectionId, node, vmid), + queryFn: () => api.getNetworkInterfaces(connectionId, node, vmid), + enabled: !!connectionId && !!node && vmid > 0, + }) +} + +export const useAddNIC = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + config, + }: { + node: string + vmid: number + config: AddNICConfig + }) => api.addNIC(activeConnectionId!, node, vmid, config), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.networks(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +export const useEditNIC = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + nic, + config, + }: { + node: string + vmid: number + nic: string + config: EditNICConfig + }) => api.editNIC(activeConnectionId!, node, vmid, nic, config), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.networks(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +export const useRemoveNIC = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + nic, + }: { + node: string + vmid: number + nic: string + }) => api.removeNIC(activeConnectionId!, node, vmid, nic), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.networks(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +// Snapshot management hooks +export const useSnapshots = (connectionId: string, node: string, vmid: number) => { + return useQuery({ + queryKey: queryKeys.snapshots(connectionId, node, vmid), + queryFn: () => api.getSnapshots(connectionId, node, vmid), + enabled: !!connectionId && !!node && vmid > 0, + }) +} + +export const useCreateSnapshot = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + config, + }: { + node: string + vmid: number + config: CreateSnapshotConfig + }) => api.createSnapshot(activeConnectionId!, node, vmid, config), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.snapshots(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +export const useDeleteSnapshot = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + name, + }: { + node: string + vmid: number + name: string + }) => api.deleteSnapshot(activeConnectionId!, node, vmid, name), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.snapshots(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +export const useRollbackSnapshot = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + name, + }: { + node: string + vmid: number + name: string + }) => api.rollbackSnapshot(activeConnectionId!, node, vmid, name), + onSuccess: (_data, variables) => { + if (activeConnectionId) { + queryClient.invalidateQueries({ + queryKey: queryKeys.snapshots(activeConnectionId, variables.node, variables.vmid), + }) + } + }, + }) +} + +// VM migration hooks +export const useMigrateVM = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ + node, + vmid, + targetNode, + online, + }: { + node: string + vmid: number + targetNode: string + online: boolean + }) => api.migrateVM(activeConnectionId!, node, vmid, targetNode, online), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(activeConnectionId) }) + } + }, + }) +} + +// Console proxy hooks +export const useVNCProxy = (connectionId: string, node: string, vmid: number) => { + return useQuery({ + queryKey: queryKeys.vncProxy(connectionId, node, vmid), + queryFn: () => api.createVNCProxy(connectionId, node, vmid), + enabled: false, // Manual trigger only + retry: false, + }) +} + +export const useTermProxy = (connectionId: string, node: string, vmid: number) => { + return useQuery({ + queryKey: queryKeys.termProxy(connectionId, node, vmid), + queryFn: () => api.createTermProxy(connectionId, node, vmid), + enabled: false, // Manual trigger only + retry: false, + }) +} + +// Backup management hooks +export const useBackupJobs = (connectionId: string | null) => { + return useQuery({ + queryKey: queryKeys.backupJobs(connectionId!), + queryFn: () => api.getBackupJobs(connectionId!), + enabled: !!connectionId, + }) +} + +export const useBackups = (connectionId: string | null, storage?: string) => { + return useQuery({ + queryKey: queryKeys.backups(connectionId!, storage), + queryFn: () => api.getBackups(connectionId!, storage), + enabled: !!connectionId, + }) +} + +export const useCreateBackupJob = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ config }: { config: BackupJobConfig }) => + api.createBackupJob(activeConnectionId!, config), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.backupJobs(activeConnectionId) }) + } + }, + }) +} + +export const useUpdateBackupJob = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ id, config }: { id: string; config: BackupJobConfig }) => + api.updateBackupJob(activeConnectionId!, id, config), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.backupJobs(activeConnectionId) }) + } + }, + }) +} + +export const useDeleteBackupJob = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ id }: { id: string }) => + api.deleteBackupJob(activeConnectionId!, id), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.backupJobs(activeConnectionId) }) + } + }, + }) +} + +export const useRunBackup = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ config }: { config: BackupJobConfig }) => + api.runBackup(activeConnectionId!, config), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.tasks(activeConnectionId) }) + } + }, + }) +} + +export const useRestoreBackup = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ volid, config }: { volid: string; config: RestoreConfig }) => + api.restoreBackup(activeConnectionId!, volid, config), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(activeConnectionId) }) + queryClient.invalidateQueries({ queryKey: queryKeys.tasks(activeConnectionId) }) + } + }, + }) +} + +export const useDeleteBackup = () => { + const queryClient = useQueryClient() + const activeConnectionId = useConnectionStore((s) => s.activeConnectionId) + + return useMutation({ + mutationFn: ({ volid }: { volid: string }) => + api.deleteBackup(activeConnectionId!, volid), + onSuccess: () => { + if (activeConnectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.backups(activeConnectionId) }) + } + }, + }) +} diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts new file mode 100644 index 0000000..afa72de --- /dev/null +++ b/src/hooks/useWebSocket.ts @@ -0,0 +1,116 @@ +import { useEffect, useCallback, useState } from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { queryKeys } from '@/hooks/useProxmox' +import * as api from '@/lib/tauri' + +interface UseWebSocketReturn { + isConnected: boolean + connect: (url: string) => Promise + disconnect: () => Promise +} + +export const useWebSocket = (connectionId: string | null): UseWebSocketReturn => { + const queryClient = useQueryClient() + const [isConnected, setIsConnected] = useState(false) + + const connect = useCallback( + async (url: string) => { + if (!connectionId) return + try { + await api.connectWebSocket(connectionId, url) + setIsConnected(true) + } catch (err) { + console.error('[useWebSocket] connect failed:', err) + setIsConnected(false) + } + }, + [connectionId], + ) + + const disconnect = useCallback(async () => { + if (!connectionId) return + try { + await api.disconnectWebSocket(connectionId) + } finally { + setIsConnected(false) + } + }, [connectionId]) + + // Listen for Tauri WebSocket events and invalidate relevant queries + useEffect(() => { + if (!connectionId) return + + let unlistenFns: Array<() => void> = [] + + const setupListeners = async () => { + const { listen } = await import('@tauri-apps/api/event') + + const unlistenTask = await listen('task-update', (event: { payload: { connection_id: string } }) => { + if (event.payload.connection_id === connectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.tasks(connectionId) }) + queryClient.invalidateQueries({ queryKey: queryKeys.vms(connectionId) }) + } + }) + unlistenFns.push(unlistenTask) + + const unlistenNode = await listen('node-status-change', (event: { payload: { connection_id: string } }) => { + if (event.payload.connection_id === connectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.nodes(connectionId) }) + queryClient.invalidateQueries({ queryKey: queryKeys.cluster(connectionId) }) + } + }) + unlistenFns.push(unlistenNode) + + const unlistenVM = await listen('vm-status-change', (event: { payload: { connection_id: string } }) => { + if (event.payload.connection_id === connectionId) { + queryClient.invalidateQueries({ queryKey: queryKeys.vms(connectionId) }) + } + }) + unlistenFns.push(unlistenVM) + } + + setupListeners() + + return () => { + unlistenFns.forEach((fn) => fn()) + unlistenFns = [] + } + }, [connectionId, queryClient]) + + // Check WebSocket connection status periodically + useEffect(() => { + if (!connectionId) { + setIsConnected(false) + return + } + + let interval: ReturnType | null = null + + const checkStatus = async () => { + try { + const connected = await api.isWebSocketConnected(connectionId) + setIsConnected(connected) + } catch { + setIsConnected(false) + } + } + + checkStatus() + interval = setInterval(checkStatus, 5000) + + return () => { + if (interval) clearInterval(interval) + } + }, [connectionId]) + + // Auto-disconnect on unmount + useEffect(() => { + return () => { + if (connectionId) { + api.disconnectWebSocket(connectionId).catch(() => {}) + } + } + }, [connectionId]) + + return { isConnected, connect, disconnect } +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..0c0085e --- /dev/null +++ b/src/index.css @@ -0,0 +1,66 @@ +@import 'tailwindcss'; + +@theme { + --color-background: oklch(1 0 0); + --color-foreground: oklch(0.145 0 0); + --color-card: oklch(1 0 0); + --color-card-foreground: oklch(0.145 0 0); + --color-popover: oklch(1 0 0); + --color-popover-foreground: oklch(0.145 0 0); + --color-primary: oklch(0.205 0 0); + --color-primary-foreground: oklch(0.985 0 0); + --color-secondary: oklch(0.97 0 0); + --color-secondary-foreground: oklch(0.205 0 0); + --color-muted: oklch(0.97 0 0); + --color-muted-foreground: oklch(0.556 0 0); + --color-accent: oklch(0.97 0 0); + --color-accent-foreground: oklch(0.205 0 0); + --color-destructive: oklch(0.577 0.245 27.325); + --color-destructive-foreground: oklch(0.577 0.245 27.325); + --color-border: oklch(0.922 0 0); + --color-input: oklch(0.922 0 0); + --color-ring: oklch(0.708 0 0); + --radius: 0.625rem; +} + +.dark { + --color-background: oklch(0.145 0 0); + --color-foreground: oklch(0.985 0 0); + --color-card: oklch(0.145 0 0); + --color-card-foreground: oklch(0.985 0 0); + --color-popover: oklch(0.145 0 0); + --color-popover-foreground: oklch(0.985 0 0); + --color-primary: oklch(0.985 0 0); + --color-primary-foreground: oklch(0.205 0 0); + --color-secondary: oklch(0.269 0 0); + --color-secondary-foreground: oklch(0.985 0 0); + --color-muted: oklch(0.269 0 0); + --color-muted-foreground: oklch(0.708 0 0); + --color-accent: oklch(0.269 0 0); + --color-accent-foreground: oklch(0.985 0 0); + --color-destructive: oklch(0.577 0.245 27.325); + --color-destructive-foreground: oklch(0.577 0.245 27.325); + --color-border: oklch(0.269 0 0); + --color-input: oklch(0.269 0 0); + --color-ring: oklch(0.439 0 0); +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +@keyframes toast-slide-in { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts new file mode 100644 index 0000000..cd14496 --- /dev/null +++ b/src/lib/tauri.ts @@ -0,0 +1,521 @@ +// Tauri IPC commands interface +// These functions call into the Rust backend via Tauri's invoke mechanism + +import type { ConnectionConfig, CertificateInfo } from '@/types/connection' +import type { + ProxmoxNode, + ProxmoxVM, + ProxmoxStorage, + ProxmoxStorageContent, + ProxmoxStorageDetail, + ProxmoxTask, + ProxmoxClusterStatus, + ProxmoxDisk, + ProxmoxNetwork, + ProxmoxSnapshot, + ProxmoxBackup, + ProxmoxBackupJob, + AddDiskConfig, + AddNICConfig, + EditNICConfig, + CreateSnapshotConfig, + BackupJobConfig, + RestoreConfig, + VNCProxyResponse, + TermProxyResponse, +} from '@/types/proxmox' + +// Check if we're running in Tauri +export const isTauri = () => { + return '__TAURI__' in window +} + +// Mock responses for development without Tauri +const mockResponse = (data: T): Promise => { + return new Promise((resolve) => setTimeout(() => resolve(data), 300)) +} + +// Connection management +export const addConnection = async (config: ConnectionConfig): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('add_connection', { config }) +} + +export const removeConnection = async (id: string): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('remove_connection', { id }) +} + +export const connectToServer = async (id: string): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('connect_to_server', { id }) +} + +export const disconnectFromServer = async (id: string): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('disconnect_from_server', { id }) +} + +export const getCertificateInfo = async (url: string): Promise => { + if (!isTauri()) { + return mockResponse({ + fingerprint: 'AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90', + issuer: 'Proxmox Virtual Environment', + subject: 'pve.local', + validFrom: '2024-01-01', + validTo: '2034-01-01', + selfSigned: true, + }) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_certificate_info', { url }) +} + +export const trustCertificate = async (id: string, fingerprint: string): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('trust_certificate', { id, fingerprint }) +} + +// API calls +export const getNodes = async (connectionId: string): Promise => { + if (!isTauri()) { + return mockResponse([ + { + node: 'pve1', + status: 'online', + cpu: 0.42, + maxcpu: 8, + mem: 16_000_000_000, + maxmem: 32_000_000_000, + disk: 200_000_000_000, + maxdisk: 500_000_000_000, + uptime: 86400, + level: '', + id: 'node/pve1', + type: 'node', + }, + ]) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_nodes', { connectionId }) +} + +export const getVMs = async (connectionId: string): Promise => { + if (!isTauri()) return mockResponse([]) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_vms', { connectionId }) +} + +export const getStorage = async (connectionId: string): Promise => { + if (!isTauri()) return mockResponse([]) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_storage', { connectionId }) +} + +export const getStorageContent = async ( + connectionId: string, + storage: string, +): Promise => { + if (!isTauri()) { + return mockResponse([ + { content: 'images', ctime: Date.now() / 1000, format: 'qcow2', size: 32 * 1024 * 1024 * 1024, volid: 'local-lvm:vm-100-disk-0' }, + { content: 'images', ctime: Date.now() / 1000, format: 'raw', size: 64 * 1024 * 1024 * 1024, volid: 'local-lvm:vm-101-disk-0' }, + { content: 'backup', ctime: Date.now() / 1000 - 86400, size: 5 * 1024 * 1024 * 1024, volid: 'local:backup/vzdump-qemu-100-2024_01_01-120000.vma.zst' }, + { content: 'iso', ctime: Date.now() / 1000 - 172800, size: 4 * 1024 * 1024 * 1024, volid: 'local:iso/ubuntu-22.04-desktop-amd64.iso' }, + ]) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_storage_content', { connectionId, storage }) +} + +export const getStorageDetail = async ( + connectionId: string, + node: string, + storage: string, +): Promise => { + if (!isTauri()) { + return mockResponse({ + storage, + type: 'lvmthin', + content: 'images,rootdir', + active: 1, + enabled: 1, + shared: 0, + used: 96 * 1024 * 1024 * 1024, + total: 200 * 1024 * 1024 * 1024, + avail: 104 * 1024 * 1024 * 1024, + node, + }) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_storage_detail', { connectionId, node, storage }) +} + +export const getTasks = async (connectionId: string): Promise => { + if (!isTauri()) return mockResponse([]) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_tasks', { connectionId }) +} + +export const getClusterStatus = async (connectionId: string): Promise => { + if (!isTauri()) { + return mockResponse({ + type: 'cluster', + name: 'home-lab', + id: 'cluster/home-lab', + nodes: [], + }) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_cluster_status', { connectionId }) +} + +// VM lifecycle +export const startVM = async (connectionId: string, node: string, vmid: number): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('start_vm', { connectionId, node, vmid }) +} + +export const stopVM = async (connectionId: string, node: string, vmid: number): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('stop_vm', { connectionId, node, vmid }) +} + +export const shutdownVM = async (connectionId: string, node: string, vmid: number): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('shutdown_vm', { connectionId, node, vmid }) +} + +export const rebootVM = async (connectionId: string, node: string, vmid: number): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('reboot_vm', { connectionId, node, vmid }) +} + +export const suspendVM = async (connectionId: string, node: string, vmid: number): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('suspend_vm', { connectionId, node, vmid }) +} + +export const resumeVM = async (connectionId: string, node: string, vmid: number): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('resume_vm', { connectionId, node, vmid }) +} + +// Disk management +export const getDisks = async ( + connectionId: string, + node: string, + vmid: number, +): Promise => { + if (!isTauri()) { + return mockResponse([ + { device: 'scsi0', size: 32 * 1024 * 1024 * 1024, storage: 'local-lvm', format: 'qcow2', usage: 'Root' }, + { device: 'scsi1', size: 64 * 1024 * 1024 * 1024, storage: 'local-lvm', format: 'qcow2' }, + ]) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_disks', { connectionId, node, vmid }) +} + +export const addDisk = async ( + connectionId: string, + node: string, + vmid: number, + config: AddDiskConfig, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('add_disk', { connectionId, node, vmid, config }) +} + +export const resizeDisk = async ( + connectionId: string, + node: string, + vmid: number, + disk: string, + size: number, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('resize_disk', { connectionId, node, vmid, disk, size }) +} + +export const removeDisk = async ( + connectionId: string, + node: string, + vmid: number, + disk: string, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('remove_disk', { connectionId, node, vmid, disk }) +} + +export const moveDisk = async ( + connectionId: string, + node: string, + vmid: number, + disk: string, + storage: string, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('move_disk', { connectionId, node, vmid, disk, storage }) +} + +// Network management +export const getNetworkInterfaces = async ( + connectionId: string, + node: string, + vmid: number, +): Promise => { + if (!isTauri()) { + return mockResponse([ + { name: 'net0', model: 'virtio', macaddr: 'BC:24:11:AA:BB:CC', bridge: 'vmbr0', firewall: 1 }, + ]) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_network_interfaces', { connectionId, node, vmid }) +} + +export const addNIC = async ( + connectionId: string, + node: string, + vmid: number, + config: AddNICConfig, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('add_nic', { connectionId, node, vmid, config }) +} + +export const editNIC = async ( + connectionId: string, + node: string, + vmid: number, + nic: string, + config: EditNICConfig, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('edit_nic', { connectionId, node, vmid, nic, config }) +} + +export const removeNIC = async ( + connectionId: string, + node: string, + vmid: number, + nic: string, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('remove_nic', { connectionId, node, vmid, nic }) +} + +// Snapshot management +export const getSnapshots = async ( + connectionId: string, + node: string, + vmid: number, +): Promise => { + if (!isTauri()) return mockResponse([]) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_snapshots', { connectionId, node, vmid }) +} + +export const createSnapshot = async ( + connectionId: string, + node: string, + vmid: number, + config: CreateSnapshotConfig, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('create_snapshot', { connectionId, node, vmid, config }) +} + +export const deleteSnapshot = async ( + connectionId: string, + node: string, + vmid: number, + name: string, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('delete_snapshot', { connectionId, node, vmid, name }) +} + +export const rollbackSnapshot = async ( + connectionId: string, + node: string, + vmid: number, + name: string, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('rollback_snapshot', { connectionId, node, vmid, name }) +} + +// VM migration +export const migrateVM = async ( + connectionId: string, + node: string, + vmid: number, + targetNode: string, + online: boolean, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('migrate_vm', { connectionId, node, vmid, targetNode, online }) +} + +// Console proxy +export const createVNCProxy = async ( + connectionId: string, + node: string, + vmid: number, +): Promise => { + if (!isTauri()) { + return mockResponse({ + ticket: 'mock-vnc-ticket-' + Date.now(), + port: 6000 + (vmid % 1000), + cert: '', + }) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('create_vnc_proxy', { connectionId, node, vmid }) +} + +export const createTermProxy = async ( + connectionId: string, + node: string, + vmid: number, +): Promise => { + if (!isTauri()) { + return mockResponse({ + ticket: 'mock-term-ticket-' + Date.now(), + port: 6100 + (vmid % 1000), + }) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('create_term_proxy', { connectionId, node, vmid }) +} + +export const getWebSocketURL = async ( + connectionId: string, + node: string, +): Promise => { + if (!isTauri()) { + return mockResponse(`wss://localhost:8006`) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_websocket_url', { connectionId, node }) +} + +// WebSocket management +export const connectWebSocket = async (connectionId: string, url: string): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('connect_websocket', { connectionId, url }) +} + +export const disconnectWebSocket = async (connectionId: string): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('disconnect_websocket', { connectionId }) +} + +export const isWebSocketConnected = async (connectionId: string): Promise => { + if (!isTauri()) return mockResponse(false) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('is_websocket_connected', { connectionId }) +} + +// Backup management +export const getBackupJobs = async (connectionId: string): Promise => { + if (!isTauri()) { + return mockResponse([ + { + id: 'job-1', + store: 'local', + schedule: '0 2 * * *', + all: 1, + enabled: 1, + mode: 'snapshot', + compress: 'zstd', + }, + ]) + } + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_backup_jobs', { connectionId }) +} + +export const getBackups = async ( + connectionId: string, + storage?: string, +): Promise => { + if (!isTauri()) return mockResponse([]) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('get_backups', { connectionId, storage }) +} + +export const createBackupJob = async ( + connectionId: string, + config: BackupJobConfig, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('create_backup_job', { connectionId, config }) +} + +export const updateBackupJob = async ( + connectionId: string, + id: string, + config: BackupJobConfig, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('update_backup_job', { connectionId, id, config }) +} + +export const deleteBackupJob = async (connectionId: string, id: string): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('delete_backup_job', { connectionId, id }) +} + +export const runBackup = async ( + connectionId: string, + config: BackupJobConfig, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('run_backup', { connectionId, config }) +} + +export const restoreBackup = async ( + connectionId: string, + volid: string, + config: RestoreConfig, +): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('restore_backup', { connectionId, volid, config }) +} + +export const deleteBackup = async (connectionId: string, volid: string): Promise => { + if (!isTauri()) return mockResponse(undefined) + const { invoke } = await import('@tauri-apps/api/core') + return invoke('delete_backup', { connectionId, volid }) +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..fed2fe9 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/stores/connectionStore.ts b/src/stores/connectionStore.ts new file mode 100644 index 0000000..4dfc945 --- /dev/null +++ b/src/stores/connectionStore.ts @@ -0,0 +1,57 @@ +import { create } from 'zustand' +import type { ConnectionConfig, ConnectionStatus } from '@/types/connection' + +interface ConnectionState { + connections: ConnectionConfig[] + activeConnectionId: string | null + isLoading: boolean + error: string | null + + // Actions + addConnection: (config: ConnectionConfig) => void + removeConnection: (id: string) => void + updateConnection: (id: string, updates: Partial) => void + setActiveConnection: (id: string | null) => void + setConnectionStatus: (id: string, status: ConnectionStatus) => void + setLoading: (loading: boolean) => void + setError: (error: string | null) => void +} + +export const useConnectionStore = create((set) => ({ + connections: [], + activeConnectionId: null, + isLoading: false, + error: null, + + addConnection: (config) => + set((state) => ({ + connections: [...state.connections, config], + })), + + removeConnection: (id) => + set((state) => ({ + connections: state.connections.filter((c) => c.id !== id), + activeConnectionId: state.activeConnectionId === id ? null : state.activeConnectionId, + })), + + updateConnection: (id, updates) => + set((state) => ({ + connections: state.connections.map((c) => + c.id === id ? { ...c, ...updates } : c + ), + })), + + setActiveConnection: (id) => + set({ activeConnectionId: id }), + + setConnectionStatus: (id, status) => + set((state) => ({ + connections: state.connections.map((c) => + c.id === id ? { ...c, status } : c + ), + })), + + setLoading: (loading) => set({ isLoading: loading }), + + setError: (error) => set({ error }), +})) diff --git a/src/stores/uiStore.ts b/src/stores/uiStore.ts new file mode 100644 index 0000000..7be8433 --- /dev/null +++ b/src/stores/uiStore.ts @@ -0,0 +1,63 @@ +import { create } from 'zustand' + +type Theme = 'light' | 'dark' | 'system' + +interface UIState { + theme: Theme + sidebarCollapsed: boolean + commandPaletteOpen: boolean + + // Actions + setTheme: (theme: Theme) => void + toggleSidebar: () => void + setSidebarCollapsed: (collapsed: boolean) => void + setCommandPaletteOpen: (open: boolean) => void +} + +function applyTheme(theme: Theme) { + const root = document.documentElement + root.classList.remove('light', 'dark') + + if (theme === 'system') { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + root.classList.add(prefersDark ? 'dark' : 'light') + } else { + root.classList.add(theme) + } + + localStorage.setItem('proxmoxdesktop-theme', theme) +} + +function getInitialTheme(): Theme { + const stored = localStorage.getItem('proxmoxdesktop-theme') + if (stored === 'light' || stored === 'dark' || stored === 'system') { + return stored + } + return 'system' +} + +// Apply theme on load +const initialTheme = getInitialTheme() +applyTheme(initialTheme) + +// Listen for system theme changes +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + const currentTheme = getInitialTheme() + if (currentTheme === 'system') { + applyTheme('system') + } +}) + +export const useUIStore = create((set) => ({ + theme: initialTheme, + sidebarCollapsed: false, + commandPaletteOpen: false, + + setTheme: (theme) => { + applyTheme(theme) + set({ theme }) + }, + toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })), + setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }), + setCommandPaletteOpen: (open) => set({ commandPaletteOpen: open }), +})) diff --git a/src/types/connection.ts b/src/types/connection.ts new file mode 100644 index 0000000..2cbf5f4 --- /dev/null +++ b/src/types/connection.ts @@ -0,0 +1,45 @@ +// Connection configuration types + +export interface ConnectionConfig { + id: string + name: string + // Primary endpoint + primary: EndpointConfig + // Fallback endpoints for cluster failover + fallbacks: EndpointConfig[] + // Certificate trust + certFingerprint?: string + trusted: boolean + // Connection state + status: ConnectionStatus + // Cluster info (populated after connection) + clusterName?: string + isCluster: boolean +} + +export interface EndpointConfig { + url: string // e.g., "https://192.168.1.10:8006" + node?: string // Node name (for display) + token?: string // API token (stored in keyring, not in config) +} + +export type ConnectionStatus = + | 'disconnected' + | 'connecting' + | 'connected' + | 'failed' + | 'failover' + +export interface CertificateInfo { + fingerprint: string + issuer: string + subject: string + validFrom: string + validTo: string + selfSigned: boolean +} + +export interface ConnectionCredentials { + connectionId: string + apiToken: string +} diff --git a/src/types/novnc.d.ts b/src/types/novnc.d.ts new file mode 100644 index 0000000..b027eb0 --- /dev/null +++ b/src/types/novnc.d.ts @@ -0,0 +1,48 @@ +// Custom type declarations for @novnc/novnc v1.7.0 +// The @types/novnc package is outdated and doesn't match the modern API + +declare module '@novnc/novnc/core/rfb.js' { + export default class RFB extends EventTarget { + constructor(target: HTMLElement, url: string, options?: RFBOptions); + disconnect(): void; + sendCtrlAltDel(): void; + clipboardPasteFrom(text: string): void; + addEventListener( + type: K, + listener: (ev: RFBEventMap[K]) => void, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (ev: RFBEventMap[K]) => void, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; + } + + export interface RFBOptions { + credentials?: { password?: string; target?: string; securityType?: string }; + shared?: boolean; + repeaterID?: string; + wsProtocols?: string[]; + } + + export interface RFBEventMap { + connect: Event; + disconnect: CustomEvent<{ clean: boolean; reason: string }>; + credentialsrequired: CustomEvent<{ types: string[] }>; + clipboard: CustomEvent<{ text: string }>; + bell: Event; + focus: Event; + blur: Event; + } +} diff --git a/src/types/proxmox.ts b/src/types/proxmox.ts new file mode 100644 index 0000000..7395509 --- /dev/null +++ b/src/types/proxmox.ts @@ -0,0 +1,229 @@ +// Proxmox API types + +export interface ProxmoxNode { + node: string + status: 'online' | 'offline' + cpu: number + maxcpu: number + mem: number + maxmem: number + disk: number + maxdisk: number + uptime: number + level: string + id: string + type: 'node' +} + +export interface ProxmoxVM { + vmid: number + name: string + status: 'running' | 'stopped' | 'paused' | 'suspended' + type: 'qemu' | 'lxc' + node: string + cpu: number + cpus: number + mem: number + maxmem: number + disk: number + maxdisk: number + uptime: number + netin: number + netout: number + diskread: number + diskwrite: number + pid?: number + template?: number + lock?: string + tags?: string +} + +export interface ProxmoxStorage { + storage: string + type: string + content: string + active: number + enabled: number + shared: number + used: number + total: number + avail: number + node: string +} + +export interface ProxmoxStorageContent { + content: string + ctime: number + format?: string + size?: number + subtype?: string + volid: string +} + +export interface ProxmoxStorageDetail { + storage: string + type: string + content: string + active: number + enabled: number + shared: number + used: number + total: number + avail: number + node: string +} + +export interface ProxmoxTask { + upid: string + node: string + pid: number + pstart: number + starttime: number + endtime?: number + type: string + id: string + user: string + status?: string + exitstatus?: string +} + +export interface ProxmoxClusterStatus { + type: 'node' | 'cluster' + name: string + id: string + nodeid?: number + version?: number + local?: number + online?: number + nodes?: ClusterNode[] +} + +export interface ClusterNode { + name: string + nodeid: number + online: number + local?: number + ip?: string +} + +export interface ProxmoxSnapshot { + name: string + description: string + snaptime: number + vmstate: number + parent?: string +} + +export interface ProxmoxBackup { + volid: string + backupid: string + 'backup-type': string + 'backup-id': string + 'backup-time': number + storage: string + size: number + ctime: number +} + +export interface ProxmoxBackupJob { + id: string + store: string + schedule: string + all: number + enabled: number + node?: string + vmid?: string + compress?: string + mode?: string + quiet?: number +} + +export interface BackupJobConfig { + id?: string + storage: string + schedule: string + mode: 'snapshot' | 'stop' | 'suspend' + compression: 'zstd' | 'lz4' | 'gzip' | 'none' + all: boolean + vmid?: string + enabled: boolean + node?: string +} + +export interface RestoreConfig { + volid: string + node: string + storage: string + vmid?: number +} + +export interface ProxmoxDisk { + device: string + size: number + storage: string + format: string + usage?: string +} + +export interface AddDiskConfig { + storage: string + size: number + busType: 'scsi' | 'virtio' | 'ide' | 'sata' +} + +export interface ResizeDiskConfig { + disk: string + size: number +} + +export interface MoveDiskConfig { + disk: string + storage: string +} + +export interface ProxmoxNetwork { + name: string + model: string + macaddr: string + bridge?: string + tag?: number + firewall?: number + link_down?: number +} + +export interface AddNICConfig { + bridge: string + model: string + macaddr?: string + tag?: number + firewall?: boolean +} + +export interface EditNICConfig { + bridge?: string + model?: string + tag?: number + firewall?: boolean +} + +export interface CreateSnapshotConfig { + name: string + description?: string + vmstate?: boolean +} + +// Console proxy types +export interface VNCProxyResponse { + ticket: string + port: number + cert: string +} + +export interface TermProxyResponse { + ticket: string + port: number +} + +export interface WebSocketInfo { + url: string +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..dc04221 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ae0dd89 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import path from 'path' + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + // Tauri compatibility + clearScreen: false, + server: { + port: 1420, + strictPort: true, + }, + envPrefix: ['VITE_', 'TAURI_'], + build: { + target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13', + minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, + sourcemap: !!process.env.TAURI_DEBUG, + }, +})