fix: corrections SW cache, nginx proxy, sw.js no-cache — 2026-05-01 05:48

This commit is contained in:
Claude Deploy
2026-05-01 05:48:38 +00:00
parent e3f3bb0ea5
commit 50758fe232
4 changed files with 721 additions and 18 deletions
+577
View File
@@ -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é (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