Files
serv_benchmark/docs/FEATURE_THUMBNAILS_IN_LIST.md
Gilles Soulier c67befc549 addon
2026-01-05 16:08:01 +01:00

11 KiB
Executable File
Raw Blame History

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 <i class="fas fa-microchip"></i>
  • 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

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

# 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 :

{
  "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

<div class="peripheral-photo">
    ${p.thumbnail_url
        ? `<img src="${escapeHtml(p.thumbnail_url)}" alt="${escapeHtml(p.nom)}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
           <i class="fas fa-microchip" style="display:none;"></i>`
        : `<i class="fas fa-microchip"></i>`
    }
</div>

Logique :

  • Si thumbnail_url existe :

    • Afficher <img> 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 <i class="fas fa-microchip"></i>

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

.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 :

{
  "id": 6,
  "nom": "USB Receiver",
  "thumbnail_url": "/uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png"
}

HTML généré :

<div class="peripheral-photo">
    <img src="/uploads/peripherals/photos/6/thumbnail/logitechreceiver_thumb_20251231_101254.png"
         alt="USB Receiver"
         onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
    <i class="fas fa-microchip" style="display:none;"></i>
</div>

Rendu : Image du thumbnail (48px de large, ratio conservé)


Exemple 2 : Périphérique sans photo

API Response :

{
  "id": 5,
  "nom": "Flash Card Reader/Writer",
  "thumbnail_url": null
}

HTML généré :

<div class="peripheral-photo">
    <i class="fas fa-microchip"></i>
</div>

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 :

{
  "id": 7,
  "nom": "Peripheral Test",
  "thumbnail_url": "/uploads/peripherals/photos/7/thumbnail/deleted.png"
}

HTML généré :

<div class="peripheral-photo">
    <img src="/uploads/peripherals/photos/7/thumbnail/deleted.png"
         alt="Peripheral Test"
         onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
    <i class="fas fa-microchip" style="display:none;"></i>
</div>

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 → <img>
   │  └─> Sinon → <i class="fas fa-microchip">
   ↓
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

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

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