# 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