Files
equitask/DOCUMENTATION.md
T

578 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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é (15)
```
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 15
├── 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 15 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é (15) :**
- 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