commit c4ebb5b2d8fa717e4c538512220d4932e40190b0 Author: jmartin Date: Fri Jun 12 11:39:51 2026 +0200 Initialise le dépôt Bonpoint avec docs et prototype web. Structure docs/, web/ (app Node existante) et mobile/ (placeholder Flutter). diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e64474e --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Secrets & données locales +.env +*.log + +# Node (web) +web/node_modules/ +web/data/*.db +web/data/*.db-* + +# Flutter / mobile (futur) +mobile/.dart_tool/ +mobile/.flutter-plugins +mobile/.flutter-plugins-dependencies +mobile/.packages +mobile/build/ +mobile/.pub-cache/ +mobile/.pub/ +mobile/**/ios/Pods/ +mobile/**/.symlinks/ + +# OS / IDE +.DS_Store +.idea/ +.vscode/ +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..9cbb4ff --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Bonpoint — Bons Points famille + +Dépôt du projet **Bons Points** (gestion de bons points pour enfants). + +## Structure + +``` +bonpoint/ +├── docs/ Documentation (cahier des charges, notes Google Home, etc.) +├── web/ Application web prototype (Node.js + SQLite + Docker) +└── mobile/ Application Android Flutter — à venir +``` + +## Web (prototype actuel) + +Application familiale déployée sur `bonpoint.ptits-pas.fr` — sert de **laboratoire** (règles, UI, logique métier) pour la future app mobile commerciale. + +```bash +cd web +cp .env.example .env # éditer PARENT_PIN, JWT_SECRET +npm install +npm start # http://localhost:3000 +``` + +Déploiement Docker : voir `web/docker-compose.yml`. + +## Mobile (prévu) + +App Android **100 % locale**, freemium, Play Store — voir `docs/CAHIER_DES_CHARGES_APP_ANDROID.md`. + +Le développement mobile se fera dans un workspace Cursor dédié, branche ou dossier `mobile/`. + +## Documentation + +| Fichier | Contenu | +|---------|---------| +| [CAHIER_DES_CHARGES_APP_ANDROID.md](docs/CAHIER_DES_CHARGES_APP_ANDROID.md) | Spec produit commercial v0.2 | +| [GOOGLE_HOME.md](docs/GOOGLE_HOME.md) | Notes intégration Nest (abandonnée) | + +## Dépôt Git + +``` +ssh://git@git.ptits-pas.fr:2222/jmartin/bonpoint.git +``` diff --git a/docs/CAHIER_DES_CHARGES_APP_ANDROID.md b/docs/CAHIER_DES_CHARGES_APP_ANDROID.md new file mode 100644 index 0000000..5f61597 --- /dev/null +++ b/docs/CAHIER_DES_CHARGES_APP_ANDROID.md @@ -0,0 +1,289 @@ +# Cahier des charges — Bons Points (app Android commerciale) + +**Version :** 0.2 +**Date :** 9 juin 2026 +**Statut :** Cadrage — produit public monétisé (pas usage famille seul) + +--- + +## 1. Vision produit + +### Objectif business +Publier sur le **Google Play Store** une application de **bons points** pour familles, **100 % locale** (pas de SaaS, pas de compte obligatoire), monétisée pour **générer des revenus** au-delà du coût du compte développeur (~25 €). + +### Proposition de valeur (marketing) +> **« Vos bons points restent chez vous — zéro cloud, zéro compte, zéro données enfants sur nos serveurs. »** + +Les données familiales (scores, historique, photos) vivent **uniquement sur le téléphone**. Seul un **mini site de redirection** (téléchargement / QR) peut exister côté serveur — sans accès au contenu familial. + +### Ce n’est PAS +- **P’titsPas** (`app.ptits-pas.fr`) — application pro garde d’enfants / collectivités (projet séparé, login, SaaS). +- L’instance familiale actuelle (Ariana, Pablo, Hélia) — elle sert de **prototype** et de source de règles par défaut, pas de produit final. + +### Hors périmètre v1 +- iOS (coût Mac + 99 €/an) +- OK Google / Nest / voix +- Synchronisation multi-téléphones en temps réel +- Comptes utilisateurs cloud (Firebase Auth, etc.) + +--- + +## 2. Modèle économique + +### Freemium (recommandé) + +| | **Gratuit** | **Premium** (achat unique) | +|--|-------------|----------------------------| +| Enfants | **1 enfant** max | **Illimité** (ou plafond raisonnable, ex. 8) | +| Règles / récompenses | Pack par défaut + édition limitée | Édition complète, familles custom | +| Publicités | **Bandeau AdMob** (bas d’écran) | **Aucune pub** | +| Export sauvegarde | ✅ | ✅ | +| Historique | 30 derniers jours | Illimité | +| Prix cible | 0 € | **2,99 € – 4,99 €** (achat in-app unique) | + +*Chiffres à affiner après étude concurrence.* + +### Publicité (AdMob) +- App ciblant **familles et enfants** → politique **Google Play Families** obligatoire. +- Pubs **taguées child-directed**, contenu **G** max, SDK Ads certifié Families. +- **Revenus pub faibles** sur ce segment ; l’IAP « sans pub + multi-enfants » doit être le levier principal. +- **Pas de pub** dans l’espace parent ni sur les écrans où l’enfant dépense ses points (UX + conformité). + +### Coûts récurrents +| Poste | Coût | +|-------|------| +| Compte Google Play | ~25 € une fois | +| Hébergement landing `/download` | ~0 € (page statique sur infra existante) | +| Backend données familiales | **0 €** | +| AdMob / Play Billing | Commission Google sur IAP (~15 %) | + +### Seuil de rentabilité (ordre de grandeur) +- Coût fixe initial : ~25 € + temps de dev. +- À 3 € IAP net ~2,55 € : **~10 achats** pour couvrir le compte Play (hors temps). +- Les pubs seules nécessitent **très nombreuses** impressions (surtout en child-directed). + +--- + +## 3. Utilisateurs cibles + +| Profil | Besoin | +|--------|--------| +| **Parent** (acheteur décisionnaire) | Gérer points, règles, récompenses, corriger erreurs | +| **Enfant** | Voir score, historique, « boutique » | +| **Parent soucieux vie privée** | Pas de compte, pas de cloud — argument d’achat | + +**Marché initial :** France, français. Extension EU possible plus tard. + +--- + +## 4. Parcours utilisateur (premier lancement) + +Pas de login. À la première ouverture : + +1. **Écran bienvenue** — promesse (local, privé, simple). +2. **Création du 1er enfant** — prénom, couleur, photo (galerie ou avatar). +3. **Choix du PIN parent** (obligatoire). +4. **Import règles par défaut** — pack « Maison & école » (~30 règles issues du prototype) ou départ vide. +5. **Tableau de bord** — prêt à l’emploi. + +Ensuite : ouverture directe sur le tableau de bord (PIN pour espace parent). + +--- + +## 5. Fonctionnalités + +### 5.1 MVP commercial (v1.0 — publication Play Store) + +#### Tableau de bord +- Liste enfants (limité à 1 en gratuit ; badge « Passer à Premium » si ajout 2e enfant). +- Accès espace parent (PIN). +- Menu : réglages, export/import, premium, politique confidentialité. + +#### Enfant +- Fiche : photo, score, historique, boutique. +- Plancher score à **0**. + +#### Espace parent (PIN) +- Multi-sélection enfants (premium si >1 enfant). +- Grille règles par familles (édition en premium ; lecture + apply en gratuit). +- Historique + **annulation** de mouvement. +- Verrouillage auto après sortie de l’écran parent. + +#### Boutique +- Récompenses configurables (premium) ou pack défaut (gratuit). + +#### Configuration (premium ou partiel gratuit) +- Ajouter / modifier / désactiver règles et récompenses. +- Ajouter enfants (premium au-delà du 1er). +- Changer PIN, couleurs, photos. +- **Export / import** sauvegarde JSON chiffrée (Google Drive, mail, fichier local). + +#### Monétisation intégrée +- Bandeau AdMob (écrans autorisés uniquement). +- Écran achat Premium (Google Play Billing). +- Restauration achat sur nouveau téléphone. + +#### Légal (obligatoire Play Store) +- Politique de confidentialité (URL statique). +- Mention « pas de collecte données enfants sur serveur ». +- Déclaration cible d’âge Play Console (familles). +- CGU simplifiées si IAP. + +### 5.2 v1.1+ +- Widget Android (scores). +- Packs de règles thématiques (téléchargement embarqué, pas serveur). +- Statistiques locales pour parents (graphique semaine). +- Traduction anglais si traction FR. + +### 5.3 Exclu +- Voix / assistant. +- Sync cloud entre parents. +- Réseau social, classements entre familles. + +--- + +## 6. Règles métier (inchangées par rapport au prototype) + +- Score entier, plancher **0**. +- Mouvement = trace de chaque action (règle, delta, date). +- Annulation = correction inverse (plancher respecté). +- Familles de règles : Maison, Routine, Fratrie, Respect, Écrans, Bonus (+ custom en premium). + +*Référence données seed : `apps/bonpoint/src/db.js` (`REGLES_DEFAUT`, `RECOMPENSES_DEFAUT`).* + +--- + +## 7. Architecture technique + +| Couche | Choix | +|--------|--------| +| Framework | **Flutter** (UI rapide, Cursor-friendly, un codebase) | +| Base locale | **drift** ou **sqflite** (SQLite) | +| État | Riverpod ou Bloc | +| IAP | `in_app_purchase` | +| Pub | `google_mobile_ads` (Families-compliant) | +| Export | Fichier JSON + checksum ; option chiffrement AES (mot de passe parent) | +| PIN | Hash local (bcrypt / argon2 via package) | + +### Réseau (minimal) +| Usage | Réseau | Données | +|-------|--------|---------| +| Usage quotidien app | **Non requis** | — | +| AdMob | Oui | Requêtes pub anonymisées (Families) | +| Play Billing | Oui | Transaction Google | +| Landing `bonpoint.ptits-pas.fr/download` | Oui | Redirection Play Store + compteur scans | +| Données familiales vers serveur | **Jamais** | — | + +### Landing marketing (serveur léger) +- URL : `https://bonpoint.ptits-pas.fr/download` (ou sous-domaine dédié). +- QR code sur cartons → comptage scan → redirect fiche Play Store. +- **Aucune** donnée enfant ; analytics agrégés uniquement. + +--- + +## 8. Distribution Play Store + +### Compte développeur +- Compte **personnel** ou **organisation** (si structure P’tits Pas / micro-entreprise — *à trancher pour facturation IAP*). + +### Avant publication publique (compte perso récent) +- **Test fermé** : minimum **12 testeurs** opt-in, **14 jours consécutifs**. +- Questionnaire « production access » rempli par Google. +- Prévoir recrutement testeurs (réseau, parents école, etc.) **avant** de viser la prod. + +### Fiche Store +- Nom : **Bons Points** (ou **Bonpoint — Bons points famille** si conflit). +- Captures : tableau de bord, espace parent, boutique. +- Argument : **100 % hors ligne, privé, sans compte**. + +--- + +## 9. Stratégie marketing (terrain) + +| Canal | Action | +|-------|--------| +| **QR cartons** | Carton imprimé → `/download` → Play Store | +| **Bouche-à-oreille** | Enfants / parents d’école (récré, WhatsApp parents) | +| **Différenciation** | Vie privée vs apps concurrentes cloud + abonnement | +| **Pas en v1** | Budget pub Facebook, influenceurs | + +--- + +## 10. Concurrence & positionnement + +Marché saturé d’apps « chores / rewards / points ». Angles possibles : + +1. **100 % local** — pas d’abonnement mensuel. +2. **Français natif** — règles et UX pensées France. +3. **Prix bas** — IAP unique vs abonnements concurrents. +4. **Sans compte** — installation en 2 minutes. + +*Étude concurrentielle à faire (2–3 apps FR/EN) avant finalisation pricing.* + +--- + +## 11. Rapport avec le prototype web + +| Élément | Décision | +|---------|----------| +| `bonpoint.ptits-pas.fr` | Devient **landing + redirect** ; app web famille optionnelle en maintenance | +| Code Node actuel | Source de vérité pour **règles par défaut** et **logique métier** ; pas porté tel quel | +| Dialogflow / Smart Home / Nest | **Abandonné** | +| Photos Ariana/Pablo/Hélia | Remplacées par avatars génériques dans l’app publique | + +--- + +## 12. Planning indicatif + +| Phase | Livrable | Durée estimée | +|-------|----------|---------------| +| **0** | Validation CDC v0.2 + étude concurrence + pricing | 1 semaine | +| **1** | Projet Flutter + BDD + onboarding + 1 enfant | 1 semaine | +| **2** | Espace parent, règles, mouvements, annulation | 1 semaine | +| **3** | Boutique, export/import, packs règles défaut | 1 semaine | +| **4** | AdMob + IAP + écran premium | 1 semaine | +| **5** | Légal, polish, tests, test fermé 14 j | 2 semaines | +| **6** | Landing QR + cartons + demande prod Play | 1 semaine | + +**Total indicatif :** 7–8 semaines à temps partiel. + +--- + +## 13. Critères d’acceptation v1.0 + +- [ ] Onboarding sans compte ; 1er enfant créé en < 3 min +- [ ] Gratuit : 1 enfant, pub visible, pack règles défaut utilisable +- [ ] Premium : achat débloque multi-enfants + sans pub + édition complète +- [ ] 100 % fonctionnel en mode avion (hors pub et achat) +- [ ] Export puis import sur autre téléphone = données identiques +- [ ] Conformité Families (pubs G, pas de tracking enfant) +- [ ] Politique confidentialité accessible depuis l’app +- [ ] Test fermé 12×14 j validé avant prod + +--- + +## 14. Points encore à trancher + +| # | Question | Options | +|---|----------|---------| +| 1 | **Nom marque Play Store** | Bons Points / Bonpoint / autre | +| 2 | **Prix IAP** | 2,99 € / 3,99 € / 4,99 € | +| 3 | **Limite enfants premium** | Illimité / 6 / 8 | +| 4 | **Compte Play** | Perso / société (micro-entreprise) | +| 5 | **Édition règles en gratuit** | Lecture seule / 5 règles custom | +| 6 | **Historique gratuit** | 30 j / 50 mouvements / illimité | +| 7 | **Landing** | Garder `bonpoint.ptits-pas.fr` / nouveau domaine | +| 8 | **Quand arrêter le site web actuel** | Dès v1 / après traction app | + +--- + +## 15. Prochaine étape + +1. Valider ce CDC v0.2 (prix IAP, limites gratuit/premium). +2. Faire une **mini étude concurrence** (3 apps, leurs prix). +3. Créer le repo Flutter `bonpoint-app` et l’onboarding. +4. En parallèle : ouvrir compte Play + préparer liste des 12 testeurs. + +--- + +*v0.1 → v0.2 : pivot produit public monétisé (freemium + AdMob + IAP), stack Flutter, landing QR conservée.* diff --git a/docs/GOOGLE_HOME.md b/docs/GOOGLE_HOME.md new file mode 100644 index 0000000..28d076d --- /dev/null +++ b/docs/GOOGLE_HOME.md @@ -0,0 +1,129 @@ +# OK Google — Bons Points sur Nest + +## Important : Dialogflow « conversation » ne marche plus sur Nest + +Depuis le **13 juin 2023**, Google a arrêté les **Conversational Actions** (Dialogflow + « Dis à Bons Points… »). + +L’endpoint `/api/voice/dialogflow` reste utile pour **tests** et pour d’autres intégrations Dialogflow, mais **ne peut plus être invoqué directement sur une enceinte Nest** via une action conversationnelle. + +La voie officielle aujourd’hui : **Google Home Smart Home** (capteurs virtuels). + +--- + +## Ce qui est déjà prêt côté serveur + +| URL | Rôle | +|-----|------| +| `GET /api/voice/ariana` | Réponse JSON (test) | +| `GET /api/voice?prenom=pablo` | Idem | +| `POST /api/voice/dialogflow` | Webhook Dialogflow (tests) | +| `POST /api/smarthome` | Fulfillment Google Home (SYNC / QUERY) | + +Test rapide : + +```bash +curl -s https://bonpoint.ptits-pas.fr/api/voice/ariana +curl -s -X POST https://bonpoint.ptits-pas.fr/api/smarthome \ + -H "Content-Type: application/json" \ + -d '{"requestId":"test","inputs":[{"intent":"action.devices.SYNC"}]}' +``` + +--- + +## Principe Smart Home (recommandé) + +Chaque enfant apparaît comme un **capteur de batterie** (0–100 %) = son score de bons points. + +Phrases à essayer sur le Nest (en français) : + +- « OK Google, **quel est le niveau de batterie d’Ariana** ? » +- « OK Google, **quelle est la batterie de Pablo** ? » +- « OK Google, **niveau de batterie Hélia** » + +> Astuce famille : expliquer aux enfants que la « batterie » = leurs bons points. + +Les scores au-dessus de 100 s’affichent comme 100 % (limite Google). + +--- + +## Configuration Google Home Developer (à faire une fois) + +### 1. Créer le projet + +1. Ouvrir [Google Home Developer Console](https://console.home.google.com/) +2. **Create project** → nom : `Bons Points` +3. Type d’intégration : **Cloud-to-cloud** + +### 2. Fulfillment + +1. Menu **Integrations** → votre intégration Cloud-to-cloud +2. **Fulfillment URL** : + + ``` + https://bonpoint.ptits-pas.fr/api/smarthome + ``` + +3. Enregistrer + +### 3. Account linking (obligatoire pour le Nest) + +Google exige un **OAuth 2.0** pour lier le compte dans l’app Google Home. + +Options possibles : + +- **Auth0** ou **Firebase Auth** (gratuit, adapté à un usage familial) +- OAuth maison (plus technique) + +Une fois OAuth configuré, vous liez l’intégration dans **Google Home** → **Paramètres** → **Travaille avec Google**. + +### 4. Test avant production + +1. Dans la console : **Test Suite** → lancer SYNC et QUERY +2. Vérifier que 3 appareils apparaissent (Ariana, Pablo, Hélia) +3. Tester une QUERY sur un appareil → le score doit correspondre à l’app + +### 5. Utilisation au quotidien + +1. Lier l’intégration sur le compte Google du foyer +2. Les 3 « capteurs » sont découverts automatiquement +3. Les enfants demandent le niveau de batterie / points à voix haute + +--- + +## Dialogflow (optionnel, tests uniquement) + +Si vous voulez quand même un agent Dialogflow pour tester le webhook : + +1. [Dialogflow ES](https://dialogflow.cloud.google.com/) → nouvel agent **fr** +2. Intent `score_enfant` avec phrases : + - Combien de points a Ariana ? + - Quel est le score de Pablo ? + - Et Hélia elle a combien ? +3. Paramètre `@sys.given-name` ou entité custom `prenom` : Ariana, Pablo, Hélia +4. Intent `score_tous` : Combien de points ? / Les scores ? +5. **Fulfillment** → activer webhook : + + ``` + https://bonpoint.ptits-pas.fr/api/voice/dialogflow + ``` + +6. Tester dans la console Dialogflow (simulateur) — **pas sur le Nest**. + +--- + +## Dépannage + +| Problème | Piste | +|----------|--------| +| Nest ne connaît pas l’action | Normal si vous utilisez encore Dialogflow conversationnel | +| Appareils invisibles | Vérifier account linking + SYNC dans Test Suite | +| Mauvais score | Vérifier `GET /api/public/scores` | +| Hélia mal reconnue | Dire « Hélia » clairement ; le serveur accepte avec/sans accent | + +--- + +## Évolutions possibles + +- OAuth simplifié intégré à l’app (panneau parent) +- Cast TTS vers le Nest après une routine (plus complexe) +- Phrases personnalisées via Home Assistant (si un jour vous l’adoptez) diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000..7090e52 --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,7 @@ +# Application mobile Android + +Emplacement prévu pour l'app **Flutter** (Bons Points — produit commercial Play Store). + +Voir le cahier des charges : [../docs/CAHIER_DES_CHARGES_APP_ANDROID.md](../docs/CAHIER_DES_CHARGES_APP_ANDROID.md). + +**Statut :** non initialisé — à développer dans un workspace Cursor dédié. diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..48c91c4 --- /dev/null +++ b/web/.env.example @@ -0,0 +1,11 @@ +# PIN parent (4 à 8 chiffres) — à changer avant mise en prod +PARENT_PIN=1234 + +# Secret JWT — générer avec : openssl rand -hex 32 +JWT_SECRET=changez-moi-avec-openssl-rand-hex-32 + +PORT=3000 +DATABASE_PATH=/data/bonpoint.db + +# Google Home Smart Home — fulfillment : https://bonpoint.ptits-pas.fr/api/smarthome +# Voir docs/GOOGLE_HOME.md pour la configuration OAuth (account linking) diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..c2412a3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +data/*.db +.env +*.log diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..414be08 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22-alpine AS build + +RUN apk add --no-cache python3 make g++ + +WORKDIR /app +COPY package.json ./ +RUN npm install --omit=dev + +FROM node:22-alpine + +RUN apk add --no-cache libstdc++ + +WORKDIR /app +COPY --from=build /app/node_modules ./node_modules +COPY package.json ./ +COPY src ./src +COPY public ./public + +ENV NODE_ENV=production +ENV PORT=3000 +ENV DATABASE_PATH=/data/bonpoint.db + +EXPOSE 3000 + +CMD ["node", "src/server.js"] diff --git a/web/data/.gitkeep b/web/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web/docker-compose.yml b/web/docker-compose.yml new file mode 100644 index 0000000..d9ccebb --- /dev/null +++ b/web/docker-compose.yml @@ -0,0 +1,21 @@ +services: + bonpoint: + build: . + container_name: bonpoint + restart: unless-stopped + env_file: .env + volumes: + - ./data:/data + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.routers.bonpoint.rule=Host(`bonpoint.ptits-pas.fr`)" + - "traefik.http.routers.bonpoint.entrypoints=websecure" + - "traefik.http.routers.bonpoint.tls.certresolver=leresolver" + - "traefik.http.services.bonpoint.loadbalancer.server.port=3000" + networks: + - proxy_network + +networks: + proxy_network: + external: true diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..000bfa2 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1997 @@ +{ + "name": "bonpoint", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bonpoint", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^3.0.2", + "better-sqlite3": "^11.10.0", + "cookie-parser": "^1.4.7", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "sharp": "^0.35.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.0.tgz", + "integrity": "sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.0.tgz", + "integrity": "sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.0.tgz", + "integrity": "sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.0.tgz", + "integrity": "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.0.tgz", + "integrity": "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.0.tgz", + "integrity": "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.0.tgz", + "integrity": "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.0.tgz", + "integrity": "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.0.tgz", + "integrity": "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.0.tgz", + "integrity": "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.0.tgz", + "integrity": "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.0.tgz", + "integrity": "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.0.tgz", + "integrity": "sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.0.tgz", + "integrity": "sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.0.tgz", + "integrity": "sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.0.tgz", + "integrity": "sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.0.tgz", + "integrity": "sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.0.tgz", + "integrity": "sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.0.tgz", + "integrity": "sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.0.tgz", + "integrity": "sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.0.tgz", + "integrity": "sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.0.tgz", + "integrity": "sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.0.tgz", + "integrity": "sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.0.tgz", + "integrity": "sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.0.tgz", + "integrity": "sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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/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/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "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/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "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/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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "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/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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/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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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/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/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=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/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "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-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "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-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/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "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.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "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/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-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/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "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/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/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/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "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/sharp": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.0.tgz", + "integrity": "sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.0", + "@img/sharp-darwin-x64": "0.35.0", + "@img/sharp-freebsd-wasm32": "0.35.0", + "@img/sharp-libvips-darwin-arm64": "1.3.0", + "@img/sharp-libvips-darwin-x64": "1.3.0", + "@img/sharp-libvips-linux-arm": "1.3.0", + "@img/sharp-libvips-linux-arm64": "1.3.0", + "@img/sharp-libvips-linux-ppc64": "1.3.0", + "@img/sharp-libvips-linux-riscv64": "1.3.0", + "@img/sharp-libvips-linux-s390x": "1.3.0", + "@img/sharp-libvips-linux-x64": "1.3.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", + "@img/sharp-libvips-linuxmusl-x64": "1.3.0", + "@img/sharp-linux-arm": "0.35.0", + "@img/sharp-linux-arm64": "0.35.0", + "@img/sharp-linux-ppc64": "0.35.0", + "@img/sharp-linux-riscv64": "0.35.0", + "@img/sharp-linux-s390x": "0.35.0", + "@img/sharp-linux-x64": "0.35.0", + "@img/sharp-linuxmusl-arm64": "0.35.0", + "@img/sharp-linuxmusl-x64": "0.35.0", + "@img/sharp-webcontainers-wasm32": "0.35.0", + "@img/sharp-win32-arm64": "0.35.0", + "@img/sharp-win32-ia32": "0.35.0", + "@img/sharp-win32-x64": "0.35.0" + } + }, + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "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/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/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/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/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" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..ec3ee5a --- /dev/null +++ b/web/package.json @@ -0,0 +1,21 @@ +{ + "name": "bonpoint", + "version": "1.0.0", + "description": "Application familiale de bons points", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "node --watch src/server.js", + "icons": "node scripts/generate-icons.js" + }, + "dependencies": { + "bcryptjs": "^3.0.2", + "better-sqlite3": "^11.10.0", + "cookie-parser": "^1.4.7", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "sharp": "^0.35.0" + } +} diff --git a/web/public/boutique.html b/web/public/boutique.html new file mode 100644 index 0000000..8e0545e --- /dev/null +++ b/web/public/boutique.html @@ -0,0 +1,190 @@ + + + + + + + Boutique — Bons Points + + + + + + + + + + +
+ + + Retour + + +
+
+
+
+
+ +

Choisis ta récompense

+
+
+ +

Enfant introuvable

+
+ + + +
+ + + + + diff --git a/web/public/css/style.css b/web/public/css/style.css new file mode 100644 index 0000000..4ecebd3 --- /dev/null +++ b/web/public/css/style.css @@ -0,0 +1,894 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Segoe UI', system-ui, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + color: #333; +} + +.container { + max-width: 480px; + margin: 0 auto; + padding: 1.5rem 1rem 2rem; +} + +h1 { + text-align: center; + color: #fff; + font-size: 1.8rem; + margin-bottom: 0.3rem; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.sous-titre { + text-align: center; + color: rgba(255, 255, 255, 0.85); + font-size: 0.95rem; + margin-bottom: 1.5rem; +} + +.carte-enfant { + background: #fff; + border-radius: 20px; + padding: 1.2rem 1.5rem; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); + transition: transform 0.2s; +} + +.carte-enfant:hover { + transform: scale(1.02); +} + +.carte-lien { + text-decoration: none; + color: inherit; + cursor: pointer; +} + +.fleche-fiche { + font-size: 1.8rem; + color: #ccc; + font-weight: 300; + line-height: 1; +} + +.btn-retour { + display: inline-flex; + align-items: center; + gap: 0.45rem; + background: #fff; + color: #667eea; + text-decoration: none; + font-size: 0.95rem; + font-weight: 700; + padding: 0.55rem 1.1rem 0.55rem 0.9rem; + border-radius: 24px; + margin-bottom: 1rem; + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.18); + transition: transform 0.15s, box-shadow 0.15s; +} + +.btn-retour-icone { + flex-shrink: 0; + display: block; +} + +.btn-retour:hover { + transform: translateY(-1px); + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.22); +} + +.btn-retour:active { + transform: scale(0.97); +} + +.fiche-entete { + display: flex; + flex-direction: column; + align-items: stretch; + margin-bottom: 1.2rem; +} + +.fiche-entete-compact { + display: flex; + align-items: center; + gap: 1rem; + text-align: left; + background: #fff; + border-radius: 20px; + padding: 1rem 1.2rem; + margin-bottom: 1rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12); +} + +.fiche-entete .fiche-avatar-centre.avatar-cercle { + width: 50%; + height: auto; + aspect-ratio: 1; + margin: 0 auto 0.75rem; + border-width: 4px; + flex-shrink: 0; +} + +.fiche-entete .score-bandeau { + width: 100%; + margin-top: 0; +} + +.avatar-grand { + width: 96px; + height: 96px; + margin: 0 auto 0.8rem; +} + +.fiche-prenom { + font-size: 1.8rem; + font-weight: 800; + color: #fff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.fiche-prenom-petit { + font-size: 1.3rem; + color: #333; + text-shadow: none; +} + +.fiche-score { + font-size: 3.5rem; + font-weight: 800; + line-height: 1; + color: #fff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); +} + +.fiche-section { + background: #fff; + border-radius: 20px; + padding: 1rem 1.2rem; + margin-bottom: 1rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12); +} + +.fiche-section h3 { + font-size: 1rem; + color: #4a5568; + margin-bottom: 0.8rem; +} + +.historique-vide { + color: #888; + font-size: 0.85rem; + text-align: center; + padding: 0.5rem 0; +} + +.historique-libelle { + flex: 1; + padding-right: 0.5rem; +} + +.historique-droite { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.1rem; + flex-shrink: 0; +} + +.historique-date { + color: #aaa; + font-size: 0.75rem; +} + +.fiche-section .historique-item { + flex-direction: row; + align-items: flex-start; + gap: 0.3rem; +} + +.solde-badge-boutique { + font-size: 1.1rem; + font-weight: 700; + color: #667eea; + margin-top: 0.2rem; +} + +.erreur-page { + text-align: center; + color: #fff; + padding: 2rem 0; +} + +.avatar-cercle { + width: 58px; + height: 58px; + border-radius: 50%; + border: 3px solid; + overflow: hidden; + flex-shrink: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); +} + +.avatar-cercle img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.info-enfant { + flex: 1; +} + +.prenom { + font-size: 1.4rem; + font-weight: 700; +} + +.score { + font-size: 2.2rem; + font-weight: 800; + line-height: 1.1; +} + +.score-label { + font-size: 0.85rem; + color: #888; +} + +.avatar-parent-accueil { + border-color: #667eea; + flex-shrink: 0; +} + +.avatar-parent-titre { + width: 2.1rem; + height: 2.1rem; + border-color: rgba(255, 255, 255, 0.85); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); +} + +.pin-entete .avatar-parent-pin.avatar-cercle { + width: 50%; + height: auto; + aspect-ratio: 1; + margin: 0 auto 0.75rem; + border-width: 4px; + border-color: #667eea; + box-shadow: 0 4px 14px rgba(102, 126, 234, 0.25); +} + +.btn-parent { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 1rem; + width: 100%; + margin-top: 0; + padding: 1.2rem 1.5rem; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.18); + border: 2px solid rgba(255, 255, 255, 0.55); + border-radius: 20px; + color: #fff; + text-decoration: none; + backdrop-filter: blur(4px); + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.12); + transition: background 0.2s, transform 0.15s; +} + +.btn-parent-texte { + flex: 1; + font-size: 1.4rem; + font-weight: 700; + line-height: 1.1; +} + +.btn-parent:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-1px); +} + +.btn-parent:active { + transform: scale(0.98); +} + +/* Zone parent */ + +body.parent-page { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +.parent-container { + max-width: 480px; + margin: 0 auto; + padding: 1.2rem 1rem 2rem; +} + +.parent-barre-haut { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.2rem; +} + +.parent-titre { + display: inline-flex; + align-items: center; + gap: 0.45rem; + font-size: 1.3rem; + color: #fff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + margin: 0; +} + +.btn-outline-blanc { + background: rgba(255, 255, 255, 0.2); + color: #fff; + border: 2px solid rgba(255, 255, 255, 0.6); + padding: 0.4rem 0.9rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.btn-outline-blanc:hover { + background: rgba(255, 255, 255, 0.35); +} + +#ecran-pin .btn-retour { + margin-bottom: 1rem; +} + +.ecran-pin-carte { + background: #fff; + border-radius: 24px; + padding: 1.75rem 1.5rem 1.5rem; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); +} + +.pin-zone { + display: flex; + flex-direction: row; + align-items: stretch; + gap: 0.6rem; + width: 100%; +} + +.pin-entete { + text-align: center; + margin-bottom: 1.25rem; +} + +.pin-entete h1 { + font-size: 1.4rem; + color: #667eea; + text-shadow: none; + margin: 0 0 0.35rem; +} + +.pin-sous-titre { + color: #888; + font-size: 0.9rem; + margin: 0; +} + +#form-pin { + flex: 1; + min-width: 0; +} + +.pin-input { + display: block; + width: 100%; + height: 100%; + box-sizing: border-box; + font-size: 1.5rem; + text-align: center; + letter-spacing: 0.35rem; + padding: 0.8rem 0.75rem; + border: 2px solid #ddd; + border-radius: 12px; + margin-bottom: 0; + direction: ltr; +} + +.pin-input:focus { + outline: none; + border-color: #667eea; +} + +.btn-connexion-pin { + flex-shrink: 0; + align-self: stretch; + padding: 0.8rem 1.25rem; + font-size: 0.95rem; + margin-top: 0; + white-space: nowrap; +} + +#ecran-pin .erreur { + text-align: center; + margin-top: 0.75rem; + margin-bottom: 0; +} + +.pin-masque { + -webkit-text-security: disc; +} + +.pin-leurre { + position: absolute; + width: 0; + height: 0; + opacity: 0; + pointer-events: none; + overflow: hidden; +} + +.btn { + display: inline-block; + padding: 0.7rem 1.5rem; + border: none; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; +} + +.btn:hover { + opacity: 0.85; +} + +.btn-principal { + background: #667eea; + color: #fff; +} + +.btn-danger { + background: #e53e3e; + color: #fff; + font-size: 0.85rem; + padding: 0.4rem 0.8rem; +} + +.erreur { + color: #e53e3e; + font-size: 0.9rem; + margin-top: 0.5rem; +} + +.section { + background: #fff; + border-radius: 20px; + padding: 1rem 1.1rem; + margin-bottom: 1rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12); +} + +.section h3 { + font-size: 0.95rem; + margin-bottom: 0.8rem; + color: #667eea; + font-weight: 700; +} + +.section-enfant .selecteur-enfant { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + width: 100%; +} + +.chip-enfant { + display: flex; + flex: 1 1 0; + align-items: center; + justify-content: center; + padding: 0.2rem; + border: none; + background: transparent; + cursor: pointer; + width: 0; + min-width: 0; + font-family: inherit; +} + +.chip-vignette { + position: relative; + width: 100%; + max-width: 6.75rem; + aspect-ratio: 1; + border-radius: 50%; + border: 3px solid; + overflow: visible; + flex-shrink: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: transform 0.2s, border-width 0.2s, box-shadow 0.2s; +} + +.chip-enfant.actif .chip-vignette { + border-width: 5px; + transform: scale(1.06); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.25); +} + +.chip-enfant:not(.actif) .chip-vignette { + border-width: 3px; +} + +.chip-vignette img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50%; + display: block; +} + +.chip-badge { + position: absolute; + right: -0.15rem; + bottom: -0.1rem; + min-width: 1.55rem; + height: 1.55rem; + padding: 0 0.25rem; + border-radius: 999px; + background: #fff; + border: 2px solid currentColor; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.72rem; + font-weight: 800; + line-height: 1; + font-variant-numeric: tabular-nums; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); +} + +.avatar-chip { + width: 44px; + height: 44px; +} + +.chip-label { + line-height: 1.1; + font-size: 0.78rem; +} + +.score-bandeau { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: #f8f9ff; + border-radius: 16px; + border-left: 4px solid; + margin-top: 0.75rem; +} + +.score-bandeau-nom { + font-weight: 800; + font-size: 1.35rem; + color: #333; + line-height: 1.1; +} + +.score-bandeau-pts { + font-size: 1.35rem; + font-weight: 800; + line-height: 1.1; + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} + +.grille-regles { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; +} + +.famille-regles { + margin-bottom: 1.1rem; +} + +.famille-regles:last-child { + margin-bottom: 0; +} + +.famille-titre { + font-size: 0.85rem; + font-weight: 700; + color: #fff; + background: #667eea; + margin-bottom: 0.6rem; + padding: 0.35rem 0.6rem; + border-radius: 8px; +} + +.btn-regle { + padding: 0.7rem 0.5rem; + border: none; + border-radius: 12px; + font-size: 0.85rem; + cursor: pointer; + text-align: center; + line-height: 1.3; + transition: transform 0.15s; +} + +.btn-regle:active { + transform: scale(0.95); +} + +.btn-regle.positif { + background: #c6f6d5; + color: #22543d; +} + +.btn-regle.negatif { + background: #fed7d7; + color: #742a2a; +} + +.historique-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid #f0f0f0; + font-size: 0.85rem; +} + +.historique-item:last-child { + border-bottom: none; +} + +.parent-page .historique-item { + align-items: flex-start; +} + +.parent-page .historique-libelle { + min-width: 0; + line-height: 1.35; + padding-right: 0.5rem; +} + +.parent-page .historique-droite { + flex-direction: row; + align-items: flex-start; + gap: 0.4rem; + padding-top: 0.05rem; + flex-shrink: 0; +} + +.parent-page .historique-points-col { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.1rem; +} + +.parent-page .btn-annuler-mouvement { + margin-left: 0.15rem; +} + +.delta-positif { + color: #38a169; + font-weight: 700; +} + +.delta-negatif { + color: #e53e3e; + font-weight: 700; +} + +.cacher { + display: none; +} + +.feedback { + position: fixed; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); + background: #2d3748; + color: #fff; + padding: 0.8rem 1.5rem; + border-radius: 12px; + font-size: 0.95rem; + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; + z-index: 100; +} + +.feedback.visible { + opacity: 1; +} + +/* Boutique */ + +.btn-boutique { + display: block; + text-align: center; + background: #fff; + color: #667eea; + font-weight: 700; + font-size: 1.1rem; + padding: 1rem; + border-radius: 16px; + text-decoration: none; + margin-top: 0.5rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); +} + +.btn-boutique:hover { + transform: scale(1.02); +} + +.etape-label { + color: #fff; + font-weight: 600; + margin-bottom: 0.8rem; + font-size: 1rem; +} + +.solde-badge { + display: inline-block; + background: rgba(255, 255, 255, 0.25); + padding: 0.2rem 0.6rem; + border-radius: 12px; + font-size: 0.85rem; + margin-left: 0.3rem; +} + +.grille-enfants { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.carte-cliquable { + cursor: pointer; + border: none; + width: 100%; + text-align: left; + font-family: inherit; +} + +.carte-cliquable:active { + transform: scale(0.98); +} + +.grille-recompenses { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.6rem; +} + +.carte-recompense { + background: #fff; + border: none; + border-radius: 16px; + padding: 1rem 0.6rem; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12); + transition: transform 0.15s; + font-family: inherit; +} + +.carte-recompense:active { + transform: scale(0.95); +} + +.carte-recompense.indisponible { + opacity: 0.4; + cursor: not-allowed; +} + +.rec-icone { + font-size: 2rem; +} + +.rec-libelle { + font-size: 0.8rem; + font-weight: 600; + text-align: center; + line-height: 1.2; +} + +.rec-cout { + font-size: 0.85rem; + color: #667eea; + font-weight: 700; +} + +.modal { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: none; + align-items: center; + justify-content: center; + z-index: 200; + padding: 1rem; +} + +.modal.ouvert { + display: flex; +} + +.modal-contenu { + background: #fff; + border-radius: 20px; + padding: 1.5rem; + max-width: 320px; + width: 100%; + text-align: center; +} + +.modal-contenu p { + font-size: 1.1rem; + margin-bottom: 1.2rem; + line-height: 1.4; +} + +.modal-boutons { + display: flex; + gap: 0.5rem; + justify-content: center; +} + +.btn-secondaire { + background: #e2e8f0; + color: #4a5568; +} + +.btn-annuler-mouvement { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.6rem; + height: 1.6rem; + background: #fff5f5; + border: 1px solid #fc8181; + color: #e53e3e; + font-size: 0.85rem; + font-weight: 700; + line-height: 1; + padding: 0; + border-radius: 50%; + cursor: pointer; + margin-left: 0.35rem; + flex-shrink: 0; + transition: background 0.15s, transform 0.15s; +} + +.btn-annuler-mouvement:hover { + background: #fed7d7; +} + +.btn-annuler-mouvement:active { + transform: scale(0.92); +} diff --git a/web/public/enfant.html b/web/public/enfant.html new file mode 100644 index 0000000..fe3c2b3 --- /dev/null +++ b/web/public/enfant.html @@ -0,0 +1,126 @@ + + + + + + + Bons Points + + + + + + + + + + +
+ + + Retour + + +
+
+
+
+
+ +
+

Mon historique

+
+
+ + 🛍️ Dépenser mes points +
+ +

Enfant introuvable

+
+ + + + + diff --git a/web/public/icons/apple-touch-icon.png b/web/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..a9acd2b Binary files /dev/null and b/web/public/icons/apple-touch-icon.png differ diff --git a/web/public/icons/icon-192.png b/web/public/icons/icon-192.png new file mode 100644 index 0000000..98c1037 Binary files /dev/null and b/web/public/icons/icon-192.png differ diff --git a/web/public/icons/icon-512.png b/web/public/icons/icon-512.png new file mode 100644 index 0000000..4858454 Binary files /dev/null and b/web/public/icons/icon-512.png differ diff --git a/web/public/icons/icon.svg b/web/public/icons/icon.svg new file mode 100644 index 0000000..83d4ba5 --- /dev/null +++ b/web/public/icons/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + P + diff --git a/web/public/icons/parent-espace.svg b/web/public/icons/parent-espace.svg new file mode 100644 index 0000000..766256f --- /dev/null +++ b/web/public/icons/parent-espace.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/public/index.html b/web/public/index.html new file mode 100644 index 0000000..2041ef1 --- /dev/null +++ b/web/public/index.html @@ -0,0 +1,71 @@ + + + + + + + Bons Points + + + + + + + + + + +
+

⭐ Bons Points ⭐

+

Qui est le plus sage ?

+
+ + + Espace parents + +
+ + + + + diff --git a/web/public/js/pwa.js b/web/public/js/pwa.js new file mode 100644 index 0000000..a97e71d --- /dev/null +++ b/web/public/js/pwa.js @@ -0,0 +1,5 @@ +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js').catch(() => {}); + }); +} diff --git a/web/public/manifest.json b/web/public/manifest.json new file mode 100644 index 0000000..7b5f8de --- /dev/null +++ b/web/public/manifest.json @@ -0,0 +1,32 @@ +{ + "name": "Bons Points", + "short_name": "Bons Points", + "description": "Suivi des bons points pour Ariana, Pablo et Hélia", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait", + "background_color": "#667eea", + "theme_color": "#667eea", + "lang": "fr", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/web/public/parent.html b/web/public/parent.html new file mode 100644 index 0000000..2add311 --- /dev/null +++ b/web/public/parent.html @@ -0,0 +1,368 @@ + + + + + + + Espace Parents — Bons Points + + + + + + + + + + +
+
+ + + Retour + +
+
+ +

Espace Parents

+

Entrez votre code PIN

+
+
+
+ + + +
+ +
+

+
+
+ +
+
+

+ + Espace Parents +

+ +
+ +
+

Enfant sélectionné

+
+
+
+ +
+

Appliquer une règle

+
+
+ +
+

Derniers mouvements

+
+
+
+
+ + + +
+ + + + + diff --git a/web/public/photos/ariana.png b/web/public/photos/ariana.png new file mode 100644 index 0000000..ff53147 Binary files /dev/null and b/web/public/photos/ariana.png differ diff --git a/web/public/photos/helia.png b/web/public/photos/helia.png new file mode 100644 index 0000000..a7d04b6 Binary files /dev/null and b/web/public/photos/helia.png differ diff --git a/web/public/photos/pablo.png b/web/public/photos/pablo.png new file mode 100644 index 0000000..afca021 Binary files /dev/null and b/web/public/photos/pablo.png differ diff --git a/web/public/photos/parents.png b/web/public/photos/parents.png new file mode 100644 index 0000000..b0e15c3 Binary files /dev/null and b/web/public/photos/parents.png differ diff --git a/web/public/reglement.html b/web/public/reglement.html new file mode 100644 index 0000000..8e67fcf --- /dev/null +++ b/web/public/reglement.html @@ -0,0 +1,250 @@ + + + + + Règlement des Bons Points + + + + +
+

⭐ Règlement des Bons Points ⭐

+

bonpoint.ptits-pas.fr — Les points ne descendent jamais en dessous de zéro

+
+ + Ariana Ariana + + + Pablo Pablo + + + Hélia Hélia + +
+
+ +
+
+

🏠 Maison & rangement

+ + + + + + + + + + + + +
🍽️ Je mets la table+1
🧹 Débarrasser la table+1
🫧 Ranger le lave-vaisselle+1
✨ Nettoyer la table du séjour+1
🎮 Ranger la salle de jeu+2
🛏️ Ranger sa chambre+1
👕 Ranger ses habits propres+1
🏕️ Ranger la cabane+1
🚲 Ranger les vélos (abri jardin)+1
👨‍👩‍👧 Ranger sur ordre des parents+1
🗄️ Piquer dans les placards−2
+
+ +
+

⏰ Matin, soir & école

+ + + + + + +
⏰ Habits prêts + s'habiller seul
Ariana 7h20 / Pablo 8h10
+1
🌙 Se préparer pour le lit sans bagarre (< 15 min)+1
📚 Devoirs faits sans rappel+2
🎨 Activité calme manuelle 1h+2
⏰ Retard le matin (Ariana/Pablo)−1
+ +

🌟 Bonus

+ + +
🌟 Bon point bonus — je suis content !+1
+ +

📱 Écrans

+ + + +
📺 Se lever pour la télé sans autorisation (semaine)−3
📱 Utiliser un téléphone sans autorisation−5
+
+ +
+

👫 Fratrie & entraide

+ + + + + + + + + + +
🤝 Aider son frère/sa sœur (sans qu'on demande)+2
🎁 Partager un jouet sans bagarre+1
🗣️ Dire « pardon » tout seul+1
👶 Aider avec Hélia (lire, jouer calmement)+1
👊 Taper, frapper−2
😤 Ne pas s'excuser auprès de frère/sœur−2
😈 Provocation volontaire de frère/sœur−2
🗣️ Accuser l'autre à tort−2
💔 Casser / abîmer un jouet (volontairement)−3
+
+ +
+

🙏 Respect & écoute

+ + + + + + + + + +
🤥 Mentir à ses parents−2
🤬 Dire des gros mots ou insultes−1
🙄 Répondre mal / faire la tête−1
🗯️ Crier / hurler−1
🚪 Faire claquer une porte−1
👅 Tirer la langue / geste irrespectueux−1
👂 Ne pas écouter une consigne (plusieurs reprises)−2
🏃 Partir sans permission (jardin, rue)−3
+
+ +
+

🛍️ Boutique — Dépenser ses points

+
+ + + + + +
🍬 Un bonbon1 pt
📺 10 min de télé1 pt
🍪 Un biscuit / cookie1 pt
🎵 Choisir la musique dans la voiture1 pt
+ + + + + + +
🍭 Une sucette2 pts
🍫 Neige3 pts
🎮 10 min de jeu vidéo3 pts
🍕 Choisir le menu du repas5 pts
🍦 Une glace5 pts
+ + + +
📓 Cahier de dessin neuf10 pts
🧸 Jouet pas cher (< 10 €)50 pts
+
+
+ +

+ 🌐 Voir ses points : bonpoint.ptits-pas.fr  |  + 👨‍👩‍👧 Parents : bonpoint.ptits-pas.fr/parent +

+
+ + diff --git a/web/public/sw.js b/web/public/sw.js new file mode 100644 index 0000000..998296c --- /dev/null +++ b/web/public/sw.js @@ -0,0 +1,47 @@ +const CACHE = 'bonpoint-v1'; +const PRECACHE = [ + '/', + '/css/style.css', + '/manifest.json', + '/icons/icon-192.png', + '/icons/icon-512.png', + '/icons/icon.svg', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()), + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys() + .then((keys) => Promise.all( + keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)), + )) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + if (request.method !== 'GET') return; + if (url.pathname.startsWith('/api/')) return; + + event.respondWith( + caches.match(request).then((cached) => { + const fetchPromise = fetch(request).then((response) => { + if (response.ok && url.origin === self.location.origin) { + const clone = response.clone(); + caches.open(CACHE).then((cache) => cache.put(request, clone)); + } + return response; + }).catch(() => cached); + + return cached || fetchPromise; + }), + ); +}); diff --git a/web/scripts/generate-icons.js b/web/scripts/generate-icons.js new file mode 100644 index 0000000..8433f56 --- /dev/null +++ b/web/scripts/generate-icons.js @@ -0,0 +1,17 @@ +const path = require('path'); +const sharp = require('sharp'); + +const svgPath = path.join(__dirname, '..', 'public', 'icons', 'icon.svg'); +const outDir = path.join(__dirname, '..', 'public', 'icons'); + +const sizes = [180, 192, 512]; + +Promise.all(sizes.map((size) => sharp(svgPath) + .resize(size, size) + .png() + .toFile(path.join(outDir, size === 180 ? 'apple-touch-icon.png' : `icon-${size}.png`)))) + .then(() => console.log('Icônes générées')) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/web/src/auth.js b/web/src/auth.js new file mode 100644 index 0000000..4cda2b5 --- /dev/null +++ b/web/src/auth.js @@ -0,0 +1,26 @@ +const jwt = require('jsonwebtoken'); + +const SECRET = process.env.JWT_SECRET || 'dev-secret-changez-moi'; +const DUREE = '30d'; + +function creerToken() { + return jwt.sign({ role: 'parent' }, SECRET, { expiresIn: DUREE }); +} + +function middlewareParent(req, res, next) { + const token = req.cookies?.bonpoint_session + || req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ erreur: 'Non authentifié' }); + } + + try { + jwt.verify(token, SECRET); + next(); + } catch { + return res.status(401).json({ erreur: 'Session expirée' }); + } +} + +module.exports = { creerToken, middlewareParent, SECRET }; diff --git a/web/src/db.js b/web/src/db.js new file mode 100644 index 0000000..20dd676 --- /dev/null +++ b/web/src/db.js @@ -0,0 +1,492 @@ +const Database = require('better-sqlite3'); +const bcrypt = require('bcryptjs'); +const path = require('path'); + +const dbPath = process.env.DATABASE_PATH + || path.join(__dirname, '..', 'data', 'bonpoint.db'); + +const db = new Database(dbPath); +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +function initialiser() { + db.exec(` + CREATE TABLE IF NOT EXISTS enfants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prenom TEXT NOT NULL UNIQUE, + date_naissance TEXT, + score INTEGER NOT NULL DEFAULT 0, + couleur TEXT NOT NULL DEFAULT '#6C63FF', + ordre INTEGER NOT NULL DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS regles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + libelle TEXT NOT NULL, + points INTEGER NOT NULL, + icone TEXT NOT NULL DEFAULT '⭐', + actif INTEGER NOT NULL DEFAULT 1, + ordre INTEGER NOT NULL DEFAULT 0, + famille TEXT NOT NULL DEFAULT '📋 Autre', + famille_ordre INTEGER NOT NULL DEFAULT 99 + ); + + CREATE TABLE IF NOT EXISTS mouvements ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enfant_id INTEGER NOT NULL, + regle_id INTEGER, + delta INTEGER NOT NULL, + note TEXT, + cree_le TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (enfant_id) REFERENCES enfants(id), + FOREIGN KEY (regle_id) REFERENCES regles(id) + ); + + CREATE TABLE IF NOT EXISTS recompenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + libelle TEXT NOT NULL, + cout_points INTEGER NOT NULL, + icone TEXT NOT NULL DEFAULT '🎁', + actif INTEGER NOT NULL DEFAULT 1, + ordre INTEGER NOT NULL DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS config ( + cle TEXT PRIMARY KEY, + valeur TEXT NOT NULL + ); + `); + + migrer(); + synchroniserRegles(); + synchroniserRecompenses(); + + const nbEnfants = db.prepare('SELECT COUNT(*) AS n FROM enfants').get().n; + if (nbEnfants === 0) { + insererDonneesInitiales(); + } +} + +const REGLES_VERSION = 'famille-v5'; + +const F = { + MAISON: '🏠 Maison & rangement', + ROUTINE: '⏰ Matin, soir & école', + FRATRIE: '👫 Fratrie & entraide', + RESPECT: '🙏 Respect & écoute', + ECRANS: '📱 Écrans', + BONUS: '🌟 Bonus', +}; + +const REGLES_DEFAUT = [ + // Maison & rangement + { libelle: 'Je mets la table', points: 1, icone: '🍽️', ordre: 1, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Débarrasser la table', points: 1, icone: '🧹', ordre: 2, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Ranger le lave-vaisselle', points: 1, icone: '🫧', ordre: 3, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Nettoyer la table du séjour', points: 1, icone: '✨', ordre: 4, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Ranger la salle de jeu', points: 2, icone: '🎮', ordre: 5, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Ranger sa chambre', points: 1, icone: '🛏️', ordre: 6, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Ranger ses habits propres', points: 1, icone: '👕', ordre: 7, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Ranger la cabane', points: 1, icone: '🏕️', ordre: 8, famille: F.MAISON, famille_ordre: 1 }, + { libelle: "Ranger les vélos (abri jardin)", points: 1, icone: '🚲', ordre: 9, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Ranger sur ordre des parents', points: 1, icone: '👨‍👩‍👧', ordre: 10, famille: F.MAISON, famille_ordre: 1 }, + { libelle: 'Piquer dans les placards', points: -2, icone: '🗄️', ordre: 11, famille: F.MAISON, famille_ordre: 1 }, + // Matin, soir & école + { + libelle: "Habits prêts + s'habiller seul (Ariana 7h20 / Pablo 8h10)", + points: 1, icone: '⏰', ordre: 1, famille: F.ROUTINE, famille_ordre: 2, + }, + { + libelle: 'Se préparer pour le lit sans bagarre (< 15 min)', + points: 1, icone: '🌙', ordre: 2, famille: F.ROUTINE, famille_ordre: 2, + }, + { libelle: 'Devoirs faits sans rappel', points: 2, icone: '📚', ordre: 3, famille: F.ROUTINE, famille_ordre: 2 }, + { libelle: 'Activité calme manuelle 1h', points: 2, icone: '🎨', ordre: 4, famille: F.ROUTINE, famille_ordre: 2 }, + { libelle: 'Retard le matin (Ariana/Pablo)', points: -1, icone: '⏰', ordre: 5, famille: F.ROUTINE, famille_ordre: 2 }, + // Fratrie & entraide + { + libelle: "Aider son frère/sa sœur (sans qu'on demande)", + points: 2, icone: '🤝', ordre: 1, famille: F.FRATRIE, famille_ordre: 3, + }, + { libelle: 'Partager un jouet sans bagarre', points: 1, icone: '🎁', ordre: 2, famille: F.FRATRIE, famille_ordre: 3 }, + { libelle: 'Dire « pardon » tout seul', points: 1, icone: '🗣️', ordre: 3, famille: F.FRATRIE, famille_ordre: 3 }, + { + libelle: 'Aider avec Hélia (lire, jouer calmement)', + points: 1, icone: '👶', ordre: 4, famille: F.FRATRIE, famille_ordre: 3, + }, + { libelle: 'Taper, frapper', points: -2, icone: '👊', ordre: 5, famille: F.FRATRIE, famille_ordre: 3 }, + { + libelle: "Ne pas s'excuser auprès de frère/sœur", + points: -2, icone: '😤', ordre: 6, famille: F.FRATRIE, famille_ordre: 3, + }, + { + libelle: 'Provocation volontaire de frère/sœur', + points: -2, icone: '😈', ordre: 7, famille: F.FRATRIE, famille_ordre: 3, + }, + { libelle: "Accuser l'autre à tort", points: -2, icone: '🗣️', ordre: 8, famille: F.FRATRIE, famille_ordre: 3 }, + { + libelle: 'Casser / abîmer un jouet (volontairement)', + points: -3, icone: '💔', ordre: 9, famille: F.FRATRIE, famille_ordre: 3, + }, + // Respect & écoute + { libelle: 'Mentir à ses parents', points: -2, icone: '🤥', ordre: 1, famille: F.RESPECT, famille_ordre: 4 }, + { libelle: 'Dire des gros mots ou insultes', points: -1, icone: '🤬', ordre: 2, famille: F.RESPECT, famille_ordre: 4 }, + { libelle: 'Répondre mal / faire la tête', points: -1, icone: '🙄', ordre: 3, famille: F.RESPECT, famille_ordre: 4 }, + { libelle: 'Crier / hurler', points: -1, icone: '🗯️', ordre: 4, famille: F.RESPECT, famille_ordre: 4 }, + { libelle: 'Faire claquer une porte', points: -1, icone: '🚪', ordre: 5, famille: F.RESPECT, famille_ordre: 4 }, + { + libelle: 'Tirer la langue / geste irrespectueux', + points: -1, icone: '👅', ordre: 6, famille: F.RESPECT, famille_ordre: 4, + }, + { + libelle: 'Ne pas écouter une consigne (plusieurs reprises)', + points: -2, icone: '👂', ordre: 7, famille: F.RESPECT, famille_ordre: 4, + }, + { + libelle: 'Partir sans permission (jardin, rue)', + points: -3, icone: '🏃', ordre: 8, famille: F.RESPECT, famille_ordre: 4, + }, + // Écrans + { + libelle: 'Se lever pour la télé sans autorisation (semaine)', + points: -3, icone: '📺', ordre: 1, famille: F.ECRANS, famille_ordre: 5, + }, + { + libelle: 'Utiliser un téléphone sans autorisation', + points: -5, icone: '📱', ordre: 2, famille: F.ECRANS, famille_ordre: 5, + }, + // Bonus + { + libelle: 'Bon point bonus — je suis content !', + points: 1, icone: '🌟', ordre: 1, famille: F.BONUS, famille_ordre: 6, + }, +]; + +function synchroniserRegles() { + const version = db.prepare(` + SELECT valeur FROM config WHERE cle = 'regles_version' + `).get(); + + if (version?.valeur === REGLES_VERSION) return; + + db.prepare('UPDATE regles SET actif = 0').run(); + + const inserer = db.prepare(` + INSERT INTO regles (libelle, points, icone, ordre, famille, famille_ordre) + VALUES (@libelle, @points, @icone, @ordre, @famille, @famille_ordre) + `); + + for (const r of REGLES_DEFAUT) { + inserer.run(r); + } + + db.prepare(` + INSERT OR REPLACE INTO config (cle, valeur) + VALUES ('regles_version', @version) + `).run({ version: REGLES_VERSION }); +} + +const RECOMPENSES_VERSION = 'famille-v2'; + +const RECOMPENSES_DEFAUT = [ + { libelle: 'Un bonbon', cout: 1, icone: '🍬', ordre: 1 }, + { libelle: '10 minutes de télé', cout: 1, icone: '📺', ordre: 2 }, + { libelle: 'Une sucette', cout: 2, icone: '🍭', ordre: 3 }, + { libelle: 'Neige', cout: 3, icone: '🍫', ordre: 4 }, + { libelle: '10 minutes de jeu vidéo', cout: 3, icone: '🎮', ordre: 5 }, + { libelle: 'Cahier de dessin neuf', cout: 10, icone: '📓', ordre: 6 }, + { libelle: 'Jouet pas cher (< 10 €)', cout: 50, icone: '🧸', ordre: 7 }, + { libelle: 'Un biscuit / cookie', cout: 1, icone: '🍪', ordre: 8 }, + { libelle: 'Choisir la musique dans la voiture', cout: 1, icone: '🎵', ordre: 9 }, + { libelle: 'Choisir le menu du repas', cout: 5, icone: '🍕', ordre: 10 }, + { libelle: 'Une glace', cout: 5, icone: '🍦', ordre: 11 }, +]; + +function synchroniserRecompenses() { + const version = db.prepare(` + SELECT valeur FROM config WHERE cle = 'recompenses_version' + `).get(); + + if (version?.valeur === RECOMPENSES_VERSION) return; + + db.prepare('UPDATE recompenses SET actif = 0').run(); + + const inserer = db.prepare(` + INSERT INTO recompenses (libelle, cout_points, icone, ordre) + VALUES (@libelle, @cout, @icone, @ordre) + `); + + for (const r of RECOMPENSES_DEFAUT) { + inserer.run(r); + } + + db.prepare(` + INSERT OR REPLACE INTO config (cle, valeur) + VALUES ('recompenses_version', @version) + `).run({ version: RECOMPENSES_VERSION }); +} + +function migrer() { + const colonnesMvt = db.prepare('PRAGMA table_info(mouvements)').all(); + const nomsMvt = colonnesMvt.map((c) => c.name); + + if (!nomsMvt.includes('recompense_id')) { + db.exec(` + ALTER TABLE mouvements + ADD COLUMN recompense_id INTEGER REFERENCES recompenses(id) + `); + } + if (!nomsMvt.includes('annule')) { + db.exec('ALTER TABLE mouvements ADD COLUMN annule INTEGER NOT NULL DEFAULT 0'); + } + + const colonnesRegles = db.prepare('PRAGMA table_info(regles)').all(); + const nomsRegles = colonnesRegles.map((c) => c.name); + + if (!nomsRegles.includes('famille')) { + db.exec(` + ALTER TABLE regles + ADD COLUMN famille TEXT NOT NULL DEFAULT '📋 Autre' + `); + } + if (!nomsRegles.includes('famille_ordre')) { + db.exec(` + ALTER TABLE regles + ADD COLUMN famille_ordre INTEGER NOT NULL DEFAULT 99 + `); + } +} + +function insererDonneesInitiales() { + const enfants = [ + { prenom: 'Ariana', date: '2017-11-29', couleur: '#E91E8C', ordre: 1 }, + { prenom: 'Pablo', date: '2020-10-23', couleur: '#2196F3', ordre: 2 }, + { prenom: 'Hélia', date: '2023-06-22', couleur: '#FF9800', ordre: 3 }, + ]; + + const insererEnfant = db.prepare(` + INSERT INTO enfants (prenom, date_naissance, score, couleur, ordre) + VALUES (@prenom, @date, 0, @couleur, @ordre) + `); + + for (const e of enfants) { + insererEnfant.run(e); + } + + const pin = process.env.PARENT_PIN || '1234'; + const hash = bcrypt.hashSync(pin, 10); + db.prepare(` + INSERT OR REPLACE INTO config (cle, valeur) VALUES ('pin_hash', @hash) + `).run({ hash }); +} + +function insererRecompensesInitiales() { + synchroniserRecompenses(); +} + +function listerEnfants() { + return db.prepare(` + SELECT id, prenom, date_naissance, score, couleur, ordre + FROM enfants ORDER BY ordre + `).all(); +} + +function trouverEnfantParPrenom(prenom) { + const normalise = prenom.trim().toLowerCase() + .normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + + return db.prepare(` + SELECT id, prenom, score, couleur + FROM enfants + `).all().find((e) => { + const p = e.prenom.toLowerCase() + .normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + return p === normalise; + }) || null; +} + +function listerRegles() { + return db.prepare(` + SELECT id, libelle, points, icone, ordre, famille, famille_ordre + FROM regles WHERE actif = 1 + ORDER BY famille_ordre, ordre + `).all(); +} + +function appliquerRegle(enfantId, regleId, note) { + const enfant = db.prepare('SELECT * FROM enfants WHERE id = ?').get(enfantId); + const regle = db.prepare('SELECT * FROM regles WHERE id = ? AND actif = 1') + .get(regleId); + + if (!enfant || !regle) { + return null; + } + + const transaction = db.transaction(() => { + const nouveauScore = Math.max(0, enfant.score + regle.points); + const deltaReel = nouveauScore - enfant.score; + + db.prepare(` + INSERT INTO mouvements (enfant_id, regle_id, delta, note) + VALUES (?, ?, ?, ?) + `).run(enfantId, regleId, deltaReel, note || null); + + db.prepare(` + UPDATE enfants SET score = ? WHERE id = ? + `).run(nouveauScore, enfantId); + }); + + transaction(); + + return db.prepare(` + SELECT id, prenom, score, couleur FROM enfants WHERE id = ? + `).get(enfantId); +} + +function listerRecompenses() { + return db.prepare(` + SELECT id, libelle, cout_points, icone, ordre + FROM recompenses WHERE actif = 1 ORDER BY ordre + `).all(); +} + +function listerMouvements(limite = 30) { + return db.prepare(` + SELECT m.id, m.delta, m.note, m.cree_le, m.recompense_id, m.annule, + e.prenom AS enfant, + r.libelle AS regle, r.icone, + rec.libelle AS recompense, rec.icone AS icone_recompense + FROM mouvements m + JOIN enfants e ON e.id = m.enfant_id + LEFT JOIN regles r ON r.id = m.regle_id + LEFT JOIN recompenses rec ON rec.id = m.recompense_id + WHERE m.annule = 0 + ORDER BY m.cree_le DESC + LIMIT ? + `).all(limite); +} + +function listerMouvementsEnfant(enfantId, limite = 30) { + return db.prepare(` + SELECT m.id, m.delta, m.note, m.cree_le, m.recompense_id, + r.libelle AS regle, r.icone, + rec.libelle AS recompense, rec.icone AS icone_recompense + FROM mouvements m + LEFT JOIN regles r ON r.id = m.regle_id + LEFT JOIN recompenses rec ON rec.id = m.recompense_id + WHERE m.annule = 0 AND m.enfant_id = ? + ORDER BY m.cree_le DESC + LIMIT ? + `).all(enfantId, limite); +} + +function acheterRecompense(enfantId, recompenseId) { + const enfant = db.prepare('SELECT * FROM enfants WHERE id = ?').get(enfantId); + const recompense = db.prepare(` + SELECT * FROM recompenses WHERE id = ? AND actif = 1 + `).get(recompenseId); + + if (!enfant || !recompense) { + return { erreur: 'introuvable' }; + } + + if (enfant.score < recompense.cout_points) { + return { erreur: 'pas_assez_de_points', score: enfant.score, + cout: recompense.cout_points }; + } + + const delta = -recompense.cout_points; + const note = `Achat : ${recompense.libelle}`; + + const transaction = db.transaction(() => { + db.prepare(` + INSERT INTO mouvements (enfant_id, recompense_id, delta, note) + VALUES (?, ?, ?, ?) + `).run(enfantId, recompenseId, delta, note); + + db.prepare(` + UPDATE enfants SET score = MAX(0, score + ?) WHERE id = ? + `).run(delta, enfantId); + }); + + transaction(); + + const enfantMaj = db.prepare(` + SELECT id, prenom, score, couleur FROM enfants WHERE id = ? + `).get(enfantId); + + return { + enfant: enfantMaj, + recompense: { + libelle: recompense.libelle, + icone: recompense.icone, + cout: recompense.cout_points, + }, + }; +} + +function annulerMouvement(mouvementId) { + const mouvement = db.prepare(` + SELECT m.*, e.prenom, e.score AS score_actuel + FROM mouvements m + JOIN enfants e ON e.id = m.enfant_id + WHERE m.id = ? AND m.annule = 0 + `).get(mouvementId); + + if (!mouvement) { + return null; + } + + const correction = -mouvement.delta; + const nouveauScore = Math.max(0, mouvement.score_actuel + correction); + + const transaction = db.transaction(() => { + db.prepare('UPDATE mouvements SET annule = 1 WHERE id = ?') + .run(mouvementId); + db.prepare(` + UPDATE enfants SET score = ? WHERE id = ? + `).run(nouveauScore, mouvement.enfant_id); + }); + + transaction(); + + return db.prepare(` + SELECT id, prenom, score, couleur FROM enfants WHERE id = ? + `).get(mouvement.enfant_id); +} + +function annulerAchat(mouvementId) { + return annulerMouvement(mouvementId); +} + +function verifierPin(pin) { + const row = db.prepare(` + SELECT valeur FROM config WHERE cle = 'pin_hash' + `).get(); + if (!row) return false; + return bcrypt.compareSync(pin, row.valeur); +} + +function changerPin(nouveauPin) { + const hash = bcrypt.hashSync(nouveauPin, 10); + db.prepare(` + INSERT OR REPLACE INTO config (cle, valeur) VALUES ('pin_hash', @hash) + `).run({ hash }); +} + +module.exports = { + db, + initialiser, + listerEnfants, + trouverEnfantParPrenom, + listerRegles, + listerRecompenses, + appliquerRegle, + acheterRecompense, + annulerAchat, + annulerMouvement, + listerMouvements, + listerMouvementsEnfant, + verifierPin, + changerPin, +}; diff --git a/web/src/routes/parent.js b/web/src/routes/parent.js new file mode 100644 index 0000000..01d862f --- /dev/null +++ b/web/src/routes/parent.js @@ -0,0 +1,140 @@ +const express = require('express'); +const { + verifierPin, appliquerRegle, annulerMouvement, listerMouvements, changerPin, + listerEnfants, listerRegles, listerRecompenses, +} = require('../db'); +const { creerToken, middlewareParent } = require('../auth'); + +const router = express.Router(); + +const tentatives = new Map(); +const MAX_TENTATIVES = 5; +const FENETRE_MS = 15 * 60 * 1000; + +function verifierRateLimit(ip) { + const maintenant = Date.now(); + const entree = tentatives.get(ip) || { count: 0, debut: maintenant }; + + if (maintenant - entree.debut > FENETRE_MS) { + entree.count = 0; + entree.debut = maintenant; + } + + entree.count += 1; + tentatives.set(ip, entree); + return entree.count <= MAX_TENTATIVES; +} + +router.post('/connexion', (req, res) => { + const ip = req.ip || 'inconnu'; + if (!verifierRateLimit(ip)) { + return res.status(429).json({ + erreur: 'Trop de tentatives. Réessayez dans 15 minutes.', + }); + } + + const { pin } = req.body; + if (!pin || !/^\d{4,8}$/.test(pin)) { + return res.status(400).json({ erreur: 'PIN invalide' }); + } + + if (!verifierPin(pin)) { + return res.status(401).json({ erreur: 'PIN incorrect' }); + } + + const token = creerToken(); + res.cookie('bonpoint_session', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + res.json({ ok: true }); +}); + +router.post('/deconnexion', (_req, res) => { + res.clearCookie('bonpoint_session'); + res.json({ ok: true }); +}); + +router.get('/session', middlewareParent, (_req, res) => { + res.json({ ok: true }); +}); + +router.get('/donnees', middlewareParent, (_req, res) => { + res.json({ + enfants: listerEnfants(), + regles: listerRegles(), + recompenses: listerRecompenses(), + mouvements: listerMouvements(20), + }); +}); + +router.post('/appliquer', middlewareParent, (req, res) => { + const { enfant_id, regle_id, note } = req.body; + + if (!enfant_id || !regle_id) { + return res.status(400).json({ erreur: 'Paramètres manquants' }); + } + + const resultat = appliquerRegle( + parseInt(enfant_id, 10), + parseInt(regle_id, 10), + note, + ); + + if (!resultat) { + return res.status(404).json({ erreur: 'Enfant ou règle introuvable' }); + } + + res.json({ enfant: resultat }); +}); + +router.post('/annuler-mouvement', middlewareParent, (req, res) => { + const { mouvement_id } = req.body; + + if (!mouvement_id) { + return res.status(400).json({ erreur: 'Paramètre manquant' }); + } + + const enfant = annulerMouvement(parseInt(mouvement_id, 10)); + + if (!enfant) { + return res.status(404).json({ erreur: 'Mouvement introuvable ou déjà annulé' }); + } + + res.json({ enfant }); +}); + +router.post('/annuler-achat', middlewareParent, (req, res) => { + const { mouvement_id } = req.body; + + if (!mouvement_id) { + return res.status(400).json({ erreur: 'Paramètre manquant' }); + } + + const enfant = annulerMouvement(parseInt(mouvement_id, 10)); + + if (!enfant) { + return res.status(404).json({ erreur: 'Mouvement introuvable ou déjà annulé' }); + } + + res.json({ enfant }); +}); + +router.post('/changer-pin', middlewareParent, (req, res) => { + const { ancien_pin, nouveau_pin } = req.body; + + if (!ancien_pin || !nouveau_pin || !/^\d{4,8}$/.test(nouveau_pin)) { + return res.status(400).json({ erreur: 'PIN invalide' }); + } + + if (!verifierPin(ancien_pin)) { + return res.status(401).json({ erreur: 'Ancien PIN incorrect' }); + } + + changerPin(nouveau_pin); + res.json({ ok: true }); +}); + +module.exports = router; diff --git a/web/src/routes/public.js b/web/src/routes/public.js new file mode 100644 index 0000000..80e585f --- /dev/null +++ b/web/src/routes/public.js @@ -0,0 +1,63 @@ +const express = require('express'); +const { + listerEnfants, trouverEnfantParPrenom, listerRegles, listerRecompenses, + acheterRecompense, listerMouvementsEnfant, +} = require('../db'); + +const router = express.Router(); + +router.get('/scores', (_req, res) => { + res.json({ enfants: listerEnfants() }); +}); + +router.get('/regles', (_req, res) => { + res.json({ regles: listerRegles() }); +}); + +router.get('/recompenses', (_req, res) => { + res.json({ recompenses: listerRecompenses() }); +}); + +router.get('/enfant/:prenom', (req, res) => { + const enfant = trouverEnfantParPrenom(req.params.prenom); + + if (!enfant) { + return res.status(404).json({ erreur: 'Enfant introuvable' }); + } + + const limite = Math.min(parseInt(req.query.limite, 10) || 30, 50); + + res.json({ + enfant, + mouvements: listerMouvementsEnfant(enfant.id, limite), + }); +}); + +router.post('/acheter', (req, res) => { + const { enfant_id, recompense_id } = req.body; + + if (!enfant_id || !recompense_id) { + return res.status(400).json({ erreur: 'Paramètres manquants' }); + } + + const resultat = acheterRecompense( + parseInt(enfant_id, 10), + parseInt(recompense_id, 10), + ); + + if (resultat.erreur === 'introuvable') { + return res.status(404).json({ erreur: 'Enfant ou récompense introuvable' }); + } + + if (resultat.erreur === 'pas_assez_de_points') { + return res.status(400).json({ + erreur: 'Pas assez de points', + score: resultat.score, + cout: resultat.cout, + }); + } + + res.json(resultat); +}); + +module.exports = router; diff --git a/web/src/routes/smarthome.js b/web/src/routes/smarthome.js new file mode 100644 index 0000000..3a8308c --- /dev/null +++ b/web/src/routes/smarthome.js @@ -0,0 +1,19 @@ +const express = require('express'); +const { traiterRequeteSmartHome } = require('../smarthome'); + +const router = express.Router(); + +router.post('/', (req, res) => { + try { + const reponse = traiterRequeteSmartHome(req.body); + res.json(reponse); + } catch (err) { + console.error('Smart Home erreur:', err); + res.status(500).json({ + requestId: req.body?.requestId || 'inconnu', + payload: { errorCode: 'transientError' }, + }); + } +}); + +module.exports = router; diff --git a/web/src/routes/voice.js b/web/src/routes/voice.js new file mode 100644 index 0000000..d3394e5 --- /dev/null +++ b/web/src/routes/voice.js @@ -0,0 +1,65 @@ +const express = require('express'); +const { trouverEnfantParPrenom, listerEnfants } = require('../db'); +const { messageScore, messageTousLesScores } = require('../vocal'); + +const router = express.Router(); + +function reponseVocale(prenom) { + if (!prenom) { + const enfants = listerEnfants(); + const speech = messageTousLesScores(enfants); + return { speech, enfants }; + } + + const enfant = trouverEnfantParPrenom(prenom); + if (!enfant) { + const speech = `Je ne connais pas ${prenom}. Les enfants sont Ariana, Pablo et Hélia.`; + return { speech, score: null }; + } + + const speech = messageScore(enfant.prenom, enfant.score); + return { speech, score: enfant.score, prenom: enfant.prenom }; +} + +router.get('/', (req, res) => { + const prenom = req.query.prenom; + const { speech, ...reste } = reponseVocale(prenom); + res.json({ speech, ...reste }); +}); + +router.get('/:prenom', (req, res) => { + const { speech, ...reste } = reponseVocale(req.params.prenom); + res.json({ speech, ...reste }); +}); + +function extrairePrenomDialogflow(body) { + const qr = body?.queryResult; + if (!qr) return null; + + const params = qr.parameters || {}; + const depuisParam = params.prenom || params['prenom-enfant'] || params.Prenom + || params.any; + if (depuisParam) return depuisParam; + + const intent = (qr.intent?.displayName || '').toLowerCase(); + if (intent.includes('tous') || intent.includes('all')) return null; + + const texte = (qr.queryText || '').toLowerCase() + .normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + for (const prenom of ['ariana', 'pablo', 'helia']) { + if (texte.includes(prenom)) return prenom; + } + return null; +} + +router.post('/dialogflow', (req, res) => { + const prenom = extrairePrenomDialogflow(req.body); + const { speech } = reponseVocale(prenom); + + res.json({ + fulfillmentText: speech, + fulfillmentMessages: [{ text: { text: [speech] } }], + }); +}); + +module.exports = router; diff --git a/web/src/server.js b/web/src/server.js new file mode 100644 index 0000000..b94d3cd --- /dev/null +++ b/web/src/server.js @@ -0,0 +1,49 @@ +const express = require('express'); +const cookieParser = require('cookie-parser'); +const path = require('path'); +const { initialiser } = require('./db'); + +initialiser(); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.set('trust proxy', 1); +app.use(express.json()); +app.use(cookieParser()); +app.get('/sw.js', (_req, res) => { + res.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.sendFile(path.join(__dirname, '..', 'public', 'sw.js')); +}); + +app.use(express.static(path.join(__dirname, '..', 'public'))); + +app.use('/api/public', require('./routes/public')); +app.use('/api/parent', require('./routes/parent')); +app.use('/api/voice', require('./routes/voice')); +app.use('/api/smarthome', require('./routes/smarthome')); + +app.get('/parent', (_req, res) => { + res.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.sendFile(path.join(__dirname, '..', 'public', 'parent.html')); +}); + +app.get('/boutique', (_req, res) => { + res.redirect('/'); +}); + +app.get('/boutique/:prenom', (_req, res) => { + res.sendFile(path.join(__dirname, '..', 'public', 'boutique.html')); +}); + +app.get('/enfant/:prenom', (_req, res) => { + res.sendFile(path.join(__dirname, '..', 'public', 'enfant.html')); +}); + +app.get('/reglement', (_req, res) => { + res.sendFile(path.join(__dirname, '..', 'public', 'reglement.html')); +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`Bonpoint démarré sur le port ${PORT}`); +}); diff --git a/web/src/smarthome.js b/web/src/smarthome.js new file mode 100644 index 0000000..77b13ba --- /dev/null +++ b/web/src/smarthome.js @@ -0,0 +1,119 @@ +const { listerEnfants } = require('./db'); + +function idEnfant(enfantId) { + return `bonpoint-enfant-${enfantId}`; +} + +function parseIdEnfant(deviceId) { + const match = /^bonpoint-enfant-(\d+)$/.exec(deviceId); + return match ? parseInt(match[1], 10) : null; +} + +function construireAppareils() { + return listerEnfants().map((e) => ({ + id: idEnfant(e.id), + type: 'action.devices.types.SENSOR', + traits: ['action.devices.traits.EnergyStorage'], + name: { + name: e.prenom, + nicknames: [e.prenom, `bons points ${e.prenom}`], + }, + roomHint: 'Bonpoint', + willReportState: true, + attributes: { + queryOnlyEnergyStorage: true, + unit: 'PERCENT', + }, + deviceInfo: { + manufacturer: 'Famille', + model: 'Bonpoint', + hwVersion: '1.0', + swVersion: '1.0', + }, + })); +} + +function reponseSync(requestId) { + return { + requestId, + payload: { + agentUserId: 'famille-bonpoint', + devices: construireAppareils(), + }, + }; +} + +function reponseQuery(requestId, deviceIds) { + const enfants = listerEnfants(); + const parId = new Map(enfants.map((e) => [idEnfant(e.id), e])); + const devices = {}; + + for (const deviceId of deviceIds) { + const enfant = parId.get(deviceId); + if (!enfant) { + devices[deviceId] = { + online: false, + status: 'ERROR', + errorCode: 'deviceNotFound', + }; + continue; + } + + const niveau = Math.min(Math.max(enfant.score, 0), 100); + devices[deviceId] = { + online: true, + status: 'SUCCESS', + descriptiveCapacityRemaining: String(niveau), + capacityRemaining: [{ rawValue: niveau, unit: 'PERCENT' }], + }; + } + + return { requestId, payload: { devices } }; +} + +function reponseExecute(requestId) { + return { + requestId, + payload: { + commands: [{ + ids: [], + status: 'ERROR', + errorCode: 'notSupported', + }], + }, + }; +} + +function reponseDisconnect(requestId) { + return { requestId, payload: {} }; +} + +function traiterRequeteSmartHome(body) { + const requestId = body.requestId || 'inconnu'; + const input = body.inputs?.[0]; + const intent = input?.intent; + + switch (intent) { + case 'action.devices.SYNC': + return reponseSync(requestId); + case 'action.devices.QUERY': { + const deviceIds = (input.payload?.devices || []).map((d) => d.id); + return reponseQuery(requestId, deviceIds); + } + case 'action.devices.EXECUTE': + return reponseExecute(requestId); + case 'action.devices.DISCONNECT': + return reponseDisconnect(requestId); + default: + return { + requestId, + payload: { errorCode: 'notSupported' }, + }; + } +} + +module.exports = { + traiterRequeteSmartHome, + idEnfant, + parseIdEnfant, +}; diff --git a/web/src/vocal.js b/web/src/vocal.js new file mode 100644 index 0000000..77e19f2 --- /dev/null +++ b/web/src/vocal.js @@ -0,0 +1,70 @@ +const UNITES = [ + 'zéro', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf', + 'dix', 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize', +]; + +const DIZAINES = [ + '', '', 'vingt', 'trente', 'quarante', 'cinquante', 'soixante', +]; + +function sousCent(n) { + if (n < 17) return UNITES[n]; + if (n < 20) return 'dix-' + UNITES[n - 10]; + if (n < 70) { + const d = Math.floor(n / 10); + const u = n % 10; + if (u === 0) return DIZAINES[d]; + if (u === 1) return DIZAINES[d] + ' et un'; + return DIZAINES[d] + '-' + UNITES[u]; + } + if (n < 80) { + const u = n - 60; + if (u === 11) return 'soixante et onze'; + return 'soixante-' + sousCent(u); + } + if (n < 100) { + const u = n - 80; + if (u === 0) return 'quatre-vingts'; + return 'quatre-vingt-' + sousCent(u); + } + return String(n); +} + +function nombreEnFrancais(n) { + if (n < 0) return 'moins ' + nombreEnFrancais(-n); + if (n < 100) return sousCent(n); + if (n < 200) { + const reste = n - 100; + return reste === 0 ? 'cent' : 'cent ' + sousCent(reste); + } + return String(n); +} + +function messageScore(prenom, score) { + const points = nombreEnFrancais(score); + const mot = score <= 1 ? 'bon point' : 'bons points'; + + if (score === 0) { + return `${prenom} n'a pas encore de bons points.`; + } + if (score < 0) { + return `${prenom} a ${points} points. Attention, c'est négatif !`; + } + return `${prenom} a ${points} ${mot} !`; +} + +function messageTousLesScores(enfants) { + if (enfants.length === 0) { + return "Je n'ai trouvé aucun enfant."; + } + + const parties = enfants.map((e) => { + const pts = nombreEnFrancais(e.score); + const mot = e.score <= 1 ? 'bon point' : 'bons points'; + return `${e.prenom} a ${pts} ${mot}`; + }); + + return parties.join('. ') + '.'; +} + +module.exports = { nombreEnFrancais, messageScore, messageTousLesScores };