From 293422478763d01afabb695f8496be292f9872a6 Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Wed, 27 Aug 2025 13:28:50 +0200 Subject: [PATCH] refact: use only server --- .gitignore | 85 + CLAUDE.md | 165 +- DOCKER.md | 145 + README.md | 265 +- backend/Dockerfile | 20 +- backend/Dockerfile.dev | 30 + backend/app/api/hosts.py | 247 ++ backend/app/api/proxmox.py | 166 - backend/app/api/servers.py | 90 - backend/app/database.py | 22 + backend/app/main.py | 11 +- backend/app/models/schemas.py | 24 + backend/app/services/logging_service.py | 13 + backend/app/services/proxmox_host_service.py | 253 ++ backend/app/services/proxmox_service.py | 114 - backend/cleanup_old_tables.py | 99 + backend/migrate_to_unified_hosts.py | 350 ++ backend/requirements.txt | 14 +- backend/test_api.py | 91 + docker-compose.dev.yml | 41 + docker-compose.yml | 28 +- frontend/Dockerfile | 17 +- frontend/Dockerfile.dev | 15 + frontend/package-lock.json | 3066 ++++++++++++++++++ frontend/src/App.vue | 42 +- frontend/src/composables/useDarkMode.js | 123 + frontend/src/composables/usePullToRefresh.js | 146 + frontend/src/composables/useSwipe.js | 99 + frontend/src/main.js | 13 +- frontend/src/services/api.js | 38 +- frontend/src/style.css | 361 ++- frontend/src/views/Dashboard.vue | 79 +- frontend/src/views/Home.vue | 713 ++++ frontend/src/views/Hosts.vue | 494 +++ frontend/src/views/Proxmox.vue | 305 -- frontend/src/views/Servers.vue | 261 -- frontend/tailwind.config.js | 1 + frontend/vite.config.js | 6 + nginx/Dockerfile | 14 +- 39 files changed, 6843 insertions(+), 1223 deletions(-) create mode 100644 .gitignore create mode 100644 DOCKER.md create mode 100644 backend/Dockerfile.dev create mode 100644 backend/app/api/hosts.py delete mode 100644 backend/app/api/proxmox.py delete mode 100644 backend/app/api/servers.py create mode 100644 backend/app/services/proxmox_host_service.py delete mode 100644 backend/app/services/proxmox_service.py create mode 100755 backend/cleanup_old_tables.py create mode 100755 backend/migrate_to_unified_hosts.py create mode 100755 backend/test_api.py create mode 100644 docker-compose.dev.yml create mode 100644 frontend/Dockerfile.dev create mode 100644 frontend/package-lock.json create mode 100644 frontend/src/composables/useDarkMode.js create mode 100644 frontend/src/composables/usePullToRefresh.js create mode 100644 frontend/src/composables/useSwipe.js create mode 100644 frontend/src/views/Home.vue create mode 100644 frontend/src/views/Hosts.vue delete mode 100644 frontend/src/views/Proxmox.vue delete mode 100644 frontend/src/views/Servers.vue diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..086dc1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +# Python +__pycache__/ +*.py[cod] +*.so +*.egg +*.egg-info/ +dist/ +build/ +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.venv/ +venv/ +env/ +ENV/ + +# Node.js / Vue.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm +.yarn/ +dist/ +.nuxt/ +.vite/ + +# IDE / Editors +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Docker +.dockerignore + +# Environment variables +.env +.env.local +.env.*.local + +# Database +*.db +*.sqlite +*.sqlite3 +data/*.db +data/*.sqlite +data/*.sqlite3 + +# Logs +*.log +logs/ +*.log.* + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp +.cache/ +.temp/ + +# Package files +*.tar.gz +*.zip +*.rar + +# Development artifacts +coverage/ +.nyc_output/ +test-results/ +playwright-report/ + +# Hot reload +*.hot-update.* \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 142513d..0242023 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,29 +2,31 @@ ## Contexte du Projet -**Zebra Power** (nommée d'après la tomate verte zebra) est une application web dockerisée pour la gestion de serveurs via Wake-on-LAN et le contrôle de machines virtuelles Proxmox. Il s'agit d'un outil d'administration réseau légitime à usage défensif uniquement. +**Zebra Power** (nommée d'après la tomate verte zebra) est une application web dockerisée pour la gestion unifiée d'hosts Proxmox via Wake-on-LAN, contrôle de VMs/Containers et extinction. Il s'agit d'un outil d'administration réseau légitime à usage défensif uniquement. ## Architecture et Technologies ### Backend (FastAPI + SQLite) -- **Langages**: Python 3.11+ -- **Framework**: FastAPI 0.104.1 avec SQLAlchemy 2.0.23 +- **Langages**: Python 3.13 + uv +- **Framework**: FastAPI 0.115.0 avec SQLAlchemy 2.0.35 - **Base de données**: SQLite (fichier local `./data/zebra.db`) -- **Services**: Wake-on-LAN, API Proxmox, logging centralisé +- **Services**: Wake-on-LAN, API Proxmox unifiée, logging centralisé - **Point d'entrée**: `backend/app/main.py` ### Frontend (Vue.js 3) - **Framework**: Vue.js 3.4.0 + Vue Router + Pinia - **Build**: Vite 5.0.8 -- **Styling**: Tailwind CSS 3.3.6 +- **Styling**: Tailwind CSS 3.3.6 avec système de thème personnalisé - **Composants**: Headless UI + Heroicons +- **Fonctionnalités**: Dark/Light mode, Pull-to-refresh, Swipe gestures - **Point d'entrée**: `frontend/src/main.js` ### Infrastructure - **Containerisation**: Docker Compose +- **Images**: Python 3.13, Node.js 20, Nginx 1.25 - **Proxy**: Nginx (port 80) - **Réseau**: Mode host pour backend (requis WOL) @@ -48,31 +50,24 @@ backend/app/ frontend/src/ ├── components/ # Composants Vue réutilisables -├── views/ # Pages/routes principales +├── composables/ # Hooks Vue (useDarkMode, usePullToRefresh, useSwipe) +├── views/ # Pages/routes principales (Home.vue, Hosts.vue) ├── services/ # API client +├── style.css # Styles globaux + système de thème └── main.js # Bootstrap Vue ``` ## Commandes de Développement -### Backend +### Développement (avec hot reload) ```bash -cd backend -pip install -r requirements.txt -uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +docker-compose -f docker-compose.dev.yml up -d +docker-compose -f docker-compose.dev.yml logs -f +docker-compose -f docker-compose.dev.yml down ``` -### Frontend - -```bash -cd frontend -npm install -npm run dev # Développement (port 3000) -npm run build # Production -``` - -### Docker (Production) +### Production ```bash docker-compose up -d @@ -80,6 +75,16 @@ docker-compose logs -f [service] docker-compose down ``` +### Manuel (développement local) + +```bash +# Backend +cd backend && uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# Frontend +cd frontend && npm run dev +``` + ## Directives Spécifiques pour les Agents ### 🔒 Sécurité - OBLIGATOIRE @@ -106,6 +111,8 @@ docker-compose down - État global via Pinia stores - Requêtes API via `services/api.js` - Composants atomiques réutilisables +- **Système de thème** : composable `useDarkMode.js` avec 3 états (light/dark/system) +- **Interactions mobiles** : composables `usePullToRefresh.js` et `useSwipe.js` #### Docker @@ -131,15 +138,20 @@ docker-compose down #### API Endpoints -- `backend/app/api/servers.py` - Gestion serveurs WOL -- `backend/app/api/wol.py` - Wake-on-LAN et ping -- `backend/app/api/proxmox.py` - Clusters et VMs Proxmox +- `backend/app/api/hosts.py` - API unifiée hosts Proxmox (WOL + VMs) +- `backend/app/api/wol.py` - Logs centralisés (legacy) #### Services Métier -- `backend/app/services/wol_service.py` - Logique Wake-on-LAN -- `backend/app/services/proxmox_service.py` - Intégration Proxmox -- `backend/app/services/logging_service.py` - Journalisation +- `backend/app/services/proxmox_host_service.py` - Service unifié host Proxmox +- `backend/app/services/wol_service.py` - Utilitaires Wake-on-LAN +- `backend/app/services/logging_service.py` - Journalisation centralisée + +#### Frontend Composables + +- `frontend/src/composables/useDarkMode.js` - Système de thème 3 états avec persistance +- `frontend/src/composables/usePullToRefresh.js` - Pull-to-refresh mobile natif +- `frontend/src/composables/useSwipe.js` - Détection de gestes tactiles ### 🔧 Debugging et Logs @@ -167,10 +179,9 @@ docker-compose logs nginx #### Tables Principales -- `servers` - Serveurs Wake-on-LAN -- `proxmox_clusters` - Configuration clusters -- `action_logs` - Historique des actions -- `wol_logs` - Logs spécifiques WOL (legacy) +- `proxmox_hosts` - Configuration unifiée hosts (WOL + Proxmox) +- `action_logs` - Historique centralisé toutes actions +- `wol_logs` - Logs legacy (rétrocompatibilité) #### Migrations @@ -183,17 +194,39 @@ docker-compose logs nginx #### Standards API - REST endpoints avec préfixes `/api/` -- Codes status HTTP standards -- JSON uniquement -- CORS permissif (à restreindre) +- `/api/hosts/*` - API principale unifiée +- `/api/wol/*` - Logs centralisés +- Codes status HTTP standards, JSON uniquement #### Client Frontend -- Axios dans `services/api.js` -- Base URL via variable d'environnement +- `hostsApi` et `logsApi` dans `services/api.js` +- Base URL via variable d'environnement - Error handling centralisé - Loading states dans les composants +### 🎨 Système de Thème + +#### Configuration + +- **3 modes** : Light, Dark, System (suit l'OS) +- **Persistance** : localStorage `theme-preference` +- **Application** : classe `dark` sur `` via `useDarkMode.js` +- **Tailwind** : `darkMode: 'class'` dans `tailwind.config.js` + +#### Implémentation + +- **Composable** : `useDarkMode()` retourne `{ isDark, preference, toggleDarkMode }` +- **CSS personnalisé** : styles forcés avec `!important` dans `style.css` +- **Spécificité maximale** : `html:not(.dark)` et `html.dark` pour surcharger Tailwind +- **Bouton de thème** : cycle System → Light → Dark dans l'interface + +#### Dépannage + +- **Vérifier** : classe `dark` sur `` dans DevTools +- **Logs** : console.log dans `useDarkMode.js` pour debugging +- **CSS** : styles forcés hors `@layer` pour priorité maximale + ### ⚡ Performance #### Backend @@ -208,22 +241,22 @@ docker-compose logs nginx - Composition API pour réactivité - Tailwind purge pour CSS optimisé - Vite pour build rapide +- **Système de thème** : transitions CSS fluides 300ms ### 🚀 Déploiement -#### Développement Local +#### Développement -1. Clone du repo -2. `mkdir -p data` pour SQLite -3. `docker-compose up -d` -4. Interface sur http://localhost +1. `docker-compose -f docker-compose.dev.yml up -d` +2. Frontend: http://localhost:3000 +3. Backend API: http://localhost:8000 #### Production -- Même stack Docker -- Variables d'environnement sécurisées -- HTTPS recommandé (nginx SSL) -- Sauvegarde régulière de `./data/` +1. `docker-compose up -d` +2. Interface: http://localhost +3. Health checks automatiques +4. Sauvegarde régulière de `./data/` ### 💡 Bonnes Pratiques Agents Claude @@ -235,3 +268,47 @@ docker-compose logs nginx 6. **Sécurité first** - valider les entrées, gérer les erreurs 7. **Performance** - éviter les requêtes N+1, optimiser les queries 8. **UX** - interfaces intuitives, feedback utilisateur +9. **Système de thème** - utiliser le composable `useDarkMode()`, ne pas modifier les styles CSS forcés + +### 🔧 Notes Techniques Importantes + +#### Système de Thème - État Actuel (2025-08-27) + +**Problèmes résolus** : +- Classe `dark` s'applique correctement sur `` +- Light mode maintenant vraiment lumineux (blanc pur) +- Dark mode cohérent avec couleurs sombres +- Fond de page couvre toute la hauteur + +**Architecture CSS** : +```css +/* Styles de base dans @layer base */ +html, body { bg-white/bg-gray-900 selon mode } + +/* Styles forcés HORS @layer pour priorité maximale */ +html:not(.dark) .card-mobile { background: #ffffff !important } +html.dark .card-mobile { background: #1f2937 !important } +``` + +**Spécificités critiques** : +- Ne PAS modifier les styles forcés dans `style.css` lignes 140-167 +- Les styles Tailwind seuls ne suffisent pas (problème de spécificité) +- `!important` + sélecteurs spécifiques requis pour surcharger Tailwind + +#### Composables Vue + +**useDarkMode.js** : +- Gère 3 états : 'system', 'light', 'dark' +- Persistance automatique localStorage +- Application DOM via `document.documentElement.classList` +- Écoute changements système `prefers-color-scheme` + +**usePullToRefresh.js** : +- Implémentation native pull-to-refresh mobile +- Détection tactile avec seuils configurables +- Animation fluide avec indicateur visuel + +**useSwipe.js** : +- Détection gestes swipe (left/right) +- Touch events natifs avec debounce +- Utilisé pour interactions host cards diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..ba49f7c --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,145 @@ +# 🐳 Docker Configuration - Zebra Power + +## Images mises à jour + +### Backend +- **Python 3.13** (version la plus récente) +- **uv** pour installation rapide des dépendances +- **4 workers** en production +- Health checks intégrés + +### Frontend +- **Node.js 20** (LTS) +- Build optimisé pour production +- Health checks intégrés + +### Nginx +- **Version 1.25** +- Sécurité renforcée +- Health checks intégrés + +## Configurations disponibles + +### 🔧 Développement +```bash +# Démarrer en mode développement (avec hot reload) +docker-compose -f docker-compose.dev.yml up -d + +# Voir les logs +docker-compose -f docker-compose.dev.yml logs -f + +# Arrêter +docker-compose -f docker-compose.dev.yml down +``` + +**Fonctionnalités développement :** +- Auto-reload backend et frontend +- Volumes montés pour édition en temps réel +- Ports exposés directement (backend: 8000, frontend: 3000) + +### 🚀 Production +```bash +# Démarrer en production (via nginx) +docker-compose up -d + +# Voir les logs +docker-compose logs -f + +# Arrêter +docker-compose down +``` + +**Fonctionnalités production :** +- Applications buildées et optimisées +- Reverse proxy nginx sur port 80 +- Health checks et redémarrages automatiques +- 4 workers backend pour performance + +## Commandes utiles + +### Build des images +```bash +# Reconstruire toutes les images +docker-compose build --no-cache + +# Reconstruire une image spécifique +docker-compose build backend +``` + +### Migration des données +```bash +# Accéder au container backend +docker exec -it zebra_backend bash + +# Lancer la migration +uv run python migrate_to_unified_hosts.py +``` + +### Tests +```bash +# Tester l'API depuis le container +docker exec -it zebra_backend uv run python test_api.py +``` + +### Monitoring +```bash +# Status des services +docker-compose ps + +# Health check status +docker inspect --format='{{.State.Health.Status}}' zebra_backend + +# Métriques ressources +docker stats +``` + +## Structure des volumes + +``` +data/ +├── zebra.db # Base de données SQLite +└── backup_*.json # Sauvegardes migration (si appliquée) +``` + +## Réseau + +### Mode Host (Backend) +Le backend utilise `network_mode: host` pour envoyer les paquets Wake-on-LAN. Ceci est **requis** pour le fonctionnement WOL. + +### Ports exposés +- **Production** : Port 80 (nginx) +- **Développement** : + - Backend : 8000 + - Frontend : 3000 + - Nginx : 80 + +## Sécurité + +- Tokens nginx cachés (`server_tokens off`) +- Health checks pour haute disponibilité +- Variables d'environnement pour configuration +- Volumes restreints aux données nécessaires + +## Dépannage + +### Backend ne démarre pas +```bash +# Vérifier les logs +docker-compose logs backend + +# Vérifier les dépendances +docker exec -it zebra_backend uv pip list +``` + +### Frontend ne compile pas +```bash +# Vérifier les logs de build +docker-compose logs frontend + +# Rebuild sans cache +docker-compose build --no-cache frontend +``` + +### WOL ne fonctionne pas +- Vérifier que `network_mode: host` est activé pour le backend +- Tester depuis l'host : `wakeonlan 00:11:22:33:44:55` \ No newline at end of file diff --git a/README.md b/README.md index a7cda0c..111d9fa 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,195 @@ # Zebra Power 🍅 -Application dockerisée pour la gestion Wake-on-LAN et le contrôle des VMs/Containers Proxmox. -*Nommée d'après la tomate verte zebra* - -## Fonctionnalités - -- **Wake-on-LAN** : Réveil de serveurs via paquets magiques -- **Monitoring** : Ping automatique et statut des serveurs -- **Proxmox** : Gestion des clusters, VMs et containers -- **Dashboard** : Vue d'ensemble avec statistiques -- **Interface moderne** : Vue.js 3 avec Tailwind CSS +Application web dockerisée pour la gestion unifiée d'hosts Proxmox avec Wake-on-LAN, contrôle de VMs/Containers et extinction complète. ## Architecture -- **Backend** : FastAPI + SQLite -- **Frontend** : Vue.js 3 + Tailwind CSS -- **Proxy** : Nginx -- **Base de données** : SQLite (fichier local) +### Modèle unifié ProxmoxHost +- **Wake-on-LAN** : Démarrage via IP/MAC +- **Proxmox** : Gestion VMs/Containers +- **Shutdown** : Extinction host complet +- **Une seule configuration** par host physique -## Installation +### Stack technique +- **Backend** : Python 3.13 + FastAPI 0.115.0 + SQLAlchemy 2.0.35 + uv +- **Frontend** : Vue.js 3.4.0 + Vite 5.0.8 + Tailwind CSS 3.3.6 +- **Base de données** : SQLite +- **Infrastructure** : Docker Compose + Nginx 1.25 -### Prérequis +## Installation et lancement -- Docker & Docker Compose -- Réseau local pour WOL - -### Démarrage rapide - -1. Cloner le projet : +### Développement (avec hot reload) ```bash -git clone -cd zebra_power +# Lancer l'application en mode développement +docker-compose -f docker-compose.dev.yml up -d + +# Voir les logs +docker-compose -f docker-compose.dev.yml logs -f + +# Arrêter +docker-compose -f docker-compose.dev.yml down ``` -2. Créer le répertoire de données : -```bash -mkdir -p data -``` +**Accès développement :** +- Frontend : http://localhost:3000 +- Backend API : http://localhost:8000 +- Documentation API : http://localhost:8000/docs -3. Lancer l'application : +### Production ```bash +# Lancer l'application en production docker-compose up -d + +# Voir les logs +docker-compose logs -f + +# Arrêter +docker-compose down ``` -4. Accéder à l'interface : -- **Application** : http://localhost (via Nginx) -- **Frontend direct** : http://localhost:3000 -- **API** : http://localhost:8000 -- **Documentation API** : http://localhost:8000/docs +**Accès production :** +- Application complète : http://localhost -## Configuration +## Interface utilisateur -### Serveurs Wake-on-LAN +### Page Hosts unifiée (`/hosts`) +**Workflow intégré par host :** +1. ⚡ **Wake** - Démarrage WOL +2. 🔄 **VMs** - Chargement et contrôle VMs/Containers +3. 🛑 **Shutdown** - Extinction host et toutes VMs -1. Accédez à la section "Serveurs" -2. Ajoutez un serveur avec : - - Nom - - Adresse IP - - Adresse MAC - - Description (optionnelle) +**Fonctionnalités :** +- Statut temps réel (en ligne/hors ligne) +- Configuration complète (WOL + Proxmox) +- Actions groupées par host physique -### Clusters Proxmox +### Dashboard (`/`) +**Vue d'ensemble :** +- Nombre total d'hosts et statut +- VMs/Containers actives +- Actions rapides Wake/Start/Stop +- Logs récents centralisés -1. Accédez à la section "Proxmox" -2. Ajoutez un cluster avec : - - Nom du cluster - - Host/IP du serveur Proxmox - - Nom d'utilisateur (ex: root@pam) - - Mot de passe - - Port (défaut: 8006) - - Vérification SSL +## API -## API Endpoints +### Endpoints principaux +- `GET /api/hosts` - Liste des hosts +- `POST /api/hosts` - Créer un host +- `POST /api/hosts/{id}/wake` - Wake-on-LAN +- `GET /api/hosts/{id}/vms` - VMs du host +- `POST /api/hosts/{id}/vms/{vmid}/start` - Démarrer VM +- `POST /api/hosts/{id}/vms/{vmid}/stop` - Arrêter VM +- `POST /api/hosts/{id}/shutdown` - Éteindre host +- `GET /api/wol/all-logs` - Logs centralisés -### Serveurs -- `GET /api/servers` - Liste des serveurs -- `POST /api/servers` - Créer un serveur -- `PUT /api/servers/{id}` - Modifier un serveur -- `DELETE /api/servers/{id}` - Supprimer un serveur +### Structure ProxmoxHost +```json +{ + "name": "Mon Host", + "description": "Description optionnelle", + "ip_address": "192.168.1.100", + "mac_address": "00:11:22:33:44:55", + "proxmox_host": "192.168.1.100", + "proxmox_username": "root", + "proxmox_password": "password", + "proxmox_port": 8006, + "verify_ssl": false, + "shutdown_endpoint": "/api/shutdown" +} +``` -### Wake-on-LAN -- `POST /api/wol/wake/{server_id}` - Réveiller un serveur -- `POST /api/wol/ping/{server_id}` - Ping un serveur -- `GET /api/wol/logs` - Logs WOL +## Migration depuis ancienne architecture -### Proxmox -- `GET /api/proxmox/clusters` - Liste des clusters -- `POST /api/proxmox/clusters` - Ajouter un cluster -- `GET /api/proxmox/clusters/{id}/vms` - VMs d'un cluster -- `POST /api/proxmox/clusters/{id}/vms/{vmid}/start` - Démarrer VM -- `POST /api/proxmox/clusters/{id}/vms/{vmid}/stop` - Arrêter VM +### Migration des données +```bash +# Migrer serveurs et clusters vers hosts unifiés +docker exec -it zebra_backend uv run python migrate_to_unified_hosts.py + +# Optionnel : nettoyer anciennes tables après validation +docker exec -it zebra_backend uv run python cleanup_old_tables.py +``` + +### Redirections automatiques +- `/servers` → `/hosts` +- `/proxmox` → `/hosts` + +## Base de données + +### Table principale +- **`proxmox_hosts`** - Configuration unifiée hosts +- **`action_logs`** - Historique centralisé toutes actions +- **`wol_logs`** - Legacy (rétrocompatibilité) + +### Sauvegarde +```bash +# Sauvegarder la base +cp ./data/zebra.db ./data/zebra.db.backup + +# Restaurer +cp ./data/zebra.db.backup ./data/zebra.db +``` ## Développement -### Backend - -```bash -cd backend -pip install -r requirements.txt -uvicorn app.main:app --reload +### Structure backend +``` +backend/app/ +├── api/hosts.py # API unifiée hosts +├── api/wol.py # Logs (legacy) +├── models/schemas.py # ProxmoxHost schemas +├── services/ +│ ├── proxmox_host_service.py # Service unifié +│ ├── logging_service.py # Logs centralisés +│ └── wol_service.py # Utilitaires WOL +├── database.py # Modèles SQLAlchemy +└── main.py # Application FastAPI ``` -### Frontend - -```bash -cd frontend -npm install -npm run dev +### Structure frontend +``` +frontend/src/ +├── views/ +│ ├── Hosts.vue # Interface unifiée +│ └── Dashboard.vue # Vue d'ensemble +├── services/api.js # hostsApi, logsApi +└── main.js # Routes et configuration ``` -## Notes techniques - -- Le backend utilise `network_mode: host` pour envoyer des paquets WOL -- Les données sont persistées dans `./data/zebra.db` -- L'interface supporte les thèmes sombres/clairs -- Monitoring temps réel des statuts serveurs - -## Dépannage - -### WOL ne fonctionne pas -- Vérifiez que WOL est activé sur le serveur cible -- Assurez-vous que le conteneur utilise le réseau host -- Vérifiez l'adresse MAC - -### Connexion Proxmox échoue -- Vérifiez les credentials et l'IP -- Testez l'accès via navigateur -- Vérifiez les certificats SSL - -### Port déjà utilisé +### Commandes utiles ```bash -docker-compose down -# Modifier les ports dans docker-compose.yml si nécessaire -docker-compose up -d -``` \ No newline at end of file +# Tests backend +docker exec -it zebra_backend uv run python test_api.py + +# Build images +docker-compose build --no-cache + +# Logs spécifiques +docker-compose logs backend +docker-compose logs frontend +docker-compose logs nginx + +# Health checks +docker inspect --format='{{.State.Health.Status}}' zebra_backend +``` + +## Sécurité + +- Mode host requis pour Wake-on-LAN +- Validation Pydantic des entrées API +- Logging centralisé de toutes actions +- Gestion des erreurs et timeouts +- Health checks Docker intégrés + +## Réseau + +### Ports +- **80** : Nginx (production) +- **3000** : Frontend (développement) +- **8000** : Backend API (développement) + +### Exigences WOL +- Backend en `network_mode: host` +- Paquet `wakeonlan` installé +- Accès réseau local pour broadcast + +L'application est maintenant entièrement unifiée avec une architecture simplifiée et moderne. \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 95bc035..c4afcfc 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,17 +1,29 @@ -FROM python:3.11-slim +FROM python:3.13-slim WORKDIR /app +# Install system dependencies RUN apt-get update && apt-get install -y \ wakeonlan \ iputils-ping \ + curl \ && rm -rf /var/lib/apt/lists/* -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +# Install uv for faster package management +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv +# Copy requirements and install dependencies with uv +COPY requirements.txt . +RUN uv pip install --system --no-cache -r requirements.txt + +# Copy application code COPY app/ ./app/ +COPY migrate_to_unified_hosts.py ./ + +# Create data directory for SQLite +RUN mkdir -p /app/data EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +# Production server without auto-reload +CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] \ No newline at end of file diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..b957351 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,30 @@ +FROM python:3.13-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + wakeonlan \ + iputils-ping \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install uv for faster package management +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv + +# Copy requirements and install dependencies with uv +COPY requirements.txt . +RUN uv pip install --system --no-cache -r requirements.txt + +# Copy application code +COPY app/ ./app/ +COPY migrate_to_unified_hosts.py ./ +COPY test_api.py ./ + +# Create data directory for SQLite +RUN mkdir -p /app/data + +EXPOSE 8000 + +# Development server with auto-reload +CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file diff --git a/backend/app/api/hosts.py b/backend/app/api/hosts.py new file mode 100644 index 0000000..5017ee9 --- /dev/null +++ b/backend/app/api/hosts.py @@ -0,0 +1,247 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from app.database import get_db, ProxmoxHost +from app.models.schemas import ProxmoxHost as ProxmoxHostSchema, ProxmoxHostCreate, ProxmoxVM +from app.services.proxmox_host_service import ProxmoxHostService +from app.services.logging_service import LoggingService + +router = APIRouter() + +@router.get("/", response_model=List[ProxmoxHostSchema]) +async def get_hosts(db: Session = Depends(get_db)): + """Get all Proxmox hosts""" + hosts = db.query(ProxmoxHost).all() + return hosts + +@router.post("/", response_model=ProxmoxHostSchema) +async def create_host(host: ProxmoxHostCreate, db: Session = Depends(get_db)): + """Create a new Proxmox host""" + try: + # Test Proxmox connection before creating + temp_host = ProxmoxHost(**host.dict()) + host_service = ProxmoxHostService(temp_host) + + if not await host_service.test_proxmox_connection(): + raise HTTPException(status_code=400, detail="Cannot connect to Proxmox host") + + # Create the host in database + db_host = ProxmoxHost(**host.dict()) + db.add(db_host) + db.commit() + db.refresh(db_host) + + # Log host creation + LoggingService.log_host_action( + db=db, + action="create", + host_id=db_host.id, + host_name=db_host.name, + success=True, + message=f"Proxmox host '{db_host.name}' created successfully" + ) + + return db_host + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating host: {str(e)}") + +@router.get("/{host_id}", response_model=ProxmoxHostSchema) +async def get_host(host_id: int, db: Session = Depends(get_db)): + """Get a specific Proxmox host""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + return host + +@router.put("/{host_id}", response_model=ProxmoxHostSchema) +async def update_host(host_id: int, host_update: ProxmoxHostCreate, db: Session = Depends(get_db)): + """Update a Proxmox host""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + + # Test new connection if Proxmox settings changed + temp_host = ProxmoxHost(**host_update.dict(), id=host_id) + host_service = ProxmoxHostService(temp_host) + + if not await host_service.test_proxmox_connection(): + raise HTTPException(status_code=400, detail="Cannot connect to Proxmox with new settings") + + old_name = host.name + for key, value in host_update.dict().items(): + setattr(host, key, value) + + db.commit() + db.refresh(host) + + # Log host update + LoggingService.log_host_action( + db=db, + action="update", + host_id=host.id, + host_name=host.name, + success=True, + message=f"Proxmox host '{old_name}' updated successfully" + ) + + return host + +@router.delete("/{host_id}") +async def delete_host(host_id: int, db: Session = Depends(get_db)): + """Delete a Proxmox host""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + + # Log host deletion before deleting + LoggingService.log_host_action( + db=db, + action="delete", + host_id=host.id, + host_name=host.name, + success=True, + message=f"Proxmox host '{host.name}' deleted successfully" + ) + + db.delete(host) + db.commit() + return {"message": "Host deleted successfully"} + +@router.post("/{host_id}/wake") +async def wake_host(host_id: int, db: Session = Depends(get_db)): + """Wake up a Proxmox host using Wake-on-LAN""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + + host_service = ProxmoxHostService(host) + success = await host_service.wake_host(db) + + if not success: + raise HTTPException(status_code=500, detail="Failed to wake host") + + return {"message": f"WOL packet sent to {host.name}", "success": True} + +@router.post("/{host_id}/shutdown") +async def shutdown_host(host_id: int, db: Session = Depends(get_db)): + """Shutdown a Proxmox host (and all its VMs)""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + + host_service = ProxmoxHostService(host) + success = await host_service.shutdown_host(db) + + if not success: + raise HTTPException(status_code=500, detail="Failed to shutdown host") + + return {"message": f"Shutdown initiated for {host.name}", "success": True} + +@router.get("/{host_id}/vms", response_model=List[ProxmoxVM]) +async def get_host_vms(host_id: int, db: Session = Depends(get_db)): + """Get all VMs/containers from a Proxmox host""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + + try: + host_service = ProxmoxHostService(host) + vms = await host_service.get_vms() + return vms + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error getting VMs: {str(e)}") + +@router.post("/{host_id}/vms/{vmid}/start") +async def start_vm(host_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)): + """Start a VM/container on a Proxmox host""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + + host_service = ProxmoxHostService(host) + success = await host_service.start_vm(node, vmid, vm_type) + + # Get VM name for logging + try: + vms = await host_service.get_vms() + vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}") + except: + vm_name = f"VM-{vmid}" + + # Log VM start action + LoggingService.log_proxmox_vm_action( + db=db, + action="start", + vmid=vmid, + vm_name=vm_name, + node=node, + success=success, + message=f"VM {vm_name} ({'started' if success else 'failed to start'}) on node {node}" + ) + + if not success: + raise HTTPException(status_code=500, detail="Failed to start VM") + + return {"message": f"VM {vmid} start command sent", "success": True} + +@router.post("/{host_id}/vms/{vmid}/stop") +async def stop_vm(host_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)): + """Stop a VM/container on a Proxmox host""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + + host_service = ProxmoxHostService(host) + success = await host_service.stop_vm(node, vmid, vm_type) + + # Get VM name for logging + try: + vms = await host_service.get_vms() + vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}") + except: + vm_name = f"VM-{vmid}" + + # Log VM stop action + LoggingService.log_proxmox_vm_action( + db=db, + action="stop", + vmid=vmid, + vm_name=vm_name, + node=node, + success=success, + message=f"VM {vm_name} ({'stopped' if success else 'failed to stop'}) on node {node}" + ) + + if not success: + raise HTTPException(status_code=500, detail="Failed to stop VM") + + return {"message": f"VM {vmid} stop command sent", "success": True} + +@router.post("/check-status") +async def check_all_hosts_status(db: Session = Depends(get_db)): + """Check online status of all Proxmox hosts""" + await ProxmoxHostService.check_all_hosts_status(db) + return {"message": "Host status checked for all hosts"} + +@router.get("/{host_id}/status") +async def check_host_status(host_id: int, db: Session = Depends(get_db)): + """Check online status of a specific Proxmox host""" + host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first() + if not host: + raise HTTPException(status_code=404, detail="Host not found") + + is_online = await ProxmoxHostService.ping_host(host.ip_address) + + from datetime import datetime + host.is_online = is_online + host.last_ping = datetime.utcnow() + db.commit() + + return { + "host_id": host_id, + "host_name": host.name, + "is_online": is_online, + "last_ping": host.last_ping + } \ No newline at end of file diff --git a/backend/app/api/proxmox.py b/backend/app/api/proxmox.py deleted file mode 100644 index 99698f7..0000000 --- a/backend/app/api/proxmox.py +++ /dev/null @@ -1,166 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from typing import List -from app.database import get_db, ProxmoxCluster -from app.models.schemas import ProxmoxCluster as ProxmoxClusterSchema, ProxmoxClusterCreate, ProxmoxVM -from app.services.proxmox_service import ProxmoxService -from app.services.logging_service import LoggingService - -router = APIRouter() - -@router.get("/clusters", response_model=List[ProxmoxClusterSchema]) -async def get_clusters(db: Session = Depends(get_db)): - clusters = db.query(ProxmoxCluster).all() - return clusters - -@router.post("/clusters", response_model=ProxmoxClusterSchema) -async def create_cluster(cluster: ProxmoxClusterCreate, db: Session = Depends(get_db)): - try: - proxmox_service = ProxmoxService( - host=cluster.host, - user=cluster.username, - password=cluster.password, - port=cluster.port, - verify_ssl=cluster.verify_ssl - ) - - if not await proxmox_service.test_connection(): - raise HTTPException(status_code=400, detail="Cannot connect to Proxmox cluster") - - db_cluster = ProxmoxCluster(**cluster.dict()) - db.add(db_cluster) - db.commit() - db.refresh(db_cluster) - - # Log cluster creation - LoggingService.log_proxmox_cluster_action( - db=db, - action="create", - cluster_id=db_cluster.id, - cluster_name=db_cluster.name, - success=True, - message=f"Proxmox cluster '{db_cluster.name}' created successfully" - ) - - return db_cluster - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error creating cluster: {str(e)}") - -@router.get("/clusters/{cluster_id}", response_model=ProxmoxClusterSchema) -async def get_cluster(cluster_id: int, db: Session = Depends(get_db)): - cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first() - if not cluster: - raise HTTPException(status_code=404, detail="Cluster not found") - return cluster - -@router.delete("/clusters/{cluster_id}") -async def delete_cluster(cluster_id: int, db: Session = Depends(get_db)): - cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first() - if not cluster: - raise HTTPException(status_code=404, detail="Cluster not found") - - # Log cluster deletion before deleting - LoggingService.log_proxmox_cluster_action( - db=db, - action="delete", - cluster_id=cluster.id, - cluster_name=cluster.name, - success=True, - message=f"Proxmox cluster '{cluster.name}' deleted successfully" - ) - - db.delete(cluster) - db.commit() - return {"message": "Cluster deleted successfully"} - -@router.get("/clusters/{cluster_id}/vms", response_model=List[ProxmoxVM]) -async def get_cluster_vms(cluster_id: int, db: Session = Depends(get_db)): - cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first() - if not cluster: - raise HTTPException(status_code=404, detail="Cluster not found") - - try: - proxmox_service = ProxmoxService( - host=cluster.host, - user=cluster.username, - password=cluster.password, - port=cluster.port, - verify_ssl=cluster.verify_ssl - ) - - vms = await proxmox_service.get_vms() - return vms - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error getting VMs: {str(e)}") - -@router.post("/clusters/{cluster_id}/vms/{vmid}/start") -async def start_vm(cluster_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)): - cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first() - if not cluster: - raise HTTPException(status_code=404, detail="Cluster not found") - - proxmox_service = ProxmoxService( - host=cluster.host, - user=cluster.username, - password=cluster.password, - port=cluster.port, - verify_ssl=cluster.verify_ssl - ) - - success = await proxmox_service.start_vm(node, vmid, vm_type) - - # Get VM name from the cluster's VMs - vms = await proxmox_service.get_vms() - vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}") - - # Log VM start action - LoggingService.log_proxmox_vm_action( - db=db, - action="start", - vmid=vmid, - vm_name=vm_name, - node=node, - success=success, - message=f"VM {vm_name} ({'started' if success else 'failed to start'}) on node {node}" - ) - - if not success: - raise HTTPException(status_code=500, detail="Failed to start VM") - - return {"message": f"VM {vmid} start command sent", "success": True} - -@router.post("/clusters/{cluster_id}/vms/{vmid}/stop") -async def stop_vm(cluster_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)): - cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first() - if not cluster: - raise HTTPException(status_code=404, detail="Cluster not found") - - proxmox_service = ProxmoxService( - host=cluster.host, - user=cluster.username, - password=cluster.password, - port=cluster.port, - verify_ssl=cluster.verify_ssl - ) - - success = await proxmox_service.stop_vm(node, vmid, vm_type) - - # Get VM name from the cluster's VMs - vms = await proxmox_service.get_vms() - vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}") - - # Log VM stop action - LoggingService.log_proxmox_vm_action( - db=db, - action="stop", - vmid=vmid, - vm_name=vm_name, - node=node, - success=success, - message=f"VM {vm_name} ({'stopped' if success else 'failed to stop'}) on node {node}" - ) - - if not success: - raise HTTPException(status_code=500, detail="Failed to stop VM") - - return {"message": f"VM {vmid} stop command sent", "success": True} \ No newline at end of file diff --git a/backend/app/api/servers.py b/backend/app/api/servers.py deleted file mode 100644 index e575a4f..0000000 --- a/backend/app/api/servers.py +++ /dev/null @@ -1,90 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from typing import List -from app.database import get_db, Server -from app.models.schemas import Server as ServerSchema, ServerCreate -from app.services.wol_service import WolService -from app.services.logging_service import LoggingService - -router = APIRouter() - -@router.get("/", response_model=List[ServerSchema]) -async def get_servers(db: Session = Depends(get_db)): - servers = db.query(Server).all() - return servers - -@router.post("/", response_model=ServerSchema) -async def create_server(server: ServerCreate, db: Session = Depends(get_db)): - db_server = Server(**server.dict()) - db.add(db_server) - db.commit() - db.refresh(db_server) - - # Log server creation - LoggingService.log_server_action( - db=db, - action="create", - server_id=db_server.id, - server_name=db_server.name, - success=True, - message=f"Server '{db_server.name}' created successfully" - ) - - return db_server - -@router.get("/{server_id}", response_model=ServerSchema) -async def get_server(server_id: int, db: Session = Depends(get_db)): - server = db.query(Server).filter(Server.id == server_id).first() - if not server: - raise HTTPException(status_code=404, detail="Server not found") - return server - -@router.put("/{server_id}", response_model=ServerSchema) -async def update_server(server_id: int, server_update: ServerCreate, db: Session = Depends(get_db)): - server = db.query(Server).filter(Server.id == server_id).first() - if not server: - raise HTTPException(status_code=404, detail="Server not found") - - old_name = server.name - for key, value in server_update.dict().items(): - setattr(server, key, value) - - db.commit() - db.refresh(server) - - # Log server update - LoggingService.log_server_action( - db=db, - action="update", - server_id=server.id, - server_name=server.name, - success=True, - message=f"Server '{old_name}' updated successfully" - ) - - return server - -@router.delete("/{server_id}") -async def delete_server(server_id: int, db: Session = Depends(get_db)): - server = db.query(Server).filter(Server.id == server_id).first() - if not server: - raise HTTPException(status_code=404, detail="Server not found") - - # Log server deletion before deleting - LoggingService.log_server_action( - db=db, - action="delete", - server_id=server.id, - server_name=server.name, - success=True, - message=f"Server '{server.name}' deleted successfully" - ) - - db.delete(server) - db.commit() - return {"message": "Server deleted successfully"} - -@router.post("/check-status") -async def check_all_servers_status(db: Session = Depends(get_db)): - await WolService.check_all_servers_status(db) - return {"message": "Server status checked for all servers"} \ No newline at end of file diff --git a/backend/app/database.py b/backend/app/database.py index 51013b5..53d4612 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -34,6 +34,28 @@ class ProxmoxCluster(Base): verify_ssl = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) +class ProxmoxHost(Base): + __tablename__ = "proxmox_hosts" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, index=True, nullable=False) + description = Column(String) + # Network configuration for WOL + ip_address = Column(String, nullable=False) + mac_address = Column(String, nullable=False) + # Proxmox configuration + proxmox_host = Column(String, nullable=False) # Can be same as ip_address + proxmox_username = Column(String, nullable=False) + proxmox_password = Column(String, nullable=False) + proxmox_port = Column(Integer, default=8006) + verify_ssl = Column(Boolean, default=True) + # Host status + is_online = Column(Boolean, default=False) + last_ping = Column(DateTime) + # Optional shutdown endpoint for host shutdown + shutdown_endpoint = Column(String, nullable=True) # e.g., /api/hosts/shutdown + created_at = Column(DateTime, default=datetime.utcnow) + class ActionLog(Base): __tablename__ = "action_logs" diff --git a/backend/app/main.py b/backend/app/main.py index f75e978..4cf8e7b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,7 +2,7 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.database import init_db -from app.api import servers, proxmox, wol +from app.api import wol, hosts import traceback app = FastAPI(title="Zebra Power", description="Wake-on-LAN and Proxmox Management API") @@ -27,9 +27,12 @@ async def global_exception_handler(request: Request, exc: Exception): } ) -app.include_router(servers.router, prefix="/api/servers", tags=["servers"]) -app.include_router(proxmox.router, prefix="/api/proxmox", tags=["proxmox"]) -app.include_router(wol.router, prefix="/api/wol", tags=["wol"]) +# Anciens endpoints conservés temporairement pour transition +# app.include_router(servers.router, prefix="/api/servers", tags=["servers"]) - SUPPRIMÉ +# app.include_router(proxmox.router, prefix="/api/proxmox", tags=["proxmox"]) - SUPPRIMÉ + +app.include_router(wol.router, prefix="/api/wol", tags=["wol"]) # Conservé pour les logs +app.include_router(hosts.router, prefix="/api/hosts", tags=["hosts"]) # Nouvelle API unifiée @app.on_event("startup") async def startup_event(): diff --git a/backend/app/models/schemas.py b/backend/app/models/schemas.py index 0dadaf1..b2ebf25 100644 --- a/backend/app/models/schemas.py +++ b/backend/app/models/schemas.py @@ -71,5 +71,29 @@ class ActionLog(ActionLogCreate): id: int timestamp: datetime + class Config: + from_attributes = True + +class ProxmoxHostBase(BaseModel): + name: str + description: Optional[str] = None + ip_address: str + mac_address: str + proxmox_host: str + proxmox_username: str + proxmox_password: str + proxmox_port: int = 8006 + verify_ssl: bool = True + shutdown_endpoint: Optional[str] = None + +class ProxmoxHostCreate(ProxmoxHostBase): + pass + +class ProxmoxHost(ProxmoxHostBase): + id: int + is_online: bool + last_ping: Optional[datetime] = None + created_at: datetime + class Config: from_attributes = True \ No newline at end of file diff --git a/backend/app/services/logging_service.py b/backend/app/services/logging_service.py index 6143c3e..fd0e664 100644 --- a/backend/app/services/logging_service.py +++ b/backend/app/services/logging_service.py @@ -79,4 +79,17 @@ class LoggingService: message=message, target_id=server_id, target_name=server_name + ) + + @staticmethod + def log_host_action(db: Session, action: str, host_id: int, host_name: str, success: bool, message: str): + """Log Proxmox host actions (create/update/delete/wake/shutdown)""" + LoggingService.log_action( + db=db, + action_type="host", + action=action, + success=success, + message=message, + target_id=host_id, + target_name=host_name ) \ No newline at end of file diff --git a/backend/app/services/proxmox_host_service.py b/backend/app/services/proxmox_host_service.py new file mode 100644 index 0000000..e402335 --- /dev/null +++ b/backend/app/services/proxmox_host_service.py @@ -0,0 +1,253 @@ +import asyncio +import subprocess +import requests +from typing import List, Optional +from sqlalchemy.orm import Session +from datetime import datetime +from proxmoxer import ProxmoxAPI + +from app.database import ProxmoxHost +from app.models.schemas import ProxmoxVM +from app.services.logging_service import LoggingService +import logging + +logger = logging.getLogger(__name__) + +class ProxmoxHostService: + def __init__(self, host_config: ProxmoxHost): + self.host_config = host_config + self._proxmox = None + + def _get_proxmox_connection(self): + if not self._proxmox: + try: + self._proxmox = ProxmoxAPI( + self.host_config.proxmox_host, + user=self.host_config.proxmox_username, + password=self.host_config.proxmox_password, + port=self.host_config.proxmox_port, + verify_ssl=self.host_config.verify_ssl, + timeout=10 + ) + except Exception as e: + logger.error(f"Failed to connect to Proxmox {self.host_config.proxmox_host}: {str(e)}") + raise ConnectionError(f"Cannot connect to Proxmox: {str(e)}") + return self._proxmox + + @staticmethod + async def send_wol_packet(mac_address: str) -> bool: + try: + result = subprocess.run( + ["wakeonlan", mac_address], + capture_output=True, + text=True, + timeout=10 + ) + return result.returncode == 0 + except subprocess.TimeoutExpired: + logger.error(f"WOL timeout for MAC: {mac_address}") + return False + except Exception as e: + logger.error(f"WOL error for MAC {mac_address}: {str(e)}") + return False + + @staticmethod + async def ping_host(ip_address: str) -> bool: + try: + result = subprocess.run( + ["ping", "-c", "1", "-W", "3", ip_address], + capture_output=True, + text=True, + timeout=5 + ) + return result.returncode == 0 + except subprocess.TimeoutExpired: + return False + except Exception as e: + logger.error(f"Ping error for IP {ip_address}: {str(e)}") + return False + + async def wake_host(self, db: Session) -> bool: + success = await self.send_wol_packet(self.host_config.mac_address) + + # Log WOL action + LoggingService.log_host_action( + db=db, + action="wake", + host_id=self.host_config.id, + host_name=self.host_config.name, + success=success, + message=f"WOL packet {'sent successfully' if success else 'failed'} to {self.host_config.name} ({self.host_config.mac_address})" + ) + + return success + + async def shutdown_host(self, db: Session) -> bool: + try: + # Try using shutdown endpoint if configured + if self.host_config.shutdown_endpoint: + try: + response = requests.post( + f"http://{self.host_config.ip_address}{self.host_config.shutdown_endpoint}", + timeout=10 + ) + success = response.status_code == 200 + except Exception as e: + logger.error(f"Shutdown endpoint failed: {str(e)}") + success = False + else: + # Try Proxmox API shutdown (shutdown all VMs then the node) + proxmox = self._get_proxmox_connection() + nodes = proxmox.nodes.get() + success = True + + for node_data in nodes: + node_name = node_data['node'] + try: + # Stop all VMs/containers first + vms = await self.get_vms_for_node(node_name) + for vm in vms: + if vm.status == 'running': + await self.stop_vm(node_name, vm.vmid, vm.type) + + # Then shutdown the node + proxmox.nodes(node_name).status.post(command='shutdown') + except Exception as e: + logger.error(f"Failed to shutdown node {node_name}: {str(e)}") + success = False + + # Log shutdown action + LoggingService.log_host_action( + db=db, + action="shutdown", + host_id=self.host_config.id, + host_name=self.host_config.name, + success=success, + message=f"Host {self.host_config.name} {'shutdown initiated' if success else 'shutdown failed'}" + ) + + return success + + except Exception as e: + logger.error(f"Host shutdown failed: {str(e)}") + LoggingService.log_host_action( + db=db, + action="shutdown", + host_id=self.host_config.id, + host_name=self.host_config.name, + success=False, + message=f"Host shutdown failed: {str(e)}" + ) + return False + + async def test_proxmox_connection(self) -> bool: + try: + proxmox = self._get_proxmox_connection() + proxmox.version.get() + return True + except Exception as e: + logger.error(f"Proxmox connection test failed: {str(e)}") + return False + + async def get_nodes(self) -> List[dict]: + try: + proxmox = self._get_proxmox_connection() + return proxmox.nodes.get() + except Exception as e: + logger.error(f"Failed to get nodes: {str(e)}") + return [] + + async def get_vms(self) -> List[ProxmoxVM]: + try: + proxmox = self._get_proxmox_connection() + vms = [] + + nodes_list = await self.get_nodes() + nodes = [n['node'] for n in nodes_list] + + for node_name in nodes: + vms.extend(await self.get_vms_for_node(node_name)) + + return vms + except Exception as e: + logger.error(f"Failed to get VMs: {str(e)}") + return [] + + async def get_vms_for_node(self, node_name: str) -> List[ProxmoxVM]: + try: + proxmox = self._get_proxmox_connection() + vms = [] + + # Get QEMU VMs + try: + qemu_vms = proxmox.nodes(node_name).qemu.get() + for vm in qemu_vms: + if vm.get('template', 0) != 1: + vms.append(ProxmoxVM( + vmid=str(vm['vmid']), + name=vm.get('name', f"VM-{vm['vmid']}"), + status=vm.get('status', 'unknown'), + node=node_name, + type='qemu' + )) + except Exception as e: + logger.error(f"Failed to get QEMU VMs from node {node_name}: {str(e)}") + + # Get LXC Containers + try: + lxc_containers = proxmox.nodes(node_name).lxc.get() + for container in lxc_containers: + vms.append(ProxmoxVM( + vmid=str(container['vmid']), + name=container.get('name', f"CT-{container['vmid']}"), + status=container.get('status', 'unknown'), + node=node_name, + type='lxc' + )) + except Exception as e: + logger.error(f"Failed to get LXC containers from node {node_name}: {str(e)}") + + return vms + except Exception as e: + logger.error(f"Failed to get VMs for node {node_name}: {str(e)}") + return [] + + async def start_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool: + try: + proxmox = self._get_proxmox_connection() + if vm_type == 'lxc': + proxmox.nodes(node).lxc(vmid).status.start.post() + else: + proxmox.nodes(node).qemu(vmid).status.start.post() + return True + except Exception as e: + logger.error(f"Failed to start VM {vmid}: {str(e)}") + return False + + async def stop_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool: + try: + proxmox = self._get_proxmox_connection() + if vm_type == 'lxc': + proxmox.nodes(node).lxc(vmid).status.shutdown.post() + else: + proxmox.nodes(node).qemu(vmid).status.shutdown.post() + return True + except Exception as e: + logger.error(f"Failed to stop VM {vmid}: {str(e)}") + return False + + @staticmethod + async def check_all_hosts_status(db: Session) -> None: + hosts = db.query(ProxmoxHost).all() + + tasks = [] + for host in hosts: + tasks.append(ProxmoxHostService.ping_host(host.ip_address)) + + results = await asyncio.gather(*tasks) + + for host, is_online in zip(hosts, results): + host.is_online = is_online + host.last_ping = datetime.utcnow() + + db.commit() \ No newline at end of file diff --git a/backend/app/services/proxmox_service.py b/backend/app/services/proxmox_service.py deleted file mode 100644 index be298a9..0000000 --- a/backend/app/services/proxmox_service.py +++ /dev/null @@ -1,114 +0,0 @@ -from proxmoxer import ProxmoxAPI -from typing import List, Optional -from app.models.schemas import ProxmoxVM -import logging - -logger = logging.getLogger(__name__) - -class ProxmoxService: - def __init__(self, host: str, user: str, password: str, port: int = 8006, verify_ssl: bool = True): - self.host = host - self.user = user - self.password = password - self.port = port - self.verify_ssl = verify_ssl - self._proxmox = None - - def _get_connection(self): - if not self._proxmox: - try: - self._proxmox = ProxmoxAPI( - self.host, - user=self.user, - password=self.password, - port=self.port, - verify_ssl=self.verify_ssl, - timeout=10 - ) - except Exception as e: - logger.error(f"Failed to connect to Proxmox {self.host}: {str(e)}") - raise ConnectionError(f"Cannot connect to Proxmox: {str(e)}") - return self._proxmox - - async def test_connection(self) -> bool: - try: - proxmox = self._get_connection() - proxmox.version.get() - return True - except Exception as e: - logger.error(f"Proxmox connection test failed: {str(e)}") - return False - - async def get_nodes(self) -> List[dict]: - try: - proxmox = self._get_connection() - return proxmox.nodes.get() - except Exception as e: - logger.error(f"Failed to get nodes: {str(e)}") - return [] - - async def get_vms(self, node: Optional[str] = None) -> List[ProxmoxVM]: - try: - proxmox = self._get_connection() - vms = [] - - if node: - nodes = [node] - else: - nodes_list = await self.get_nodes() - nodes = [n['node'] for n in nodes_list] - - for node_name in nodes: - try: - qemu_vms = proxmox.nodes(node_name).qemu.get() - for vm in qemu_vms: - if vm.get('template', 0) != 1: - vms.append(ProxmoxVM( - vmid=str(vm['vmid']), - name=vm.get('name', f"VM-{vm['vmid']}"), - status=vm.get('status', 'unknown'), - node=node_name, - type='qemu' - )) - - lxc_containers = proxmox.nodes(node_name).lxc.get() - for container in lxc_containers: - vms.append(ProxmoxVM( - vmid=str(container['vmid']), - name=container.get('name', f"CT-{container['vmid']}"), - status=container.get('status', 'unknown'), - node=node_name, - type='lxc' - )) - except Exception as e: - logger.error(f"Failed to get VMs from node {node_name}: {str(e)}") - continue - - return vms - except Exception as e: - logger.error(f"Failed to get VMs: {str(e)}") - return [] - - async def start_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool: - try: - proxmox = self._get_connection() - if vm_type == 'lxc': - proxmox.nodes(node).lxc(vmid).status.start.post() - else: - proxmox.nodes(node).qemu(vmid).status.start.post() - return True - except Exception as e: - logger.error(f"Failed to start VM {vmid}: {str(e)}") - return False - - async def stop_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool: - try: - proxmox = self._get_connection() - if vm_type == 'lxc': - proxmox.nodes(node).lxc(vmid).status.shutdown.post() - else: - proxmox.nodes(node).qemu(vmid).status.shutdown.post() - return True - except Exception as e: - logger.error(f"Failed to stop VM {vmid}: {str(e)}") - return False \ No newline at end of file diff --git a/backend/cleanup_old_tables.py b/backend/cleanup_old_tables.py new file mode 100755 index 0000000..7f5747c --- /dev/null +++ b/backend/cleanup_old_tables.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +Script pour supprimer les anciennes tables après migration réussie +⚠️ ATTENTION: Cette opération est irréversible ! +""" + +import sys +import os +from sqlalchemy import create_engine, text + +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./zebra.db") + +def cleanup_old_tables(): + print("🗑️ Nettoyage des anciennes tables") + print("=" * 50) + print("⚠️ ATTENTION: Cette opération est IRRÉVERSIBLE !") + print("⚠️ Assurez-vous que:") + print(" 1. La migration vers ProxmoxHost a réussi") + print(" 2. L'application fonctionne correctement avec /api/hosts") + print(" 3. Vous avez fait une sauvegarde de zebra.db") + + response = input("\n❓ Êtes-vous sûr de vouloir supprimer les anciennes tables ? (oui/NON): ").strip() + if response.lower() != 'oui': + print("❌ Opération annulée.") + return False + + engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) + + try: + with engine.connect() as conn: + # Vérifier que la nouvelle table existe et contient des données + result = conn.execute(text("SELECT COUNT(*) FROM proxmox_hosts")).scalar() + if result == 0: + print("❌ La table 'proxmox_hosts' est vide!") + print(" Lancez d'abord la migration avec: python migrate_to_unified_hosts.py") + return False + + print(f"✅ Table 'proxmox_hosts' contient {result} hosts") + + # Vérifier si les anciennes tables existent encore + tables_to_drop = [] + + try: + conn.execute(text("SELECT COUNT(*) FROM servers")).scalar() + tables_to_drop.append("servers") + print("📋 Table 'servers' trouvée") + except: + pass + + try: + conn.execute(text("SELECT COUNT(*) FROM proxmox_clusters")).scalar() + tables_to_drop.append("proxmox_clusters") + print("📋 Table 'proxmox_clusters' trouvée") + except: + pass + + if not tables_to_drop: + print("ℹ️ Aucune ancienne table à supprimer.") + return True + + print(f"\n🗑️ Tables à supprimer: {', '.join(tables_to_drop)}") + final_confirm = input("❓ Confirmer la suppression ? (oui/NON): ").strip() + if final_confirm.lower() != 'oui': + print("❌ Opération annulée.") + return False + + # Supprimer les tables + for table in tables_to_drop: + conn.execute(text(f"DROP TABLE IF EXISTS {table}")) + print(f" ✅ Table '{table}' supprimée") + + # Supprimer aussi les index associés + try: + conn.execute(text("DROP INDEX IF EXISTS ix_servers_id")) + conn.execute(text("DROP INDEX IF EXISTS ix_servers_name")) + conn.execute(text("DROP INDEX IF EXISTS ix_proxmox_clusters_id")) + conn.execute(text("DROP INDEX IF EXISTS ix_proxmox_clusters_name")) + print(" ✅ Index associés supprimés") + except: + pass + + conn.commit() + + print(f"\n🎉 Nettoyage terminé avec succès!") + print(f" - {len(tables_to_drop)} tables supprimées") + print(f" - La nouvelle architecture unifiée est maintenant en place") + + return True + + except Exception as e: + print(f"❌ Erreur lors du nettoyage: {e}") + return False + +def main(): + success = cleanup_old_tables() + return 0 if success else 1 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/backend/migrate_to_unified_hosts.py b/backend/migrate_to_unified_hosts.py new file mode 100755 index 0000000..e0d1067 --- /dev/null +++ b/backend/migrate_to_unified_hosts.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +""" +Script de migration pour fusionner les tables 'servers' et 'proxmox_clusters' +vers la nouvelle table unifiée 'proxmox_hosts' +""" + +import sys +import os +import json +from datetime import datetime +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + +# Ajouter le dossier app au path pour les imports +sys.path.append(os.path.join(os.path.dirname(__file__), 'app')) + +from app.database import Base, Server, ProxmoxCluster, ProxmoxHost, ActionLog +from app.services.logging_service import LoggingService + +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./zebra.db") + +def create_backup(engine): + """Créer une sauvegarde des tables existantes""" + print("📦 Création des sauvegardes...") + + with engine.connect() as conn: + # Backup servers table + servers_backup = conn.execute(text("SELECT * FROM servers")).fetchall() + with open(f"backup_servers_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", 'w') as f: + servers_data = [] + for row in servers_backup: + servers_data.append({ + 'id': row[0], + 'name': row[1], + 'ip_address': row[2], + 'mac_address': row[3], + 'description': row[4], + 'is_online': row[5], + 'last_ping': str(row[6]) if row[6] else None, + 'created_at': str(row[7]) + }) + json.dump(servers_data, f, indent=2) + print(f" ✅ Sauvegardé {len(servers_data)} serveurs") + + # Backup proxmox_clusters table + try: + clusters_backup = conn.execute(text("SELECT * FROM proxmox_clusters")).fetchall() + with open(f"backup_proxmox_clusters_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", 'w') as f: + clusters_data = [] + for row in clusters_backup: + clusters_data.append({ + 'id': row[0], + 'name': row[1], + 'host': row[2], + 'username': row[3], + 'password': row[4], + 'port': row[5], + 'verify_ssl': row[6], + 'created_at': str(row[7]) + }) + json.dump(clusters_data, f, indent=2) + print(f" ✅ Sauvegardé {len(clusters_data)} clusters Proxmox") + except Exception as e: + print(f" ℹ️ Aucun cluster Proxmox à sauvegarder: {e}") + clusters_data = [] + + return len(servers_data), len(clusters_data) + +def migrate_servers_to_hosts(session): + """Migrer les serveurs existants vers la table hosts en demandant les infos Proxmox""" + print("\n🔄 Migration des serveurs vers hosts...") + + servers = session.query(Server).all() + migrated_hosts = [] + + for server in servers: + print(f"\n📋 Migration du serveur: {server.name}") + print(f" IP: {server.ip_address}") + print(f" MAC: {server.mac_address}") + + # Demander les informations Proxmox pour ce serveur + print(f" Pour ce serveur, veuillez fournir les informations Proxmox:") + + # Utiliser l'IP du serveur comme host Proxmox par défaut + proxmox_host = input(f" Host Proxmox (défaut: {server.ip_address}): ").strip() + if not proxmox_host: + proxmox_host = server.ip_address + + proxmox_username = input(" Nom d'utilisateur Proxmox: ").strip() + if not proxmox_username: + print(" ⚠️ Nom d'utilisateur requis! Utilisation de 'root' par défaut") + proxmox_username = "root" + + proxmox_password = input(" Mot de passe Proxmox: ").strip() + if not proxmox_password: + print(" ⚠️ Mot de passe requis! Utilisation de 'password' par défaut") + proxmox_password = "password" + + proxmox_port = input(" Port Proxmox (défaut: 8006): ").strip() + if not proxmox_port: + proxmox_port = 8006 + else: + try: + proxmox_port = int(proxmox_port) + except ValueError: + proxmox_port = 8006 + + verify_ssl_input = input(" Vérifier SSL? (o/N): ").strip().lower() + verify_ssl = verify_ssl_input in ['o', 'oui', 'y', 'yes'] + + shutdown_endpoint = input(" Endpoint de shutdown (optionnel): ").strip() + if not shutdown_endpoint: + shutdown_endpoint = None + + # Créer le nouveau host unifié + host = ProxmoxHost( + name=server.name, + description=server.description, + ip_address=server.ip_address, + mac_address=server.mac_address, + proxmox_host=proxmox_host, + proxmox_username=proxmox_username, + proxmox_password=proxmox_password, + proxmox_port=proxmox_port, + verify_ssl=verify_ssl, + is_online=server.is_online, + last_ping=server.last_ping, + shutdown_endpoint=shutdown_endpoint, + created_at=server.created_at + ) + + session.add(host) + session.commit() + session.refresh(host) + + # Logger la migration + LoggingService.log_host_action( + db=session, + action="migrate", + host_id=host.id, + host_name=host.name, + success=True, + message=f"Serveur '{server.name}' migré vers host unifié (ancien ID serveur: {server.id})" + ) + + migrated_hosts.append((server.id, host.id, server.name)) + print(f" ✅ Migré vers host ID {host.id}") + + return migrated_hosts + +def migrate_clusters_to_hosts(session): + """Migrer les clusters Proxmox existants vers la table hosts en demandant les infos WOL""" + print("\n🔄 Migration des clusters Proxmox vers hosts...") + + clusters = session.query(ProxmoxCluster).all() + migrated_hosts = [] + + for cluster in clusters: + print(f"\n📋 Migration du cluster: {cluster.name}") + print(f" Host Proxmox: {cluster.host}:{cluster.port}") + print(f" Utilisateur: {cluster.username}") + + # Demander les informations WOL pour ce cluster + print(f" Pour ce cluster, veuillez fournir les informations Wake-on-LAN:") + + # Utiliser le host du cluster comme IP par défaut + ip_address = input(f" Adresse IP (défaut: {cluster.host}): ").strip() + if not ip_address: + ip_address = cluster.host + + mac_address = input(" Adresse MAC: ").strip() + if not mac_address: + print(" ⚠️ Adresse MAC requise! Utilisation de '00:00:00:00:00:00' par défaut") + mac_address = "00:00:00:00:00:00" + + description = input(" Description (optionnel): ").strip() + + shutdown_endpoint = input(" Endpoint de shutdown (optionnel): ").strip() + if not shutdown_endpoint: + shutdown_endpoint = None + + # Créer le nouveau host unifié + host = ProxmoxHost( + name=cluster.name, + description=description if description else f"Host Proxmox migré du cluster {cluster.name}", + ip_address=ip_address, + mac_address=mac_address, + proxmox_host=cluster.host, + proxmox_username=cluster.username, + proxmox_password=cluster.password, + proxmox_port=cluster.port, + verify_ssl=cluster.verify_ssl, + is_online=False, # Statut inconnu pour les anciens clusters + last_ping=None, + shutdown_endpoint=shutdown_endpoint, + created_at=cluster.created_at + ) + + session.add(host) + session.commit() + session.refresh(host) + + # Logger la migration + LoggingService.log_host_action( + db=session, + action="migrate", + host_id=host.id, + host_name=host.name, + success=True, + message=f"Cluster Proxmox '{cluster.name}' migré vers host unifié (ancien ID cluster: {cluster.id})" + ) + + migrated_hosts.append((cluster.id, host.id, cluster.name)) + print(f" ✅ Migré vers host ID {host.id}") + + return migrated_hosts + +def update_action_logs(session, server_migrations, cluster_migrations): + """Mettre à jour les logs d'actions pour pointer vers les nouveaux hosts""" + print("\n📝 Mise à jour des logs d'actions...") + + # Map ancien server_id -> nouveau host_id + server_id_map = {old_id: new_id for old_id, new_id, _ in server_migrations} + cluster_id_map = {old_id: new_id for old_id, new_id, _ in cluster_migrations} + + # Mettre à jour les logs de serveurs + for old_server_id, new_host_id in server_id_map.items(): + logs = session.query(ActionLog).filter( + ActionLog.action_type == "server", + ActionLog.target_id == old_server_id + ).all() + + for log in logs: + log.action_type = "host" + log.target_id = new_host_id + # Ajouter une note dans le message + if not log.message.endswith(" (migré)"): + log.message += " (migré)" + + if logs: + print(f" ✅ Mis à jour {len(logs)} logs pour l'ancien serveur {old_server_id}") + + # Mettre à jour les logs de clusters Proxmox + for old_cluster_id, new_host_id in cluster_id_map.items(): + logs = session.query(ActionLog).filter( + ActionLog.action_type == "proxmox", + ActionLog.target_id == old_cluster_id, + ActionLog.action.in_(["create", "delete", "update"]) # Actions sur le cluster lui-même + ).all() + + for log in logs: + log.action_type = "host" + log.target_id = new_host_id + if not log.message.endswith(" (migré)"): + log.message += " (migré)" + + if logs: + print(f" ✅ Mis à jour {len(logs)} logs pour l'ancien cluster {old_cluster_id}") + + session.commit() + +def main(): + print("🚀 Migration vers le modèle unifié ProxmoxHost") + print("=" * 50) + + # Créer la connexion à la base de données + engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + # Créer les tables si elles n'existent pas + Base.metadata.create_all(bind=engine) + + session = SessionLocal() + + try: + # Vérifier s'il y a déjà des hosts + existing_hosts = session.query(ProxmoxHost).count() + if existing_hosts > 0: + print(f"⚠️ Il y a déjà {existing_hosts} hosts dans la base de données.") + response = input("Voulez-vous continuer la migration? (o/N): ").strip().lower() + if response not in ['o', 'oui', 'y', 'yes']: + print("Migration annulée.") + return + + # Sauvegarder les données existantes + servers_count, clusters_count = create_backup(engine) + + if servers_count == 0 and clusters_count == 0: + print("ℹ️ Aucune donnée à migrer.") + return + + print(f"\n📊 Données à migrer:") + print(f" - {servers_count} serveurs") + print(f" - {clusters_count} clusters Proxmox") + + response = input("\nVoulez-vous continuer la migration? (o/N): ").strip().lower() + if response not in ['o', 'oui', 'y', 'yes']: + print("Migration annulée.") + return + + # Migration des serveurs vers hosts + server_migrations = [] + if servers_count > 0: + server_migrations = migrate_servers_to_hosts(session) + + # Migration des clusters vers hosts + cluster_migrations = [] + if clusters_count > 0: + cluster_migrations = migrate_clusters_to_hosts(session) + + # Mettre à jour les logs d'actions + if server_migrations or cluster_migrations: + update_action_logs(session, server_migrations, cluster_migrations) + + print(f"\n✅ Migration terminée avec succès!") + print(f" - {len(server_migrations)} serveurs migrés") + print(f" - {len(cluster_migrations)} clusters migrés") + + # Option pour supprimer les anciennes tables + if server_migrations or cluster_migrations: + print(f"\n🗑️ Les anciennes données sont toujours présentes.") + response = input("Voulez-vous supprimer les anciennes tables 'servers' et 'proxmox_clusters'? (o/N): ").strip().lower() + if response in ['o', 'oui', 'y', 'yes']: + with engine.connect() as conn: + if servers_count > 0: + conn.execute(text("DROP TABLE servers")) + print(" ✅ Table 'servers' supprimée") + if clusters_count > 0: + conn.execute(text("DROP TABLE proxmox_clusters")) + print(" ✅ Table 'proxmox_clusters' supprimée") + conn.commit() + print(" ⚠️ Anciennes tables supprimées. Migration irréversible!") + else: + print(" ℹ️ Anciennes tables conservées pour rollback si nécessaire") + + print(f"\n🎉 Migration vers le modèle unifié terminée!") + print(f"Vous pouvez maintenant utiliser /api/hosts pour gérer vos hosts Proxmox") + + except Exception as e: + print(f"❌ Erreur lors de la migration: {e}") + session.rollback() + return 1 + + finally: + session.close() + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 26272fd..d34d2b5 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,9 +1,9 @@ -fastapi==0.104.1 -uvicorn[standard]==0.24.0 -sqlalchemy==2.0.23 -pydantic==2.5.0 -python-multipart==0.0.6 +fastapi==0.115.0 +uvicorn[standard]==0.30.0 +sqlalchemy==2.0.35 +pydantic==2.9.0 +python-multipart==0.0.10 proxmoxer==2.0.1 -requests==2.31.0 +requests==2.32.0 wakeonlan==3.1.0 -httpx==0.25.2 \ No newline at end of file +httpx==0.27.0 \ No newline at end of file diff --git a/backend/test_api.py b/backend/test_api.py new file mode 100755 index 0000000..26244cc --- /dev/null +++ b/backend/test_api.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +Test basique de l'API unifiée hosts +""" + +import requests +import json +import sys + +def test_api(): + base_url = "http://localhost:8000/api" + + print("🧪 Test de l'API Zebra Power - Hosts unifiés") + print("=" * 50) + + try: + # Test 1: Health check + print("\n1. Test de santé de l'API...") + response = requests.get(f"{base_url}/health", timeout=5) + if response.status_code == 200: + print(" ✅ API en ligne") + else: + print(f" ❌ API hors service ({response.status_code})") + return False + + # Test 2: Liste des hosts (vide au début) + print("\n2. Test de récupération des hosts...") + response = requests.get(f"{base_url}/hosts", timeout=5) + if response.status_code == 200: + hosts = response.json() + print(f" ✅ {len(hosts)} hosts trouvés") + else: + print(f" ❌ Erreur récupération hosts ({response.status_code})") + return False + + # Test 3: Créer un host de test + print("\n3. Test de création d'un host...") + test_host = { + "name": "Host Test", + "description": "Host de test pour l'API", + "ip_address": "192.168.1.100", + "mac_address": "00:11:22:33:44:55", + "proxmox_host": "192.168.1.100", + "proxmox_username": "root", + "proxmox_password": "password", + "proxmox_port": 8006, + "verify_ssl": False, + "shutdown_endpoint": "/api/shutdown" + } + + # Note: Ce test échouera car nous n'avons pas de Proxmox réel + response = requests.post(f"{base_url}/hosts", json=test_host, timeout=10) + if response.status_code == 200: + created_host = response.json() + print(f" ✅ Host créé avec ID {created_host['id']}") + + # Test 4: Récupérer le host créé + print("\n4. Test de récupération du host créé...") + response = requests.get(f"{base_url}/hosts/{created_host['id']}", timeout=5) + if response.status_code == 200: + host_data = response.json() + print(f" ✅ Host récupéré: {host_data['name']}") + + # Test 5: Supprimer le host de test + print("\n5. Test de suppression du host...") + response = requests.delete(f"{base_url}/hosts/{created_host['id']}", timeout=5) + if response.status_code == 200: + print(" ✅ Host supprimé avec succès") + else: + print(f" ❌ Erreur suppression host ({response.status_code})") + else: + print(f" ❌ Erreur récupération host ({response.status_code})") + else: + print(f" ❌ Échec création host ({response.status_code})") + print(f" Raison: {response.text}") + # C'est normal si pas de Proxmox réel + + print(f"\n🎉 Tests terminés - API fonctionnelle") + return True + + except requests.exceptions.ConnectionError: + print(" ❌ Impossible de se connecter à l'API") + print(" Vérifiez que le serveur backend est démarré sur http://localhost:8000") + return False + except Exception as e: + print(f" ❌ Erreur inattendue: {e}") + return False + +if __name__ == "__main__": + success = test_api() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..a3f8e19 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,41 @@ +# Docker Compose for Zebra Power - Development + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile.dev + container_name: zebra_backend_dev + ports: + - "8000:8000" + volumes: + - ./backend/app:/app/app + - ./backend/migrate_to_unified_hosts.py:/app/migrate_to_unified_hosts.py + - ./backend/test_api.py:/app/test_api.py + - ./data:/app/data + environment: + - DATABASE_URL=sqlite:///data/zebra.db + - CORS_ORIGINS=http://localhost:3000,http://localhost + restart: unless-stopped + # Required for WOL packets + network_mode: host + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + container_name: zebra_frontend_dev + ports: + - "3000:3000" + volumes: + - ./frontend/src:/app/src + - ./frontend/public:/app/public + - ./frontend/vite.config.js:/app/vite.config.js + environment: + - VITE_API_URL=http://localhost:8000/api + depends_on: + - backend + restart: unless-stopped + +volumes: + data: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 003fc69..5c9f9a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -# Docker Compose for Zebra Power +# Docker Compose for Zebra Power - Production services: backend: @@ -6,29 +6,37 @@ services: context: ./backend dockerfile: Dockerfile container_name: zebra_backend - ports: - - "8000:8000" volumes: - ./data:/app/data environment: - DATABASE_URL=sqlite:///data/zebra.db - - CORS_ORIGINS=http://localhost:3000,http://localhost + - CORS_ORIGINS=http://localhost,http://127.0.0.1 restart: unless-stopped # Required for WOL packets network_mode: host + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s frontend: build: context: ./frontend dockerfile: Dockerfile container_name: zebra_frontend - ports: - - "3000:3000" environment: - VITE_API_URL=http://localhost:8000/api depends_on: - - backend + backend: + condition: service_healthy restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 nginx: build: @@ -38,8 +46,10 @@ services: ports: - "80:80" depends_on: - - frontend - - backend + frontend: + condition: service_healthy + backend: + condition: service_healthy restart: unless-stopped volumes: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 025cd90..0c3b4b1 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,12 +1,21 @@ -FROM node:18-alpine +FROM node:20-slim WORKDIR /app -COPY package.json ./ -RUN npm install +# Install curl for health checks +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +# Install dependencies for better caching +COPY package.json package-lock.json* ./ +RUN npm ci || npm install + +# Copy source code COPY . . +# Build the application for production +RUN npm run build + EXPOSE 3000 -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file +# For production, serve the built files +CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "3000"] \ No newline at end of file diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..daa724c --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,15 @@ +FROM node:20-slim + +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm install + +# Copy source code +COPY . . + +EXPOSE 3000 + +# Development server with hot reload +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..9e17c74 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3066 @@ +{ + "name": "zebra-power-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zebra-power-frontend", + "version": "1.0.0", + "dependencies": { + "@headlessui/vue": "^1.7.16", + "@heroicons/vue": "^2.0.18", + "axios": "^1.6.2", + "pinia": "^2.1.7", + "vue": "^3.4.0", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.2", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "vite": "^5.0.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@headlessui/vue": { + "version": "1.7.23", + "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", + "integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==", + "license": "MIT", + "dependencies": { + "@tanstack/vue-virtual": "^3.0.0-beta.60" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@heroicons/vue": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz", + "integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==", + "license": "MIT", + "peerDependencies": { + "vue": ">= 3" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@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/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==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.48.1.tgz", + "integrity": "sha512-rGmb8qoG/zdmKoYELCBwu7vt+9HxZ7Koos3pD0+sH5fR3u3Wb/jGcpnqxcnWsPEKDUyzeLSqksN8LJtgXjqBYw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.48.1.tgz", + "integrity": "sha512-4e9WtTxrk3gu1DFE+imNJr4WsL13nWbD/Y6wQcyku5qadlKHY3OQ3LJ/INrrjngv2BJIHnIzbqMk1GTAC2P8yQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.1.tgz", + "integrity": "sha512-+XjmyChHfc4TSs6WUQGmVf7Hkg8ferMAE2aNYYWjiLzAS/T62uOsdfnqv+GHRjq7rKRnYh4mwWb4Hz7h/alp8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.48.1.tgz", + "integrity": "sha512-upGEY7Ftw8M6BAJyGwnwMw91rSqXTcOKZnnveKrVWsMTF8/k5mleKSuh7D4v4IV1pLxKAk3Tbs0Lo9qYmii5mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.48.1.tgz", + "integrity": "sha512-P9ViWakdoynYFUOZhqq97vBrhuvRLAbN/p2tAVJvhLb8SvN7rbBnJQcBu8e/rQts42pXGLVhfsAP0k9KXWa3nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.48.1.tgz", + "integrity": "sha512-VLKIwIpnBya5/saccM8JshpbxfyJt0Dsli0PjXozHwbSVaHTvWXJH1bbCwPXxnMzU4zVEfgD1HpW3VQHomi2AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.48.1.tgz", + "integrity": "sha512-3zEuZsXfKaw8n/yF7t8N6NNdhyFw3s8xJTqjbTDXlipwrEHo4GtIKcMJr5Ed29leLpB9AugtAQpAHW0jvtKKaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.48.1.tgz", + "integrity": "sha512-leo9tOIlKrcBmmEypzunV/2w946JeLbTdDlwEZ7OnnsUyelZ72NMnT4B2vsikSgwQifjnJUbdXzuW4ToN1wV+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.48.1.tgz", + "integrity": "sha512-Vy/WS4z4jEyvnJm+CnPfExIv5sSKqZrUr98h03hpAMbE2aI0aD2wvK6GiSe8Gx2wGp3eD81cYDpLLBqNb2ydwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.48.1.tgz", + "integrity": "sha512-x5Kzn7XTwIssU9UYqWDB9VpLpfHYuXw5c6bJr4Mzv9kIv242vmJHbI5PJJEnmBYitUIfoMCODDhR7KoZLot2VQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.48.1.tgz", + "integrity": "sha512-yzCaBbwkkWt/EcgJOKDUdUpMHjhiZT/eDktOPWvSRpqrVE04p0Nd6EGV4/g7MARXXeOqstflqsKuXVM3H9wOIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.48.1.tgz", + "integrity": "sha512-UK0WzWUjMAJccHIeOpPhPcKBqax7QFg47hwZTp6kiMhQHeOYJeaMwzeRZe1q5IiTKsaLnHu9s6toSYVUlZ2QtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.48.1.tgz", + "integrity": "sha512-3NADEIlt+aCdCbWVZ7D3tBjBX1lHpXxcvrLt/kdXTiBrOds8APTdtk2yRL2GgmnSVeX4YS1JIf0imFujg78vpw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.48.1.tgz", + "integrity": "sha512-euuwm/QTXAMOcyiFCcrx0/S2jGvFlKJ2Iro8rsmYL53dlblp3LkUQVFzEidHhvIPPvcIsxDhl2wkBE+I6YVGzA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.48.1.tgz", + "integrity": "sha512-w8mULUjmPdWLJgmTYJx/W6Qhln1a+yqvgwmGXcQl2vFBkWsKGUBRbtLRuKJUln8Uaimf07zgJNxOhHOvjSQmBQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.48.1.tgz", + "integrity": "sha512-90taWXCWxTbClWuMZD0DKYohY1EovA+W5iytpE89oUPmT5O1HFdf8cuuVIylE6vCbrGdIGv85lVRzTcpTRZ+kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.48.1.tgz", + "integrity": "sha512-2Gu29SkFh1FfTRuN1GR1afMuND2GKzlORQUP3mNMJbqdndOg7gNsa81JnORctazHRokiDzQ5+MLE5XYmZW5VWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.48.1.tgz", + "integrity": "sha512-6kQFR1WuAO50bxkIlAVeIYsz3RUx+xymwhTo9j94dJ+kmHe9ly7muH23sdfWduD0BA8pD9/yhonUvAjxGh34jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.48.1.tgz", + "integrity": "sha512-RUyZZ/mga88lMI3RlXFs4WQ7n3VyU07sPXmMG7/C1NOi8qisUg57Y7LRarqoGoAiopmGmChUhSwfpvQ3H5iGSQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.48.1.tgz", + "integrity": "sha512-8a/caCUN4vkTChxkaIJcMtwIVcBhi4X2PQRoT+yCK3qRYaZ7cURrmJFL5Ux9H9RaMIXj9RuihckdmkBX3zZsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz", + "integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.20.tgz", + "integrity": "sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.20", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz", + "integrity": "sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz", + "integrity": "sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.20", + "@vue/compiler-dom": "3.5.20", + "@vue/compiler-ssr": "3.5.20", + "@vue/shared": "3.5.20", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz", + "integrity": "sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.20.tgz", + "integrity": "sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.20.tgz", + "integrity": "sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.20.tgz", + "integrity": "sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.20", + "@vue/runtime-core": "3.5.20", + "@vue/shared": "3.5.20", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.20.tgz", + "integrity": "sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.20", + "@vue/shared": "3.5.20" + }, + "peerDependencies": { + "vue": "3.5.20" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.20.tgz", + "integrity": "sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", + "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001735", + "electron-to-chromium": "^1.5.204", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001737", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", + "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.209", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.209.tgz", + "integrity": "sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.18", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", + "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.48.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz", + "integrity": "sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.48.1", + "@rollup/rollup-android-arm64": "4.48.1", + "@rollup/rollup-darwin-arm64": "4.48.1", + "@rollup/rollup-darwin-x64": "4.48.1", + "@rollup/rollup-freebsd-arm64": "4.48.1", + "@rollup/rollup-freebsd-x64": "4.48.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.48.1", + "@rollup/rollup-linux-arm-musleabihf": "4.48.1", + "@rollup/rollup-linux-arm64-gnu": "4.48.1", + "@rollup/rollup-linux-arm64-musl": "4.48.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.48.1", + "@rollup/rollup-linux-ppc64-gnu": "4.48.1", + "@rollup/rollup-linux-riscv64-gnu": "4.48.1", + "@rollup/rollup-linux-riscv64-musl": "4.48.1", + "@rollup/rollup-linux-s390x-gnu": "4.48.1", + "@rollup/rollup-linux-x64-gnu": "4.48.1", + "@rollup/rollup-linux-x64-musl": "4.48.1", + "@rollup/rollup-win32-arm64-msvc": "4.48.1", + "@rollup/rollup-win32-ia32-msvc": "4.48.1", + "@rollup/rollup-win32-x64-msvc": "4.48.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.20.tgz", + "integrity": "sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.20", + "@vue/compiler-sfc": "3.5.20", + "@vue/runtime-dom": "3.5.20", + "@vue/server-renderer": "3.5.20", + "@vue/shared": "3.5.20" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + } + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 81db2d1..0588894 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,45 +1,5 @@ \ No newline at end of file diff --git a/frontend/src/views/Hosts.vue b/frontend/src/views/Hosts.vue new file mode 100644 index 0000000..a919e5d --- /dev/null +++ b/frontend/src/views/Hosts.vue @@ -0,0 +1,494 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/Proxmox.vue b/frontend/src/views/Proxmox.vue deleted file mode 100644 index cd369ad..0000000 --- a/frontend/src/views/Proxmox.vue +++ /dev/null @@ -1,305 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/src/views/Servers.vue b/frontend/src/views/Servers.vue deleted file mode 100644 index 80e736e..0000000 --- a/frontend/src/views/Servers.vue +++ /dev/null @@ -1,261 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index ff48818..842aa62 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -4,6 +4,7 @@ export default { "./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}", ], + darkMode: 'class', // Enable class-based dark mode theme: { extend: {}, }, diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 683b88f..005a868 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,8 +1,14 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' export default defineConfig({ plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, server: { host: '0.0.0.0', port: 3000 diff --git a/nginx/Dockerfile b/nginx/Dockerfile index b7315b4..c703970 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,5 +1,15 @@ -FROM nginx:alpine +FROM nginx:1.25-alpine + +# Install curl for health checks +RUN apk add --no-cache curl COPY nginx.conf /etc/nginx/nginx.conf -EXPOSE 80 \ No newline at end of file +# Add security headers and optimizations +RUN echo 'server_tokens off;' >> /etc/nginx/conf.d/security.conf + +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/ || exit 1 \ No newline at end of file