Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
17 KiB
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 indisponibleproduct-search(5) — arrêtable sans impact. Seule la recherche OpenFoodFacts devient indisponiblesearxng(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 :
- Compression côté client (Canvas API → WebP) avant envoi — réduit la bande passante
- Validation format serveur (JPG, PNG, SVG, WebP acceptés)
- Génération automatique d'une miniature (Pillow) — stockée séparément
- 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-barresGET /search?q={nom}→ OpenFoodFacts par nom de produitGET /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-themeobligatoire 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