b87c96ceab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
389 lines
17 KiB
Markdown
389 lines
17 KiB
Markdown
# HomeHub — Spécification Fonctionnelle
|
||
|
||
> Version 0.1 — mai 2026
|
||
> Application d'organisation personnelle auto-hébergée (PWA)
|
||
> Stack : FastAPI · React/Vite/TypeScript · PostgreSQL 16 · Docker Compose
|
||
|
||
---
|
||
|
||
## 1. Contexte et objectifs
|
||
|
||
HomeHub est une PWA auto-hébergée sur Proxmox 9, conçue pour un usage intensif mobile (smartphone en magasin, sur le terrain) et une consultation plus complète sur laptop. L'application est **mono-utilisateur** au démarrage, avec une architecture prévue pour évoluer vers le multi-utilisateur sans refactoring majeur (colonne `owner_id` nullable dans toutes les tables).
|
||
|
||
---
|
||
|
||
## 2. Architecture technique
|
||
|
||
### 2.1 Services Docker Compose (6 services)
|
||
|
||
| Service | Image | Port interne | Rôle |
|
||
|---------|-------|-------------|------|
|
||
| `frontend` | Nginx léger | 3000 | Sert le build React/Vite (PWA statique) |
|
||
| `backend` | Python 3.12 / FastAPI | 8000 | API REST + MCP server |
|
||
| `db` | PostgreSQL 16 | 5432 | Base de données multi-schémas |
|
||
| `ocr` | Tesseract + Pillow | 8001 | Service OCR partagé (toutes les photos de l'app) |
|
||
| `product-search` | Python slim | 8002 | Lookup OpenFoodFacts + fallback SearXNG |
|
||
| `searxng` | SearXNG officiel | 8080 | Métamoteur de recherche — fallback image produits |
|
||
|
||
**Services isolés et arrêtables indépendamment :**
|
||
- `ocr` (4) — arrêtable sans impact sur la liste de courses. Seule la saisie OCR de prix devient indisponible
|
||
- `product-search` (5) — arrêtable sans impact. Seule la recherche OpenFoodFacts devient indisponible
|
||
- `searxng` (6) — arrêtable sans impact. Seul le fallback image produit devient indisponible
|
||
|
||
Les services 1/2/3 (`frontend`, `backend`, `db`) sont les seuls **obligatoires** pour le fonctionnement de base. Un `docker compose stop ocr product-search searxng` laisse l'app 100% fonctionnelle pour la liste de courses.
|
||
|
||
**Nginx Proxy Manager** (déjà en place sur le homelab) gère l'entrée HTTPS et le routage vers ces services.
|
||
|
||
### 2.2 Évolutions prévues (non bloquantes)
|
||
|
||
- Redis → ajouté avec la sync Google Calendar (Phase 6)
|
||
- Auth JWT → activation de `owner_id` + écran de connexion (Phase 7)
|
||
- Agent Hermes (Vision LLM) → analyse photo frigo
|
||
- Webhooks Gitea → intégration Kanban
|
||
- Home Assistant → capteurs tâches en retard
|
||
|
||
### 2.3 Stockage fichiers et module media
|
||
|
||
Volume Docker `/uploads/` servi par le backend. Toutes les images passent par le module `media` du backend à la réception.
|
||
|
||
**Pipeline de traitement à l'upload :**
|
||
1. Compression côté client (Canvas API → WebP) avant envoi — réduit la bande passante
|
||
2. Validation format serveur (JPG, PNG, SVG, WebP acceptés)
|
||
3. Génération automatique d'une miniature (Pillow) — stockée séparément
|
||
4. Les deux fichiers sont enregistrés sur le volume
|
||
|
||
**Structure du volume :**
|
||
```
|
||
/uploads/
|
||
images/
|
||
originals/{uuid}.webp ← image compressée (pleine résolution utile)
|
||
thumbnails/{uuid}_thumb.webp ← miniature générée par Pillow
|
||
audio/
|
||
{uuid}.webm ← enregistrements audio
|
||
```
|
||
|
||
**Tailles de miniatures par contexte :**
|
||
|
||
| Contexte | Taille thumbnail |
|
||
|----------|-----------------|
|
||
| Produit catalogue shopping | 150 × 150 px (carré, centré) |
|
||
| Note (vignette liste) | 300 × 300 px (carré, centré) |
|
||
| Pièce jointe (aperçu inline) | 400 × 300 px (paysage, centré) |
|
||
|
||
Les miniatures sont toujours servies pour les vues liste/grille. L'original n'est chargé qu'à la demande (plein écran, zoom). Formats audio acceptés : WebM, M4A.
|
||
|
||
### 2.4 MCP Server
|
||
|
||
Intégré dans FastAPI sur `/mcp` (protocole SSE). Expose les outils suivants aux agents IA (Hermes, Claude, etc.) :
|
||
|
||
| Outil | Description |
|
||
|-------|-------------|
|
||
| `get_todos()` | Retourne les tâches urgentes / en cours |
|
||
| `add_todo(title, due_date)` | Crée une tâche |
|
||
| `get_shopping_list()` | Retourne la liste de courses active |
|
||
| `add_shopping_item(name, category)` | Ajoute un article à la liste active |
|
||
| `search_notes(query)` | Recherche full-text dans les notes |
|
||
| `add_note(title, content, tags)` | Crée une note |
|
||
|
||
---
|
||
|
||
## 3. Schéma de base de données
|
||
|
||
### 3.1 Schema `shopping`
|
||
|
||
#### `shopping.products` — Catalogue global
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| id | UUID PK | |
|
||
| name | VARCHAR(150) | Nom produit |
|
||
| brand | VARCHAR(100) | Marque |
|
||
| category | VARCHAR(50) | Rayon magasin (tri in-store) |
|
||
| image_path | VARCHAR(255) | Chemin original `/uploads/images/originals/` |
|
||
| thumbnail_path | VARCHAR(255) | Chemin miniature `/uploads/images/thumbnails/` |
|
||
| default_unit | VARCHAR(20) | kg / L / unité |
|
||
| barcode | VARCHAR(50) | Code-barres (scan futur) |
|
||
| frequency_score | INT DEFAULT 0 | Score d'habitude (suggestions auto) |
|
||
| owner_id | UUID NULL | Prévu multi-user |
|
||
| created_at | TIMESTAMPTZ | |
|
||
|
||
**Catégories produits (tri rayon en magasin) :**
|
||
`Fruits` · `Légumes` · `Viandes` · `Charcuterie` · `Poissons` · `Produits laitiers` · `Boulangerie` · `Épicerie salée` · `Épicerie sucrée` · `Condiments` · `Boissons` · `Entretien` · `Pharmacie` · `Animaux` · `Carburant` · `Électronique` · `Divers`
|
||
|
||
**Magasins pré-configurés (seed) :** Lidl · Intermarché · Super U · Gamm Vert · Weldom · Cosi · Bricocash · Tinel · Marie Blachère
|
||
|
||
**Données de démarrage :** 113 produits dans `backend/app/data/seed_products.json`, 9 magasins dans `seed_stores.json` — chargés automatiquement au premier démarrage.
|
||
|
||
#### `shopping.stores` — Magasins
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| id | UUID PK | |
|
||
| name | VARCHAR(100) | Ex: Lidl, Carrefour |
|
||
| location | TEXT | Adresse ou coordonnées GPS |
|
||
| owner_id | UUID NULL | |
|
||
|
||
#### `shopping.price_history` — Historique des prix
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| id | UUID PK | |
|
||
| product_id | UUID → products | |
|
||
| store_id | UUID → stores NULL | |
|
||
| price | NUMERIC(8,2) | Prix relevé |
|
||
| unit | VARCHAR(20) | kg / L / unité |
|
||
| quantity | NUMERIC(8,3) | Quantité associée |
|
||
| source | VARCHAR(20) | `manual` / `ocr_tag` / `ocr_receipt` |
|
||
| recorded_at | TIMESTAMPTZ | |
|
||
|
||
#### `shopping.lists` — Listes de courses
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| id | UUID PK | |
|
||
| name | VARCHAR(100) | Ex: "Semaine du 26 mai" |
|
||
| store_id | UUID NULL | Magasin cible |
|
||
| week_date | DATE | Date de la semaine cible |
|
||
| status | VARCHAR(20) | `draft` / `active` / `done` |
|
||
| owner_id | UUID NULL | |
|
||
| created_at | TIMESTAMPTZ | |
|
||
|
||
#### `shopping.list_items` — Articles dans une liste
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| id | UUID PK | |
|
||
| list_id | UUID → lists | |
|
||
| product_id | UUID → products NULL | Lié au catalogue (optionnel) |
|
||
| custom_name | VARCHAR(150) | Si article hors catalogue |
|
||
| quantity | NUMERIC(8,3) | |
|
||
| unit | VARCHAR(20) | |
|
||
| is_checked | BOOLEAN DEFAULT FALSE | Coché en magasin |
|
||
| price_recorded | NUMERIC(8,2) | Prix relevé pendant les courses |
|
||
| carried_over | BOOLEAN DEFAULT FALSE | Reporté depuis la semaine précédente |
|
||
| sort_order | INT | Tri par rayon/catégorie |
|
||
|
||
### 3.2 Schema `todos`
|
||
|
||
#### `todos.items`
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| id | UUID PK | |
|
||
| title | VARCHAR(255) | |
|
||
| body | TEXT | Texte libre |
|
||
| url | TEXT | Lien optionnel |
|
||
| domain | VARCHAR(50) | Voir liste des domaines |
|
||
| category | VARCHAR(50) | Sous-catégorie libre |
|
||
| tags | VARCHAR(50)[] | Array · index GIN |
|
||
| status | VARCHAR(20) | `pending` / `done` / `cancelled` |
|
||
| priority | VARCHAR(10) | `low` / `medium` / `high` |
|
||
| due_date | TIMESTAMPTZ | Date objectif |
|
||
| postponed_count | INT DEFAULT 0 | Nombre de reports |
|
||
| created_at | TIMESTAMPTZ DEFAULT NOW() | Auto |
|
||
| updated_at | TIMESTAMPTZ | |
|
||
| owner_id | UUID NULL | |
|
||
|
||
**Domaines disponibles** : `informatique` · `diy` · `electronique` · `domotique` · `bricolage` · `jardin` · `cuisine` · `voyage` · `animaux`
|
||
|
||
### 3.3 Schema `notes`
|
||
|
||
#### `notes.items`
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| id | UUID PK | |
|
||
| title | VARCHAR(255) | Optionnel |
|
||
| content | TEXT NOT NULL | Texte de la note |
|
||
| category | VARCHAR(50) | |
|
||
| tags | VARCHAR(50)[] | Index GIN |
|
||
| gps_lat | NUMERIC(10,7) | Latitude GPS |
|
||
| gps_lon | NUMERIC(10,7) | Longitude GPS |
|
||
| metadata | JSONB | Paires clé/valeur libres (référence, magasin…) |
|
||
| created_at | TIMESTAMPTZ DEFAULT NOW() | |
|
||
| owner_id | UUID NULL | |
|
||
|
||
Index : `GIN(tags)` + `GIN(to_tsvector('french', title || ' ' || content))` (recherche full-text)
|
||
|
||
#### `notes.attachments`
|
||
| Colonne | Type | Description |
|
||
|---------|------|-------------|
|
||
| id | UUID PK | |
|
||
| note_id | UUID → items | |
|
||
| file_path | VARCHAR(255) | Chemin original `/uploads/images/originals/` |
|
||
| thumbnail_path | VARCHAR(255) | Chemin miniature (NULL si audio) |
|
||
| file_type | VARCHAR(20) | `image` / `audio` |
|
||
| original_name | VARCHAR(255) | |
|
||
| created_at | TIMESTAMPTZ | |
|
||
|
||
---
|
||
|
||
## 4. Modules fonctionnels
|
||
|
||
### 4.1 Module Shopping — Liste de courses
|
||
|
||
#### Principe fondamental — ne jamais bloquer
|
||
|
||
> **L'objectif principal est simple : créer une liste en début de semaine, cocher les articles pendant les courses.**
|
||
> Toutes les autres fonctionnalités (prix, OCR, stats, suggestions automatiques) sont **optionnelles** et n'apparaissent jamais dans le chemin principal. Un utilisateur qui ignore tout ça doit pouvoir utiliser l'app sans friction.
|
||
|
||
#### Concept général
|
||
La liste de courses est générée **à partir du catalogue global** de produits. Le `frequency_score` de chaque produit augmente à chaque achat — ce score alimente les suggestions automatiques de la semaine. L'utilisateur valide/modifie/complète la liste avant de partir en courses.
|
||
|
||
Les articles **non cochés** à la fin des courses sont automatiquement reportés (`carried_over = true`) dans la liste de la semaine suivante.
|
||
|
||
La saisie de prix est **toujours optionnelle** — le champ prix est discret, accessible d'un tap long ou d'un bouton secondaire, jamais dans le chemin de cocher un article.
|
||
|
||
#### Interface mobile (priorité)
|
||
- **Création de liste** : interface ultra-simple — picker de la semaine, suggestion automatique des produits habituels (top frequency_score), ajout rapide d'un produit (recherche dans le catalogue ou création à la volée)
|
||
- **Mode courses** (liste active) :
|
||
- Grands boutons tactiles (min 48px)
|
||
- Articles triés par rayon/catégorie
|
||
- Tap pour cocher un article
|
||
- Swipe gauche → supprimer de la liste
|
||
- Champ prix rapide au tap (clavier numérique)
|
||
- **Wake Lock API** : l'écran ne se verrouille pas pendant les courses
|
||
- Bouton "Terminer les courses" → génère la liste suivante avec les articles non cochés
|
||
|
||
#### Interface laptop (complète)
|
||
- Vue tableau avec filtre par magasin, catégorie, statut
|
||
- Gestion du catalogue global de produits (CRUD complet)
|
||
- Gestion des magasins
|
||
- Graphiques d'évolution des prix par produit
|
||
- Export CSV de l'historique des prix
|
||
|
||
#### Module OCR (service Docker isolé, partagé entre tous les modules)
|
||
|
||
Le service OCR tourne dans son propre conteneur Docker (`ocr:8001`). Il est utilisable par tous les modules de l'application via l'endpoint unifié `POST /api/ocr/extract` du backend.
|
||
|
||
| Module | Usage OCR |
|
||
|--------|-----------|
|
||
| Shopping | Lecture étiquette prix en rayon → pré-remplissage prix |
|
||
| Shopping | Lecture ticket de caisse → réconciliation finale liste |
|
||
| Notes | Extraction texte depuis une photo (photo de document, panneau, référence) |
|
||
| Futur | Tout import photo dans l'application |
|
||
|
||
Le module OCR est **toujours optionnel** : si le service est arrêté, les autres fonctionnalités de l'app ne sont pas impactées. Backend : Tesseract (local). Fallback possible : Ollama Vision (Hermes) en remplaçant simplement le service `ocr` dans le Docker Compose.
|
||
|
||
#### Suivi des prix
|
||
Chaque prix saisi (manuel, OCR étiquette ou OCR ticket) alimente `price_history`. Sur laptop, un graphique en courbe par produit montre l'évolution du prix dans le temps, par magasin.
|
||
|
||
### 4.2 Module Notes / Listes diverses
|
||
|
||
Saisie rapide de notes avec support multimédia et géolocalisation.
|
||
|
||
**Champs** :
|
||
- Titre (optionnel)
|
||
- Contenu texte libre (saisie rapide)
|
||
- Date (auto à la création, modifiable)
|
||
- Catégorie
|
||
- Tags (multi-valeurs, autocomplétion)
|
||
- Photo(s) : capture directe via Camera API ou import depuis galerie. Compression WebP avant upload
|
||
- Audio : enregistrement via MediaRecorder API. Format WebM/M4A
|
||
- GPS : bouton "Localiser" → Geolocation API → stocke lat/lon, affiche adresse via reverse geocoding (optionnel)
|
||
- Métadonnées libres : paires clé/valeur (ex: "Référence: X12-34", "Magasin: Brico Dépôt")
|
||
|
||
**Recherche** : full-text PostgreSQL en français sur titre + contenu + métadonnées. Filtres par catégorie, tags, présence de photo/audio/GPS.
|
||
|
||
### 4.3 Module Todos
|
||
|
||
Gestion de tâches classées par domaine.
|
||
|
||
**Champs** :
|
||
- Titre
|
||
- Texte libre (description)
|
||
- URL (lien externe optionnel)
|
||
- Domaine (liste fermée : voir section 3.2)
|
||
- Catégorie (libre)
|
||
- Tags (multi-valeurs)
|
||
- Statut : `pending` / `done` / `cancelled`
|
||
- Priorité : `low` / `medium` / `high`
|
||
- Date de création (auto)
|
||
- Date objectif (due_date)
|
||
- Compteur de reports (`postponed_count`)
|
||
|
||
**Interface mobile** : ajout rapide en 1 clic, vue liste épurée, actions swipe (reporter / terminer). Boutons "Reporter d'1 jour", "Reporter à la semaine prochaine".
|
||
|
||
**Interface laptop** : vue complète avec filtres multi-critères (domaine, statut, priorité, tags), tri, recherche textuelle.
|
||
|
||
---
|
||
|
||
## 5. Module scan code-barres / QR code
|
||
|
||
### Fonctionnement
|
||
- Librairie JS `zxing-js` (open-source, cross-platform) intégrée dans le frontend
|
||
- Accès via le flux Camera API (pas de capture photo — lecture en temps réel)
|
||
- Fonctionne sur iOS Safari et Android Chrome
|
||
- Déclenché par un bouton 📷 "Scanner" dans le formulaire d'ajout de produit
|
||
|
||
### Flux de résolution produit
|
||
```
|
||
Scan code-barres (EAN-13 / QR)
|
||
→ service product-search : GET /lookup?barcode={code}
|
||
→ 1. OpenFoodFacts API (base ~3M produits alimentaires)
|
||
→ trouvé : nom, marque, catégorie, image → auto-remplit le formulaire
|
||
→ 2. Si pas trouvé : retourne vide → saisie manuelle
|
||
→ enrichissement optionnel via recherche texte + SearXNG (laptop uniquement)
|
||
```
|
||
|
||
### Service `product-search` (Docker)
|
||
API Python légère qui centralise :
|
||
- `GET /lookup?barcode={code}` → OpenFoodFacts par code-barres
|
||
- `GET /search?q={nom}` → OpenFoodFacts par nom de produit
|
||
- `GET /image-search?q={nom}` → SearXNG image search (fallback, laptop uniquement)
|
||
|
||
Le backend FastAPI expose ces fonctionnalités via `/api/products/lookup`, `/api/products/search`, `/api/products/image-search`.
|
||
|
||
### Service `searxng` (Docker)
|
||
Instance SearXNG auto-hébergée, utilisée exclusivement pour la recherche d'images de produits non trouvés dans OpenFoodFacts. Accessible uniquement depuis l'interface laptop lors de l'enrichissement du catalogue. Non exposé publiquement via NPM.
|
||
|
||
---
|
||
|
||
## 6. Capacités natives smartphone (PWA)
|
||
|
||
| API navigateur | Usage |
|
||
|---------------|-------|
|
||
| Camera API | Capture photo directe (notes, OCR, produits) |
|
||
| MediaRecorder API | Enregistrement audio (notes vocales) |
|
||
| Geolocation API | Localisation GPS sur les notes |
|
||
| Wake Lock API | Écran actif pendant les courses |
|
||
| Web Share API | Partage de notes/listes vers autres apps |
|
||
| Liens `webcal://` | Abonnement calendrier natif iOS/Android (futur) |
|
||
|
||
---
|
||
|
||
## 7. Design system
|
||
|
||
Le design system **Gruvbox seventies** est intégré dès le départ.
|
||
|
||
- Fichiers : `design_system/tokens/tokens.css` · `design_system/components/ui-kit.jsx`
|
||
- Thème : dark par défaut, light disponible via `data-theme`
|
||
- Palette : orange brûlé `#fe8019` (accent), fond brun `#2a231d`, texte `#f2e5c7`
|
||
- 14 composants React disponibles (Button, IconButton, Toggle, StatusLed, BatteryGauge, RadialGauge, Popup, TreeNav, Sparkline, LineChart, Icon…)
|
||
- Fonts : Inter (UI) · JetBrains Mono (données) · Share Tech Mono (terminal/logs)
|
||
- Règles absolues : jamais de hex en dur, toujours `var(--token)`, `data-theme` obligatoire sur un parent
|
||
|
||
---
|
||
|
||
## 8. Interface utilisateur — principes
|
||
|
||
### Mobile-first
|
||
- Touch target minimum : 48px
|
||
- Composants d'action accessibles en 1 main
|
||
- Swipe sur les listes (reporter, terminer, supprimer)
|
||
- Chargement hors-ligne via Service Worker (cache assets + données critiques)
|
||
- Feedback haptique si disponible
|
||
|
||
### Distinction mobile / laptop
|
||
Certains modules ont deux pages dédiées selon la taille d'écran :
|
||
|
||
| Module | Mobile | Laptop |
|
||
|--------|--------|--------|
|
||
| Shopping | Création liste simple + mode courses | Catalogue produits + gestion magasins + graphiques prix |
|
||
| Todos | Ajout rapide + liste swipeable | Tableau filtrable multi-critères |
|
||
| Notes | Saisie rapide + media capture | Liste complète + recherche avancée |
|
||
|
||
---
|
||
|
||
## 9. Hors scope initial (Phase 1-5)
|
||
|
||
- Authentification multi-utilisateurs (Phase 7)
|
||
- Google Calendar / CalDAV (Phase 6)
|
||
- Kanban / Gitea webhooks (Phase 8)
|
||
- Home Assistant (Phase 8)
|
||
- Analyse frigo par Vision LLM / Hermes (Phase 9)
|
||
- Scan code-barres produits
|