Files
equitask/DOCUMENTATION.md

23 KiB
Raw Permalink Blame History

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
  2. Architecture globale
  3. Stack technique
  4. Structure des fichiers
  5. Base de données
  6. API REST — référence complète
  7. Frontend — pages et composants
  8. Système de score
  9. Mode offline / PWA
  10. Déploiement et infrastructure
  11. Variables d'environnement
  12. Seed automatique
  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

-- 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

{
  "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

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

{
  "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 :

# 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/ :

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