# 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
? `
`
: `
`
}
```
**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
```
**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
```
**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