feat: Banque des Coquillages v1.0 avec Docker et configuration NPM
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
*/node_modules
|
||||||
|
*/*/node_modules
|
||||||
|
.git
|
||||||
|
.gemini
|
||||||
|
back/data/*.sqlite*
|
||||||
|
back/uploads/*
|
||||||
|
front/dist
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Dépendances Node
|
||||||
|
node_modules/
|
||||||
|
back/node_modules/
|
||||||
|
front/node_modules/
|
||||||
|
skill/lambda/node_modules/
|
||||||
|
|
||||||
|
# Builds et artefacts
|
||||||
|
front/dist/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Données locales persistées via Docker volumes
|
||||||
|
data/
|
||||||
|
uploads/
|
||||||
|
back/data/*.sqlite*
|
||||||
|
!back/data/.gitkeep
|
||||||
|
back/uploads/*
|
||||||
|
!back/uploads/.gitkeep
|
||||||
|
|
||||||
|
# Fichiers d'environnement
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
|
||||||
|
# Fichiers système & IDE
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.gemini/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
124
README.md
Normal file
124
README.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# 🐚 La Banque des Coquillages — Hailey, Violette & Andreas
|
||||||
|
|
||||||
|
Application familiale **Physique + Digitale** (Site Web + API + Skill Alexa Echo Show) pour gérer les coquillages de **Hailey**, **Violette** et **Andreas**, leurs activités communes du mois, leur tirelire en euros (max 5 € / mois = 10 coquillages) et leur livret d'épargne bloquée avec intérêts mensuels.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Structure du Projet
|
||||||
|
|
||||||
|
```text
|
||||||
|
arrweek/
|
||||||
|
├── back/ # API REST Node.js 22 + Express + SQLite (node:sqlite) + Webhook Alexa
|
||||||
|
│ └── Dockerfile # Image Docker backend de production (Node 22 Alpine)
|
||||||
|
├── front/ # Application Web React + Vite + Tailwind CSS
|
||||||
|
│ ├── Dockerfile # Image Docker frontend multi-stage (Node 22 + Nginx Alpine)
|
||||||
|
│ └── nginx.conf # Configuration Nginx avec reverse proxy (/api/ & /uploads/)
|
||||||
|
├── skill/ # Skill Alexa officiel
|
||||||
|
│ ├── interactionModels/ # Modèle d'interaction vocal (fr-FR.json)
|
||||||
|
│ └── lambda/ # Handler ASK SDK avec écrans APL 1.8 tactiles pour Echo Show
|
||||||
|
├── docker-compose.yml # Déploiement Docker complet (backend + frontend + volumes)
|
||||||
|
└── package.json # Monorepo npm workspaces
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Déploiement Docker & Nginx Proxy Manager (NPM)
|
||||||
|
|
||||||
|
### 1. Démarrer avec Docker Compose
|
||||||
|
Sur votre serveur Docker :
|
||||||
|
```bash
|
||||||
|
# Cloner le dépôt depuis votre git perso
|
||||||
|
git clone https://git.home.arrondeau.fr/<votre-utilisateur>/<votre-depot>.git
|
||||||
|
cd <votre-depot>
|
||||||
|
|
||||||
|
# Créer les dossiers de persistance
|
||||||
|
mkdir -p data uploads
|
||||||
|
|
||||||
|
# Lancer la stack en arrière-plan
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Services démarrés :
|
||||||
|
- **`coquillages-frontend`** : port `5180` (sert l'application React et relaie automatiquement `/api/` et `/uploads/` vers le backend).
|
||||||
|
- **`coquillages-backend`** : port `4050` (API Node.js + base SQLite persistée dans `./data` et images dans `./uploads`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🌐 2. Configuration dans Nginx Proxy Manager (NPM)
|
||||||
|
|
||||||
|
Dans l'interface web de votre **Nginx Proxy Manager** :
|
||||||
|
|
||||||
|
#### Option A (Recommandée — Domaine unique tout-en-un) :
|
||||||
|
Créez un nouveau **Proxy Host** :
|
||||||
|
1. **Domain Names** : `coquillages.home.arrondeau.fr` (ou votre sous-domaine choisi)
|
||||||
|
2. **Scheme** : `http`
|
||||||
|
3. **Forward Hostname / IP** : `IP_DE_VOTRE_SERVEUR_DOCKER` (ou le nom du conteneur si NPM est sur le même réseau Docker : `coquillages-frontend`)
|
||||||
|
4. **Forward Port** : `5180` (ou `80` en interne)
|
||||||
|
5. **Options recommandées** :
|
||||||
|
- [x] **Block Common Exploits**
|
||||||
|
- [x] **Websockets Support**
|
||||||
|
6. **Onglet SSL** :
|
||||||
|
- Sélectionnez ou générez un certificat SSL Let's Encrypt
|
||||||
|
- [x] **Force SSL**
|
||||||
|
- [x] **HTTP/2 Support**
|
||||||
|
- [x] **HSTS Enabled**
|
||||||
|
|
||||||
|
> 🎉 **Résultat :**
|
||||||
|
> - **Application Web** : `https://coquillages.home.arrondeau.fr`
|
||||||
|
> - **API REST** : `https://coquillages.home.arrondeau.fr/api/...`
|
||||||
|
> - **Endpoint Alexa Webhook permanent** : `https://coquillages.home.arrondeau.fr/api/alexa/webhook` *(plus besoin de tunnel SSH ou localhost.run !)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎙️ 3. Configuration de l'Endpoint dans la Console Alexa Developer
|
||||||
|
|
||||||
|
Une fois exposé via NPM avec votre certificat HTTPS :
|
||||||
|
1. Allez sur la [Console Alexa Developer](https://developer.amazon.com/alexa/console/ask).
|
||||||
|
2. Ouvrez votre Skill **La Banque des Coquillages**.
|
||||||
|
3. Dans **Endpoint** :
|
||||||
|
- Sélectionnez **HTTPS**.
|
||||||
|
- **Default Region** : `https://coquillages.home.arrondeau.fr/api/alexa/webhook`
|
||||||
|
- Sélectionnez l'option de certificat SSL : *« My development endpoint has a Certificate from a trusted certificate authority »* (Let's Encrypt).
|
||||||
|
- Cliquez sur **Save Endpoints**.
|
||||||
|
4. Dans **Interaction Model > JSON Editor** :
|
||||||
|
- Collez le contenu de [`skill/interactionModels/custom/fr-FR.json`](./skill/interactionModels/custom/fr-FR.json).
|
||||||
|
- Cliquez sur **Save Model** puis **Build Model**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 4. Initialisation & Push sur votre Git perso (`git.home.arrondeau.fr`)
|
||||||
|
|
||||||
|
Pour initialiser ce dépôt local et le pousser sur votre instance Git personnelle :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Initialiser le dépôt git localement
|
||||||
|
git init
|
||||||
|
|
||||||
|
# 2. Ajouter tous les fichiers (le .gitignore protège les node_modules et sqlite)
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# 3. Premier commit
|
||||||
|
git commit -m "feat: Banque des Coquillages v1.0 avec Docker et support NPM"
|
||||||
|
|
||||||
|
# 4. Définir la branche principale
|
||||||
|
git branch -M main
|
||||||
|
|
||||||
|
# 5. Lier votre serveur Git perso
|
||||||
|
git remote add origin https://git.home.arrondeau.fr/<votre-utilisateur>/<nom-du-depot>.git
|
||||||
|
|
||||||
|
# 6. Pousser le code
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Développement Local (sans Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lancer Backend + Frontend simultanément
|
||||||
|
npm run start:all
|
||||||
|
|
||||||
|
# Ou séparément :
|
||||||
|
npm run dev:back # Backend sur http://localhost:4050
|
||||||
|
npm run dev:front # Frontend Vite sur http://localhost:5180
|
||||||
|
```
|
||||||
27
back/Dockerfile
Normal file
27
back/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copie des définitions de packages du monorepo
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY back/package.json ./back/
|
||||||
|
COPY skill/lambda/package.json ./skill/lambda/
|
||||||
|
|
||||||
|
# Installation des dépendances de production
|
||||||
|
RUN npm install --omit=dev --workspace=back --workspace=skill/lambda
|
||||||
|
|
||||||
|
# Copie des sources du backend et du skill Alexa (utilisé par le webhook Alexa)
|
||||||
|
COPY back ./back
|
||||||
|
COPY skill ./skill
|
||||||
|
|
||||||
|
# Création des dossiers de persistance
|
||||||
|
RUN mkdir -p /app/back/data /app/back/uploads
|
||||||
|
|
||||||
|
WORKDIR /app/back
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=4050
|
||||||
|
|
||||||
|
EXPOSE 4050
|
||||||
|
|
||||||
|
CMD ["node", "src/server.js"]
|
||||||
0
back/data/.gitkeep
Normal file
0
back/data/.gitkeep
Normal file
15
back/package.json
Normal file
15
back/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "arrweek-back",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Backend API & Moteur Bancaire pour la Banque des Coquillages (Hailey, Violette & Andreas)",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/server.js",
|
||||||
|
"dev": "node --watch src/server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.21.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
270
back/src/db.js
Normal file
270
back/src/db.js
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { DatabaseSync } from 'node:sqlite';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const dataDir = path.join(__dirname, '..', 'data');
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadsDir = path.join(__dirname, '..', 'uploads');
|
||||||
|
if (!fs.existsSync(uploadsDir)) {
|
||||||
|
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbPath = path.join(dataDir, 'coquillages.sqlite');
|
||||||
|
export const db = new DatabaseSync(dbPath);
|
||||||
|
|
||||||
|
export const DEFAULT_ACTIVITY_IMAGES = {
|
||||||
|
pizza: 'https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=1000&q=80',
|
||||||
|
bowling: 'https://images.unsplash.com/photo-1538511059256-06459b067f41?auto=format&fit=crop&w=1000&q=80',
|
||||||
|
aquatique: 'https://images.unsplash.com/photo-1582653291997-079a1c04e5a1?auto=format&fit=crop&w=1000&q=80',
|
||||||
|
accrobranche: 'https://images.unsplash.com/photo-1542273917363-3b1817f69a2d?auto=format&fit=crop&w=1000&q=80',
|
||||||
|
default: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=1000&q=80'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeName(str = '') {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initDb() {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS children (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
avatar TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
available_shells INTEGER NOT NULL DEFAULT 0,
|
||||||
|
locked_shells INTEGER NOT NULL DEFAULT 0,
|
||||||
|
euros_balance REAL NOT NULL DEFAULT 0,
|
||||||
|
shells_converted_month INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS activities (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
emoji TEXT NOT NULL DEFAULT '🎡',
|
||||||
|
image_url TEXT NOT NULL DEFAULT '',
|
||||||
|
cost_shells INTEGER NOT NULL,
|
||||||
|
current_shells INTEGER NOT NULL DEFAULT 0,
|
||||||
|
status TEXT NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_contributions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
activity_id INTEGER NOT NULL,
|
||||||
|
child_id INTEGER NOT NULL,
|
||||||
|
shells INTEGER NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (activity_id) REFERENCES activities(id),
|
||||||
|
FOREIGN KEY (child_id) REFERENCES children(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pending_requests (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
child_id INTEGER NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
shells INTEGER NOT NULL,
|
||||||
|
activity_id INTEGER,
|
||||||
|
source TEXT NOT NULL DEFAULT 'ALEXA',
|
||||||
|
status TEXT NOT NULL DEFAULT 'PENDING',
|
||||||
|
note TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
resolved_at TEXT,
|
||||||
|
FOREIGN KEY (child_id) REFERENCES children(id),
|
||||||
|
FOREIGN KEY (activity_id) REFERENCES activities(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
child_id INTEGER,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
shells_delta INTEGER NOT NULL DEFAULT 0,
|
||||||
|
euros_delta REAL NOT NULL DEFAULT 0,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (child_id) REFERENCES children(id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Migration douce : ajouter image_url si la table activities existait déjà sans cette colonne
|
||||||
|
const columns = db.prepare("PRAGMA table_info('activities')").all();
|
||||||
|
const hasImageUrl = columns.some((c) => c.name === 'image_url');
|
||||||
|
if (!hasImageUrl) {
|
||||||
|
db.exec("ALTER TABLE activities ADD COLUMN image_url TEXT NOT NULL DEFAULT ''");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration douce : ajouter alexa_person_id sur la table children pour la reconnaissance vocale Alexa (Voice ID)
|
||||||
|
const childColumns = db.prepare("PRAGMA table_info('children')").all();
|
||||||
|
const hasPersonId = childColumns.some((c) => c.name === 'alexa_person_id');
|
||||||
|
if (!hasPersonId) {
|
||||||
|
db.exec("ALTER TABLE children ADD COLUMN alexa_person_id TEXT DEFAULT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration douce : ajouter proposed_title sur la table pending_requests pour les propositions d'activités des enfants
|
||||||
|
const prColumns = db.prepare("PRAGMA table_info('pending_requests')").all();
|
||||||
|
const hasProposedTitle = prColumns.some((c) => c.name === 'proposed_title');
|
||||||
|
if (!hasProposedTitle) {
|
||||||
|
db.exec("ALTER TABLE pending_requests ADD COLUMN proposed_title TEXT DEFAULT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remplir les images par défaut pour les activités existantes qui n'en ont pas encore
|
||||||
|
const existingActivities = db.prepare('SELECT id, title, image_url FROM activities').all();
|
||||||
|
const updateImg = db.prepare('UPDATE activities SET image_url = ? WHERE id = ?');
|
||||||
|
for (const act of existingActivities) {
|
||||||
|
if (!act.image_url) {
|
||||||
|
const norm = normalizeName(act.title);
|
||||||
|
let img = DEFAULT_ACTIVITY_IMAGES.default;
|
||||||
|
if (norm.includes('pizza') || norm.includes('cinema')) img = DEFAULT_ACTIVITY_IMAGES.pizza;
|
||||||
|
else if (norm.includes('bowling')) img = DEFAULT_ACTIVITY_IMAGES.bowling;
|
||||||
|
else if (norm.includes('aquatique') || norm.includes('piscine')) img = DEFAULT_ACTIVITY_IMAGES.aquatique;
|
||||||
|
else if (norm.includes('accrobranche')) img = DEFAULT_ACTIVITY_IMAGES.accrobranche;
|
||||||
|
updateImg.run(img, act.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed default settings if empty
|
||||||
|
const existingRate = db.prepare('SELECT value FROM settings WHERE key = ?').get('interest_rate_percent');
|
||||||
|
if (!existingRate) {
|
||||||
|
const insertSetting = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
|
||||||
|
insertSetting.run('interest_rate_percent', '20');
|
||||||
|
insertSetting.run('shells_per_euro', '2');
|
||||||
|
insertSetting.run('max_euros_per_month', '5');
|
||||||
|
insertSetting.run('max_shells_euro_month', '10');
|
||||||
|
insertSetting.run('last_interest_run', 'Jamais');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed children: Hailey, Violette, Andreas
|
||||||
|
const childCount = db.prepare('SELECT COUNT(*) as count FROM children').get().count;
|
||||||
|
if (childCount === 0) {
|
||||||
|
const insertChild = db.prepare(`
|
||||||
|
INSERT INTO children (name, slug, avatar, color, available_shells, locked_shells, euros_balance, shells_converted_month)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
insertChild.run('Hailey', 'hailey', '🧜♀️', 'pink', 12, 10, 1.5, 3);
|
||||||
|
insertChild.run('Violette', 'violette', '🐬', 'purple', 8, 15, 2.0, 4);
|
||||||
|
insertChild.run('Andreas', 'andreas', '🦈', 'cyan', 14, 10, 1.0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed activities if empty
|
||||||
|
const activityCount = db.prepare('SELECT COUNT(*) as count FROM activities').get().count;
|
||||||
|
if (activityCount === 0) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const insertActivity = db.prepare(`
|
||||||
|
INSERT INTO activities (title, description, emoji, image_url, cost_shells, current_shells, status, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
insertActivity.run(
|
||||||
|
'Soirée Pizza & Cinéma',
|
||||||
|
'Chacun choisit sa pizza et on regarde un grand film en famille avec du pop-corn !',
|
||||||
|
'🍕',
|
||||||
|
DEFAULT_ACTIVITY_IMAGES.pizza,
|
||||||
|
25,
|
||||||
|
12,
|
||||||
|
'ACTIVE',
|
||||||
|
now
|
||||||
|
);
|
||||||
|
insertActivity.run(
|
||||||
|
'Bowling en famille',
|
||||||
|
'Sortie au bowling avec 2 parties et boissons pour tout le monde.',
|
||||||
|
'🎳',
|
||||||
|
DEFAULT_ACTIVITY_IMAGES.bowling,
|
||||||
|
45,
|
||||||
|
15,
|
||||||
|
'ACTIVE',
|
||||||
|
now
|
||||||
|
);
|
||||||
|
insertActivity.run(
|
||||||
|
'Parc Aquatique',
|
||||||
|
'Une après-midi complète dans les toboggans géants et la piscine à vagues.',
|
||||||
|
'🌊',
|
||||||
|
DEFAULT_ACTIVITY_IMAGES.aquatique,
|
||||||
|
70,
|
||||||
|
0,
|
||||||
|
'ACTIVE',
|
||||||
|
now
|
||||||
|
);
|
||||||
|
insertActivity.run(
|
||||||
|
'Sortie Accrobranche',
|
||||||
|
'Parcours dans les arbres et tyroliennes pour Hailey, Violette et Andreas.',
|
||||||
|
'🧗',
|
||||||
|
DEFAULT_ACTIVITY_IMAGES.accrobranche,
|
||||||
|
60,
|
||||||
|
0,
|
||||||
|
'ACTIVE',
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
const insertContribution = db.prepare(`
|
||||||
|
INSERT INTO activity_contributions (activity_id, child_id, shells, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
insertContribution.run(1, 1, 4, now);
|
||||||
|
insertContribution.run(1, 2, 4, now);
|
||||||
|
insertContribution.run(1, 3, 4, now);
|
||||||
|
|
||||||
|
insertContribution.run(2, 1, 5, now);
|
||||||
|
insertContribution.run(2, 2, 5, now);
|
||||||
|
insertContribution.run(2, 3, 5, now);
|
||||||
|
|
||||||
|
const insertRequest = db.prepare(`
|
||||||
|
INSERT INTO pending_requests (child_id, type, shells, activity_id, source, status, note, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
insertRequest.run(
|
||||||
|
1,
|
||||||
|
'DEPOSIT_LOCKED',
|
||||||
|
6,
|
||||||
|
null,
|
||||||
|
'ALEXA',
|
||||||
|
'PENDING',
|
||||||
|
'Hailey a demandé sur Alexa de déposer et bloquer 6 coquillages physiques pour gagner des intérêts.',
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
const insertTx = db.prepare(`
|
||||||
|
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
insertTx.run(1, 'INIT', 22, 1.5, 'Ouverture du compte coquillages de Hailey', now);
|
||||||
|
insertTx.run(2, 'INIT', 23, 2.0, 'Ouverture du compte coquillages de Violette', now);
|
||||||
|
insertTx.run(3, 'INIT', 24, 1.0, 'Ouverture du compte coquillages d\'Andreas', now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSettings() {
|
||||||
|
const rows = db.prepare('SELECT key, value FROM settings').all();
|
||||||
|
const map = {};
|
||||||
|
for (const r of rows) {
|
||||||
|
map[r.key] = r.value;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
interest_rate_percent: Number(map.interest_rate_percent ?? 20),
|
||||||
|
shells_per_euro: Number(map.shells_per_euro ?? 2),
|
||||||
|
max_euros_per_month: Number(map.max_euros_per_month ?? 5),
|
||||||
|
max_shells_euro_month: Number(map.max_shells_euro_month ?? 10),
|
||||||
|
last_interest_run: map.last_interest_run ?? 'Jamais'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateProjectedInterest(lockedShells, ratePercent) {
|
||||||
|
if (lockedShells <= 0 || ratePercent <= 0) return 0;
|
||||||
|
return Math.ceil((lockedShells * ratePercent) / 100);
|
||||||
|
}
|
||||||
1203
back/src/server.js
Normal file
1203
back/src/server.js
Normal file
File diff suppressed because it is too large
Load Diff
0
back/uploads/.gitkeep
Normal file
0
back/uploads/.gitkeep
Normal file
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: back/Dockerfile
|
||||||
|
container_name: coquillages-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Port exposé pour accès direct ou pour Nginx Proxy Manager (NPM)
|
||||||
|
- "4050:4050"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=4050
|
||||||
|
volumes:
|
||||||
|
# Persistance de la base SQLite et des images téléversées
|
||||||
|
- ./data:/app/back/data
|
||||||
|
- ./uploads:/app/back/uploads
|
||||||
|
networks:
|
||||||
|
- coquillages-net
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: front/Dockerfile
|
||||||
|
container_name: coquillages-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Port exposé pour l'accès web ou pour Nginx Proxy Manager (NPM)
|
||||||
|
- "5180:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- coquillages-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
coquillages-net:
|
||||||
|
driver: bridge
|
||||||
30
front/Dockerfile
Normal file
30
front/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Étape 1 : Build de l'application React Vite
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copie des configurations de paquets du monorepo
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY front/package.json ./front/
|
||||||
|
|
||||||
|
# Installation des dépendances du frontend
|
||||||
|
RUN npm install --workspace=front
|
||||||
|
|
||||||
|
# Copie du code source frontend
|
||||||
|
COPY front ./front
|
||||||
|
|
||||||
|
# Construction du bundle statique de production
|
||||||
|
RUN npm --workspace=front run build
|
||||||
|
|
||||||
|
# Étape 2 : Image Nginx ultra-légère pour servir les fichiers statiques et relayer l'API
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copie des fichiers statiques générés
|
||||||
|
COPY --from=builder /app/front/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Configuration Nginx personnalisée avec reverse proxy vers backend:4050
|
||||||
|
COPY front/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
13
front/index.html
Normal file
13
front/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>🐚 La Banque des Coquillages — Hailey, Violette & Andreas</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-950 text-slate-100 antialiased selection:bg-amber-400 selection:text-slate-950">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
40
front/nginx.conf
Normal file
40
front/nginx.conf
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# Compression gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Relai vers le backend Express pour toutes les requêtes API
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:4050/api/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Relai vers les images uploadées servies par le backend
|
||||||
|
location /uploads/ {
|
||||||
|
proxy_pass http://backend:4050/uploads/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Application React (Single Page Application - fallback sur index.html)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
front/package.json
Normal file
25
front/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "arrweek-front",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 5180",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.475.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"tailwindcss": "^4.0.6",
|
||||||
|
"vite": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1762
front/src/App.jsx
Normal file
1762
front/src/App.jsx
Normal file
File diff suppressed because it is too large
Load Diff
2
front/src/index.css
Normal file
2
front/src/index.css
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
11
front/src/main.jsx
Normal file
11
front/src/main.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App.jsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
21
front/vite.config.js
Normal file
21
front/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
server: {
|
||||||
|
port: 5180,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:4050',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/uploads': {
|
||||||
|
target: 'http://localhost:4050',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
3391
package-lock.json
generated
Normal file
3391
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "arrweek-monorepo",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"workspaces": [
|
||||||
|
"back",
|
||||||
|
"front",
|
||||||
|
"skill/lambda"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"start:all": "node start-all.js",
|
||||||
|
"tunnel": "ssh -o StrictHostKeyChecking=no -R 80:localhost:4050 nokey@localhost.run",
|
||||||
|
"start:back": "npm --workspace=back start",
|
||||||
|
"dev:back": "npm --workspace=back run dev",
|
||||||
|
"dev:front": "npm --workspace=front run dev",
|
||||||
|
"build:front": "npm --workspace=front run build",
|
||||||
|
"test:skill": "npm --workspace=skill/lambda test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
388
skill/interactionModels/custom/fr-FR.json
Normal file
388
skill/interactionModels/custom/fr-FR.json
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
{
|
||||||
|
"interactionModel": {
|
||||||
|
"languageModel": {
|
||||||
|
"invocationName": "les coquillages",
|
||||||
|
"intents": [
|
||||||
|
{
|
||||||
|
"name": "AMAZON.CancelIntent",
|
||||||
|
"samples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AMAZON.HelpIntent",
|
||||||
|
"samples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AMAZON.StopIntent",
|
||||||
|
"samples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AMAZON.NavigateHomeIntent",
|
||||||
|
"samples": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OpenBankIntent",
|
||||||
|
"slots": [],
|
||||||
|
"samples": [
|
||||||
|
"la banque",
|
||||||
|
"ouvre la banque",
|
||||||
|
"la banque des coquillages",
|
||||||
|
"ouvre la banque des coquillages",
|
||||||
|
"la boîte",
|
||||||
|
"ouvre la boîte",
|
||||||
|
"la boîte à coquillages",
|
||||||
|
"la boîte des coquillages",
|
||||||
|
"ouvre la boîte à coquillages",
|
||||||
|
"ouvre la boîte des coquillages",
|
||||||
|
"tableau de bord",
|
||||||
|
"retour à la banque",
|
||||||
|
"retour à l'accueil"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IdentifySpeakerIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ChildName",
|
||||||
|
"type": "CHILD_NAME"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"je suis {ChildName}",
|
||||||
|
"c'est {ChildName}",
|
||||||
|
"moi c'est {ChildName}",
|
||||||
|
"associe ma voix à {ChildName}",
|
||||||
|
"enregistre ma voix pour {ChildName}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GetBalanceIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ChildName",
|
||||||
|
"type": "CHILD_NAME"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"combien j'ai de coquillages",
|
||||||
|
"bonjour combien j'ai de coquillages",
|
||||||
|
"combien ai-je de coquillages",
|
||||||
|
"j'ai combien de coquillages",
|
||||||
|
"combien j'ai",
|
||||||
|
"quel est mon solde",
|
||||||
|
"où en est mon compte",
|
||||||
|
"combien j'ai dans ma tirelire",
|
||||||
|
"combien de coquillages a {ChildName}",
|
||||||
|
"quel est le solde de {ChildName}",
|
||||||
|
"combien a {ChildName}",
|
||||||
|
"combien {ChildName} a de coquillages",
|
||||||
|
"où en est {ChildName}",
|
||||||
|
"quel est notre bilan",
|
||||||
|
"combien on a de coquillages en banque",
|
||||||
|
"quel est le solde de la famille",
|
||||||
|
"combien d'argent a {ChildName}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ListActivitiesIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ShellCount",
|
||||||
|
"type": "AMAZON.NUMBER"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"quelles sont les activités",
|
||||||
|
"quelles sont les activités du mois",
|
||||||
|
"quelles sont les activités disponibles",
|
||||||
|
"qu'est-ce qu'on peut faire comme activité",
|
||||||
|
"que peut-on faire avec {ShellCount} coquillages",
|
||||||
|
"quelles activités on peut payer avec {ShellCount} coquillages",
|
||||||
|
"qu'est-ce qu'on peut acheter avec {ShellCount} coquillages",
|
||||||
|
"liste les activités"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DepositShellsIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ChildName",
|
||||||
|
"type": "CHILD_NAME"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ShellCount",
|
||||||
|
"type": "AMAZON.NUMBER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AccountType",
|
||||||
|
"type": "ACCOUNT_TYPE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"je veux bloquer {ShellCount} coquillages",
|
||||||
|
"je bloque {ShellCount} coquillages",
|
||||||
|
"je veux placer {ShellCount} coquillages",
|
||||||
|
"je dépose {ShellCount} coquillages",
|
||||||
|
"je place {ShellCount} coquillages sur mon compte {AccountType}",
|
||||||
|
"{ChildName} veut bloquer {ShellCount} coquillages",
|
||||||
|
"{ChildName} bloque {ShellCount} coquillages",
|
||||||
|
"{ChildName} veut placer {ShellCount} coquillages",
|
||||||
|
"{ChildName} place {ShellCount} coquillages sur son compte {AccountType}",
|
||||||
|
"{ChildName} veut déposer {ShellCount} coquillages sur son compte {AccountType}",
|
||||||
|
"{ChildName} dépose {ShellCount} coquillages",
|
||||||
|
"bloquer {ShellCount} coquillages pour {ChildName}",
|
||||||
|
"placer {ShellCount} coquillages pour {ChildName}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ContributeActivityIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ChildName",
|
||||||
|
"type": "CHILD_NAME"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ShellCount",
|
||||||
|
"type": "AMAZON.NUMBER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ActivityName",
|
||||||
|
"type": "ACTIVITY_NAME"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"je veux ajouter des coquillages sur {ActivityName}",
|
||||||
|
"je veux mettre des coquillages pour {ActivityName}",
|
||||||
|
"j'ajoute des coquillages pour {ActivityName}",
|
||||||
|
"je participe à {ActivityName}",
|
||||||
|
"{ChildName} veut ajouter des coquillages sur {ActivityName}",
|
||||||
|
"{ChildName} veut mettre des coquillages pour {ActivityName}",
|
||||||
|
"{ChildName} ajoute des coquillages sur {ActivityName}",
|
||||||
|
"{ChildName} participe à {ActivityName}",
|
||||||
|
"je mets {ShellCount} coquillages pour {ActivityName}",
|
||||||
|
"je donne {ShellCount} coquillages pour {ActivityName}",
|
||||||
|
"je veux mettre {ShellCount} coquillages sur {ActivityName}",
|
||||||
|
"je cotise {ShellCount} coquillages pour {ActivityName}",
|
||||||
|
"{ChildName} met {ShellCount} coquillages pour {ActivityName}",
|
||||||
|
"{ChildName} donne {ShellCount} coquillages pour {ActivityName}",
|
||||||
|
"{ChildName} veut mettre {ShellCount} coquillages sur {ActivityName}",
|
||||||
|
"mettre {ShellCount} coquillages de {ChildName} pour {ActivityName}",
|
||||||
|
"{ChildName} cotise {ShellCount} coquillages pour {ActivityName}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ConvertEurosIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ChildName",
|
||||||
|
"type": "CHILD_NAME"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ShellCount",
|
||||||
|
"type": "AMAZON.NUMBER"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"je veux convertir {ShellCount} coquillages en euros",
|
||||||
|
"je veux échanger {ShellCount} coquillages contre des euros",
|
||||||
|
"j'échange {ShellCount} coquillages en euros",
|
||||||
|
"je mets {ShellCount} coquillages dans ma tirelire en euros",
|
||||||
|
"{ChildName} veut convertir {ShellCount} coquillages en euros",
|
||||||
|
"{ChildName} veut échanger {ShellCount} coquillages contre des euros",
|
||||||
|
"{ChildName} échange {ShellCount} coquillages en euros",
|
||||||
|
"convertir {ShellCount} coquillages en euros pour {ChildName}",
|
||||||
|
"{ChildName} met {ShellCount} coquillages dans sa tirelire en euros"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SimulateInterestIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ShellCount",
|
||||||
|
"type": "AMAZON.NUMBER"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"combien rapportent {ShellCount} coquillages bloqués",
|
||||||
|
"quel est l'intérêt pour {ShellCount} coquillages",
|
||||||
|
"si je bloque {ShellCount} coquillages je gagne combien",
|
||||||
|
"calcule les intérêts pour {ShellCount} coquillages"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GetActivityDetailIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ActivityName",
|
||||||
|
"type": "ACTIVITY_NAME"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"affiche les détails de {ActivityName}",
|
||||||
|
"montre les détails de {ActivityName}",
|
||||||
|
"donne moi les détails de {ActivityName}",
|
||||||
|
"affiche l'activité {ActivityName}",
|
||||||
|
"montre moi l'activité {ActivityName}",
|
||||||
|
"où en est {ActivityName}",
|
||||||
|
"où en est l'activité {ActivityName}",
|
||||||
|
"combien il manque pour {ActivityName}",
|
||||||
|
"détails de {ActivityName}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ProposeActivityIntent",
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"name": "ChildName",
|
||||||
|
"type": "CHILD_NAME"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ActivityTitle",
|
||||||
|
"type": "AMAZON.SearchQuery"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"samples": [
|
||||||
|
"je propose l'activité {ActivityTitle}",
|
||||||
|
"je propose l'activité de {ActivityTitle}",
|
||||||
|
"je propose comme activité {ActivityTitle}",
|
||||||
|
"je propose {ActivityTitle}",
|
||||||
|
"je veux proposer l'activité {ActivityTitle}",
|
||||||
|
"je voudrais proposer l'activité {ActivityTitle}",
|
||||||
|
"je veux proposer comme activité {ActivityTitle}",
|
||||||
|
"je voudrais proposer {ActivityTitle}",
|
||||||
|
"je veux proposer {ActivityTitle}",
|
||||||
|
"propose l'activité {ActivityTitle}",
|
||||||
|
"propose comme activité {ActivityTitle}",
|
||||||
|
"proposer l'activité {ActivityTitle}",
|
||||||
|
"nouvelle activité {ActivityTitle}",
|
||||||
|
"ajoute l'activité {ActivityTitle}",
|
||||||
|
"je veux faire l'activité {ActivityTitle}",
|
||||||
|
"on pourrait faire l'activité {ActivityTitle}",
|
||||||
|
"on pourrait faire {ActivityTitle}",
|
||||||
|
"{ChildName} propose l'activité {ActivityTitle}",
|
||||||
|
"{ChildName} propose comme activité {ActivityTitle}",
|
||||||
|
"{ChildName} propose {ActivityTitle}",
|
||||||
|
"{ChildName} veut proposer l'activité {ActivityTitle}",
|
||||||
|
"{ChildName} veut proposer {ActivityTitle}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "CHILD_NAME",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"id": "hailey",
|
||||||
|
"name": {
|
||||||
|
"value": "Hailey",
|
||||||
|
"synonyms": [
|
||||||
|
"Hailey",
|
||||||
|
"Haley",
|
||||||
|
"Ailey",
|
||||||
|
"Élie",
|
||||||
|
"Hély"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "violette",
|
||||||
|
"name": {
|
||||||
|
"value": "Violette",
|
||||||
|
"synonyms": [
|
||||||
|
"Violette",
|
||||||
|
"Violet"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "andreas",
|
||||||
|
"name": {
|
||||||
|
"value": "Andreas",
|
||||||
|
"synonyms": [
|
||||||
|
"Andreas",
|
||||||
|
"Andréas",
|
||||||
|
"Andrea",
|
||||||
|
"Andréa"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACCOUNT_TYPE",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"id": "bloque",
|
||||||
|
"name": {
|
||||||
|
"value": "bloqué",
|
||||||
|
"synonyms": [
|
||||||
|
"bloqué",
|
||||||
|
"épargne",
|
||||||
|
"livret",
|
||||||
|
"intérêts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "disponible",
|
||||||
|
"name": {
|
||||||
|
"value": "disponible",
|
||||||
|
"synonyms": [
|
||||||
|
"disponible",
|
||||||
|
"courant",
|
||||||
|
"normal",
|
||||||
|
"virtuel"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACTIVITY_NAME",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"value": "Soirée Pizza et Cinéma",
|
||||||
|
"synonyms": [
|
||||||
|
"pizza",
|
||||||
|
"cinéma",
|
||||||
|
"soirée pizza",
|
||||||
|
"film"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"value": "Bowling en famille",
|
||||||
|
"synonyms": [
|
||||||
|
"bowling",
|
||||||
|
"le bowling"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"value": "Parc Aquatique",
|
||||||
|
"synonyms": [
|
||||||
|
"parc aquatique",
|
||||||
|
"piscine",
|
||||||
|
"toboggans"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"value": "Sortie Accrobranche",
|
||||||
|
"synonyms": [
|
||||||
|
"accrobranche",
|
||||||
|
"l'accrobranche",
|
||||||
|
"tyrolienne"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1197
skill/lambda/index.js
Normal file
1197
skill/lambda/index.js
Normal file
File diff suppressed because it is too large
Load Diff
47
skill/lambda/local-test.js
Normal file
47
skill/lambda/local-test.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { handler } from './index.js';
|
||||||
|
|
||||||
|
function buildIntentEvent(intentName, slots = {}) {
|
||||||
|
const formattedSlots = {};
|
||||||
|
for (const [k, v] of Object.entries(slots)) {
|
||||||
|
formattedSlots[k] = { name: k, value: String(v) };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: '1.0',
|
||||||
|
session: { new: false, sessionId: 'SessionId.test', application: { applicationId: 'amzn1.ask.skill.test' } },
|
||||||
|
context: { System: { application: { applicationId: 'amzn1.ask.skill.test' } } },
|
||||||
|
request: {
|
||||||
|
type: 'IntentRequest',
|
||||||
|
requestId: 'EdwRequestId.test',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
locale: 'fr-FR',
|
||||||
|
intent: {
|
||||||
|
name: intentName,
|
||||||
|
confirmationStatus: 'NONE',
|
||||||
|
slots: formattedSlots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
console.log('🎙️ Test local du Skill Alexa "La Banque des Coquillages"...\n');
|
||||||
|
const tests = [
|
||||||
|
{ label: 'Solde de Violette', event: buildIntentEvent('GetBalanceIntent', { ChildName: 'Violette' }) },
|
||||||
|
{ label: 'Activités du mois', event: buildIntentEvent('ListActivitiesIntent', {}) },
|
||||||
|
{ label: 'Simulation intérêts (10 coquillages)', event: buildIntentEvent('SimulateInterestIntent', { ShellCount: 10 }) }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of tests) {
|
||||||
|
try {
|
||||||
|
const res = await new Promise((resolve, reject) => {
|
||||||
|
handler(t.event, {}, (err, response) => (err ? reject(err) : resolve(response)));
|
||||||
|
});
|
||||||
|
console.log(`✅ [${t.label}] ->`, res.response.outputSpeech.ssml);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`❌ [${t.label}] -> Erreur (assurez-vous que 'back' tourne sur le port 4000):`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
|
|
||||||
15
skill/lambda/package.json
Normal file
15
skill/lambda/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "banque-coquillages-skill-lambda",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "AWS Lambda / Alexa Skill Handler pour La Banque des Coquillages (Hailey, Violette et Andreas)",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node local-test.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ask-sdk-core": "^2.14.0",
|
||||||
|
"ask-sdk-model": "^1.40.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
43
skill/skill.json
Normal file
43
skill/skill.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"manifest": {
|
||||||
|
"publishingInformation": {
|
||||||
|
"locales": {
|
||||||
|
"fr-FR": {
|
||||||
|
"summary": "La Banque des Coquillages pour Hailey, Violette et Andreas",
|
||||||
|
"examplePhrases": [
|
||||||
|
"Alexa, ouvre la banque des coquillages",
|
||||||
|
"Alexa, demande à la banque des coquillages combien de coquillages a Hailey",
|
||||||
|
"Alexa, demande à la banque des coquillages quelles sont les activités du mois"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"coquillages",
|
||||||
|
"enfants",
|
||||||
|
"récompenses",
|
||||||
|
"épargne",
|
||||||
|
"famille"
|
||||||
|
],
|
||||||
|
"name": "La Banque des Coquillages",
|
||||||
|
"description": "Skill familial permettant à Hailey, Violette et Andreas de consulter leurs coquillages, de financer des activités communes et de placer leurs coquillages sur un livret bloqué à intérêts."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isAvailableWorldwide": false,
|
||||||
|
"testingInstructions": "Utiliser avec le backend API familial.",
|
||||||
|
"category": "EDUCATION_AND_REFERENCE",
|
||||||
|
"distributionCountries": [
|
||||||
|
"FR",
|
||||||
|
"BE",
|
||||||
|
"CH",
|
||||||
|
"CA"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"apis": {
|
||||||
|
"custom": {
|
||||||
|
"endpoint": {
|
||||||
|
"uri": "arn:aws:lambda:eu-west-1:000000000000:function:banque-coquillages-skill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manifestVersion": "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
54
start-all.js
Normal file
54
start-all.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
function runNode(name, colorCode, args, cwd) {
|
||||||
|
const child = spawn(process.execPath, args, {
|
||||||
|
cwd,
|
||||||
|
stdio: 'pipe'
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
const lines = data.toString().split(/\r?\n/).filter(Boolean);
|
||||||
|
for (const line of lines) {
|
||||||
|
console.log(`\x1b[${colorCode}m[${name}]\x1b[0m ${line}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
const lines = data.toString().split(/\r?\n/).filter(Boolean);
|
||||||
|
for (const line of lines) {
|
||||||
|
console.error(`\x1b[${colorCode}m[${name}]\x1b[0m ${line}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🐚 Démarrage simultané du Backend (port 4050) et du Frontend (port 5180)...');
|
||||||
|
|
||||||
|
const back = runNode(
|
||||||
|
'BACK ',
|
||||||
|
'36',
|
||||||
|
['--watch', path.join(__dirname, 'back', 'src', 'server.js')],
|
||||||
|
path.join(__dirname, 'back')
|
||||||
|
);
|
||||||
|
|
||||||
|
const front = runNode(
|
||||||
|
'FRONT',
|
||||||
|
'33',
|
||||||
|
[path.join(__dirname, 'node_modules', 'vite', 'bin', 'vite.js')],
|
||||||
|
path.join(__dirname, 'front')
|
||||||
|
);
|
||||||
|
|
||||||
|
function shutdown() {
|
||||||
|
back.kill();
|
||||||
|
front.kill();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
Reference in New Issue
Block a user