# Fonctionnalité : Miniatures dans la liste des périphériques ## 🎯 Objectif Afficher les miniatures (thumbnails) de 48px dans la liste des périphériques au lieu de l'icône générique. **Comportement** : - Si le périphérique a une photo principale → Afficher la miniature - Si pas de photo → Afficher l'icône `` - Si l'image ne charge pas (erreur) → Fallback vers l'icône --- ## ✅ Implémentation ### 1. Backend - Schéma API **Fichier** : `backend/app/schemas/peripheral.py` (ligne 150) **Modification** : Ajout du champ `thumbnail_url` au schéma `PeripheralSummary` ```python class PeripheralSummary(BaseModel): """Summary schema for peripheral lists""" id: int nom: str type_principal: str sous_type: Optional[str] marque: Optional[str] modele: Optional[str] etat: str rating: float prix: Optional[float] en_pret: bool is_complete_device: bool quantite_disponible: int thumbnail_url: Optional[str] = None # ← Nouveau champ class Config: from_attributes = True ``` **Résultat** : L'API retourne maintenant l'URL du thumbnail pour chaque périphérique dans la liste. --- ### 2. Backend - Service **Fichier** : `backend/app/services/peripheral_service.py` (lignes 179-210) **Modification** : Récupération de la photo principale et construction de l'URL ```python # Import PeripheralPhoto here to avoid circular import from app.models.peripheral import PeripheralPhoto # Convert to summary items = [] for p in peripherals: # Get primary photo thumbnail thumbnail_url = None primary_photo = db.query(PeripheralPhoto).filter( PeripheralPhoto.peripheral_id == p.id, PeripheralPhoto.is_primary == True ).first() if primary_photo and primary_photo.thumbnail_path: # Convert file path to URL thumbnail_url = primary_photo.thumbnail_path.replace('/app/uploads/', '/uploads/') items.append(PeripheralSummary( id=p.id, nom=p.nom, type_principal=p.type_principal, sous_type=p.sous_type, marque=p.marque, modele=p.modele, etat=p.etat or "Inconnu", rating=p.rating or 0.0, prix=p.prix, en_pret=p.en_pret or False, is_complete_device=p.is_complete_device or False, quantite_disponible=p.quantite_disponible or 0, thumbnail_url=thumbnail_url # ← Ajout du thumbnail )) ``` **Logique** : 1. Pour chaque périphérique, requête SQL pour trouver la photo avec `is_primary = True` 2. Si une photo principale existe et a un `thumbnail_path` : - Convertir le chemin serveur (`/app/uploads/...`) en URL web (`/uploads/...`) 3. Sinon : `thumbnail_url = None` **Exemple de résultat API** : ```json { "items": [ { "id": 6, "nom": "USB Receiver", "thumbnail_url": "/uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png" }, { "id": 5, "nom": "Flash Card Reader/Writer", "thumbnail_url": null } ], "total": 46, "page": 1, "page_size": 10, "total_pages": 5 } ``` --- ### 3. Frontend - JavaScript **Fichier** : `frontend/js/peripherals.js` (lignes 325-329) **Modification** : Affichage conditionnel de l'image ou de l'icône ```javascript
${p.thumbnail_url ? `${escapeHtml(p.nom)} ` : `` }
``` **Logique** : - **Si `thumbnail_url` existe** : - Afficher `` avec l'URL du thumbnail - Ajouter un `onerror` handler : si l'image ne charge pas, cacher l'image et afficher l'icône - Icône en `display:none` par défaut (visible seulement en cas d'erreur) - **Si pas de `thumbnail_url`** : - Afficher directement l'icône `` **Cas gérés** : 1. ✅ Périphérique avec photo → Image affichée 2. ✅ Périphérique sans photo → Icône affichée 3. ✅ Image qui ne charge pas (404, erreur réseau) → Fallback vers icône --- ### 4. Frontend - CSS **Fichier** : `frontend/css/peripherals.css` (lignes 156-176) **Modification** : Style pour conteneur et images ```css .peripheral-photo { width: 50px; height: 50px; background: #232323; border: 1px solid #3e3d32; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #66d9ef; font-size: 1.5rem; overflow: hidden; /* ← Ajouté */ } .peripheral-photo img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; /* ← Conserve ratio */ } ``` **Caractéristiques** : - **Conteneur** : 50×50px, fond sombre, bordure, centré - **Image** : - `max-width/max-height: 100%` → Ne dépasse jamais le conteneur - `width/height: auto` → Préserve le ratio d'aspect - `object-fit: contain` → L'image entière est visible sans déformation - Centré grâce au `display: flex` du parent **Rendu visuel** : ``` ┌─────────────────────────────────────────────────────┐ │ Nom │ Type │ Photo │ ├─────────────────────────────────────────────────────┤ │ USB Receiver │ USB │ [🖼️ Thumbnail] │ │ Flash Card Reader │ Stockage│ [💾 Icône] │ │ ConBee II │ USB │ [🖼️ Thumbnail] │ │ CS9711Fingprint │ USB │ [🖼️ Thumbnail] │ │ TL-WN823N │ Réseau │ [💾 Icône] │ └─────────────────────────────────────────────────────┘ ``` --- ## 📊 Exemples ### Exemple 1 : Périphérique avec photo **API Response** : ```json { "id": 6, "nom": "USB Receiver", "thumbnail_url": "/uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png" } ``` **HTML généré** : ```html
USB Receiver
``` **Rendu** : Image du thumbnail (48px de large, ratio conservé) --- ### Exemple 2 : Périphérique sans photo **API Response** : ```json { "id": 5, "nom": "Flash Card Reader/Writer", "thumbnail_url": null } ``` **HTML généré** : ```html
``` **Rendu** : Icône générique de puce électronique --- ### Exemple 3 : Image qui ne charge pas (erreur) **Scénario** : Le fichier thumbnail est supprimé mais l'URL existe en base **API Response** : ```json { "id": 7, "nom": "Peripheral Test", "thumbnail_url": "/uploads/peripherals/photos/7/thumbnail/deleted.png" } ``` **HTML généré** : ```html
Peripheral Test
``` **Comportement** : 1. Navigateur tente de charger l'image 2. Image introuvable → Event `onerror` déclenché 3. JavaScript cache l'image (`display:none`) 4. JavaScript affiche l'icône (`display:flex`) **Rendu final** : Icône générique (fallback automatique) --- ## 🔄 Flux complet ``` 1. User charge page /peripherals.html ↓ 2. JavaScript appelle GET /api/peripherals/?page=1&page_size=10 ↓ 3. Backend service list_peripherals() │ ├─> Query périphériques (avec pagination) │ └─> Pour chaque périphérique: │ ├─> Query photo principale (is_primary=True) │ └─> Si photo existe: thumbnail_url = "/uploads/..." ↓ 4. API retourne JSON avec items[].thumbnail_url ↓ 5. JavaScript génère HTML du tableau │ ├─> Si thumbnail_url → │ └─> Sinon → ↓ 6. Navigateur affiche la liste │ ├─> Charge les images (si URLs présentes) │ └─> Affiche icônes (si pas d'URL ou erreur) ``` --- ## 📝 Fichiers modifiés ### Backend | Fichier | Lignes | Modification | |---------|--------|--------------| | `backend/app/schemas/peripheral.py` | 150 | Ajout `thumbnail_url: Optional[str] = None` | | `backend/app/services/peripheral_service.py` | 179-210 | Query photo principale + construction URL | ### Frontend | Fichier | Lignes | Modification | |---------|--------|--------------| | `frontend/js/peripherals.js` | 325-329 | Affichage conditionnel image/icône | | `frontend/css/peripherals.css` | 170-176 | Style pour `img` dans `.peripheral-photo` | --- ## 🧪 Tests ### Test 1 : API retourne thumbnail_url ```bash curl -s "http://localhost:8007/api/peripherals/?page=1&page_size=2" | \ python3 -c "import sys, json; data=json.load(sys.stdin); print(data['items'][0].get('thumbnail_url'))" ``` **Résultat attendu** : ``` /uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png ``` ### Test 2 : Plusieurs périphériques avec/sans photos ```bash curl -s "http://localhost:8007/api/peripherals/?page=1&page_size=10" | \ python3 -c " import sys, json data = json.load(sys.stdin) for item in data['items'][:5]: thumb = item.get('thumbnail_url', 'None') print(f'{item[\"nom\"][:30]:30} → {thumb}') " ``` **Résultat attendu** : ``` USB Receiver → /uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png Flash Card Reader/Writer → None ConBee II → /uploads/peripherals/photos/4/thumbnail/conbee2_thumb_20251231_101147.png CS9711Fingprint → /uploads/peripherals/photos/3/thumbnail/csfingerprint_thumb_20251231_101537.png TL-WN823N v2/v3 → None ``` ### Test 3 : Affichage visuel 1. Ouvrir `http://10.0.0.50:8087/peripherals.html` 2. Vérifier la colonne "Photo" : - ✅ Les périphériques avec photos affichent la miniature - ✅ Les périphériques sans photos affichent l'icône puce - ✅ Les images sont bien dimensionnées (max 50×50px) --- ## 💡 Améliorations futures - [ ] Cache des requêtes thumbnail (éviter N+1 queries) - [ ] Lazy loading des images (`loading="lazy"`) - [ ] Preview au survol (hover) de la miniature - [ ] Optimisation : JOIN au lieu de requête séparée par périphérique - [ ] Placeholder animé pendant chargement de l'image --- **Date** : 31 décembre 2025 **Statut** : ✅ Implémenté et testé **Impact** : Affichage des miniatures dans la liste des périphériques avec fallback automatique vers icône