# 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