fix: corrections SW cache, nginx proxy, sw.js no-cache — 2026-05-01 05:48
This commit is contained in:
@@ -0,0 +1,577 @@
|
|||||||
|
# EquiTask — Documentation technique complète
|
||||||
|
|
||||||
|
> Application PWA de mesure et de répartition des tâches domestiques
|
||||||
|
> Version actuelle : **1.0.0** — Live sur **https://equitask.domench.fr**
|
||||||
|
> Dernière mise à jour : avril 2026
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table des matières
|
||||||
|
|
||||||
|
1. [Vision et concept](#1-vision-et-concept)
|
||||||
|
2. [Architecture globale](#2-architecture-globale)
|
||||||
|
3. [Stack technique](#3-stack-technique)
|
||||||
|
4. [Structure des fichiers](#4-structure-des-fichiers)
|
||||||
|
5. [Base de données](#5-base-de-données)
|
||||||
|
6. [API REST — référence complète](#6-api-rest--référence-complète)
|
||||||
|
7. [Frontend — pages et composants](#7-frontend--pages-et-composants)
|
||||||
|
8. [Système de score](#8-système-de-score)
|
||||||
|
9. [Mode offline / PWA](#9-mode-offline--pwa)
|
||||||
|
10. [Déploiement et infrastructure](#10-déploiement-et-infrastructure)
|
||||||
|
11. [Variables d'environnement](#11-variables-denvironnement)
|
||||||
|
12. [Seed automatique](#12-seed-automatique)
|
||||||
|
13. [Limites connues de la V1](#13-limites-connues-de-la-v1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vision et concept
|
||||||
|
|
||||||
|
EquiTask permet à un foyer de **mesurer objectivement qui fait quoi** dans les tâches du quotidien. Chaque tâche est quantifiée par un **score = durée × coefficient de pénibilité**, ce qui permet de comparer l'investissement réel de chaque membre plutôt que le simple nombre de tâches.
|
||||||
|
|
||||||
|
**Flux utilisateur principal :**
|
||||||
|
1. Première visite → wizard Setup (nom du foyer + membres)
|
||||||
|
2. Sélection du profil (qui effectue la tâche ?)
|
||||||
|
3. Page Saisie → clic sur une tâche → modal de confirmation → score enregistré
|
||||||
|
4. Dashboard → visualisation de la répartition + indicateur d'équilibre
|
||||||
|
|
||||||
|
**Concept clé : le score**
|
||||||
|
```
|
||||||
|
score_final = durée_minutes × coefficient_pénibilité (1–5)
|
||||||
|
```
|
||||||
|
Exemples : Nettoyage WC (10 min × 4) = 40 pts ; Préparation repas (45 min × 3) = 135 pts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture globale
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Navigateur │
|
||||||
|
│ React PWA (Vite + Tailwind) │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │
|
||||||
|
│ │ Setup │ │ Saisie │ │ Dashboard │ │ Paramètres │ │
|
||||||
|
│ └──────────┘ └──────────┘ └───────────┘ └──────────────┘ │
|
||||||
|
│ IndexedDB (queue offline) Service Worker (cache) │
|
||||||
|
└──────────────────────┬──────────────────────────────────────┘
|
||||||
|
│ HTTP /api/*
|
||||||
|
┌──────────────────────▼──────────────────────────────────────┐
|
||||||
|
│ Express.js (Node 20) │
|
||||||
|
│ /api/foyer /api/membres /api/categories │
|
||||||
|
│ /api/taches /api/saisies /api/saisies/stats │
|
||||||
|
│ Fichiers statiques frontend servis depuis /public │
|
||||||
|
└──────────────────────┬──────────────────────────────────────┘
|
||||||
|
│ better-sqlite3
|
||||||
|
┌──────────────────────▼──────────────────────────────────────┐
|
||||||
|
│ SQLite — /data/equitask.db │
|
||||||
|
│ foyer | membres | categories | taches_recurrentes | saisies│
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Tout tourne dans **un seul conteneur Docker** : l'Express sert à la fois l'API et les fichiers statiques buildés du frontend. La base SQLite est persistée dans un **volume Docker** (`equitask-data:/data`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Stack technique
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
| Technologie | Version | Rôle |
|
||||||
|
|---|---|---|
|
||||||
|
| React | 18.2 | UI |
|
||||||
|
| Vite | 5.2 | Build + dev server |
|
||||||
|
| TypeScript | 5.2 | Typage |
|
||||||
|
| Tailwind CSS | 3.4 | Styles utilitaires |
|
||||||
|
| React Router | 6.22 | Navigation SPA |
|
||||||
|
| TanStack Query | 5.28 | Fetching + cache serveur |
|
||||||
|
| Recharts | 2.12 | Graphiques (bar, area, pie) |
|
||||||
|
| date-fns | 3.6 | Manipulation de dates |
|
||||||
|
| idb | 8.0 | IndexedDB (queue offline) |
|
||||||
|
| vite-plugin-pwa | 0.19 | Service Worker + manifest |
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
| Technologie | Version | Rôle |
|
||||||
|
|---|---|---|
|
||||||
|
| Node.js | 20 (Alpine) | Runtime |
|
||||||
|
| Express | 4.18 | HTTP server |
|
||||||
|
| TypeScript | 5.4 | Typage |
|
||||||
|
| Drizzle ORM | 0.30 | ORM SQLite |
|
||||||
|
| better-sqlite3 | 9.4 | Driver SQLite synchrone |
|
||||||
|
| helmet | 7.1 | Headers sécurité |
|
||||||
|
| cors | 2.8 | CORS (dev uniquement) |
|
||||||
|
| morgan | 1.10 | Logs HTTP |
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
| Composant | Détail |
|
||||||
|
|---|---|
|
||||||
|
| Hébergement | VPS OVH — 57.131.33.182 |
|
||||||
|
| Orchestration | Coolify 4.0.0-beta.474 |
|
||||||
|
| Reverse proxy | Traefik v3.6 |
|
||||||
|
| DNS | OVH — wildcard `*.domench.fr` → IP VPS |
|
||||||
|
| Source code | Gitea — https://git.domench.fr/gael/equitask |
|
||||||
|
| HTTPS | Let's Encrypt via Traefik |
|
||||||
|
| Registry | Pas de registry externe — build local sur le VPS |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Structure des fichiers
|
||||||
|
|
||||||
|
```
|
||||||
|
equitask/
|
||||||
|
├── Dockerfile # Multi-stage build (3 stages)
|
||||||
|
├── docker-compose.yml # Pour dev local
|
||||||
|
├── .dockerignore
|
||||||
|
├── .gitignore
|
||||||
|
│
|
||||||
|
├── backend/
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── tsconfig.json
|
||||||
|
│ └── src/
|
||||||
|
│ ├── index.ts # Point d'entrée Express + seed auto
|
||||||
|
│ ├── db/
|
||||||
|
│ │ ├── index.ts # Connexion SQLite + initDb() + CREATE TABLE
|
||||||
|
│ │ ├── schema.ts # Schéma Drizzle (tables + types TS)
|
||||||
|
│ │ ├── seed.ts # Seed async (non utilisé en prod)
|
||||||
|
│ │ └── seed-sync.ts # Seed synchrone
|
||||||
|
│ └── routes/
|
||||||
|
│ ├── foyer.ts # CRUD foyer
|
||||||
|
│ ├── membres.ts # CRUD membres
|
||||||
|
│ ├── categories.ts # CRUD catégories
|
||||||
|
│ ├── taches.ts # CRUD tâches récurrentes
|
||||||
|
│ └── saisies.ts # CRUD saisies + stats + export
|
||||||
|
│
|
||||||
|
└── frontend/
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json # "types": ["vite/client"] requis !
|
||||||
|
├── tsconfig.node.json
|
||||||
|
├── vite.config.ts # PWA + proxy /api → :3001
|
||||||
|
├── tailwind.config.js
|
||||||
|
├── postcss.config.js
|
||||||
|
├── index.html
|
||||||
|
├── public/
|
||||||
|
│ ├── favicon.svg
|
||||||
|
│ └── icons/
|
||||||
|
│ ├── icon-192.png
|
||||||
|
│ └── icon-512.png
|
||||||
|
└── src/
|
||||||
|
├── main.tsx
|
||||||
|
├── App.tsx # Router + QueryClient + AppProvider
|
||||||
|
├── index.css
|
||||||
|
├── types/
|
||||||
|
│ └── index.ts # Tous les types TS de l'app
|
||||||
|
├── api/
|
||||||
|
│ ├── client.ts # fetch wrapper typé
|
||||||
|
│ └── index.ts # foyerApi, membresApi, tachesApi, saisiesApi, dashboardApi
|
||||||
|
├── context/
|
||||||
|
│ └── AppContext.tsx # foyer, membreActif, isOnline, queueCount
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useOfflineSync.ts # Sync auto IndexedDB → API au retour en ligne
|
||||||
|
│ └── useToast.ts
|
||||||
|
├── offline/
|
||||||
|
│ └── queue.ts # IndexedDB : enqueue / dequeue / getQueue / countQueue
|
||||||
|
├── pages/
|
||||||
|
│ ├── Setup.tsx # Wizard première config (foyer + membres)
|
||||||
|
│ ├── SelectionProfil.tsx # Écran de sélection du membre
|
||||||
|
│ ├── Saisie.tsx # Grille tâches + modals (récurrente + one-shot)
|
||||||
|
│ ├── Dashboard.tsx # Graphiques + indicateur équilibre + historique
|
||||||
|
│ └── Parametres.tsx # Gestion membres / catégories / tâches
|
||||||
|
└── components/
|
||||||
|
└── ui/
|
||||||
|
├── Layout.tsx # Shell avec nav bas mobile
|
||||||
|
├── Modal.tsx # Modale générique
|
||||||
|
├── PenibiliteSelector.tsx # 5 boutons 1–5
|
||||||
|
├── ScoreBadge.tsx # Badge score coloré
|
||||||
|
└── Toast.tsx # Notifications
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Base de données
|
||||||
|
|
||||||
|
### Schéma SQLite
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Un seul enregistrement — configuration du foyer
|
||||||
|
CREATE TABLE foyer (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nom TEXT NOT NULL,
|
||||||
|
cree_le TEXT DEFAULT (datetime('now', 'localtime'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Membres du foyer (max 7 recommandé)
|
||||||
|
CREATE TABLE membres (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nom TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL CHECK(role IN ('adulte', 'enfant')),
|
||||||
|
couleur TEXT NOT NULL, -- ex: #ef4444
|
||||||
|
actif INTEGER DEFAULT 1, -- soft delete
|
||||||
|
ordre INTEGER DEFAULT 0,
|
||||||
|
cree_le TEXT DEFAULT (datetime('now', 'localtime'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Catégories de tâches (éditables)
|
||||||
|
CREATE TABLE categories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nom TEXT NOT NULL,
|
||||||
|
icone TEXT NOT NULL, -- emoji
|
||||||
|
couleur TEXT NOT NULL,
|
||||||
|
ordre INTEGER DEFAULT 0,
|
||||||
|
actif INTEGER DEFAULT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Catalogue des tâches habituelles
|
||||||
|
CREATE TABLE taches_recurrentes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nom TEXT NOT NULL,
|
||||||
|
categorie_id INTEGER NOT NULL REFERENCES categories(id),
|
||||||
|
duree_moyenne_min INTEGER NOT NULL,
|
||||||
|
coefficient_penibilite INTEGER NOT NULL CHECK(coefficient_penibilite BETWEEN 1 AND 5),
|
||||||
|
actif INTEGER DEFAULT 1 -- soft delete
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Historique de toutes les saisies
|
||||||
|
CREATE TABLE saisies (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
tache_recurrente_id INTEGER REFERENCES taches_recurrentes(id), -- nullable
|
||||||
|
nom_tache_oneshot TEXT, -- nullable (tâche ponctuelle)
|
||||||
|
categorie_id INTEGER NOT NULL REFERENCES categories(id),
|
||||||
|
membre_id INTEGER NOT NULL REFERENCES membres(id),
|
||||||
|
date_heure TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||||
|
duree_reelle_min INTEGER, -- override de la durée par défaut
|
||||||
|
coefficient_penibilite INTEGER NOT NULL CHECK(coefficient_penibilite BETWEEN 1 AND 5),
|
||||||
|
score_final INTEGER NOT NULL, -- snapshot = duree * coef
|
||||||
|
notes TEXT,
|
||||||
|
synced INTEGER DEFAULT 1,
|
||||||
|
cree_le TEXT DEFAULT (datetime('now', 'localtime')),
|
||||||
|
CHECK (tache_recurrente_id IS NOT NULL OR nom_tache_oneshot IS NOT NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index de performance
|
||||||
|
CREATE INDEX idx_saisies_membre ON saisies(membre_id);
|
||||||
|
CREATE INDEX idx_saisies_date ON saisies(date_heure);
|
||||||
|
CREATE INDEX idx_saisies_categorie ON saisies(categorie_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Règles importantes
|
||||||
|
- **Soft delete** partout : `actif = 0` pour membres, catégories et tâches (jamais de DELETE réel)
|
||||||
|
- **score_final est un snapshot** : stocké au moment de la saisie, non recalculé en cas de modification ultérieure de la tâche
|
||||||
|
- Une saisie doit avoir soit `tache_recurrente_id` soit `nom_tache_oneshot` (contrainte CHECK)
|
||||||
|
- `duree_reelle_min` est nullable : si null, la durée d'affichage est celle de la tâche référencée
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API REST — référence complète
|
||||||
|
|
||||||
|
Base URL : `/api`
|
||||||
|
Content-Type : `application/json`
|
||||||
|
|
||||||
|
### Foyer
|
||||||
|
|
||||||
|
| Méthode | Route | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/foyer` | Récupère le foyer (`{ foyer: Foyer \| null }`) |
|
||||||
|
| POST | `/foyer` | Crée le foyer (`{ nom }`) |
|
||||||
|
| PUT | `/foyer/:id` | Modifie le nom du foyer |
|
||||||
|
|
||||||
|
### Membres
|
||||||
|
|
||||||
|
| Méthode | Route | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/membres` | Liste les membres actifs |
|
||||||
|
| POST | `/membres` | Crée un membre (`{ nom, role, couleur, ordre? }`) |
|
||||||
|
| PUT | `/membres/:id` | Modifie un membre |
|
||||||
|
| DELETE | `/membres/:id` | Soft delete (actif = 0) |
|
||||||
|
|
||||||
|
### Catégories
|
||||||
|
|
||||||
|
| Méthode | Route | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/categories` | Liste les catégories actives, triées par `ordre` |
|
||||||
|
| POST | `/categories` | Crée (`{ nom, icone, couleur, ordre? }`) |
|
||||||
|
| PUT | `/categories/:id` | Modifie |
|
||||||
|
| DELETE | `/categories/:id` | Soft delete |
|
||||||
|
|
||||||
|
### Tâches récurrentes
|
||||||
|
|
||||||
|
| Méthode | Route | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/taches` | Liste + `score_calcule` joint, triées catégorie/nom |
|
||||||
|
| POST | `/taches` | Crée (`{ nom, categorie_id, duree_moyenne_min, coefficient_penibilite }`) |
|
||||||
|
| PUT | `/taches/:id` | Modifie |
|
||||||
|
| DELETE | `/taches/:id` | Soft delete |
|
||||||
|
|
||||||
|
### Saisies
|
||||||
|
|
||||||
|
| Méthode | Route | Body / Query | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/saisies` | `?debut&fin&membre_id&categorie_id&type&page&limit` | Liste paginée (max 200/page) avec jointures |
|
||||||
|
| POST | `/saisies` | `{ tache_recurrente_id?, nom_tache_oneshot?, categorie_id, membre_id, date_heure?, duree_reelle_min?, coefficient_penibilite, score_final, notes? }` | Crée une saisie |
|
||||||
|
| POST | `/saisies/batch` | `{ saisiesData: [...] }` | Sync offline (ignore les erreurs individuelles) |
|
||||||
|
| PUT | `/saisies/:id` | champs modifiables | Modifie une saisie |
|
||||||
|
| DELETE | `/saisies/:id` | — | Suppression réelle (pas de soft delete) |
|
||||||
|
| GET | `/saisies/stats` | `?debut&fin&inclure_enfants&categorie_id` | Stats dashboard (voir ci-dessous) |
|
||||||
|
| GET | `/saisies/export` | `?format=json\|csv` | Télécharge toutes les saisies |
|
||||||
|
|
||||||
|
### Réponse `/saisies/stats`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scores_par_membre": [
|
||||||
|
{ "membre_id": 1, "nom": "Alice", "couleur": "#ef4444", "role": "adulte",
|
||||||
|
"score_total": 3420, "pourcentage": 62, "nb_saisies": 28 }
|
||||||
|
],
|
||||||
|
"scores_par_categorie": [
|
||||||
|
{ "categorie_id": 1, "nom": "Cuisine", "icone": "🍳", "couleur": "#ef4444",
|
||||||
|
"scores_membres": [
|
||||||
|
{ "membre_id": 1, "nom": "Alice", "couleur": "#ef4444", "score": 1200 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"evolution_temporelle": [
|
||||||
|
{ "date": "2026-04-20",
|
||||||
|
"scores": [{ "membre_id": 1, "nom": "Alice", "couleur": "#ef4444", "score": 315 }] }
|
||||||
|
],
|
||||||
|
"indicateur_equilibre": {
|
||||||
|
"adulte1": { "membre_id": 1, "nom": "Alice", "couleur": "#ef4444", "score": 3420, "pourcentage": 62 },
|
||||||
|
"adulte2": { "membre_id": 2, "nom": "Bob", "couleur": "#3b82f6", "score": 2100, "pourcentage": 38 },
|
||||||
|
"ecart_pct": 24,
|
||||||
|
"statut": "orange" // "vert" ≤10%, "orange" ≤25%, "rouge" >25%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Santé
|
||||||
|
```
|
||||||
|
GET /api/health → { "ok": true, "version": "1.0.0", "timestamp": "..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Frontend — pages et composants
|
||||||
|
|
||||||
|
### Flux de navigation
|
||||||
|
|
||||||
|
```
|
||||||
|
/ (SelectionProfil)
|
||||||
|
├── [pas de foyer] → /setup (Setup)
|
||||||
|
└── [foyer OK] → sélection membre → mémorisation dans sessionStorage
|
||||||
|
├── /saisie (Layout > Saisie)
|
||||||
|
├── /dashboard (Layout > Dashboard)
|
||||||
|
└── /parametres (Layout > Parametres)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pages
|
||||||
|
|
||||||
|
**Setup** — Wizard en 2 étapes : nom du foyer, puis ajout des membres (min 1 adulte). Initialise le foyer via POST `/api/foyer` puis POST `/api/membres`.
|
||||||
|
|
||||||
|
**SelectionProfil** — Grille de cartes membres. Le clic stocke le membre dans `sessionStorage` (via `AppContext.setMembreActif`) et redirige vers `/saisie`.
|
||||||
|
|
||||||
|
**Saisie** — Grille de cartes tâches groupées par catégorie (tabs horizontaux). Deux modals :
|
||||||
|
- `ModalConfirmTache` : confirme une tâche récurrente, permet d'override durée/pénibilité
|
||||||
|
- `ModalOneShot` : saisie libre avec option "ajouter au catalogue"
|
||||||
|
|
||||||
|
**Dashboard** — 4 blocs de visualisation :
|
||||||
|
1. Scores cumulés par membre (barres de progression)
|
||||||
|
2. Indicateur d'équilibre couple (barre bicolore + statut vert/orange/rouge)
|
||||||
|
3. Répartition par catégorie (BarChart Recharts, grouped)
|
||||||
|
4. Évolution temporelle (AreaChart Recharts)
|
||||||
|
Plus un tableau historique paginé avec suppression.
|
||||||
|
|
||||||
|
Filtres : période (semaine/mois/7j/30j/personnalisé), catégorie, toggle enfants. Export CSV/JSON.
|
||||||
|
|
||||||
|
**Parametres** — Trois onglets :
|
||||||
|
- Membres : CRUD + couleur + rôle
|
||||||
|
- Catégories : CRUD + icône emoji + couleur
|
||||||
|
- Tâches : CRUD filtré par catégorie + durée + pénibilité
|
||||||
|
|
||||||
|
### Composants UI
|
||||||
|
|
||||||
|
| Composant | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `Layout` | Shell : contenu + barre de navigation bas (mobile-first) avec icônes + badge offline |
|
||||||
|
| `Modal` | Modale générique avec backdrop, animation, gestion focus |
|
||||||
|
| `PenibiliteSelector` | 5 boutons numérotés 1–5 avec couleurs graduées |
|
||||||
|
| `ScoreBadge` | Badge score avec couleur selon valeur (vert → rouge) |
|
||||||
|
| `Toast` | Notification éphémère (succès/erreur/info) avec animation |
|
||||||
|
|
||||||
|
### AppContext
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AppContextType {
|
||||||
|
foyer: Foyer | null;
|
||||||
|
setFoyer: (f: Foyer | null) => void;
|
||||||
|
membreActif: Membre | null; // persisté dans sessionStorage
|
||||||
|
setMembreActif: (m: Membre | null) => void;
|
||||||
|
isOnline: boolean; // window online/offline events
|
||||||
|
queueCount: number; // nb saisies en attente de sync
|
||||||
|
setQueueCount: (n: number) => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Système de score
|
||||||
|
|
||||||
|
Le score est la métrique centrale de l'app. Il cherche à rendre comparables des tâches de nature très différente.
|
||||||
|
|
||||||
|
```
|
||||||
|
score = durée_minutes × coefficient_pénibilité
|
||||||
|
```
|
||||||
|
|
||||||
|
**Coefficient de pénibilité (1–5) :**
|
||||||
|
- 1 = tâche agréable ou automatique
|
||||||
|
- 2 = tâche neutre
|
||||||
|
- 3 = tâche un peu contraignante
|
||||||
|
- 4 = tâche pénible (WC, impôts...)
|
||||||
|
- 5 = réservé aux tâches exceptionnellement pénibles
|
||||||
|
|
||||||
|
**Exemples issus du seed :**
|
||||||
|
| Tâche | Durée | Coef | Score |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Nettoyage WC | 10 min | 4 | 40 pts |
|
||||||
|
| Préparation repas | 45 min | 3 | 135 pts |
|
||||||
|
| Batch cooking | 90 min | 3 | 270 pts |
|
||||||
|
| Déclarations impôts | 60 min | 4 | 240 pts |
|
||||||
|
|
||||||
|
**Indicateur d'équilibre :**
|
||||||
|
- Calculé uniquement sur les adultes (les enfants sont exclus par défaut)
|
||||||
|
- `écart_pct = |score_adulte1 - score_adulte2| / score_total × 100`
|
||||||
|
- Vert ≤ 10% — Orange ≤ 25% — Rouge > 25%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Mode offline / PWA
|
||||||
|
|
||||||
|
### Service Worker (Workbox)
|
||||||
|
Configuré dans `vite.config.ts` via `vite-plugin-pwa` :
|
||||||
|
- **Assets statiques** : précachés au build (JS, CSS, HTML, PNG, SVG)
|
||||||
|
- **Appels API** (`/api/*`) : stratégie `NetworkFirst` avec fallback cache, TTL 24h, max 100 entrées
|
||||||
|
|
||||||
|
### Queue offline (IndexedDB)
|
||||||
|
Quand `isOnline === false`, les saisies ne sont pas envoyées au serveur mais stockées dans IndexedDB via `src/offline/queue.ts` (base `equitask-offline`, store `saisies-queue`).
|
||||||
|
|
||||||
|
Au retour en ligne (`useOfflineSync.ts`), toutes les saisies en queue sont envoyées une par une via `POST /api/saisies`. En cas d'erreur réseau, la synchronisation s'arrête (les saisies restent en queue).
|
||||||
|
|
||||||
|
### Manifest PWA
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "EquiTask - Répartition tâches",
|
||||||
|
"short_name": "EquiTask",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"theme_color": "#1e1b4b",
|
||||||
|
"background_color": "#0f172a"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Icônes : 192×192 et 512×512 (maskable).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Déploiement et infrastructure
|
||||||
|
|
||||||
|
### Dockerfile (multi-stage)
|
||||||
|
|
||||||
|
```
|
||||||
|
Stage 1 — frontend-builder (node:20-alpine)
|
||||||
|
npm install --include=dev ← OBLIGATOIRE (sinon vite/tsc pas installés)
|
||||||
|
npm run build → dist/
|
||||||
|
|
||||||
|
Stage 2 — backend-builder (node:20-alpine)
|
||||||
|
apk add python3 make g++ ← pour compiler better-sqlite3 (module natif)
|
||||||
|
npm install --include=dev ← OBLIGATOIRE (sinon tsc pas installé)
|
||||||
|
npm run build → dist/
|
||||||
|
|
||||||
|
Stage 3 — runtime (node:20-alpine)
|
||||||
|
apk add libstdc++ ← runtime better-sqlite3
|
||||||
|
Copie node_modules du builder (incluant better-sqlite3 natif)
|
||||||
|
Copie dist/ backend
|
||||||
|
Copie dist/ frontend → public/
|
||||||
|
ENV NODE_ENV=production PORT=3001 DATABASE_PATH=/data/equitask.db
|
||||||
|
VOLUME ["/data"]
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Points critiques Docker :**
|
||||||
|
- Utiliser `npm install --include=dev` et non `npm ci` (pas de lockfile commité)
|
||||||
|
- `NODE_ENV=production` supprime les devDependencies → toujours passer `--include=dev` dans les stages builder
|
||||||
|
- `better-sqlite3` compile un module natif → outils de compilation nécessaires au build, `libstdc++` au runtime
|
||||||
|
|
||||||
|
### Infrastructure Coolify
|
||||||
|
|
||||||
|
```
|
||||||
|
Coolify URL : http://100.94.204.91:8000
|
||||||
|
App UUID : bunmdt8jmb2i594zuhzvdhfy
|
||||||
|
Gitea repo : https://git.domench.fr/gael/equitask.git
|
||||||
|
Branche : main
|
||||||
|
Build pack : dockerfile
|
||||||
|
Port exposé : 3001
|
||||||
|
Domaine : https://equitask.domench.fr
|
||||||
|
Volume : equitask-data → /data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Script de déploiement
|
||||||
|
|
||||||
|
`deploy.py` (à la racine de `Applications VPS/`) automatise la création d'une nouvelle app :
|
||||||
|
```bash
|
||||||
|
# Depuis Windows cmd (pas PowerShell)
|
||||||
|
py -X utf8 deploy.py equitask ./equitask
|
||||||
|
```
|
||||||
|
Lit `config-deploy.json`, crée le repo Gitea, pousse le code, crée l'app Coolify, configure le volume, déclenche le build.
|
||||||
|
|
||||||
|
**Particularités Coolify API v1 découvertes :**
|
||||||
|
- `fqdn` en lecture seule → utiliser `domains` dans PATCH
|
||||||
|
- `POST /applications/public` tronque le hostname du `git_repository` → re-PATCH après création
|
||||||
|
- Storage API : utiliser `custom_docker_run_options: "--mount type=volume,..."` (pas l'API `/storages`)
|
||||||
|
- Déploiement : `GET /api/v1/deploy?uuid={app_uuid}&force=false` (pas POST)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Variables d'environnement
|
||||||
|
|
||||||
|
| Variable | Défaut | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `NODE_ENV` | `production` | Désactive CORS en production |
|
||||||
|
| `PORT` | `3001` | Port d'écoute Express |
|
||||||
|
| `DATABASE_PATH` | `/data/equitask.db` | Chemin absolu de la base SQLite |
|
||||||
|
|
||||||
|
En développement local, créer un `.env` à la racine de `backend/` :
|
||||||
|
```env
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3001
|
||||||
|
DATABASE_PATH=./data/equitask.db
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Seed automatique
|
||||||
|
|
||||||
|
Au démarrage, `backend/src/index.ts` vérifie si la table `categories` est vide. Si oui, il insère automatiquement :
|
||||||
|
|
||||||
|
**7 catégories :** Cuisine 🍳, Ménage 🧹, Courses 🛒, Enfants 👶, Administratif 📋, Entretien maison 🔧, Charge mentale 🧠
|
||||||
|
|
||||||
|
**29 tâches récurrentes** couvrant les activités domestiques courantes, avec durées et coefficients préréglés.
|
||||||
|
|
||||||
|
Ce seed ne s'exécute qu'une seule fois (première instance vierge). Les données sont ensuite modifiables depuis la page Paramètres.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Limites connues de la V1
|
||||||
|
|
||||||
|
**Fonctionnelles :**
|
||||||
|
- Pas d'authentification — une seule instance par déploiement (un seul foyer)
|
||||||
|
- Pas de gestion multi-foyers
|
||||||
|
- Pas de notifications push (rappels, suggestions)
|
||||||
|
- Pas d'objectifs ou de gamification
|
||||||
|
- Les statistiques ne couvrent pas les tendances long terme (ex: évolution mensuelle sur 6 mois)
|
||||||
|
- Pas de vue "récapitulatif hebdomadaire" envoyé par email/notification
|
||||||
|
- Le tableau historique n'a pas de tri par colonne
|
||||||
|
- Pas de possibilité de modifier une saisie existante depuis l'historique (seulement supprimer)
|
||||||
|
- Pas de saisie rapide ("j'ai fait X aujourd'hui à 8h30" sans passer par la grille)
|
||||||
|
|
||||||
|
**Techniques :**
|
||||||
|
- Pas de migrations Drizzle — schéma recréé via `CREATE TABLE IF NOT EXISTS` (fragile pour les évolutions)
|
||||||
|
- Pas de tests automatisés (ni backend ni frontend)
|
||||||
|
- Pas de CI/CD : le déploiement est déclenché manuellement via `deploy.py`
|
||||||
|
- `score_final` est un snapshot immuable — pas de recalcul si on modifie le coefficient d'une tâche
|
||||||
|
- Pas de rate limiting sur l'API
|
||||||
|
- Pas d'authentification API
|
||||||
|
- `sessionStorage` pour le membre actif → perdu si on ferme l'onglet
|
||||||
|
- Offline sync séquentielle (une saisie à la fois) → lente si beaucoup de saisies en queue
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Prompt de reprise — EquiTask V2
|
||||||
|
|
||||||
|
> À coller tel quel en début de nouvelle session pour démarrer le développement de la V2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexte du projet
|
||||||
|
|
||||||
|
Tu vas m'aider à développer **EquiTask V2**, l'évolution d'une application PWA de mesure de répartition des tâches domestiques.
|
||||||
|
|
||||||
|
**L'application V1 est déjà en production** à l'adresse `https://equitask.domench.fr`. Le code source se trouve dans mon dossier de travail `equitask/` (dossier sélectionné dans Cowork).
|
||||||
|
|
||||||
|
Avant de commencer, lis les deux fichiers de référence :
|
||||||
|
1. `equitask/DOCUMENTATION.md` — documentation technique complète de la V1 (architecture, stack, BDD, API, composants, limites)
|
||||||
|
2. `equitask/PROMPT_REPRISE_V2.md` — ce fichier (contexte V2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce qui existe en V1 (à ne pas recoder)
|
||||||
|
|
||||||
|
- **Backend** : Express + TypeScript + SQLite (Drizzle ORM) + better-sqlite3
|
||||||
|
- Routes : `/api/foyer`, `/api/membres`, `/api/categories`, `/api/taches`, `/api/saisies`
|
||||||
|
- Stats dashboard : `GET /api/saisies/stats?debut&fin&inclure_enfants&categorie_id`
|
||||||
|
- Export CSV/JSON : `GET /api/saisies/export?format=json|csv`
|
||||||
|
|
||||||
|
- **Frontend** : React 18 + Vite + Tailwind + TanStack Query + Recharts
|
||||||
|
- Pages : Setup (wizard), SelectionProfil, Saisie (grille tâches), Dashboard (4 graphiques), Paramètres (3 onglets)
|
||||||
|
- Offline : queue IndexedDB + sync automatique au retour en ligne
|
||||||
|
- PWA : Service Worker Workbox, manifest, icônes
|
||||||
|
|
||||||
|
- **Infra** : Docker multi-stage → Coolify → https://equitask.domench.fr
|
||||||
|
- Déploiement via `deploy.py` depuis le dossier parent
|
||||||
|
- Volume SQLite persisté : `equitask-data:/data`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Système de score (inchangé en V2)
|
||||||
|
|
||||||
|
```
|
||||||
|
score_final = durée_minutes × coefficient_pénibilité (1–5)
|
||||||
|
```
|
||||||
|
L'indicateur d'équilibre mesure l'écart en % entre les deux adultes du foyer.
|
||||||
|
Seuils : vert ≤ 10%, orange ≤ 25%, rouge > 25%.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objectifs de la V2
|
||||||
|
|
||||||
|
> **À compléter par Gaël avant de démarrer** — noter ici les fonctionnalités souhaitées.
|
||||||
|
> Exemples de pistes identifiées lors du développement de la V1 :
|
||||||
|
|
||||||
|
### Fonctionnalités prioritaires (à valider)
|
||||||
|
- [ ] **Authentification simple** — code PIN par membre ou mot de passe foyer pour éviter que n'importe qui modifie les données
|
||||||
|
- [ ] **Notifications / rappels** — push notification ou email hebdomadaire avec le récap de répartition
|
||||||
|
- [ ] **Vue récapitulatif hebdomadaire** — résumé automatique chaque dimanche soir
|
||||||
|
- [ ] **Objectifs** — définir un objectif d'équilibre cible et suivre la progression
|
||||||
|
- [ ] **Modification d'une saisie** — pouvoir éditer une saisie depuis l'historique (actuellement suppression seulement)
|
||||||
|
- [ ] **Saisie rapide** — ajouter une tâche passée sans passer par la grille (ex: "j'ai fait X hier")
|
||||||
|
- [ ] **Tri du tableau historique** — tri par colonne (date, membre, score...)
|
||||||
|
- [ ] **Tendances long terme** — graphique mensuel sur 6/12 mois
|
||||||
|
- [ ] **Badges/gamification** — récompenses pour les périodes bien équilibrées
|
||||||
|
|
||||||
|
### Améliorations techniques (à valider)
|
||||||
|
- [ ] **Migrations Drizzle** — remplacer les `CREATE TABLE IF NOT EXISTS` par un vrai système de migrations
|
||||||
|
- [ ] **Tests** — au moins les routes API critiques (saisies, stats)
|
||||||
|
- [ ] **CI/CD automatique** — déclencher le déploiement sur push Gitea (webhook → Coolify)
|
||||||
|
- [ ] **Rate limiting** — protéger l'API des abus
|
||||||
|
- [ ] **Recalcul des scores** — option pour recalculer les scores historiques si les paramètres d'une tâche changent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contraintes techniques à respecter
|
||||||
|
|
||||||
|
1. **Rester dans la même stack** : Express/Node 20, React 18, SQLite, TypeScript, Tailwind
|
||||||
|
2. **Compatibilité Docker** : le Dockerfile multi-stage doit continuer à fonctionner
|
||||||
|
- Toujours `npm install --include=dev` (pas `npm ci`, pas de lockfile)
|
||||||
|
- `NODE_ENV=production` coupe les devDependencies — le `--include=dev` est obligatoire dans les builder stages
|
||||||
|
3. **Volume SQLite** : les données sont dans `/data/equitask.db` — les migrations doivent être non-destructives
|
||||||
|
4. **Frontend `tsconfig.json`** : doit avoir `"types": ["vite/client"]` pour `import.meta.env`
|
||||||
|
5. **API** : rester rétrocompatible avec V1 (mêmes routes, mêmes structures de réponse)
|
||||||
|
6. **Design** : dark theme slate-950/slate-900, accents indigo-600, cards rounded-2xl — maintenir la cohérence visuelle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infra de déploiement
|
||||||
|
|
||||||
|
```
|
||||||
|
VPS OVH : 57.131.33.182
|
||||||
|
Coolify : http://100.94.204.91:8000
|
||||||
|
Gitea : https://git.domench.fr/gael/equitask
|
||||||
|
Branche : main
|
||||||
|
App Coolify : bunmdt8jmb2i594zuhzvdhfy
|
||||||
|
URL prod : https://equitask.domench.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour redéployer après modifications :
|
||||||
|
```bash
|
||||||
|
# Depuis Windows — cmd (pas PowerShell)
|
||||||
|
cd C:\Users\GALWAL~1\Documents\Claude\Projects\APPLIC~1
|
||||||
|
py -X utf8 deploy.py equitask ./equitask
|
||||||
|
```
|
||||||
|
Ou : pusher sur `main` et déclencher manuellement dans Coolify.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Points de vigilance hérités de la V1
|
||||||
|
|
||||||
|
Ces bugs/pièges ont déjà été rencontrés — à ne pas reproduire :
|
||||||
|
|
||||||
|
- **`npm ci` échoue** → pas de `package-lock.json` commité, toujours utiliser `npm install`
|
||||||
|
- **`tsc: not found` pendant le build Docker** → `NODE_ENV=production` coupe les devDependencies, toujours `--include=dev`
|
||||||
|
- **TypeScript `import.meta.env` introuvable** → vérifier `"types": ["vite/client"]` dans `frontend/tsconfig.json`
|
||||||
|
- **Type `role: string` trop large** → dans les mutations TanStack Query, typer explicitement `role: 'adulte' | 'enfant'`
|
||||||
|
- **Coolify API `fqdn` read-only** → utiliser le champ `domains` dans les PATCH
|
||||||
|
- **Coolify API git_repository tronqué** → re-PATCH après création pour rétablir l'URL complète
|
||||||
|
- **`py` vs `python`** → sur ce Windows, `py` fonctionne en cmd mais pas `python3`
|
||||||
|
- **Chemin avec `ë`** → utiliser le chemin court Windows `GALWAL~1` dans cmd si nécessaire
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pour démarrer la session
|
||||||
|
|
||||||
|
1. Lis `equitask/DOCUMENTATION.md` (documentation technique complète)
|
||||||
|
2. Explore le code existant si besoin : `equitask/backend/src/` et `equitask/frontend/src/`
|
||||||
|
3. Demande-moi quelles fonctionnalités V2 je veux prioriser
|
||||||
|
4. Propose un plan de développement incrémental (backend d'abord, puis frontend)
|
||||||
|
5. Travaille fichier par fichier en préservant la compatibilité avec l'existant
|
||||||
+16
-15
@@ -108,19 +108,20 @@ app.get('/api/health', (_req, res) => {
|
|||||||
// ─── Frontend statique (en production) ──────────────────────────────────────
|
// ─── Frontend statique (en production) ──────────────────────────────────────
|
||||||
const frontendPath = path.join(__dirname, '..', 'public');
|
const frontendPath = path.join(__dirname, '..', 'public');
|
||||||
if (fs.existsSync(frontendPath)) {
|
if (fs.existsSync(frontendPath)) {
|
||||||
|
// Service worker — jamais mis en cache (sinon l'ancien SW continue après déploiement)
|
||||||
|
app.get('/sw.js', (_req, res) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.sendFile(path.join(frontendPath, 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manifeste PWA — pas de cache long
|
||||||
|
app.get(/\.webmanifest$/, (_req, res, next) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
app.use(express.static(frontendPath));
|
app.use(express.static(frontendPath));
|
||||||
// SPA fallback - toutes les routes non-API servent index.html
|
// SPA fallback - toutes les routes non-API ser
|
||||||
app.get('*', (req, res) => {
|
|
||||||
if (!req.path.startsWith('/api')) {
|
|
||||||
res.sendFile(path.join(frontendPath, 'index.html'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Démarrage ───────────────────────────────────────────────────────────────
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`🚀 EquiTask API démarrée sur le port ${PORT}`);
|
|
||||||
console.log(` Mode : ${process.env.NODE_ENV || 'development'}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app;
|
|
||||||
@@ -25,12 +25,13 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: /^\/api\/.*/i,
|
urlPattern: /^\/api\/.*/i,
|
||||||
handler: 'NetworkFirst',
|
handler: 'NetworkFirst',
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'api-cache',
|
cacheName: 'equitask-api-cache',
|
||||||
expiration: { maxEntries: 100, maxAgeSeconds: 86400 },
|
expiration: { maxEntries: 100, maxAgeSeconds: 86400 },
|
||||||
networkTimeoutSeconds: 10,
|
networkTimeoutSeconds: 10,
|
||||||
},
|
},
|
||||||
@@ -45,7 +46,4 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': { target: 'http://localhost:3001', changeOrigin: true },
|
'/api': { target: 'http://localhost
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user