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