23 KiB
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
- Vision et concept
- Architecture globale
- Stack technique
- Structure des fichiers
- Base de données
- API REST — référence complète
- Frontend — pages et composants
- Système de score
- Mode offline / PWA
- Déploiement et infrastructure
- Variables d'environnement
- Seed automatique
- 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 :
- Première visite → wizard Setup (nom du foyer + membres)
- Sélection du profil (qui effectue la tâche ?)
- Page Saisie → clic sur une tâche → modal de confirmation → score enregistré
- 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
-- 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 = 0pour 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_idsoitnom_tache_oneshot(contrainte CHECK) duree_reelle_minest 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
{
"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 :
- Scores cumulés par membre (barres de progression)
- Indicateur d'équilibre couple (barre bicolore + statut vert/orange/rouge)
- Répartition par catégorie (BarChart Recharts, grouped)
- É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
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égieNetworkFirstavec 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
{
"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=devet nonnpm ci(pas de lockfile commité) NODE_ENV=productionsupprime les devDependencies → toujours passer--include=devdans les stages builderbetter-sqlite3compile 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 :
# 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 :
fqdnen lecture seule → utiliserdomainsdans PATCHPOST /applications/publictronque le hostname dugit_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/ :
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_finalest 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
sessionStoragepour le membre actif → perdu si on ferme l'onglet- Offline sync séquentielle (une saisie à la fois) → lente si beaucoup de saisies en queue