commit 6d154ed5fbf021ca16d23917b30a55d8af1253f2 Author: Mathis Date: Fri Oct 2 19:51:13 2026 +0200 Météo Exam v0.1.0 — calendrier des évaluations en bulletin météo - API Express + SQLite : classes, évaluations, SSE, auth Bearer - Front Vite + TS + Tailwind : frise 7 jours, Exam Cards, modales - Capacitor Android : APK debug, CapacitorHttp natif - Seed : classe Terminale S2 (code TERMS2) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b375f65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Dépendances +node_modules/ + +# Build +dist/ +android/app/build/ +android/.gradle/ +android/local.properties +android/app/src/main/assets/public/ +*.apk + +# Base de données +*.db +*.db-wal +*.db-shm +server/data/ + +# Env +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +nohup.out diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab10e91 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Météo Exam + +Le calendrier des évaluations scolaires, traduit en bulletin météorologique interactif. +Application Android (Capacitor) + API Node.js. + +## Structure + +``` +meteo-exam/ +├── shared/ Types + correspondance type d'éval ↔ météo +├── server/ API Express + SQLite (classes, évaluations, SSE) +├── web/ Front Vite + TypeScript (compilé dans Capacitor) +│ └── android/ Projet Capacitor Android +└── design/prototype/meteo-exam-reference.html Prototype visuel d'origine +``` + +## Démarrage rapide + +### 1. Installer les dépendances + +```bash +cd meteo-exam +npm install +``` + +### 2. Lancer le serveur + +```bash +# Base de démonstration (Terminale S2, code d'invitation TERMS2) +npm run seed + +# Serveur API (port 8787) +npm run dev:server +``` + +### 3. Lancer le front en développement + +```bash +npm run dev:web +# → http://localhost:5173 +``` + +### 4. Builder l'APK Android + +```bash +cd web +npx cap sync android +cd android +./gradlew assembleDebug +# → android/app/build/outputs/apk/debug/app-debug.apk +``` + +## Comptes de démonstration + +| Rôle | Classe | Code | Pseudo | +|------|--------|------|--------| +| Admin | Terminale S2 | `TERMS2` | `Thomas S.` | +| Élève | Terminale S2 | `TERMS2` | n'importe quel | + +L'admin peut ajouter / modifier / supprimer les évaluations. +Les élèves consultent et reçoivent les mises à jour en temps réel (SSE). + +## Configuration + +### Serveur + +| Variable | Défaut | Description | +|----------|--------|-------------| +| `PORT` | `8787` | Port d'écoute de l'API | +| `METEO_DB` | `server/data/meteo-exam.db` | Chemin du fichier SQLite | +| `METEO_ORIGINS` | `*` | Origines CORS autorisées (séparez par une virgule) | + +### App Android + +Dans l'onglet **Plus** de l'app, l'adresse du serveur est configurable. +Sur l'émulateur Android, utilisez `http://10.0.2.2:8787` (l'hôte de la machine). +Sur un téléphone physique, utilisez l'IP locale de la machine qui tourne le serveur. + +## API + +| Méthode | Route | Accès | Description | +|---------|-------|-------|-------------| +| `POST` | `/api/classes` | public | Créer une classe → `{ token, code, member, klass }` | +| `POST` | `/api/classes/join` | public | Rejoindre via un code | +| `GET` | `/api/classes/me` | membre | Session courante | +| `GET` | `/api/exams?from&to` | membre | Liste des évaluations | +| `POST` | `/api/exams` | admin | Ajouter une évaluation | +| `PATCH` | `/api/exams/:id` | admin | Modifier | +| `DELETE` | `/api/exams/:id` | admin | Supprimer | +| `GET` | `/api/stream` | membre | Flux SSE (ping + exam.created/updated/deleted) | + +Authentification : `Authorization: Bearer ` ou `?token=` (SSE uniquement). + +## Statuts météo + +| Icône | Nom | Coefficient | Signification | +|-------|-----|-------------|---------------| +| ☀️ | Grand Soleil | 0 | Aucune évaluation | +| 🌫️ | Brouillard | — | Rumeur non confirmée | +| 💨 | Coup de vent | 1 | Interro surprise, coeff faible | +| 🌩️ | Orage | 2–3 | DS / contrôle continu | +| 🚨 | Alerte Rouge | 4+ | Examen blanc / épreuve commune | + +## Phase 2 (prévu) + +- Radar de rumeurs (vote communautaire, seuil de conversion 65 %) +- Métriques : pression atmosphérique (coefficient) + indice de vigilance 1-5 +- Flash info du jour généré automatiquement +- PWA / service worker diff --git a/design/prototype/meteo-exam-reference.html b/design/prototype/meteo-exam-reference.html new file mode 100644 index 0000000..540d24c --- /dev/null +++ b/design/prototype/meteo-exam-reference.html @@ -0,0 +1,898 @@ + + + + + + Météo Exam - Application Mobile + + + + + + + + + + + + + + + + +
+ + +
+
+
+ ME +
+
+

Météo Exam

+

Terminale S2 • Lycée

+
+
+
+ + + Direct + +
+
+ + +
+ + +
+ + +
+ +
+ +
+ + Aujourd'hui • 08h30 + + 🌩️ +
+ +
+
+ ⚠️ ALERTE ROUGE +
+

DS de Mathématiques

+

Analyse, Intégrales & Géométrie dans l'espace

+
+ +
+
+ Salle 204 + 2 Hours +
+ Coeff 4.0 +
+
+ + +
+

Prévisions de la semaine

+ 7 prochains jours +
+ + +
+ + +
+
+
+ 💨 +
+
+

Demain • Vendredi

+

Physique-Chimie

+

Coup de vent : Interro surprise possible

+
+
+ Coeff 1.0 +
+ + +
+
+
+ ☀️ +
+
+

Lundi 5 Octobre

+

Français & Philosophie

+

Grand Soleil : Aucune évaluation répertoriée

+
+
+ Calme +
+ + +
+
+
+ 🌫️ +
+
+

Mardi 6 Octobre

+

SVT

+

Brouillard : Rumeur de TP évalué

+
+
+ Coeff 2.0 +
+ + +
+
+
+ 🌩️ +
+
+

Mercredi 7 Octobre

+

Histoire-Géographie

+

Orage : Contrôle sur la Guerre Froide

+
+
+ Coeff 3.0 +
+ +
+
+ + +
+ + +
+
+
+ +
+ +
+
+ Anonyme pour la classe + +
+
+ + +
+ Glisse vers la Droite pour Confirmer, Gauche pour Rejeter ! +
+ + +
+ + +
+
+ REJETER +
+
+ CONFIRMER +
+
+
+
+ + Matière : Anglais +
+ Il y a 10 min +
+

« La prof a dit qu'on aurait une compréhension orale surprise cette semaine ! »

+
+ Certitude : 68% (14 votes) + +
+ + +
+
+
+
+ + +
+
+ REJETER +
+
+ CONFIRMER +
+
+
+
+ + Matière : Philosophie +
+ Il y a 45 min +
+

« Ramassage des dissertations à la fin de l'heure mardi prochain. »

+
+ Certitude : 85% (22 votes) +
+ + +
+
+
+
+ + +
+
+ REJETER +
+
+ CONFIRMER +
+
+
+
+ + Matière : Histoire +
+ Il y a 2h +
+

« Examen Blanc déplacé au mois prochain d'après le délégué. »

+
+ Certitude : 32% (19 votes) +
+ + +
+
+
+
+ +
+
+ + +
+ +
+
+

Carte Météorologique

+

Secteurs géographiques par matière

+
+ + Interactive + +
+ + +
+ + + + + 🌩️ + MATHS + + + + 💨 + SVT + + + + 🌫️ + PHYSIQUE + + + + ☀️️ + HISTOIRE + + + + ☀️ + FRANÇAIS + +
+ + +
+
+
+ 🌩️ +
+

Secteur : Mathématiques

+

Alerte Rouge • Perturbations Majeures

+
+
+ Coeff 4.0 +
+

+ Avis de forte tempête pour la journée d'aujourd'hui. Contrôle continu à fort coefficient programmé. Prévisibilité maximale (98%). +

+
+ +
+ + +
+ + +
+
+
+ TS +
+
+

Thomas S.

+

Délégué de classe • TS2

+
+
+ + En ligne + +
+ + +
+

Préférences

+ +
+
+ + Alertes "Alerte Rouge" Push +
+ +
+ +
+ +
+
+ + Mode Sombre Automatique +
+ +
+
+ + +
+

Légende Météorologique

+ +
+ +
+ ☀️ +
+

Grand Soleil

+

Journée calme. Aucun examen répertorié ni suspecté.

+
+
+ +
+ 🌫️ +
+

Brouillard

+

Zone d'incertitude. Rumeur ou sous-entendu d'un professeur.

+
+
+ +
+ 💨 +
+

Coup de vent

+

Interrogation surprise potentielle ou petit QCM (Coeff faible).

+
+
+ +
+ 🌩️️ +
+

Orage / Pluie

+

Devoir Surveillé (DS) confirmé ou évaluation de chapitre.

+
+
+ +
+ 🚨 +
+

Alerte Rouge / Tempête

+

Examen blanc, épreuve commune ou gros devoir de fin de trimestre.

+
+
+ +
+
+ +
+ +
+ + + + + +
+ + Notification +
+ +
+ + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..345a84b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4684 @@ +{ + "name": "meteo-exam", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meteo-exam", + "version": "0.1.0", + "workspaces": [ + "shared", + "server", + "web" + ], + "devDependencies": { + "concurrently": "^9.1.2" + } + }, + "node_modules/@capacitor/android": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.5.2.tgz", + "integrity": "sha512-rtiaz78egCcS+vs29wkWCL3C+RhazKMSygXcjutwWP8NmO6r5UBxOvGf3Wjz5+ahobyQ723tn9gCePwRwcNDZQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.5.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.2.tgz", + "integrity": "sha512-O2kpA5ZPJUAmliDCcmpMf8xnJwMezxlFPUrlyOAjbL3U9MFNY3l4ZpPI/UXwvyPmR8ETcVF9pFr+VdWq61rqTQ==", + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xcode": "^3.0.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/core": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.2.tgz", + "integrity": "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@meteo-exam/server": { + "resolved": "server", + "link": true + }, + "node_modules/@meteo-exam/shared": { + "resolved": "shared", + "link": true + }, + "node_modules/@meteo-exam/web": { + "resolved": "web", + "link": true + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.6.tgz", + "integrity": "sha512-G6xF9OVRWsbHadMfoPNUDwW/Jt70QYG+ZDUvtYCFxjf3SPhqVAO0u9ShXZ7Cf5silVoRWyN4bDjCEKnJkXkw0w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.6.tgz", + "integrity": "sha512-Us/kTH5e2anr1CQvO8MEq4eeCVcIXI3Ag7OFxDI30n2OW7Q7xWuxo8ItbYq28PG2Sm1NWmZzG9+SqVEaj25SFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.6.tgz", + "integrity": "sha512-fwaSNrSHp9PJuEh4bKtd0mYC+G3FeVDjSTmsSiSlIYu7TFCpxrfB6lk7Ao8cIbswFyklPB1ZDUV8LkLBM15VWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.6.tgz", + "integrity": "sha512-QR2tx26gCeGh4eAGnUasfnJuRc+d7Vc0Jo1/fYtKGuiKqCuhbVpbNEvqgYGBwGDZGyS04jj+zdHi9W8uvb1Sdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.6.tgz", + "integrity": "sha512-7E1wRJEW8r7WlqmCy58ugndWHrzoEV/pRt2x0D9el7Inra62+Cw00lYW9XhW6iUCtFjhNw49McsSCIzJ/nG8uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.6.tgz", + "integrity": "sha512-13KJeF+vDswMzEHmMIouCVcEQdov3CzAA3ZhM8QFmBJV/FZ/VMZI49UHDvPXTK+wokk86kYPmQ9Qb3xGGJbZoQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.6.tgz", + "integrity": "sha512-hV+W1r8HM84PER4If94QuzrrD0DNk4CxobNhVUiz9ZiiWHDzJss4Jra2AYGH1a7GOTzjS1sIyZ35yYEhw/aq+g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.6.tgz", + "integrity": "sha512-5dWs/GENZufRph8PEfl0TrNH/R3DHJA+VxF/dqlfV2oUZfxRtN3qmFXg2DZj8DYM8d+Vd3dgZtuVmoBE/nb2YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.6.tgz", + "integrity": "sha512-inQYLPVIUvYk+s7zpxQldqwrajUNs+t4TJjPGzsdPTvsT8gsQXVcW/sQFolHZ3YSWVk0nMMwU7TtRi0Ow+QMuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.6.tgz", + "integrity": "sha512-sH+0MV1HmDC1q7+bQnlN9ywKBmHqkA2EdD/hHYrrz654ybc+vNC1FRNyzuYKicMQVtZZ4X2jz0osJYmsOtnj4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.6.tgz", + "integrity": "sha512-mCECvRr6HGekdBc6dvHzUWubqEti4JyOqlNePrOzl4Wimk26iW5Zy9rld2mQbC9id1NYjfF8nHLFq5fr3CuOKQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.6.tgz", + "integrity": "sha512-bb9rdoCPwM4DEsbOP3CJgFnvhSJ3XKCUYyUhxnlSs/XGT0DknpOYgLCPbK8VUzwl+cZGzFQm0KbBYRaSGZC2YQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.6.tgz", + "integrity": "sha512-aIJRoGCHev45JSu7JMzVrMzu1NX3nQrXx1Vb/6y4AblxW7JpCMpk4e8C6lHzvSElxi3UEcMdFJldsuWQjV1ddA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.6.tgz", + "integrity": "sha512-LvjnulezHjiaM5Ga9MPCDLTVddjvcrJhZc6vEz9fxeFRGPJsPAX9HuTuPTUQ3l8d+nYpZB/xWzUpuKJlLQm3VQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.6.tgz", + "integrity": "sha512-YmhSBeYZwJ937s8tKpYAhms48N+tj7e+oFkbkVVxfMRdAiynK2AxqRapJXB4utyXxc/iYTkMfPRQ+7ZP0IO78w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.6.tgz", + "integrity": "sha512-9+YukhzqTvJvzfDXJJzkURK9TDY01nofNH3pyHGuucGC4xPvBVtIfCAqJjkjnmBEGJYNXB8sBc2EDt7xDgGenw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.6.tgz", + "integrity": "sha512-9G5AtEkpN/A89BILHHEL9aYRYTuFjwa8PUQqoEg8SDrwOZBKpwYqNlQCBNI5QPwGrf7qFlb6LNFilo8FCSA3Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.6.tgz", + "integrity": "sha512-Ezx2E5D6Siz805j41x91JiGbzuCm8i2U98U37SQ7ytY737+wsJq+P9nPL6UmqDww491zO7MTivYOTrtmTTeCSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.6.tgz", + "integrity": "sha512-8EZ1Q3PB7yUjvR4MbFAnCBF1wF0XRe5QQqbtlJXP7ZX5C8iYlSQJugUhj9oS0gbUP4jp9qGhaIgFnV9Nvod86Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.6.tgz", + "integrity": "sha512-3JI27TALItfZ/qaxXKdbRXfV6WepUoUa3IN07FE+1xxoRdl3JIyrp5PiDF0vOHsPgEiOFkF8s3B3BRymQkdwGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.6.tgz", + "integrity": "sha512-BNOGeNFNRZDT7Hfc7b9JbTZFXkpAXeXl/CZWtxdd5CNfBaor/0RkJjg+q+E2gwVIvGqqp/NclThKFGrhuScVhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.6.tgz", + "integrity": "sha512-D3eEyLSIim5WF5fHbwnx+ryk0delN7fJyBrDTO3wVCCNBZ/cA8aCGwLS3SiuK+vRI4E7dqikAlStGuR20dBMuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.6.tgz", + "integrity": "sha512-xwbs9g8zLbHzj1poG1uh27xtubsu27Gk2nuN6kvvGDybjiTfwMksQAnF+phlvI5oeXkJWWDc6kg1t3XpgUYvrg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.6.tgz", + "integrity": "sha512-Tr6rhRNnFthvXr45zWVZQ5P8jZ5UwZnXNc7Qnu80cP/VVYtSioOPmvv7wP6UMgRfkBCJr2Pm60ua0YBDSJjgnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.6.tgz", + "integrity": "sha512-0RAxO1pS/6lY1mSoD2Apm+6aPyfUlm+OWKgncIdj/eVN9I+pDY3lKgD7xsUNFei1u7MZQETGOo6m8POXMDp53A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", + "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "license": "MIT" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz", + "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/better-sqlite3": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/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/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.6.tgz", + "integrity": "sha512-w4+GKyTBqshj84F1GF3s2zxAPFZPEmNkCbQtqilsYS7GsksmX6VoV7UggfZg8sUHCCOkBe7UtQp6fZwbH311FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.6", + "@rollup/rollup-android-arm64": "4.63.6", + "@rollup/rollup-darwin-arm64": "4.63.6", + "@rollup/rollup-darwin-x64": "4.63.6", + "@rollup/rollup-freebsd-arm64": "4.63.6", + "@rollup/rollup-freebsd-x64": "4.63.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.6", + "@rollup/rollup-linux-arm-musleabihf": "4.63.6", + "@rollup/rollup-linux-arm64-gnu": "4.63.6", + "@rollup/rollup-linux-arm64-musl": "4.63.6", + "@rollup/rollup-linux-loong64-gnu": "4.63.6", + "@rollup/rollup-linux-loong64-musl": "4.63.6", + "@rollup/rollup-linux-ppc64-gnu": "4.63.6", + "@rollup/rollup-linux-ppc64-musl": "4.63.6", + "@rollup/rollup-linux-riscv64-gnu": "4.63.6", + "@rollup/rollup-linux-riscv64-musl": "4.63.6", + "@rollup/rollup-linux-s390x-gnu": "4.63.6", + "@rollup/rollup-linux-x64-gnu": "4.63.6", + "@rollup/rollup-linux-x64-musl": "4.63.6", + "@rollup/rollup-openbsd-x64": "4.63.6", + "@rollup/rollup-openharmony-arm64": "4.63.6", + "@rollup/rollup-win32-arm64-msvc": "4.63.6", + "@rollup/rollup-win32-ia32-msvc": "4.63.6", + "@rollup/rollup-win32-x64-gnu": "4.63.6", + "@rollup/rollup-win32-x64-msvc": "4.63.6", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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==", + "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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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==", + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "server": { + "name": "@meteo-exam/server", + "version": "0.1.0", + "dependencies": { + "@meteo-exam/shared": "*", + "better-sqlite3": "^13.0.3", + "cookie-parser": "^1.4.7", + "express": "^5.2.1", + "zod": "^4.6.5" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^5.0.0", + "@types/node": "^22.10.0", + "tsx": "^4.23.15", + "typescript": "^5.6.0" + } + }, + "shared": { + "name": "@meteo-exam/shared", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.6.0" + } + }, + "web": { + "name": "@meteo-exam/web", + "version": "0.1.0", + "dependencies": { + "@capacitor/android": "^8.5.2", + "@capacitor/cli": "^8.5.2", + "@capacitor/core": "^8.5.2", + "@meteo-exam/shared": "*" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "tailwindcss": "^4.3.3", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..46cb23d --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "meteo-exam", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "Météo Exam — le calendrier des évaluations en bulletin météorologique", + "workspaces": [ + "shared", + "server", + "web" + ], + "scripts": { + "dev": "concurrently -n server,web -c blue,magenta \"npm:dev -w @meteo-exam/server\" \"npm:dev -w @meteo-exam/web\"", + "dev:server": "npm run dev -w @meteo-exam/server", + "dev:web": "npm run dev -w @meteo-exam/web", + "seed": "npm run seed -w @meteo-exam/server", + "build": "npm run build -w @meteo-exam/shared && npm run typecheck -w @meteo-exam/server && npm run build -w @meteo-exam/web", + "typecheck": "npm run typecheck --workspaces --if-present", + "start": "npm run start -w @meteo-exam/server" + }, + "devDependencies": { + "concurrently": "^9.1.2" + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..b8c15ff --- /dev/null +++ b/server/package.json @@ -0,0 +1,28 @@ +{ + "name": "@meteo-exam/server", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "scripts": { + "dev": "tsx watch src/index.ts", + "start": "tsx src/index.ts", + "seed": "tsx src/seed.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@meteo-exam/shared": "*", + "better-sqlite3": "^13.0.3", + "cookie-parser": "^1.4.7", + "express": "^5.2.1", + "zod": "^4.6.5" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^5.0.0", + "@types/node": "^22.10.0", + "tsx": "^4.23.15", + "typescript": "^5.6.0" + } +} diff --git a/server/src/auth.ts b/server/src/auth.ts new file mode 100644 index 0000000..3470486 --- /dev/null +++ b/server/src/auth.ts @@ -0,0 +1,138 @@ +import { randomBytes } from 'node:crypto' +import type { Request, Response, NextFunction } from 'express' +import type { ClassInfo, Member, Role } from '@meteo-exam/shared' +import { CLASS_CODE_LENGTH } from '@meteo-exam/shared' +import { db, nowIso } from './db.js' + +interface MemberRow { + id: number + class_id: number + nickname: string + role: Role + class_name: string + class_level: string | null +} + +export const newToken = (): string => randomBytes(32).toString('base64url') + +/** Code d'invitation lisible au tableau : pas de 0/O ni de 1/I. */ +const CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' + +export function newClassCode(): string { + const codes = db.prepare<[], { code: string }>('SELECT code FROM classes').all() + const taken = new Set(codes.map((r) => r.code)) + for (let attempt = 0; attempt < 50; attempt++) { + let code = '' + for (let i = 0; i < CLASS_CODE_LENGTH; i++) { + code += CODE_ALPHABET[randomBytes(1)[0]! % CODE_ALPHABET.length] + } + if (!taken.has(code)) return code + } + throw new Error('Impossible de générer un code de classe unique') +} + +export interface NewSession { + token: string + member: Member + klass: ClassInfo +} + +export function createClass(name: string, level: string | null, nickname: string): NewSession & { code: string } { + const code = newClassCode() + const now = nowIso() + const token = newToken() + const create = db.transaction((): NewSession => { + const classId = db + .prepare('INSERT INTO classes (name, level, code, created_at) VALUES (?, ?, ?, ?)') + .run(name, level, code, now).lastInsertRowid as number + const memberId = db + .prepare('INSERT INTO members (class_id, nickname, role, token, created_at) VALUES (?, ?, ?, ?, ?)') + .run(classId, nickname, 'admin', token, now).lastInsertRowid as number + return { + token, + member: { id: memberId, classId, nickname, role: 'admin' }, + klass: { id: classId, name, level }, + } + }) + return { code, ...create() } +} + +export function joinClass(code: string, nickname: string): NewSession | null { + const row = db + .prepare<[string], { id: number; name: string; level: string | null }>( + 'SELECT id, name, level FROM classes WHERE code = ?', + ) + .get(code) + if (!row) return null + const token = newToken() + const memberId = db + .prepare('INSERT INTO members (class_id, nickname, role, token, created_at) VALUES (?, ?, ?, ?, ?)') + .run(row.id, nickname, 'student', token, nowIso()).lastInsertRowid as number + return { + token, + member: { id: memberId, classId: row.id, nickname, role: 'student' }, + klass: { id: row.id, name: row.name, level: row.level }, + } +} + +export function findSession(token: string): { member: Member; klass: ClassInfo } | null { + const row = db + .prepare<[string], MemberRow>( + `SELECT m.id, m.class_id, m.nickname, m.role, + c.name AS class_name, c.level AS class_level + FROM members m + JOIN classes c ON c.id = m.class_id + WHERE m.token = ?`, + ) + .get(token) + if (!row) return null + return { + member: { id: row.id, classId: row.class_id, nickname: row.nickname, role: row.role }, + klass: { id: row.class_id, name: row.class_name, level: row.class_level }, + } +} + +declare global { + namespace Express { + interface Request { + session?: { member: Member; klass: ClassInfo } + } + } +} + +/** + * Jeton Bearer en en-tête `Authorization`. Sur Android le WebView Capacitor + * est en cross-origin : un cookie httpOnly ne serait pas envoyé par le + * navigateur embarqué, donc le client stocke le jeton et le renvoie ici. + * + * `?token=` est également accepté, car `EventSource` ne sait pas poser + * d'en-têtes — c'est le seul moyen d'authentifier le flux SSE. + */ +function readToken(req: Request): string | null { + const header = req.headers.authorization + if (typeof header === 'string' && header.startsWith('Bearer ')) { + return header.slice(7).trim() || null + } + const query = req.query['token'] + return typeof query === 'string' && query.length > 0 ? query : null +} + +export function requireSession(req: Request, res: Response, next: NextFunction): void { + const token = readToken(req) + const session = token ? findSession(token) : null + if (!session) { + res.status(401).json({ error: 'Session inconnue. Rejoignez une classe pour continuer.' }) + return + } + req.session = session + next() +} + +/** Réservé à l'admin de la classe (celui qui l'a créée). */ +export function requireAdmin(req: Request, res: Response, next: NextFunction): void { + if (req.session?.member.role !== 'admin') { + res.status(403).json({ error: 'Seul l’admin de la classe peut modifier le calendrier.' }) + return + } + next() +} diff --git a/server/src/db.ts b/server/src/db.ts new file mode 100644 index 0000000..e5547d7 --- /dev/null +++ b/server/src/db.ts @@ -0,0 +1,58 @@ +import Database from 'better-sqlite3' +import { mkdirSync } from 'node:fs' +import { dirname, resolve } from 'node:path' + +const DB_PATH = process.env['METEO_DB'] ?? resolve(process.cwd(), 'data/meteo-exam.db') + +mkdirSync(dirname(DB_PATH), { recursive: true }) + +export const db = new Database(DB_PATH) + +db.pragma('journal_mode = WAL') +db.pragma('foreign_keys = ON') + +db.exec(` + CREATE TABLE IF NOT EXISTS classes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + level TEXT, + code TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS members ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE, + nickname TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('admin','student')), + token TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_members_token ON members(token); + CREATE INDEX IF NOT EXISTS idx_members_class ON members(class_id); + + CREATE TABLE IF NOT EXISTS exams ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE, + subject TEXT NOT NULL, + teacher TEXT, + room TEXT, + chapter TEXT, + starts_at TEXT NOT NULL, + duration_min INTEGER NOT NULL, + weight REAL NOT NULL DEFAULT 1, + kind TEXT NOT NULL, + note TEXT, + created_by INTEGER REFERENCES members(id) ON DELETE SET NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_exams_class_start ON exams(class_id, starts_at); +`) + +export const nowIso = (): string => new Date().toISOString() + +/** Raccourci : préparer une requête dont `.get()` renvoie `changes` (DELETE). */ +export const prepareChanges = db.prepare.bind(db) diff --git a/server/src/events.ts b/server/src/events.ts new file mode 100644 index 0000000..91e9edf --- /dev/null +++ b/server/src/events.ts @@ -0,0 +1,30 @@ +import type { Response } from 'express' +import type { StreamEvent } from '@meteo-exam/shared' + +/** clients SSE ouverts, groupés par classId. */ +const clients = new Map>() + +export function addClient(classId: number, res: Response): () => void { + let set = clients.get(classId) + if (!set) { + set = new Set() + clients.set(classId, set) + } + set.add(res) + + return () => { + set.delete(res) + if (set.size === 0) clients.delete(classId) + } +} + +export function broadcast(classId: number, event: StreamEvent): void { + const set = clients.get(classId) + if (!set) return + const payload = `data: ${JSON.stringify(event)}\n\n` + for (const res of set) { + res.write(payload) + } +} + +export const clientCount = (classId: number): number => clients.get(classId)?.size ?? 0 diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..140ce63 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,68 @@ +import express from 'express' +import { existsSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { classesRouter } from './routes/classes.js' +import { examsRouter } from './routes/exams.js' +import { streamRouter } from './routes/stream.js' + +const here = dirname(fileURLToPath(import.meta.url)) +const PORT = Number(process.env['PORT'] ?? 8787) + +/** Origines autorisées. L'app Android (WebView) est en cross-origin. */ +const ALLOW_ORIGINS = (process.env['METEO_ORIGINS'] ?? '*') + .split(',') + .map((o) => o.trim()) + .filter(Boolean) + +const app = express() +app.disable('x-powered-by') +app.use(express.json({ limit: '64kb' })) + +// L'API ne renvoie aucun cookie : le client porte un jeton Bearer. +// `Authorization` doit donc être autorisé explicitement. +app.use((req, res, next) => { + const origin = req.headers.origin + res.set('Access-Control-Allow-Origin', ALLOW_ORIGINS.includes('*') ? (origin ?? '*') : (origin && ALLOW_ORIGINS.includes(origin) ? origin : ALLOW_ORIGINS[0] ?? '*')) + res.set('Vary', 'Origin') + res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization') + res.set('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS') + res.set('Access-Control-Expose-Headers', 'Content-Type') + res.set('Access-Control-Max-Age', '600') + if (req.method === 'OPTIONS') { + res.status(204).end() + return + } + next() +}) + +app.get('/api/health', (_req, res) => { + res.json({ ok: true }) +}) +app.use('/api/classes', classesRouter) +app.use('/api/exams', examsRouter) +app.use('/api/stream', streamRouter) + +app.use('/api', (_req, res) => { + res.status(404).json({ error: 'Route inconnue' }) +}) + +// En production, le front compilé est servi par le même serveur. Utile pour +// un déploiement web ; sur Android, l'app Capacitor charge ses assets locale. +const webDist = resolve(here, '../../web/dist') +if (existsSync(webDist)) { + app.use(express.static(webDist)) + app.get(/.*/, (_req, res) => { + res.sendFile(resolve(webDist, 'index.html')) + }) +} + +app.listen(PORT, () => { + console.log(`Météo Exam — API sur http://localhost:${PORT}`) + if (ALLOW_ORIGINS.includes('*')) { + console.log('CORS ouvert à toutes les origines. Restreignez avec METEO_ORIGINS en production.') + } + if (!existsSync(webDist)) { + console.log('Front non compilé : lancez `npm run dev` (Vite sur 5173) ou `npm run build`.') + } +}) diff --git a/server/src/routes/classes.ts b/server/src/routes/classes.ts new file mode 100644 index 0000000..a6867a9 --- /dev/null +++ b/server/src/routes/classes.ts @@ -0,0 +1,49 @@ +import { Router } from 'express' +import { z } from 'zod' +import { createClass, joinClass, requireSession } from '../auth.js' + +export const classesRouter = Router() + +const createSchema = z.object({ + name: z.string().trim().min(2, 'Nom de classe trop court').max(60), + level: z.string().trim().max(40).nullish(), + nickname: z.string().trim().min(1, 'Choisissez un pseudo').max(30), +}) + +const joinSchema = z.object({ + code: z.string().trim().min(1, 'Entrez le code de classe').max(12), + nickname: z.string().trim().min(1, 'Choisissez un pseudo').max(30), +}) + +/** Crée une classe, ouvre une session admin et renvoie le jeton + le code d'invitation. */ +classesRouter.post('/', (req, res) => { + const parsed = createSchema.safeParse(req.body) + if (!parsed.success) { + res.status(400).json({ error: parsed.error.issues[0]?.message ?? 'Requête invalide' }) + return + } + const { name, level, nickname } = parsed.data + const { code, token, member, klass } = createClass(name, level ?? null, nickname) + res.status(201).json({ token, code, member, klass }) +}) + +/** Rejoint une classe existante via son code. */ +classesRouter.post('/join', (req, res) => { + const parsed = joinSchema.safeParse(req.body) + if (!parsed.success) { + res.status(400).json({ error: parsed.error.issues[0]?.message ?? 'Requête invalide' }) + return + } + const { code, nickname } = parsed.data + const session = joinClass(code.toUpperCase(), nickname) + if (!session) { + res.status(404).json({ error: 'Code de classe inconnu. Vérifiez les caractères.' }) + return + } + res.status(201).json(session) +}) + +/** Session courante : sert à valider un jeton au démarrage de l'app. */ +classesRouter.get('/me', requireSession, (req, res) => { + res.json(req.session) +}) diff --git a/server/src/routes/exams.ts b/server/src/routes/exams.ts new file mode 100644 index 0000000..dec51c7 --- /dev/null +++ b/server/src/routes/exams.ts @@ -0,0 +1,186 @@ +import { Router } from 'express' +import { z } from 'zod' +import type { Exam, WeatherKind } from '@meteo-exam/shared' +import { isWeatherKind, kindFromWeight } from '@meteo-exam/shared' +import { db, nowIso } from '../db.js' +import { requireSession, requireAdmin } from '../auth.js' +import { broadcast } from '../events.js' + +export const examsRouter = Router() + +interface ExamRow { + id: number + class_id: number + subject: string + teacher: string | null + room: string | null + chapter: string | null + starts_at: string + duration_min: number + weight: number + kind: WeatherKind + note: string | null + created_at: string + updated_at: string +} + +const toExam = (row: ExamRow): Exam => ({ + id: row.id, + classId: row.class_id, + subject: row.subject, + teacher: row.teacher, + room: row.room, + chapter: row.chapter, + startsAt: row.starts_at, + durationMin: row.duration_min, + weight: row.weight, + kind: row.kind, + note: row.note, + createdAt: row.created_at, + updatedAt: row.updated_at, +}) + +const bodySchema = z.object({ + subject: z.string().trim().min(1, 'La matière est obligatoire').max(60), + teacher: z.string().trim().max(60).nullish(), + room: z.string().trim().max(30).nullish(), + chapter: z.string().trim().max(160).nullish(), + startsAt: z.string().datetime({ message: 'Date invalide' }), + durationMin: z.number().int().min(5, 'Durée minimum : 5 min').max(480, 'Durée maximum : 8 h'), + weight: z.number().min(0).max(20), + kind: z.enum(['soleil', 'brouillard', 'vent', 'orage', 'tempete']).nullish(), + note: z.string().trim().max(400).nullish(), +}) + +const patchSchema = bodySchema.partial() + +/** Liste les évaluations d'une fenêtre temporelle, triées par date croissante. */ +examsRouter.get('/', requireSession, (req, res) => { + const classId = req.session!.member.classId + const from = typeof req.query['from'] === 'string' ? req.query['from'] : null + const to = typeof req.query['to'] === 'string' ? req.query['to'] : null + + const clauses = ['class_id = ?'] + const params: (string | number)[] = [classId] + if (from) { + clauses.push('starts_at >= ?') + params.push(from) + } + if (to) { + clauses.push('starts_at < ?') + params.push(to) + } + + const rows = db + .prepare( + `SELECT * FROM exams WHERE ${clauses.join(' AND ')} ORDER BY starts_at ASC`, + ) + .all(...params) + res.json(rows.map(toExam)) +}) + +examsRouter.post('/', requireSession, requireAdmin, (req, res) => { + const parsed = bodySchema.safeParse(req.body) + if (!parsed.success) { + res.status(400).json({ error: parsed.error.issues[0]?.message ?? 'Requête invalide' }) + return + } + const data = parsed.data + const now = nowIso() + const info = db + .prepare( + `INSERT INTO exams + (class_id, subject, teacher, room, chapter, starts_at, duration_min, weight, kind, note, created_by, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .run( + req.session!.member.classId, + data.subject, + data.teacher ?? null, + data.room ?? null, + data.chapter ?? null, + new Date(data.startsAt).toISOString(), + data.durationMin, + data.weight, + data.kind ?? kindFromWeight(data.weight), + data.note ?? null, + req.session!.member.id, + now, + now, + ) + + const examId = Number(info.lastInsertRowid) + const row = db.prepare<[number], ExamRow>('SELECT * FROM exams WHERE id = ?').get(examId)! + const exam = toExam(row) + broadcast(exam.classId, { type: 'exam.created', exam }) + res.status(201).json(exam) +}) + +examsRouter.patch('/:id', requireSession, requireAdmin, (req, res) => { + const id = Number(req.params['id']) + if (!Number.isInteger(id)) { + res.status(400).json({ error: 'Identifiant invalide' }) + return + } + const existing = db + .prepare<[number, number], ExamRow>('SELECT * FROM exams WHERE id = ? AND class_id = ?') + .get(id, req.session!.member.classId) + if (!existing) { + res.status(404).json({ error: 'Évaluation introuvable' }) + return + } + + const parsed = patchSchema.safeParse(req.body) + if (!parsed.success) { + res.status(400).json({ error: parsed.error.issues[0]?.message ?? 'Requête invalide' }) + return + } + const d = parsed.data + const nextWeight = d.weight ?? existing.weight + const kind = d.kind ?? (d.weight !== undefined ? kindFromWeight(nextWeight) : existing.kind) + if (!isWeatherKind(kind)) { + res.status(400).json({ error: 'Statut météo inconnu' }) + return + } + + db.prepare( + `UPDATE exams + SET subject = ?, teacher = ?, room = ?, chapter = ?, starts_at = ?, + duration_min = ?, weight = ?, kind = ?, note = ?, updated_at = ? + WHERE id = ? AND class_id = ?`, + ).run( + d.subject ?? existing.subject, + d.teacher === undefined ? existing.teacher : d.teacher, + d.room === undefined ? existing.room : d.room, + d.chapter === undefined ? existing.chapter : d.chapter, + d.startsAt ? new Date(d.startsAt).toISOString() : existing.starts_at, + d.durationMin ?? existing.duration_min, + nextWeight, + kind, + d.note === undefined ? existing.note : d.note, + nowIso(), + id, + req.session!.member.classId, + ) + + const row = db.prepare<[number], ExamRow>('SELECT * FROM exams WHERE id = ?').get(id)! + const exam = toExam(row) + broadcast(exam.classId, { type: 'exam.updated', exam }) + res.json(exam) +}) + +examsRouter.delete('/:id', requireSession, requireAdmin, (req, res) => { + const id = Number(req.params['id']) + if (!Number.isInteger(id)) { + res.status(400).json({ error: 'Identifiant invalide' }) + return + } + const classId = req.session!.member.classId + const info = db.prepare<[number, number], { changes: number }>('DELETE FROM exams WHERE id = ? AND class_id = ?').run(id, classId) + if (info.changes === 0) { + res.status(404).json({ error: 'Évaluation introuvable' }) + return + } + broadcast(classId, { type: 'exam.deleted', examId: id }) + res.status(204).end() +}) diff --git a/server/src/routes/stream.ts b/server/src/routes/stream.ts new file mode 100644 index 0000000..7a433ee --- /dev/null +++ b/server/src/routes/stream.ts @@ -0,0 +1,40 @@ +import { Router } from 'express' +import { requireSession } from '../auth.js' +import { addClient } from '../events.js' +import type { StreamEvent } from '@meteo-exam/shared' + +export const streamRouter = Router() + +const send = (res: import('express').Response, event: StreamEvent): void => { + res.write(`data: ${JSON.stringify(event)}\n\n`) +} + +/** + * Flux SSE du calendrier. Le cookie de session est lu au handshake, ce qui + * évite un jeton en query string (et le partage d'URL cassé par EventSource). + */ +streamRouter.get('/', requireSession, (req, res) => { + const classId = req.session!.member.classId + + res.status(200) + res.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + Connection: 'keep-alive', + 'X-Accel-Buffering': 'no', + }) + res.flushHeaders() + + // Commente d'ouverture : certains proxys n'envoient rien avant le premier event. + res.write(': connected\n\n') + send(res, { type: 'ping' }) + + const remove = addClient(classId, res) + const keepAlive = setInterval(() => send(res, { type: 'ping' }), 25_000) + + req.on('close', () => { + clearInterval(keepAlive) + remove() + res.end() + }) +}) diff --git a/server/src/seed.ts b/server/src/seed.ts new file mode 100644 index 0000000..349ff7c --- /dev/null +++ b/server/src/seed.ts @@ -0,0 +1,130 @@ +/** + * Jeu de données de démonstration : la classe Terminale S2 avec deux semaines + * d'évaluations. Idempotent — relancer `npm run seed` ne duplique rien. + * Usage : npm run seed [-- --reset] + */ +import { existsSync, rmSync } from 'node:fs' +import { resolve } from 'node:path' +import { db, nowIso } from './db.js' +import { newToken } from './auth.js' +import type { WeatherKind } from '@meteo-exam/shared' + +const DB_PATH = process.env['METEO_DB'] ?? resolve(process.cwd(), 'data/meteo-exam.db') + +if (process.argv.includes('--reset')) { + for (const suffix of ['', '-wal', '-shm']) { + const file = `${DB_PATH}${suffix}` + if (existsSync(file)) rmSync(file) + } + console.log('Base effacée. Relancez `npm run seed`.') + process.exit(0) +} + +const existing = db.prepare<[], { n: number }>('SELECT COUNT(*) AS n FROM classes').get()! +if (existing.n > 0) { + console.log('La base contient déjà des classes — seed ignoré. Utilisez --reset pour repartir de zéro.') + process.exit(0) +} + +const CLASS_NAME = 'Terminale S2' +const LEVEL = 'Lycée' +const ADMIN_NICK = 'Thomas S.' +const CLASS_CODE = 'TERMS2' + +const DAY_MS = 86_400_000 +const isWeekend = (d: Date): boolean => d.getDay() === 0 || d.getDay() === 6 + +const midnight = new Date() +midnight.setHours(0, 0, 0, 0) + +/** + * `dayOffset` compte les jours **scolaires** : 0 = aujourd'hui (même si c'est + * samedi, le seed reste lisible), puis on saute les weekends. + */ +function schoolDay(offset: number): Date { + const date = new Date(midnight) + let counted = 0 + for (let step = 0; step < 90; step++) { + if (counted === offset) break + date.setTime(date.getTime() + DAY_MS) + if (!isWeekend(date)) counted++ + } + return date +} + +interface SeedExam { + dayOffset: number + hour: number + subject: string + teacher: string | null + room: string | null + chapter: string | null + durationMin: number + weight: number + kind: WeatherKind + note: string | null +} + +const exams: SeedExam[] = [ + { dayOffset: 0, hour: 8, subject: 'Mathématiques', teacher: 'M. Roussel', room: 'B204', chapter: 'Intégrales, primitives et dérivation', durationMin: 120, weight: 4, kind: 'tempete', note: 'Calculatrice autorisée, calculatrice graphique non.' }, + { dayOffset: 0, hour: 14, subject: 'Histoire-Géographie', teacher: 'Mme Ferrand', room: 'A112', chapter: null, durationMin: 55, weight: 1, kind: 'vent', note: 'Interro surprise probable en début de cours.' }, + { dayOffset: 1, hour: 9, subject: 'Physique-Chimie', teacher: 'Mme Nguyen', room: 'Labo 3', chapter: 'Mécanique : second principe', durationMin: 60, weight: 2, kind: 'orage', note: 'Paquet de calculatrices fourni.' }, + { dayOffset: 1, hour: 15, subject: 'Anglais', teacher: 'M. Whitfield', room: 'C007', chapter: 'Compréhension orale — Unit 4', durationMin: 30, weight: 1, kind: 'vent', note: 'Rumeur d’une dictée surprise.' }, + { dayOffset: 2, hour: 8, subject: 'SVT', teacher: 'Mme Perrin', room: 'S215', chapter: 'Génétique des populations', durationMin: 90, weight: 3, kind: 'orage', note: 'TP suivi d’une synthèse écrite.' }, + { dayOffset: 3, hour: 10, subject: 'Philosophie', teacher: 'M. Aubert', room: 'A008', chapter: 'La raison et le doute', durationMin: 120, weight: 3, kind: 'orage', note: 'Dissertation, sujet au choix.' }, + { dayOffset: 3, hour: 16, subject: 'EPS', teacher: 'Mme Costa', room: 'Gymnase', chapter: null, durationMin: 60, weight: 1, kind: 'soleil', note: 'Basket obligatoire.' }, + { dayOffset: 4, hour: 9, subject: 'Mathématiques', teacher: 'M. Roussel', room: 'B204', chapter: 'Probabilités conditionnelles', durationMin: 60, weight: 2, kind: 'orage', note: null }, + { dayOffset: 5, hour: 10, subject: 'Français', teacher: 'Mme Dumas', room: 'A101', chapter: 'Guerre froide — contexte', durationMin: 60, weight: 1, kind: 'vent', note: 'Lecture suivie d’une étude de texte.' }, + { dayOffset: 6, hour: 8, subject: 'Examen blanc', teacher: null, room: 'Amphi A', chapter: 'Toutes matières', durationMin: 180, weight: 5, kind: 'tempete', note: 'Blanc général, convocation à 8h précises.' }, + { dayOffset: 7, hour: 9, subject: 'SVT', teacher: 'Mme Perrin', room: 'S215', chapter: 'Écosystèmes et bilan de matière', durationMin: 45, weight: 1, kind: 'brouillard', note: 'Brouillard : TP évalué, date encore floue.' }, + { dayOffset: 8, hour: 14, subject: 'Anglais', teacher: 'M. Whitfield', room: 'C007', chapter: 'Expression écrite — sujet d’invention', durationMin: 60, weight: 2, kind: 'orage', note: null }, + { dayOffset: 9, hour: 8, subject: 'Histoire-Géographie', teacher: 'Mme Ferrand', room: 'A112', chapter: 'La décolonisation', durationMin: 60, weight: 2, kind: 'orage', note: null }, + { dayOffset: 10, hour: 10, subject: 'Philosophie', teacher: 'M. Aubert', room: 'A008', chapter: 'Le sujet et l’autre', durationMin: 60, weight: 2, kind: 'orage', note: 'Plan imposé.' }, +] + +const seed = db.transaction(() => { + db.prepare('INSERT INTO classes (name, level, code, created_at) VALUES (?, ?, ?, ?)') + .run(CLASS_NAME, LEVEL, CLASS_CODE, nowIso()) + + const classId = db.prepare<[string], { id: number }>('SELECT id FROM classes WHERE code = ?') + .get(CLASS_CODE)!.id + + const adminToken = newToken() + const adminId = db + .prepare('INSERT INTO members (class_id, nickname, role, token, created_at) VALUES (?, ?, ?, ?, ?)') + .run(classId, ADMIN_NICK, 'admin', adminToken, nowIso()).lastInsertRowid as number + + // Quelques élèves : ils donneront du volume aux votes du radar plus tard. + for (const nick of ['Léa M.', 'Karim B.', 'Chloé D.', 'Inès R.', 'Lucas P.', 'Mehdi A.']) { + db.prepare('INSERT INTO members (class_id, nickname, role, token, created_at) VALUES (?, ?, ?, ?, ?)') + .run(classId, nick, 'student', newToken(), nowIso()) + } + + const insert = db.prepare( + `INSERT INTO exams + (class_id, subject, teacher, room, chapter, starts_at, duration_min, weight, kind, note, created_by, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + + for (const e of exams) { + const date = schoolDay(e.dayOffset) + date.setHours(e.hour, 0, 0, 0) + const now = nowIso() + insert.run( + classId, e.subject, e.teacher, e.room, e.chapter, + date.toISOString(), e.durationMin, e.weight, e.kind, e.note, + adminId, now, now, + ) + } + + return { classId, adminToken } +}) + +const { adminToken } = seed() + +console.log('Seed terminé.') +console.log(` Classe : ${CLASS_NAME} (${LEVEL})`) +console.log(` Code invite : ${CLASS_CODE}`) +console.log(` Admin : ${ADMIN_NICK}`) +console.log(` Évaluations : ${exams.length}`) +console.log(` Jeton admin : ${adminToken}`) diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..cea937f --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 0000000..2362824 --- /dev/null +++ b/shared/package.json @@ -0,0 +1,18 @@ +{ + "name": "@meteo-exam/shared", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/shared/src/index.ts b/shared/src/index.ts new file mode 100644 index 0000000..155a95e --- /dev/null +++ b/shared/src/index.ts @@ -0,0 +1,175 @@ +/** + * Types et métadonnées partagés entre le serveur et le front. + * Aucun import runtime ici : ce module est consommé tel quel par les deux côtés. + */ + +/** Statut météorologique d'une évaluation. */ +export type WeatherKind = 'soleil' | 'brouillard' | 'vent' | 'orage' | 'tempete' + +export const WEATHER_KINDS: WeatherKind[] = ['soleil', 'brouillard', 'vent', 'orage', 'tempete'] + +/** Du plus rassurant au plus menaçant. Sert aux tris et au calcul du statut d'une journée. */ +export const SEVERITY: Record = { + soleil: 0, + brouillard: 1, + vent: 2, + orage: 3, + tempete: 4, +} + +export interface WeatherMeta { + /** Nom affiché, ex. « Alerte Rouge ». */ + label: string + /** Titre long pour les modales et la légende. */ + title: string + icon: string + /** Description pédagogique affichée dans la légende. */ + hint: string + /** Couleur d'accent en dur, pour les SVG et jauges. */ + hex: string + /** Classes Tailwind de la pastille (fond / bordure / texte). */ + chip: string + /** Classe de couleur du texte seule, pour les libellés sur fond neutre. */ + accent: string + /** Classes Tailwind du badge plein. */ + badge: string +} + +export const WEATHER: Record = { + soleil: { + label: 'Grand Soleil', + title: 'Grand Soleil', + icon: '☀️', + hint: 'Journée calme. Aucun examen répertorié ni suspecté.', + hex: '#0284c7', + chip: 'bg-sky-500/10 border-sky-500/20 text-sky-400', + accent: 'text-sky-400', + badge: 'bg-sky-500/15 text-sky-300 border-sky-400/30', + }, + brouillard: { + label: 'Brouillard', + title: 'Brouillard', + icon: '🌫️', + hint: 'Zone d’incertitude. Rumeur ou sous-entendu d’un professeur.', + hex: '#7c3aed', + chip: 'bg-purple-500/10 border-purple-500/20 text-purple-400', + accent: 'text-purple-400', + badge: 'bg-purple-500/15 text-purple-300 border-purple-400/30', + }, + vent: { + label: 'Coup de vent', + title: 'Coup de vent', + icon: '💨', + hint: 'Interrogation surprise potentielle ou petit QCM, coefficient faible.', + hex: '#d97706', + chip: 'bg-amber-500/10 border-amber-500/20 text-amber-400', + accent: 'text-amber-400', + badge: 'bg-amber-500/15 text-amber-300 border-amber-400/30', + }, + orage: { + label: 'Orage', + title: 'Orage / Pluie battante', + icon: '🌩️', + hint: 'Devoir surveillé (DS) confirmé ou évaluation de chapitre.', + hex: '#e11d48', + chip: 'bg-rose-500/10 border-rose-500/20 text-rose-400', + accent: 'text-rose-400', + badge: 'bg-rose-500/15 text-rose-300 border-rose-400/30', + }, + tempete: { + label: 'Alerte Rouge', + title: 'Alerte Rouge / Tempête', + icon: '🚨', + hint: 'Examen blanc, épreuve commune ou gros devoir de fin de trimestre.', + hex: '#dc2626', + chip: 'bg-red-500/10 border-red-500/20 text-red-500', + accent: 'text-red-500', + badge: 'bg-red-500/15 text-red-300 border-red-400/30', + }, +} + +export function isWeatherKind(value: unknown): value is WeatherKind { + return typeof value === 'string' && (WEATHER_KINDS as string[]).includes(value) +} + +/** + * Statut par défaut déduit du coefficient, pour pré-remplir le formulaire d'ajout. + * 0 = rien de prévu, 1 = interro surprise, 2-3 = DS, 4+ = alerte rouge. + */ +export function kindFromWeight(weight: number): WeatherKind { + if (weight <= 0) return 'soleil' + if (weight < 2) return 'vent' + if (weight < 4) return 'orage' + return 'tempete' +} + +/** Les coefficients proposés dans le formulaire, avec leur statut déduit. */ +export const WEIGHT_PRESETS: { value: number; label: string }[] = [ + { value: 0, label: '0 — coursework' }, + { value: 1, label: '1 — interro' }, + { value: 2, label: '2 — contrôle' }, + { value: 3, label: '3 — DS' }, + { value: 4, label: '4 — examen blanc' }, + { value: 5, label: '5 — épreuve commune' }, +] + +/** Statut le plus sévère d'une journée ; `soleil` si la journée est vide. */ +export function dominantKind(exams: { kind: WeatherKind }[]): WeatherKind { + let best: WeatherKind = 'soleil' + for (const exam of exams) { + if (SEVERITY[exam.kind] > SEVERITY[best]) best = exam.kind + } + return best +} + +export type Role = 'admin' | 'student' + +export interface Exam { + id: number + classId: number + subject: string + teacher: string | null + room: string | null + chapter: string | null + /** ISO 8601 UTC. */ + startsAt: string + durationMin: number + /** Coefficient : alimente la future « pression atmosphérique ». */ + weight: number + kind: WeatherKind + note: string | null + createdAt: string + updatedAt: string +} + +export interface ClassInfo { + id: number + name: string + level: string | null +} + +export interface Member { + id: number + classId: number + nickname: string + role: Role +} + +/** Charge utile de `GET /api/me`, renvoyée aussi à la création et à l'adhésion. */ +export interface Session { + member: Member + klass: ClassInfo +} + +export interface ApiError { + error: string +} + +/** Payloads émis par le flux SSE. */ +export type StreamEvent = + | { type: 'exam.created'; exam: Exam } + | { type: 'exam.updated'; exam: Exam } + | { type: 'exam.deleted'; examId: number } + | { type: 'ping' } + +export const CLASS_CODE_LENGTH = 6 diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 0000000..d67e295 --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/web/android/.gitignore b/web/android/.gitignore new file mode 100644 index 0000000..48354a3 --- /dev/null +++ b/web/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/web/android/app/.gitignore b/web/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/web/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/web/android/app/build.gradle b/web/android/app/build.gradle new file mode 100644 index 0000000..01e578a --- /dev/null +++ b/web/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace = "fr.meteo.exam" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "fr.meteo.exam" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/web/android/app/capacitor.build.gradle b/web/android/app/capacitor.build.gradle new file mode 100644 index 0000000..bbfb44f --- /dev/null +++ b/web/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/web/android/app/proguard-rules.pro b/web/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/web/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/web/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/web/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f2c2217 --- /dev/null +++ b/web/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/web/android/app/src/main/AndroidManifest.xml b/web/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2dd6d3c --- /dev/null +++ b/web/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/android/app/src/main/java/fr/meteo/exam/MainActivity.java b/web/android/app/src/main/java/fr/meteo/exam/MainActivity.java new file mode 100644 index 0000000..b77aab5 --- /dev/null +++ b/web/android/app/src/main/java/fr/meteo/exam/MainActivity.java @@ -0,0 +1,5 @@ +package fr.meteo.exam; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity {} diff --git a/web/android/app/src/main/res/drawable-land-hdpi/splash.png b/web/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000..e31573b Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-land-mdpi/splash.png b/web/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-land-xhdpi/splash.png b/web/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000..8077255 Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/web/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000..14c6c8f Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/web/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000..244ca25 Binary files /dev/null and b/web/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-hdpi/splash.png b/web/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 0000000..74faaa5 Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-mdpi/splash.png b/web/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 0000000..e944f4a Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-xhdpi/splash.png b/web/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 0000000..564a82f Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/web/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000..bfabe68 Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/web/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000..6929071 Binary files /dev/null and b/web/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/web/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/web/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/web/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/web/android/app/src/main/res/drawable/ic_launcher_background.xml b/web/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/web/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/android/app/src/main/res/drawable/splash.png b/web/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/web/android/app/src/main/res/drawable/splash.png differ diff --git a/web/android/app/src/main/res/layout/activity_main.xml b/web/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/web/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/web/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c023e50 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2127973 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..b441f37 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..72905b8 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8ed0605 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..9502e47 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..4d1e077 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..df0f158 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..853db04 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..6cdf97c Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2960cbb Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e3093a Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..46de6e2 Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d2ea9ab Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a40d73e Binary files /dev/null and b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/web/android/app/src/main/res/values/ic_launcher_background.xml b/web/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/web/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/web/android/app/src/main/res/values/strings.xml b/web/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..fb2e40b --- /dev/null +++ b/web/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Météo Exam + Météo Exam + fr.meteo.exam + fr.meteo.exam + diff --git a/web/android/app/src/main/res/values/styles.xml b/web/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..be874e5 --- /dev/null +++ b/web/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/web/android/app/src/main/res/xml/file_paths.xml b/web/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/web/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/android/app/src/main/res/xml/network_security_config.xml b/web/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..6a03d06 --- /dev/null +++ b/web/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/web/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/web/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 0000000..0297327 --- /dev/null +++ b/web/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/web/android/build.gradle b/web/android/build.gradle new file mode 100644 index 0000000..f8f0e43 --- /dev/null +++ b/web/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/web/android/capacitor.settings.gradle b/web/android/capacitor.settings.gradle new file mode 100644 index 0000000..874b109 --- /dev/null +++ b/web/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor') diff --git a/web/android/gradle.properties b/web/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/web/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/web/android/gradle/wrapper/gradle-wrapper.jar b/web/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/web/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/web/android/gradle/wrapper/gradle-wrapper.properties b/web/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7705927 --- /dev/null +++ b/web/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/web/android/gradlew b/web/android/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/web/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/web/android/gradlew.bat b/web/android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/web/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/web/android/settings.gradle b/web/android/settings.gradle new file mode 100644 index 0000000..3b4431d --- /dev/null +++ b/web/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/web/android/variables.gradle b/web/android/variables.gradle new file mode 100644 index 0000000..ee4ba41 --- /dev/null +++ b/web/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} \ No newline at end of file diff --git a/web/capacitor.config.ts b/web/capacitor.config.ts new file mode 100644 index 0000000..11dae04 --- /dev/null +++ b/web/capacitor.config.ts @@ -0,0 +1,17 @@ +import type { CapacitorConfig } from '@capacitor/cli' + +const config: CapacitorConfig = { + appId: 'fr.meteo.exam', + appName: 'Météo Exam', + webDir: 'dist', + // L'app Android pointe vers le serveur API distant ; le front est embarqué. + // Scheme HTTP : évite le mixed content (page HTTPS → API HTTP). + server: { + androidScheme: 'http', + }, + android: { + allowMixedContent: true, + }, +} + +export default config diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..325a6ed --- /dev/null +++ b/web/index.html @@ -0,0 +1,23 @@ + + + + + + + Météo Exam + + + + + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..1b6282b --- /dev/null +++ b/web/package.json @@ -0,0 +1,24 @@ +{ + "name": "@meteo-exam/web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@capacitor/android": "^8.5.2", + "@capacitor/cli": "^8.5.2", + "@capacitor/core": "^8.5.2", + "@meteo-exam/shared": "*" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "tailwindcss": "^4.3.3", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } +} diff --git a/web/src/api.ts b/web/src/api.ts new file mode 100644 index 0000000..c1f21c3 --- /dev/null +++ b/web/src/api.ts @@ -0,0 +1,135 @@ +import { Capacitor, CapacitorHttp } from '@capacitor/core' +import type { Exam, Session, StreamEvent } from '@meteo-exam/shared' +import { getBaseUrl, getToken } from './config' + +export class ApiFailure extends Error { + constructor( + message: string, + readonly status: number, + ) { + super(message) + this.name = 'ApiFailure' + } +} + +interface RequestOptions { + method?: 'GET' | 'POST' | 'PATCH' | 'DELETE' + body?: unknown + /** Jeton explicite, sinon celui du stockage local. */ + token?: string | null +} + +interface HttpResult { + status: number + data: unknown +} + +/** + * Sur Android (Capacitor natif), `CapacitorHttp` passe par OkHttp : le WebView + * n'est pas impliqué, pas de mixed content ni de blocage réseau embarqué. + * Dans un navigateur (dev), on retombe sur `fetch`. + */ +async function http(path: string, options: RequestOptions): Promise { + const { method = 'GET', body, token = getToken() } = options + const url = `${getBaseUrl()}${path}` + const headers: Record = { Accept: 'application/json' } + if (body !== undefined) headers['Content-Type'] = 'application/json' + if (token) headers['Authorization'] = `Bearer ${token}` + + if (Capacitor.isNativePlatform()) { + try { + const response = await CapacitorHttp.request({ + url, + method, + headers, + data: body as Record | undefined, + }) + return { status: response.status, data: response.data } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + throw new ApiFailure(`Serveur injoignable. Vérifiez l’adresse dans les réglages. (${message})`, 0) + } + } + + let response: Response + try { + response = await fetch(url, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + }) + } catch { + throw new ApiFailure('Serveur injoignable. Vérifiez l’adresse dans les réglages.', 0) + } + + if (response.status === 204) return { status: 204, data: null } + + const text = await response.text() + return { status: response.status, data: text ? JSON.parse(text) : null } +} + +async function request(path: string, options: RequestOptions = {}): Promise { + const { status, data } = await http(path, options) + + if (status === 204) return undefined as T + + if (status < 200 || status >= 300) { + const message = + data && typeof data === 'object' && 'error' in data && typeof data.error === 'string' + ? data.error + : `Erreur ${status}` + throw new ApiFailure(message, status) + } + return data as T +} + +export const api = { + health: () => request<{ ok: boolean }>('/api/health'), + + createClass: (input: { name: string; level?: string | null; nickname: string }) => + request('/api/classes', { + method: 'POST', + body: input, + }), + + joinClass: (input: { code: string; nickname: string }) => + request('/api/classes/join', { method: 'POST', body: input }), + + me: (token?: string | null) => request('/api/classes/me', { token }), + + listExams: (from: string, to: string) => + request(`/api/exams?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`), + + createExam: (input: unknown) => request('/api/exams', { method: 'POST', body: input }), + + updateExam: (id: number, input: unknown) => + request(`/api/exams/${id}`, { method: 'PATCH', body: input }), + + deleteExam: (id: number) => request(`/api/exams/${id}`, { method: 'DELETE' }), +} + +/** + * Flux SSE du calendrier. `EventSource` ne permettant pas de poser + * d'en-têtes, le jeton passe en query string (le serveur l'accepte). + */ +export function openStream(onEvent: (event: StreamEvent) => void): () => void { + const token = getToken() + if (!token) return () => {} + + const source = new EventSource(`${getBaseUrl()}/api/stream?token=${encodeURIComponent(token)}`) + + const handle = (event: MessageEvent): void => { + try { + onEvent(JSON.parse(event.data) as StreamEvent) + } catch { + // trame illisible : on l'ignore plutôt que de casser le flux + } + } + + source.onmessage = handle + source.onerror = () => { + // EventSource reconnecte tout seul ; on ne ferme pas ici. + } + + return () => source.close() +} diff --git a/web/src/config.ts b/web/src/config.ts new file mode 100644 index 0000000..5572adb --- /dev/null +++ b/web/src/config.ts @@ -0,0 +1,17 @@ +const TOKEN_KEY = 'meteo-exam.token' +const BASE_KEY = 'meteo-exam.baseUrl' + +/** `10.0.2.2` = l'hôte de la machine depuis l'émulateur Android. */ +export const DEFAULT_BASE_URL = 'http://10.0.2.2:8787' + +export const getToken = (): string | null => localStorage.getItem(TOKEN_KEY) +export const setToken = (token: string): void => localStorage.setItem(TOKEN_KEY, token) +export const clearToken = (): void => localStorage.removeItem(TOKEN_KEY) + +export const getBaseUrl = (): string => localStorage.getItem(BASE_KEY) ?? DEFAULT_BASE_URL + +export function setBaseUrl(url: string): string { + const clean = url.trim().replace(/\/+$/, '') + localStorage.setItem(BASE_KEY, clean) + return clean +} diff --git a/web/src/dates.ts b/web/src/dates.ts new file mode 100644 index 0000000..7d095e7 --- /dev/null +++ b/web/src/dates.ts @@ -0,0 +1,63 @@ +export const DAY_MS = 86_400_000 + +const DAYS = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'] as const +const MONTHS = [ + 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', + 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre', +] as const + +/** Clé de regroupement locale : `AAAA-MM-JJ`. Utile comme clé de Map. */ +export function dayKey(date: Date): string { + const y = date.getFullYear() + const m = String(date.getMonth() + 1).padStart(2, '0') + const d = String(date.getDate()).padStart(2, '0') + return `${y}-${m}-${d}` +} + +export function parseDayKey(key: string): Date { + const [y, m, d] = key.split('-').map(Number) + return new Date(y!, (m ?? 1) - 1, d ?? 1) +} + +export const startOfDay = (date: Date): Date => + new Date(date.getFullYear(), date.getMonth(), date.getDate()) + +/** Les `count` jours qui suivent aujourd'hui, en incluant aujourd'hui. */ +export function upcomingDays(count: number): Date[] { + const first = startOfDay(new Date()) + return Array.from({ length: count }, (_, i) => new Date(first.getTime() + i * DAY_MS)) +} + +export const isSameDay = (a: Date, b: Date): boolean => dayKey(a) === dayKey(b) + +export const dayName = (date: Date): string => DAYS[date.getDay()]! + +export const dayNameShort = (date: Date): string => dayName(date).slice(0, 3).toUpperCase() + +export const dayNumber = (date: Date): string => String(date.getDate()) + +export function longDate(date: Date): string { + return `${dayName(date)} ${date.getDate()} ${MONTHS[date.getMonth()]}` +} + +export function shortDate(date: Date): string { + return `${date.getDate()} ${MONTHS[date.getMonth()]!.slice(0, 4)}.` +} + +export const isoDayInput = (date: Date): string => dayKey(date) + +export function formatTime(iso: string): string { + return new Date(iso).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) +} + +/** « 1 h 30 », « 45 min ». */ +export function formatDuration(minutes: number): string { + if (minutes < 60) return `${minutes} min` + const h = Math.floor(minutes / 60) + const m = minutes % 60 + return m === 0 ? `${h} h` : `${h} h ${String(m).padStart(2, '0')}` +} + +export function formatWeight(weight: number): string { + return Number.isInteger(weight) ? weight.toFixed(1) : weight.toString() +} diff --git a/web/src/dom.ts b/web/src/dom.ts new file mode 100644 index 0000000..4798619 --- /dev/null +++ b/web/src/dom.ts @@ -0,0 +1,46 @@ +export function h( + tag: K, + props: Partial & { class?: string; dataset?: Record } = {}, + ...children: (Node | string | null | undefined | false)[] +): HTMLElementTagNameMap[K] { + const el = document.createElement(tag) + + for (const [key, value] of Object.entries(props)) { + if (value === undefined || value === null) continue + if (key === 'class') el.className = String(value) + else if (key === 'dataset') Object.assign(el.dataset, value) + else if (key === 'style') Object.assign(el.style, value) + else if (key.startsWith('on') && typeof value === 'function') { + el.addEventListener(key.slice(2).toLowerCase(), value as EventListener) + } else if (key === 'html') el.innerHTML = String(value) + else if (key in el) (el as unknown as Record)[key] = value + else el.setAttribute(key, String(value)) + } + + for (const child of children) { + if (child === null || child === undefined || child === false) continue + el.append(typeof child === 'string' ? document.createTextNode(child) : child) + } + return el +} + +export const clear = (el: HTMLElement): void => { + el.replaceChildren() +} + +/** Remplace les enfants d'un conteneur en une seule passe. */ +export const mount = (el: HTMLElement, ...children: Node[]): void => { + el.replaceChildren(...children) +} + +/** + * Échappe une valeur destinée à `innerHTML` (libellés de 서버, notes libres). + * Toutes les données de classe sont saisies par des élèves : rien n'est fiable. + */ +export const escapeHtml = (value: string): string => + value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..6d53013 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,109 @@ +import './styles.css' +import { h, mount } from './dom' +import { bootstrap, getState, isAdmin, subscribe } from './store' +import { renderJoin } from './views/join' +import { renderWeek } from './views/week' +import { renderRadar } from './views/radar' +import { renderMap } from './views/map' +import { renderPlus } from './views/plus' +import { getBaseUrl } from './config' + +type Tab = 'meteo' | 'radar' | 'carte' | 'plus' + +const app = document.getElementById('app')! +let activeTab: Tab = 'meteo' + +const TABS: { id: Tab; label: string; icon: string }[] = [ + { id: 'meteo', label: 'Météo', icon: 'fa-cloud-sun' }, + { id: 'radar', label: 'Radar', icon: 'fa-satellite-dish' }, + { id: 'carte', label: 'La Carte', icon: 'fa-map-marked-alt' }, + { id: 'plus', label: 'Plus', icon: 'fa-ellipsis-h' }, +] + +function renderApp(): void { + const state = getState() + + // Écran d'accueil : pas de session valide. + if (!state.session) { + const container = h('div', { class: 'mobile-container' }) + const main = h('main', { class: 'flex-1 overflow-y-auto no-scrollbar p-5' }) + renderJoin(main, renderApp) + container.append(main) + mount(app, container) + return + } + + // App pleine : header + contenu + nav. + const container = h('div', { class: 'mobile-container' }) + + const header = h( + 'header', + { class: 'pt-8 pb-3 px-6 bg-slate-900/80 backdrop-blur-md flex justify-between items-center z-20 border-b border-slate-800/60' }, + h( + 'div', + { class: 'flex items-center gap-3' }, + h('div', { class: 'w-9 h-9 rounded-2xl bg-gradient-to-tr from-sky-400 to-indigo-500 flex items-center justify-center text-white font-black shadow-md text-sm' }, 'ME'), + h('div', {}, + h('h1', { class: 'text-lg font-extrabold text-white tracking-tight leading-none' }, 'Météo Exam'), + h('p', { class: 'text-xs font-bold text-sky-400 mt-0.5' }, `${state.session.klass.name}${state.session.klass.level ? ` · ${state.session.klass.level}` : ''}`), + ), + ), + h( + 'div', + { class: 'flex items-center gap-2' }, + isAdmin() + ? h('span', { class: 'inline-flex items-center px-2.5 py-1 rounded-full text-xs font-extrabold bg-sky-500/10 text-sky-400 border border-sky-500/20' }, + h('i', { class: 'fa-solid fa-user-shield mr-1' }), 'Admin') + : null, + h('span', { class: 'inline-flex items-center px-2.5 py-1 rounded-full text-xs font-extrabold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' }, + h('span', { class: 'w-2 h-2 rounded-full bg-emerald-500 animate-ping mr-1.5' }), + 'Direct'), + ), + ) + + const main = h('main', { class: 'flex-1 overflow-y-auto no-scrollbar p-5 pb-28 relative' }) + const rerender = (): void => renderApp() + + switch (activeTab) { + case 'meteo': renderWeek(main, rerender); break + case 'radar': renderRadar(main, rerender); break + case 'carte': renderMap(main, rerender); break + case 'plus': renderPlus(main, rerender); break + } + + const nav = h( + 'nav', + { class: 'absolute bottom-0 left-0 right-0 bg-slate-900/95 backdrop-blur-xl border-t border-slate-800 px-4 py-3 flex justify-around items-center z-30' }, + ...TABS.map((tab) => { + const active = tab.id === activeTab + return h( + 'button', + { + class: `flex flex-col items-center gap-1 transition-colors ${active ? 'text-sky-400' : 'text-slate-500'}`, + onclick: () => { + activeTab = tab.id + renderApp() + }, + }, + tab.id === 'radar' + ? h('div', { class: 'relative' }, + h('i', { class: `fa-solid ${tab.icon} text-xl` }), + h('span', { class: 'absolute -top-1 -right-1 w-2 h-2 bg-rose-500 rounded-full' })) + : h('i', { class: `fa-solid ${tab.icon} text-xl` }), + h('span', { class: 'text-[10px] font-black tracking-wide' }, tab.label), + ) + }), + ) + + container.append(header, main, nav) + mount(app, container) +} + +// --- Bootstrapping ----------------------------------------------------------- + +subscribe(renderApp) + +void bootstrap().then(() => { + renderApp() + console.log(`Météo Exam — serveur : ${getBaseUrl()}`) +}) diff --git a/web/src/store.ts b/web/src/store.ts new file mode 100644 index 0000000..a3a7acb --- /dev/null +++ b/web/src/store.ts @@ -0,0 +1,188 @@ +import type { Exam, Session } from '@meteo-exam/shared' +import { dominantKind, type WeatherKind } from '@meteo-exam/shared' +import { api, openStream } from './api' +import { clearToken, getToken, setToken } from './config' +import { dayKey, upcomingDays } from './dates' + +const WINDOW_DAYS = 14 + +type Listener = () => void + +interface State { + session: Session | null + exams: Exam[] + /** Jours affichés par la frise, indépendamment de la fenêtre chargée. */ + stripDays: Date[] + selectedDay: string | null + loading: boolean + error: string | null + /** Passé à true quand le jeton est refusé : l'app revient à l'écran d'accueil. */ + signedOut: boolean +} + +const state: State = { + session: null, + exams: [], + stripDays: upcomingDays(7), + selectedDay: null, + loading: false, + error: null, + signedOut: false, +} + +const listeners = new Set() +let closeStream: (() => void) | null = null + +export const subscribe = (listener: Listener): (() => void) => { + listeners.add(listener) + return () => listeners.delete(listener) +} + +const emit = (): void => { + for (const listener of listeners) listener() +} + +export const getState = (): Readonly => state + +const windowBounds = (): { from: string; to: string } => { + const from = new Date() + from.setHours(0, 0, 0, 0) + // On charge large : la frise fait 7 jours mais les cartes listent la fenêtre. + const to = new Date(from.getTime() + WINDOW_DAYS * 86_400_000) + return { from: from.toISOString(), to: to.toISOString() } +} + +export async function loadExams(): Promise { + const token = getToken() + if (!token) return + const { from, to } = windowBounds() + try { + state.exams = await api.listExams(from, to) + state.error = null + } catch (error) { + state.error = error instanceof Error ? error.message : 'Chargement impossible' + } + emit() +} + +function connectStream(): void { + closeStream?.() + closeStream = openStream((event) => { + if (event.type === 'ping') return + if (event.type === 'exam.deleted') { + const idx = state.exams.findIndex((e) => e.id === event.examId) + if (idx >= 0) state.exams.splice(idx, 1) + } else { + const index = state.exams.findIndex((e) => e.id === event.exam.id) + if (index >= 0) state.exams[index] = event.exam + else state.exams.push(event.exam) + } + state.exams.sort((a, b) => a.startsAt.localeCompare(b.startsAt)) + emit() + }) +} + +/** Restaure la session au démarrage à partir du jeton stocké. */ +export async function bootstrap(): Promise { + const token = getToken() + if (!token) { + emit() + return + } + state.loading = true + emit() + try { + state.session = await api.me(token) + state.signedOut = false + await loadExams() + connectStream() + } catch { + clearToken() + state.session = null + state.exams = [] + state.signedOut = true + } finally { + state.loading = false + emit() + } +} + +export async function loginAs(token: string, session: Session): Promise { + setToken(token) + state.session = session + state.signedOut = false + state.loading = true + emit() + await loadExams() + connectStream() + state.loading = false + emit() +} + +export function signOut(): void { + closeStream?.() + closeStream = null + clearToken() + state.session = null + state.exams = [] + state.selectedDay = null + state.signedOut = false + state.error = null + emit() +} + +export function selectDay(key: string | null): void { + state.selectedDay = state.selectedDay === key ? null : key + emit() +} + +export function setError(message: string | null): void { + state.error = message + emit() +} + +// --- Sélecteurs ------------------------------------------------------------- + +export const isAdmin = (): boolean => state.session?.member.role === 'admin' + +export function examsByDay(): Map { + const grouped = new Map() + for (const exam of state.exams) { + const key = dayKey(new Date(exam.startsAt)) + const bucket = grouped.get(key) + if (bucket) bucket.push(exam) + else grouped.set(key, [exam]) + } + return grouped +} + +export function dayStatus(key: string): WeatherKind { + return dominantKind(examsByDay().get(key) ?? []) +} + +export function todayExams(): Exam[] { + return examsByDay().get(dayKey(new Date())) ?? [] +} + +export function findExam(id: number): Exam | undefined { + return state.exams.find((e) => e.id === id) +} + +// --- Écriture (admin) ------------------------------------------------------- + +export async function saveExam(input: unknown, id?: number): Promise { + const exam = id === undefined ? await api.createExam(input) : await api.updateExam(id, input) + const index = state.exams.findIndex((e) => e.id === exam.id) + if (index >= 0) state.exams[index] = exam + else state.exams.push(exam) + state.exams.sort((a, b) => a.startsAt.localeCompare(b.startsAt)) + state.error = null + emit() + return exam +} + +export async function removeExam(id: number): Promise { + await api.deleteExam(id) + state.exams = state.exams.filter((e) => e.id !== id) + emit() +} diff --git a/web/src/styles.css b/web/src/styles.css new file mode 100644 index 0000000..810be84 --- /dev/null +++ b/web/src/styles.css @@ -0,0 +1,66 @@ +@import 'tailwindcss'; + +/* Les classes Tailwind provenant de @meteo-exam/shared doivent être scannées + par Tailwind : elles ne sont pas dans les sources du package web. */ +@source '../../shared/src'; + +html, +body { + height: 100%; +} + +body { + background-color: #020617; + -webkit-tap-highlight-color: transparent; + overscroll-behavior-y: none; + user-select: none; + font-family: 'Nunito', sans-serif; +} + +/* Hide scrollbars */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} +.no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* Mobile phone container (même forme que le prototype) */ +.mobile-container { + width: 100%; + max-width: 430px; + height: 100vh; + max-height: 932px; + margin: 0 auto; + background-color: #0f172a; + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; +} + +@media (min-width: 450px) { + .mobile-container { + height: 90vh; + border-radius: 48px; + margin-top: 5vh; + border: 8px solid #1e293b; + box-shadow: 0 25px 60px -15px rgba(0, 0, 0, 0.7); + } +} + +@keyframes pulseGlow { + 0%, + 100% { + opacity: 0.8; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.05); + } +} +.animate-pulse-glow { + animation: pulseGlow 3s infinite ease-in-out; +} diff --git a/web/src/toast.ts b/web/src/toast.ts new file mode 100644 index 0000000..c7ea73b --- /dev/null +++ b/web/src/toast.ts @@ -0,0 +1,39 @@ +type ToastKind = 'success' | 'reject' | 'info' + +let container: HTMLElement | null = null +let hideTimer: number | undefined + +function ensureContainer(): HTMLElement { + if (container && document.body.contains(container)) return container + container = document.createElement('div') + container.className = + 'fixed top-20 left-1/2 -translate-x-1/2 z-[100] flex items-center gap-2 px-4 py-2 rounded-full text-xs font-bold shadow-2xl border border-slate-700 bg-slate-800 text-slate-100 transition-all duration-300 opacity-0 pointer-events-none' + document.body.append(container) + return container +} + +const ICONS: Record = { + success: 'fa-solid fa-circle-check text-emerald-400', + reject: 'fa-solid fa-circle-xmark text-rose-400', + info: 'fa-solid fa-circle-info text-sky-400', +} + +export function showToast(message: string, kind: ToastKind = 'success'): void { + const el = ensureContainer() + el.replaceChildren() + + const icon = document.createElement('i') + icon.className = ICONS[kind] + const text = document.createElement('span') + text.textContent = message + el.append(icon, text) + + el.classList.remove('opacity-0', '-translate-y-2', 'pointer-events-none') + el.classList.add('opacity-100', 'translate-y-0') + + window.clearTimeout(hideTimer) + hideTimer = window.setTimeout(() => { + el.classList.remove('opacity-100', 'translate-y-0') + el.classList.add('opacity-0', '-translate-y-2', 'pointer-events-none') + }, 2600) +} diff --git a/web/src/views/cards.ts b/web/src/views/cards.ts new file mode 100644 index 0000000..f1cc299 --- /dev/null +++ b/web/src/views/cards.ts @@ -0,0 +1,103 @@ +import { WEATHER, type WeatherKind } from '@meteo-exam/shared' +import { h } from '../dom' +import { formatDuration, formatTime, formatWeight, longDate } from '../dates' +import type { Exam } from '@meteo-exam/shared' + +/** Carte compacte d'une évaluation dans la liste. */ +export function examCard(exam: Exam, onOpen: (exam: Exam) => void): HTMLElement { + const meta = WEATHER[exam.kind] + + return h( + 'button', + { + class: + 'w-full text-left bg-slate-800/80 border border-slate-700/60 rounded-2xl p-4 flex items-center justify-between gap-3.5 shadow-sm active:scale-[0.99] transition-transform', + onclick: () => onOpen(exam), + }, + h( + 'div', + { class: 'flex items-center gap-3.5 min-w-0' }, + h( + 'div', + { class: `w-12 h-12 shrink-0 rounded-2xl border flex items-center justify-center text-2xl ${meta.chip}` }, + meta.icon, + ), + h( + 'div', + { class: 'min-w-0' }, + h('p', { class: `text-xs font-extrabold uppercase tracking-wide truncate ${meta.accent}` }, + `${formatTime(exam.startsAt)} · ${meta.label}`), + h('h4', { class: 'text-base font-bold text-slate-100 truncate' }, exam.subject), + h( + 'p', + { class: 'text-xs text-slate-400 font-semibold truncate' }, + [exam.chapter, exam.room].filter(Boolean).join(' · ') || formatDuration(exam.durationMin), + ), + ), + ), + h( + 'div', + { class: 'shrink-0 flex flex-col items-end gap-1.5' }, + h( + 'span', + { class: 'px-2.5 py-1 rounded-lg bg-slate-700/50 text-slate-300 text-xs font-bold' }, + exam.weight > 0 ? `Coeff ${formatWeight(exam.weight)}` : ' coursework', + ), + h('span', { class: 'text-[10px] font-bold text-slate-500' }, formatDuration(exam.durationMin)), + ), + ) +} + +/** Fiche détaillée dans la modale. */ +export function examDetail(exam: Exam): HTMLElement { + const meta = WEATHER[exam.kind] + + const row = (icon: string, label: string, value: string | null | undefined): HTMLElement | null => { + if (!value) return null + return h( + 'div', + { class: 'flex items-start gap-3 py-2.5' }, + h('i', { class: `fa-solid ${icon} text-slate-500 w-4 text-center mt-0.5` }), + h( + 'div', + { class: 'min-w-0' }, + h('p', { class: 'text-[10px] font-black uppercase tracking-wider text-slate-500' }, label), + h('p', { class: 'text-sm font-bold text-slate-200 break-words' }, value), + ), + ) + } + + return h( + 'div', + { class: 'space-y-1' }, + h( + 'div', + { class: 'flex items-center gap-3.5 pb-3' }, + h('div', { class: `w-14 h-14 rounded-2xl border flex items-center justify-center text-3xl ${meta.chip}` }, meta.icon), + h( + 'div', + { class: 'min-w-0' }, + h('h2', { class: 'text-xl font-black text-slate-100 leading-tight' }, exam.subject), + h('p', { class: `text-xs font-black uppercase tracking-wider mt-0.5 ${meta.accent}` }, meta.title), + ), + ), + h('div', { class: 'h-px bg-slate-700/60' }), + row('fa-solid fa-user-tie', 'Enseignant', exam.teacher), + row('fa-solid fa-door-open', 'Salle', exam.room), + row('fa-solid fa-book-open', 'Chapitre', exam.chapter), + row('fa-solid fa-calendar-day', 'Date', longDate(new Date(exam.startsAt))), + row('fa-solid fa-clock', 'Heure', formatTime(exam.startsAt)), + row('fa-solid fa-hourglass-half', 'Durée', formatDuration(exam.durationMin)), + row('fa-solid fa-gauge-high', 'Coefficient', exam.weight > 0 ? formatWeight(exam.weight) : ' coursework'), + exam.note ? row('fa-solid fa-note-sticky', 'Note', exam.note) : null, + h( + 'p', + { class: 'text-xs text-slate-400 font-medium leading-relaxed pt-3 mt-1 border-t border-slate-700/60' }, + meta.hint, + ), + ) +} + +export const weatherBadge = (kind: WeatherKind): HTMLElement => + h('span', { class: `inline-block px-3 py-1 rounded-xl text-xs font-black uppercase tracking-wider border ${WEATHER[kind].badge}` }, + `${WEATHER[kind].icon} ${WEATHER[kind].label}`) diff --git a/web/src/views/exam-form.ts b/web/src/views/exam-form.ts new file mode 100644 index 0000000..92d6ecc --- /dev/null +++ b/web/src/views/exam-form.ts @@ -0,0 +1,235 @@ +import type { Exam, WeatherKind } from '@meteo-exam/shared' +import { WEATHER, WEATHER_KINDS, WEIGHT_PRESETS, kindFromWeight } from '@meteo-exam/shared' +import { h } from '../dom' +import { isoDayInput } from '../dates' +import { saveExam, setError } from '../store' +import { showToast } from '../toast' + +interface Draft { + subject: string + teacher: string + room: string + chapter: string + day: string + time: string + durationMin: number + weight: number + kind: WeatherKind + note: string +} + +const inputClass = + 'w-full bg-slate-900/80 border border-slate-700/80 rounded-xl px-3.5 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-sky-500 transition-colors' + +const labelClass = 'block text-[10px] font-black uppercase tracking-wider text-slate-500 mb-1.5' + +function field(label: string, control: HTMLElement, hint?: string): HTMLElement { + return h( + 'label', + { class: 'block' }, + h('span', { class: labelClass }, label), + control, + hint ? h('span', { class: 'block text-[10px] text-slate-600 font-semibold mt-1' }, hint) : null, + ) +} + +function draftFrom(exam: Exam | undefined): Draft { + if (exam) { + const date = new Date(exam.startsAt) + return { + subject: exam.subject, + teacher: exam.teacher ?? '', + room: exam.room ?? '', + chapter: exam.chapter ?? '', + day: isoDayInput(date), + time: `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`, + durationMin: exam.durationMin, + weight: exam.weight, + kind: exam.kind, + note: exam.note ?? '', + } + } + const now = new Date() + now.setMinutes(0, 0, 0) + now.setHours(Math.max(8, now.getHours() + 1)) + return { + subject: '', + teacher: '', + room: '', + chapter: '', + day: isoDayInput(now), + time: `${String(now.getHours()).padStart(2, '0')}:00`, + durationMin: 60, + weight: 2, + kind: 'orage', + note: '', + } +} + +function closeOverlay(overlay: HTMLElement): void { + overlay.remove() +} + +/** Modale d'ajout / modification. `exam` absent = création. */ +export function openExamForm(exam: Exam | undefined, onSaved: () => void): void { + const draft = draftFrom(exam) + const editing = exam !== undefined + + const overlay = h('div', { + class: 'fixed inset-0 z-[95] bg-slate-950/80 backdrop-blur-sm flex items-end sm:items-center justify-center', + }) + + const errorBox = h('p', { class: 'hidden text-xs font-bold text-rose-300 bg-rose-500/10 border border-rose-500/25 rounded-xl px-3 py-2' }) + + const showError = (message: string): void => { + errorBox.textContent = message + errorBox.classList.remove('hidden') + } + + // --- Champs --------------------------------------------------------------- + const subject = h('input', { class: inputClass, value: draft.subject, placeholder: 'Mathématiques' }) + const teacher = h('input', { class: inputClass, value: draft.teacher, placeholder: 'M. Roussel' }) + const room = h('input', { class: inputClass, value: draft.room, placeholder: 'B204' }) + const chapter = h('input', { class: inputClass, value: draft.chapter, placeholder: 'Chapitre ou thème' }) + const day = h('input', { type: 'date', class: inputClass, value: draft.day }) + const time = h('input', { type: 'time', class: inputClass, value: draft.time }) + const note = h('textarea', { class: `${inputClass} min-h-[70px] resize-none`, placeholder: 'Circonscription, calculatrice autorisée…' }) + note.value = draft.note + + const duration = h('input', { type: 'number', class: inputClass, value: String(draft.durationMin), min: '5', max: '480', step: '5' }) + const weight = h('input', { type: 'number', class: inputClass, value: String(draft.weight), min: '0', max: '20', step: '0.5' }) + + // Le statut découle du coefficient tant qu'on ne le surcharge pas à la main. + let kindOverride = false + const kindPreview = h('div', { class: 'flex flex-wrap gap-1.5' }) + + const renderKinds = (): void => { + kindPreview.replaceChildren( + ...WEATHER_KINDS.map((kind) => { + const active = draft.kind === kind + return h( + 'button', + { + type: 'button', + class: `px-2.5 py-1.5 rounded-xl text-[11px] font-extrabold border transition-colors ${ + active ? WEATHER[kind].badge : 'bg-slate-800/60 border-slate-700 text-slate-400' + }`, + onclick: () => { + draft.kind = kind + kindOverride = true + renderKinds() + }, + }, + `${WEATHER[kind].icon} ${WEATHER[kind].label}`, + ) + }), + ) + } + + weight.addEventListener('input', () => { + const value = Number(weight.value) + if (!Number.isFinite(value)) return + draft.weight = value + if (!kindOverride) { + draft.kind = kindFromWeight(value) + renderKinds() + } + }) + renderKinds() + + // --- Actions -------------------------------------------------------------- + const submit = async (): Promise => { + if (!subject.value.trim()) { + showError('La matière est obligatoire.') + return + } + const startsAt = new Date(`${day.value}T${time.value || '08:00'}:00`) + if (Number.isNaN(startsAt.getTime())) { + showError('Date ou heure invalide.') + return + } + const durationMin = Number(duration.value) + if (!Number.isFinite(durationMin) || durationMin < 5) { + showError('Durée minimum : 5 minutes.') + return + } + + submitBtn.disabled = true + submitBtn.classList.add('opacity-60') + try { + await saveExam( + { + subject: subject.value.trim(), + teacher: teacher.value.trim() || null, + room: room.value.trim() || null, + chapter: chapter.value.trim() || null, + startsAt: startsAt.toISOString(), + durationMin, + weight: Number(weight.value) || 0, + kind: draft.kind, + note: note.value.trim() || null, + }, + exam?.id, + ) + showToast(editing ? 'Évaluation mise à jour' : 'Évaluation ajoutée', 'success') + closeOverlay(overlay) + onSaved() + } catch (error) { + setError(error instanceof Error ? error.message : 'Enregistrement impossible') + showError(error instanceof Error ? error.message : 'Enregistrement impossible') + submitBtn.disabled = false + submitBtn.classList.remove('opacity-60') + } + } + + const submitBtn = h( + 'button', + { class: 'flex-1 bg-sky-500 text-slate-950 text-sm font-extrabold py-3 rounded-xl active:scale-95 transition-transform', onclick: submit }, + editing ? 'Enregistrer' : 'Ajouter au bulletin', + ) + + const panel = h( + 'div', + { class: 'w-full sm:max-w-md bg-slate-900 border border-slate-700 rounded-t-[2rem] sm:rounded-[2rem] p-5 max-h-[90vh] overflow-y-auto no-scrollbar shadow-2xl' }, + h( + 'div', + { class: 'flex items-start justify-between gap-3 mb-4' }, + h( + 'div', + {}, + h('h2', { class: 'text-lg font-black text-slate-100' }, editing ? 'Modifier l’évaluation' : 'Nouvelle évaluation'), + h('p', { class: 'text-xs text-slate-400 font-semibold mt-0.5' }, 'Le statut météo découle du coefficient.'), + ), + h('button', { class: 'text-slate-400 hover:text-slate-200 text-lg', onclick: () => closeOverlay(overlay) }, + h('i', { class: 'fa-solid fa-xmark' })), + ), + h( + 'div', + { class: 'space-y-3.5' }, + field('Matière', subject), + h('div', { class: 'grid grid-cols-2 gap-3' }, field('Enseignant', teacher), field('Salle', room)), + field('Chapitre', chapter), + h('div', { class: 'grid grid-cols-2 gap-3' }, field('Date', day), field('Heure', time)), + h('div', { class: 'grid grid-cols-2 gap-3' }, field('Durée (min)', duration), field('Coefficient', weight)), + field( + 'Statut météo', + kindPreview, + kindOverride ? 'Statut forcé manuellement.' : WEIGHT_PRESETS.find((p) => p.value === draft.weight)?.label, + ), + field('Note', note), + errorBox, + h('div', { class: 'flex gap-2 pt-1' }, + h('button', { class: 'bg-slate-800 border border-slate-700 text-slate-300 text-sm font-extrabold px-4 py-3 rounded-xl active:scale-95 transition-transform', onclick: () => closeOverlay(overlay) }, + 'Annuler'), + submitBtn, + ), + ), + ) + + overlay.append(panel) + overlay.addEventListener('click', (event) => { + if (event.target === overlay) closeOverlay(overlay) + }) + document.body.append(overlay) + subject.focus() +} diff --git a/web/src/views/join.ts b/web/src/views/join.ts new file mode 100644 index 0000000..7cd9d4c --- /dev/null +++ b/web/src/views/join.ts @@ -0,0 +1,167 @@ +import { h, mount } from '../dom' +import { api } from '../api' +import { getBaseUrl, setBaseUrl } from '../config' +import { loginAs } from '../store' +import { showToast } from '../toast' +import { DEFAULT_BASE_URL } from '../config' + +const inputClass = + 'w-full bg-slate-900/80 border border-slate-700/80 rounded-xl px-3.5 py-3 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-sky-500 transition-colors' + +const labelClass = 'block text-[10px] font-black uppercase tracking-wider text-slate-500 mb-1.5' + +type Mode = 'join' | 'create' + +export function renderJoin(root: HTMLElement, rerender: () => void): void { + let _mode: Mode = 'join' + const getMode = (): Mode => _mode + + const nameInput = h('input', { class: inputClass, placeholder: 'Terminale S2' }) + const levelInput = h('input', { class: inputClass, placeholder: 'Lycée' }) + const codeInput = h('input', { class: inputClass, placeholder: 'TERMS2', autocomplete: 'off' }) + const nicknameInput = h('input', { class: inputClass, placeholder: 'Votre pseudo' }) + const serverInput = h('input', { + class: inputClass, + placeholder: DEFAULT_BASE_URL, + value: getBaseUrl(), + autocomplete: 'off', + inputMode: 'url', + }) + + const errorBox = h('p', { class: 'hidden text-xs font-bold text-rose-300 bg-rose-500/10 border border-rose-500/25 rounded-xl px-3 py-2' }) + const codeHint = h('p', { class: 'hidden text-xs text-slate-500 font-semibold' }) + const submitBtn = h('button', { + class: 'w-full bg-sky-500 text-slate-950 text-sm font-extrabold py-3.5 rounded-xl active:scale-[0.98] transition-transform', + }) + const toggleBtn = h('button', { + class: 'w-full text-xs font-black text-sky-400 py-2.5 active:scale-95 transition-transform', + }) + + const showError = (message: string): void => { + errorBox.textContent = message + errorBox.classList.remove('hidden') + } + + const renderMode = (): void => { + const create = getMode() === 'create' + submitBtn.replaceChildren( + create ? 'Créer la classe' : 'Rejoindre la classe', + ) + toggleBtn.replaceChildren( + create ? 'Déjà un code ? Rejoindre une classe' : 'Créer une nouvelle classe', + ) + codeHint.classList.toggle('hidden', create) + nameInput.parentElement?.classList.toggle('hidden', !create) + levelInput.parentElement?.classList.toggle('hidden', !create) + codeInput.parentElement?.classList.toggle('hidden', create) + errorBox.classList.add('hidden') + } + + const submit = async (): Promise => { + const nickname = nicknameInput.value.trim() + if (!nickname) { + showError('Choisissez un pseudo.') + return + } + const baseUrl = serverInput.value.trim().replace(/\/+$/, '') || DEFAULT_BASE_URL + setBaseUrl(baseUrl) + + submitBtn.disabled = true + submitBtn.classList.add('opacity-60') + try { + if (getMode() === 'create') { + const name = nameInput.value.trim() + if (name.length < 2) { + showError('Nom de classe trop court.') + submitBtn.disabled = false + submitBtn.classList.remove('opacity-60') + return + } + const result = await api.createClass({ name, level: levelInput.value.trim() || null, nickname }) + showToast(`Classe créée — code ${result.code}`, 'success') + await loginAs(result.token, { member: result.member, klass: result.klass }) + } else { + const code = codeInput.value.trim() + if (!code) { + showError('Entrez le code de classe.') + submitBtn.disabled = false + submitBtn.classList.remove('opacity-60') + return + } + const result = await api.joinClass({ code, nickname }) + await loginAs(result.token, { member: result.member, klass: result.klass }) + } + rerender() + } catch (error) { + const message = error instanceof Error ? error.message : 'Connexion impossible' + showError(message) + submitBtn.disabled = false + submitBtn.classList.remove('opacity-60') + } + } + + submitBtn.addEventListener('click', submit) + toggleBtn.addEventListener('click', () => { + _mode = _mode === 'join' ? 'create' : 'join' + renderMode() + }) + codeInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') submit() + }) + nicknameInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') submit() + }) + codeInput.addEventListener('input', () => { + codeInput.value = codeInput.value.toUpperCase() + }) + + renderMode() + + mount( + root, + h( + 'div', + { class: 'flex flex-col items-center justify-center min-h-[70vh] px-6' }, + h( + 'div', + { class: 'w-20 h-20 rounded-[1.75rem] bg-gradient-to-tr from-sky-400 to-indigo-500 flex items-center justify-center text-white font-black text-3xl shadow-float mb-6' }, + 'ME', + ), + h('h1', { class: 'text-2xl font-black text-white text-center tracking-tight' }, 'Météo Exam'), + h('p', { class: 'text-xs font-bold text-sky-400 mt-1 mb-8 text-center' }, + 'Le calendrier des évaluations, en bulletin météo.'), + h( + 'div', + { class: 'w-full space-y-4' }, + field('Votre pseudo', nicknameInput), + getMode() === 'join' ? field('Code de classe', codeInput) : null, + getMode() === 'join' + ? h('p', { class: 'text-[11px] text-slate-500 font-semibold -mt-2' }, + 'Demander le code à l’admin de la classe (ex. TERMS2).') + : null, + getMode() === 'create' ? field('Nom de la classe', nameInput) : null, + getMode() === 'create' ? field('Niveau (optionnel)', levelInput) : null, + field('Serveur', serverInput, 'Adresse de l’API. Emulateur Android : 10.0.2.2:8787'), + errorBox, + submitBtn, + toggleBtn, + codeHint, + ), + h( + 'p', + { class: 'text-[10px] text-slate-600 font-semibold mt-8 text-center' }, + 'L’admin de la classe crée le calendrier. Les élèves le consultent et participent au radar.', + ), + ), + ) + + function field(label: string, control: HTMLElement, hint?: string): HTMLElement { + return h( + 'label', + { class: 'block' }, + h('span', { class: labelClass }, label), + control, + hint ? h('span', { class: 'block text-[10px] text-slate-600 font-semibold mt-1' }, hint) : null, + ) + } +} diff --git a/web/src/views/map.ts b/web/src/views/map.ts new file mode 100644 index 0000000..39f4b4f --- /dev/null +++ b/web/src/views/map.ts @@ -0,0 +1,152 @@ +import { WEATHER } from '@meteo-exam/shared' +import { h, mount } from '../dom' + +const SVG_NS = 'http://www.w3.org/2000/svg' + +function svgEl( + tag: K, + attrs: Record = {}, +): SVGElementTagNameMap[K] { + const el = document.createElementNS(SVG_NS, tag) + for (const [key, value] of Object.entries(attrs)) el.setAttribute(key, String(value)) + return el +} + +interface Region { + id: string + subject: string + kind: keyof typeof WEATHER + weight: number + desc: string + d: string + labelX: number + labelY: number + emojiX: number + emojiY: number +} + +const REGIONS: Region[] = [ + { id: 'maths', subject: 'Mathématiques', kind: 'tempete', weight: 4, d: 'M 150,40 Q 250,20 350,50 L 320,160 Q 220,180 160,150 Z', labelX: 230, labelY: 120, emojiX: 230, emojiY: 90, desc: 'Avis de forte tempête. Contrôle continu à fort coefficient programmé.' }, + { id: 'svt', subject: 'SVT', kind: 'vent', weight: 2, d: 'M 350,50 Q 460,80 440,200 L 310,220 Q 300,160 320,160 Z', labelX: 375, labelY: 160, emojiX: 375, emojiY: 130, desc: 'Risque de petit QCM surprise en début d’heure sur le chapitre de la géologie.' }, + { id: 'physique', subject: 'Physique-Chimie', kind: 'brouillard', weight: 2, d: 'M 160,150 Q 220,180 310,220 L 280,320 Q 180,310 140,240 Z', labelX: 220, labelY: 255, emojiX: 220, emojiY: 225, desc: 'Suspicion de TP évalué la semaine prochaine. Rumeurs soutenues par la classe.' }, + { id: 'histoire', subject: 'Histoire-Géo', kind: 'soleil', weight: 1, d: 'M 150,40 L 160,150 Q 140,240 60,220 Q 30,120 150,40 Z', labelX: 90, labelY: 160, emojiX: 90, emojiY: 130, desc: 'Conditions idéales. Aucun contrôle prévu d’ici la fin de la semaine.' }, + { id: 'francais', subject: 'Français', kind: 'soleil', weight: 1, d: 'M 140,240 Q 180,310 280,320 L 260,380 Q 120,390 60,220 Z', labelX: 170, labelY: 340, emojiX: 170, emojiY: 315, desc: 'Aucune perturbation à signaler. Cours magistral standard.' }, +] + +/** Carte SVG par matière — figée, alimentée par les données en phase 2. */ +export function renderMap(root: HTMLElement, _rerender: () => void): void { + let selected = 'maths' + + const detailTitle = h('h3', { class: 'text-base font-black text-slate-100' }) + const detailStatus = h('p', { class: 'text-xs font-bold' }) + const detailCoeff = h('span', { class: 'bg-slate-700 text-slate-200 text-xs font-black px-2.5 py-1 rounded-lg' }) + const detailDesc = h('p', { class: 'text-xs text-slate-300 font-medium leading-relaxed mt-2 pt-2 border-t border-slate-700/60' }) + const detailIcon = h('span', { class: 'text-3xl' }) + + const statusClasses: Record = { + tempete: 'text-rose-400', + orage: 'text-rose-400', + vent: 'text-amber-400', + brouillard: 'text-purple-400', + soleil: 'text-emerald-400', + } + + const updateDetail = (region: Region): void => { + const meta = WEATHER[region.kind] + detailIcon.textContent = meta.icon + detailTitle.textContent = `Secteur : ${region.subject}` + detailStatus.textContent = `${meta.title} • ${meta.hint}` + detailStatus.className = `text-xs font-bold ${statusClasses[region.kind] ?? 'text-slate-400'}` + detailCoeff.textContent = region.weight > 0 ? `Coeff ${region.weight.toFixed(1)}` : ' Calme' + detailDesc.textContent = region.desc + } + + const paths = new Map() + const svg = svgEl('svg', { viewBox: '0 0 500 400' }) + svg.setAttribute('class', 'w-full h-full drop-shadow-md') + + for (const region of REGIONS) { + const meta = WEATHER[region.kind] + const path = svgEl('path', { + id: `region-${region.id}`, + d: region.d, + fill: meta.hex, + }) + path.setAttribute('class', 'map-region') + path.setAttribute('style', 'transition:all 0.3s cubic-bezier(0.4,0,0.2,1);cursor:pointer;stroke:#0f172a;stroke-width:2.5') + + path.addEventListener('click', () => { + selected = region.id + for (const p of paths.values()) p.classList.remove('active-region') + path.classList.add('active-region') + updateDetail(region) + }) + + paths.set(region.id, path) + svg.append(path) + + const emoji = svgEl('text', { + x: region.emojiX, + y: region.emojiY, + 'font-size': '22', + 'pointer-events': 'none', + }) + emoji.textContent = meta.icon + svg.append(emoji) + + const label = svgEl('text', { + x: region.labelX, + y: region.labelY, + 'font-size': '12', + 'font-weight': '900', + fill: '#ffffff', + 'text-anchor': 'middle', + 'pointer-events': 'none', + }) + label.textContent = region.subject + svg.append(label) + } + + paths.get(selected)?.classList.add('active-region') + updateDetail(REGIONS[0]!) + + mount( + root, + h( + 'div', + { class: 'space-y-4' }, + h( + 'div', + { class: 'flex justify-between items-center' }, + h('div', {}, + h('h2', { class: 'text-lg font-black text-slate-100' }, 'Carte Météorologique'), + h('p', { class: 'text-xs font-semibold text-slate-400' }, 'Secteurs géographiques par matière'), + ), + h('span', { class: 'px-2.5 py-1 rounded-full bg-sky-500/10 text-sky-400 text-xs font-bold border border-sky-500/20' }, + 'Interactive'), + ), + h( + 'div', + { class: 'bg-slate-900 border border-slate-800 rounded-3xl p-3 shadow-lg relative overflow-hidden aspect-[4/3] flex items-center justify-center' }, + svg, + ), + h( + 'div', + { class: 'bg-slate-800 border border-slate-700 rounded-2xl p-4' }, + h('div', { class: 'flex justify-between items-center mb-2' }, + h('div', { class: 'flex items-center gap-2.5' }, + detailIcon, + h('div', {}, detailTitle, detailStatus), + ), + detailCoeff, + ), + detailDesc, + ), + h( + 'p', + { class: 'text-[10px] text-slate-600 font-semibold text-center' }, + 'Les secteurs seront alimentés par les données de la classe en phase 2.', + ), + ), + ) +} diff --git a/web/src/views/plus.ts b/web/src/views/plus.ts new file mode 100644 index 0000000..d14cecf --- /dev/null +++ b/web/src/views/plus.ts @@ -0,0 +1,87 @@ +import { h, mount } from '../dom' +import { getBaseUrl, setBaseUrl, DEFAULT_BASE_URL } from '../config' +import { signOut } from '../store' +import { showToast } from '../toast' + +export function renderPlus(root: HTMLElement, rerender: () => void): void { + const serverInput = h('input', { + class: 'w-full bg-slate-900/80 border border-slate-700/80 rounded-xl px-3.5 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-sky-500 transition-colors', + value: getBaseUrl(), + autocomplete: 'off', + inputMode: 'url', + }) + + const legendItems: { icon: string; title: string; accent: string; desc: string }[] = [ + { icon: '☀️', title: 'Grand Soleil', accent: 'text-sky-400', desc: 'Journée calme. Aucun examen répertorié ni suspecté.' }, + { icon: '🌫️', title: 'Brouillard', accent: 'text-purple-400', desc: 'Zone d’incertitude. Rumeur ou sous-entendu d’un professeur.' }, + { icon: '💨', title: 'Coup de vent', accent: 'text-amber-400', desc: 'Interrogation surprise potentielle ou petit QCM (coeff faible).' }, + { icon: '🌩️', title: 'Orage / Pluie', accent: 'text-rose-400', desc: 'Devoir Surveillé (DS) confirmé ou évaluation de chapitre.' }, + { icon: '🚨', title: 'Alerte Rouge / Tempête', accent: 'text-red-500', desc: 'Examen blanc, épreuve commune ou gros devoir de fin de trimestre.' }, + ] + + mount( + root, + h( + 'div', + { class: 'space-y-5' }, + h( + 'div', + { class: 'bg-slate-800 border border-slate-700 rounded-2xl p-4' }, + h('h3', { class: 'text-xs font-black text-slate-400 uppercase tracking-wider mb-3' }, 'Serveur API'), + serverInput, + h('p', { class: 'text-[10px] text-slate-500 font-semibold mt-2' }, + 'Adresse de l’API. Sur l’émulateur Android : 10.0.2.2:8787'), + h( + 'button', + { + class: 'mt-3 w-full bg-sky-500 text-slate-950 text-xs font-extrabold py-2.5 rounded-xl active:scale-95 transition-transform', + onclick: () => { + setBaseUrl(serverInput.value.trim() || DEFAULT_BASE_URL) + showToast('Adresse du serveur enregistrée', 'success') + rerender() + }, + }, + 'Enregistrer l’adresse', + ), + ), + h( + 'div', + { class: 'bg-slate-800 border border-slate-700 rounded-2xl p-4' }, + h('h3', { class: 'text-xs font-black text-slate-400 uppercase tracking-wider mb-1' }, 'Session'), + h( + 'button', + { + class: 'w-full bg-rose-500/10 border border-rose-500/25 text-rose-400 text-xs font-extrabold py-2.5 rounded-xl active:scale-95 transition-transform', + onclick: () => { + signOut() + rerender() + }, + }, + h('i', { class: 'fa-solid fa-right-from-bracket mr-1.5' }), + 'Se déconnecter', + ), + ), + h('div', {}, + h('h3', { class: 'text-xs font-black text-slate-400 uppercase tracking-wider mb-2' }, 'Légende météorologique'), + h('div', { class: 'grid grid-cols-1 gap-2.5' }, + ...legendItems.map((item) => + h( + 'div', + { class: 'bg-slate-800/80 border border-slate-700/60 rounded-xl p-3 flex items-center gap-3' }, + h('span', { class: 'text-3xl' }, item.icon), + h('div', {}, + h('h4', { class: `text-sm font-black ${item.accent}` }, item.title), + h('p', { class: 'text-xs text-slate-300 font-medium' }, item.desc), + ), + ), + ), + ), + ), + h( + 'p', + { class: 'text-[10px] text-slate-600 font-semibold text-center' }, + 'Météo Exam — v0.1.0 · Radar de rumeurs et métriques en phase 2', + ), + ), + ) +} diff --git a/web/src/views/radar.ts b/web/src/views/radar.ts new file mode 100644 index 0000000..ef50cc9 --- /dev/null +++ b/web/src/views/radar.ts @@ -0,0 +1,57 @@ +import { h, mount } from '../dom' + +/** Radar de rumeurs — figé, branché sur le backend en phase 2. */ +export function renderRadar(root: HTMLElement, _rerender: () => void): void { + mount( + root, + h( + 'div', + { class: 'space-y-4' }, + h( + 'div', + { class: 'bg-slate-800 border border-slate-700 rounded-2xl p-3.5 shadow-md' }, + h( + 'div', + { class: 'flex gap-3' }, + h( + 'div', + { class: 'w-9 h-9 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center font-bold text-sm shrink-0' }, + h('i', { class: 'fa-solid fa-bullhorn' }), + ), + h('input', { + type: 'text', + placeholder: 'Signaler une suspicion d’examen… (bientôt disponible)', + disabled: true, + class: 'w-full bg-slate-900/80 border border-slate-700/80 rounded-xl px-3.5 py-2 text-sm text-slate-400 placeholder-slate-500 focus:outline-none', + }), + ), + h( + 'div', + { class: 'flex justify-between items-center mt-3 pt-2 border-t border-slate-700/50' }, + h('span', { class: 'text-xs text-slate-500 font-semibold' }, + h('i', { class: 'fa-solid fa-circle-info text-slate-500 mr-1' }), + 'Module à venir en phase 2'), + h('button', { + class: 'bg-slate-700 text-slate-400 px-4 py-1.5 rounded-xl font-extrabold text-xs', + disabled: true, + }, 'Publier'), + ), + ), + h( + 'div', + { class: 'bg-indigo-500/10 border border-indigo-500/20 rounded-xl p-3 flex items-center justify-between text-xs text-indigo-300 font-bold' }, + h('span', {}, + h('i', { class: 'fa-solid fa-hand-pointer mr-1.5' }), + 'Glissez pour confirmer ou rejeter les rumeurs — bientôt !'), + ), + h( + 'div', + { class: 'text-center py-16 px-6' }, + h('div', { class: 'text-5xl mb-4' }, '📡'), + h('p', { class: 'text-sm font-black text-slate-200' }, 'Radar de rumeurs — phase 2'), + h('p', { class: 'text-xs text-slate-400 font-semibold mt-1.5 leading-relaxed' }, + 'Le module collaboratif (soumettre une rumeur, voter, conversion automatique au seuil de 65 %) sera branché sur le serveur dans la prochaine itération.'), + ), + ), + ) +} diff --git a/web/src/views/week.ts b/web/src/views/week.ts new file mode 100644 index 0000000..3bd8221 --- /dev/null +++ b/web/src/views/week.ts @@ -0,0 +1,312 @@ +import type { Exam } from '@meteo-exam/shared' +import { WEATHER } from '@meteo-exam/shared' +import { h, mount } from '../dom' +import { + dayKey, + dayNameShort, + dayNumber, + formatDuration, + formatTime, + isSameDay, + longDate, + shortDate, +} from '../dates' +import { examCard, examDetail, weatherBadge } from './cards' +import { + dayStatus, + examsByDay, + getState, + isAdmin, + removeExam, + selectDay, + setError, +} from '../store' +import { showToast } from '../toast' +import { openExamForm } from './exam-form' + +/** Carte géante « Aujourd'hui » : l'évaluation la plus menaçante du jour. */ +function todayHero(onOpen: (exam: Exam) => void): HTMLElement { + const today = new Date() + const exams = examsByDay().get(dayKey(today)) ?? [] + + if (exams.length === 0) { + return h( + 'div', + { class: 'relative w-full bg-gradient-to-br from-sky-600 via-cyan-600 to-teal-600 rounded-[2.2rem] p-6 text-white shadow-float overflow-hidden border border-sky-400/30' }, + h('div', { class: 'absolute -right-8 -bottom-8 w-44 h-44 bg-white/20 rounded-full blur-2xl' }), + h( + 'div', + { class: 'flex justify-between items-start relative z-10' }, + h('span', { class: 'bg-black/30 backdrop-blur-md px-3.5 py-1 rounded-full text-xs font-black uppercase tracking-wider border border-white/10' }, + longDate(today)), + h('span', { class: 'text-4xl' }, '☀️'), + ), + h( + 'div', + { class: 'mt-6 relative z-10' }, + h('div', { class: 'inline-block bg-white/15 backdrop-blur-sm text-white text-xs font-black px-3 py-1 rounded-xl mb-2 border border-white/20' }, + 'GRAND SOLEIL'), + h('h2', { class: 'text-3xl font-black tracking-tight leading-none' }, 'Journée calme'), + h('p', { class: 'text-sky-50 font-semibold text-sm mt-1.5 opacity-90' }, + 'Aucune évaluation répertoriée. Profitez-en.'), + ), + ) + } + + const main = exams.reduce((worst, e) => + (e.weight > worst.weight ? e : worst), exams[0]!) + const meta = WEATHER[main.kind] + const gradients: Record = { + soleil: 'from-sky-600 via-cyan-600 to-teal-600', + brouillard: 'from-purple-700 via-violet-600 to-slate-600', + vent: 'from-amber-600 via-orange-600 to-rose-500', + orage: 'from-rose-700 via-rose-600 to-orange-600', + tempete: 'from-red-700 via-rose-600 to-amber-600', + } + + return h( + 'button', + { + class: `relative w-full text-left bg-gradient-to-br ${gradients[main.kind]} rounded-[2.2rem] p-6 text-white shadow-float overflow-hidden border border-white/20 active:scale-[0.99] transition-transform`, + onclick: () => onOpen(main), + }, + h('div', { class: 'absolute -right-8 -bottom-8 w-44 h-44 bg-yellow-300/20 rounded-full blur-2xl' }), + h( + 'div', + { class: 'flex justify-between items-start relative z-10' }, + h('span', { class: 'bg-black/30 backdrop-blur-md px-3.5 py-1 rounded-full text-xs font-black uppercase tracking-wider border border-white/10' }, + `Aujourd'hui • ${formatTime(main.startsAt)}`), + h('span', { class: 'text-4xl' }, meta.icon), + ), + h( + 'div', + { class: 'mt-6 relative z-10' }, + h('div', { class: 'inline-block bg-black/25 backdrop-blur-sm text-white text-xs font-black px-3 py-1 rounded-xl mb-2 border border-white/15' }, + meta.title.toUpperCase()), + h('h2', { class: 'text-3xl font-black tracking-tight leading-none' }, main.subject), + h('p', { class: 'text-amber-50 font-semibold text-sm mt-1.5 opacity-90' }, + main.chapter ?? (exams.length > 1 ? `+ ${exams.length - 1} autre(s) évaluation(s)` : meta.hint)), + ), + h( + 'div', + { class: 'mt-6 pt-4 border-t border-white/15 flex justify-between items-center relative z-10 text-xs font-bold' }, + h( + 'div', + { class: 'flex items-center gap-2' }, + main.room ? h('span', { class: 'bg-white/20 px-3 py-1.5 rounded-xl' }, + h('i', { class: 'fa-solid fa-door-open mr-1.5' }), main.room) : null, + h('span', { class: 'bg-white/20 px-3 py-1.5 rounded-xl' }, + h('i', { class: 'fa-solid fa-clock mr-1.5' }), formatDuration(main.durationMin)), + ), + h('span', { class: 'bg-white text-slate-900 px-3 py-1.5 rounded-xl font-black shadow-sm' }, + `Coeff ${main.weight.toFixed(1)}`), + ), + ) +} + +/** Frise horizontale des 7 prochains jours. */ +function weekStrip(): HTMLElement { + const { stripDays, selectedDay } = getState() + const today = new Date() + + return h( + 'div', + { class: 'flex gap-2 overflow-x-auto no-scrollbar -mx-5 px-5 pb-1' }, + ...stripDays.map((date) => { + const key = dayKey(date) + const status = dayStatus(key) + const count = examsByDay().get(key)?.length ?? 0 + const isToday = isSameDay(date, today) + const active = selectedDay === key + + return h( + 'button', + { + class: `shrink-0 w-[3.75rem] rounded-2xl border p-2 flex flex-col items-center gap-1 transition-all active:scale-95 ${ + active + ? 'bg-slate-700/60 border-sky-400/60' + : 'bg-slate-800/70 border-slate-700/60' + }`, + onclick: () => selectDay(key), + }, + h('span', { class: `text-[9px] font-black tracking-wider ${isToday ? 'text-sky-400' : 'text-slate-500'}` }, + isToday ? "AUJ." : dayNameShort(date)), + h('span', { class: 'text-lg font-black text-slate-100 leading-none' }, dayNumber(date)), + h('span', { class: 'text-base leading-none' }, WEATHER[status].icon), + h('span', { class: 'text-[9px] font-bold text-slate-500' }, + count === 0 ? '—' : `${count} éval.`), + ) + }), + ) +} + +/** Liste groupée par jour, filtrée ou non par la frise. */ +function dayList(onOpen: (exam: Exam) => void): HTMLElement { + const { selectedDay, stripDays } = getState() + const visibleKeys = selectedDay ? [selectedDay] : stripDays.map(dayKey) + const grouped = examsByDay() + + const sections = visibleKeys + .map((key) => { + const exams = grouped.get(key) ?? [] + if (exams.length === 0) return null + const [y, m, d] = key.split('-').map(Number) + const date = new Date(y!, (m ?? 1) - 1, d ?? 1) + return { key, date, exams } + }) + .filter((s): s is { key: string; date: Date; exams: Exam[] } => s !== null) + + if (sections.length === 0) { + return h( + 'div', + { class: 'text-center py-10 px-6' }, + h('div', { class: 'text-4xl mb-3' }, WEATHER[dayStatus(selectedDay ?? dayKey(new Date()))].icon), + h('p', { class: 'text-sm font-black text-slate-200' }, + selectedDay ? 'Grand Soleil' : 'Aucune évaluation sur les 7 jours'), + h('p', { class: 'text-xs text-slate-400 font-semibold mt-1' }, + selectedDay ? 'Rien de prévu ce jour-là.' : 'Profitez du calme, ou ajoutez une évaluation si vous êtes admin.'), + selectedDay + ? h('button', { class: 'mt-4 text-xs font-black text-sky-400', onclick: () => selectDay(null) }, + 'Voir toute la semaine') + : null, + ) + } + + return h( + 'div', + { class: 'space-y-4' }, + ...sections.map(({ key, date, exams }) => + h( + 'div', + { class: 'space-y-2.5' }, + h( + 'div', + { class: 'flex items-center gap-2.5 pt-1' }, + h('span', { class: 'text-sm font-black text-slate-200' }, shortDate(date)), + weatherBadge(dayStatus(key)), + ), + ...exams.map((exam) => examCard(exam, onOpen)), + ), + ), + ) +} + +/** Modale plein écran avec la fiche détaillée et les actions admin. */ +function openDetail(exam: Exam, rerender: () => void): void { + const overlay = h('div', { + class: + 'fixed inset-0 z-[90] bg-slate-950/80 backdrop-blur-sm flex items-end sm:items-center justify-center p-0 sm:p-6', + }) + + const close = (): void => overlay.remove() + + const panel = h( + 'div', + { class: 'w-full sm:max-w-md bg-slate-900 border border-slate-700 rounded-t-[2rem] sm:rounded-[2rem] p-5 max-h-[88vh] overflow-y-auto no-scrollbar shadow-2xl' }, + h( + 'div', + { class: 'flex items-start justify-between gap-3 mb-4' }, + h('span', { class: 'text-[10px] font-black uppercase tracking-widest text-slate-500' }, 'Fiche d’examen'), + h('button', { class: 'text-slate-400 hover:text-slate-200 text-lg', onclick: close }, + h('i', { class: 'fa-solid fa-xmark' })), + ), + examDetail(exam), + ) + + if (isAdmin()) { + panel.append( + h( + 'div', + { class: 'flex gap-2 mt-5 pt-4 border-t border-slate-700/60' }, + h( + 'button', + { + class: 'flex-1 bg-slate-800 border border-slate-700 text-slate-200 text-xs font-extrabold py-2.5 rounded-xl active:scale-95 transition-transform', + onclick: () => { + close() + openExamForm(exam, rerender) + }, + }, + h('i', { class: 'fa-solid fa-pen mr-1.5' }), 'Modifier', + ), + h( + 'button', + { + class: 'flex-1 bg-rose-500/10 border border-rose-500/25 text-rose-400 text-xs font-extrabold py-2.5 rounded-xl active:scale-95 transition-transform', + onclick: async () => { + close() + try { + await removeExam(exam.id) + showToast('Évaluation supprimée', 'success') + rerender() + } catch (error) { + setError(error instanceof Error ? error.message : 'Suppression impossible') + showToast('Suppression impossible', 'reject') + } + }, + }, + h('i', { class: 'fa-solid fa-trash mr-1.5' }), 'Supprimer', + ), + ), + ) + } + + overlay.append(panel) + overlay.addEventListener('click', (event) => { + if (event.target === overlay) close() + }) + document.body.append(overlay) +} + +export function renderWeek(root: HTMLElement, rerender: () => void): void { + const { error, session } = getState() + const open = (exam: Exam): void => openDetail(exam, rerender) + + const children: (Node | null)[] = [ + todayHero(open), + h( + 'div', + { class: 'flex justify-between items-center pt-2' }, + h('h3', { class: 'text-base font-extrabold text-slate-200 uppercase tracking-wider' }, 'La carte de la semaine'), + h('span', { class: 'text-xs font-bold text-sky-400' }, '7 prochains jours'), + ), + weekStrip(), + ] + + if (error) { + children.push( + h( + 'div', + { class: 'bg-rose-500/10 border border-rose-500/25 rounded-2xl p-3.5 flex items-center gap-2.5' }, + h('i', { class: 'fa-solid fa-triangle-exclamation text-rose-400' }), + h('p', { class: 'text-xs font-bold text-rose-200 flex-1' }, error), + ), + ) + } + + children.push(dayList(open)) + + if (isAdmin()) { + children.push( + h( + 'button', + { + class: + 'w-full bg-sky-500/10 border border-sky-500/25 text-sky-300 text-sm font-extrabold py-3.5 rounded-2xl active:scale-[0.98] transition-transform', + onclick: () => openExamForm(undefined, rerender), + }, + h('i', { class: 'fa-solid fa-plus mr-2' }), 'Ajouter une évaluation', + ), + ) + } + + children.push( + h( + 'p', + { class: 'text-[10px] text-slate-600 font-semibold text-center pt-2' }, + session ? `${session.klass.name} · ${getState().exams.length} évaluation(s) sur la période` : '', + ), + ) + + mount(root, ...children.filter((c): c is Node => c !== null)) +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..4aa0d35 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..3ba6cc1 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [tailwindcss()], + server: { + port: 5173, + // Le cookie de session est same-origin : on proxifie l'API plutôt que + // d'activer CORS, sinon le cookie ne partirait pas en cross-origin. + proxy: { + '/api': { + target: 'http://localhost:8787', + changeOrigin: false, + }, + }, + }, + build: { + outDir: 'dist', + sourcemap: true, + }, +})