Imago/doc/PROMPT_PHASE3_claude-opus.md
Bruno Charest cc99fea20a
Some checks failed
CI / Lint & Format (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
Add comprehensive test suite for image processing and related services
- Implement tests for database generator to ensure proper session handling.
- Create tests for EXIF extraction and conversion functions.
- Add tests for image-related endpoints, ensuring proper data retrieval and isolation between clients.
- Develop tests for OCR functionality, including language detection and text extraction.
- Introduce tests for the image processing pipeline, covering success and failure scenarios.
- Validate rate limiting functionality and ensure independent counters for different clients.
- Implement scraper tests to verify HTML content fetching and error handling.
- Add unit tests for various services, including storage and filename generation.
- Establish worker entry point for ARQ to handle background image processing tasks.
2026-02-24 11:22:10 -05:00

37 KiB
Raw Blame History

Prompt Claude Code — Phase 3 : Expérience développeur

Modèle : claude-opus-4-6

Usage : claude --model claude-opus-4-6 -p "$(cat PROMPT_PHASE3_claude-opus.md)"

ou coller directement dans une session Claude Code interactive


Tu es un ingénieur backend senior Python spécialisé en conception d'APIs publiques, expérience développeur (DX) et intégration de systèmes. Tu travailles sur le projet Imago pour livrer sa phase finale : rendre ce hub aussi simple et agréable à intégrer que possible pour ses clients.

Les Phases 1 (sécurité, multi-tenants, rate limiting) et Phase 2 (ARQ + Redis, StorageBackend abstrait, structlog, Prometheus, CI/CD) sont entièrement terminées et validées. Tu prends le relais pour la dernière phase : WebSockets temps réel, versioning de l'API, SDK Python officiel, dashboard admin et intégration complète avec Shaarli.

<project_context>

État du projet après Phase 1 et Phase 2

Stack complète en place

# Phase 0 — Base
fastapi==0.115.0                          # framework web async
uvicorn[standard]==0.30.6                 # serveur ASGI
sqlalchemy[asyncio]==2.0.35               # ORM async
alembic==1.13.3                           # migrations
pydantic-settings==2.5.2                  # config depuis .env
pillow==10.4.0                            # traitement images + thumbnails
piexif==1.1.3                             # extraction EXIF avancée
pytesseract==0.3.13                       # OCR Tesseract
anthropic==0.34.2 / httpx==0.27.2        # Vision AI Claude
beautifulsoup4==4.12.3                    # scraping web
aiofiles==24.1.0                          # I/O fichiers async

# Phase 1 — Sécurité
python-jose[cryptography]==3.3.0          # JWT
passlib[bcrypt]==1.7.4                    # hachage API Keys
slowapi==0.1.9                            # rate limiting par client/plan

# Phase 2 — Robustesse
arq==0.25.0                               # file de tâches async persistante
redis==5.0.8                              # client Redis (Pub/Sub + ARQ)
aioboto3==13.0.0                          # stockage S3/MinIO/R2 async
structlog==24.4.0                         # logs JSON structurés
prometheus-fastapi-instrumentator==0.14.0 # métriques Prometheus

Structure complète du projet (après Phase 2)

imago/
├── app/
│   ├── main.py                      # App FastAPI, lifespan, middlewares, /metrics, /health
│   ├── config.py                    # Settings complets (auth, redis, S3, logging, quotas)
│   ├── database.py                  # Engine SQLAlchemy async + session factory
│   ├── logging_config.py            # Configuration structlog JSON
│   ├── metrics.py                   # Compteurs/Histogrammes Prometheus custom
│   ├── models/
│   │   ├── image.py                 # Image (client_id FK, EXIF, OCR, AI, statut pipeline)
│   │   └── client.py                # APIClient (id, name, hash, scopes, plan, quotas, storage_used)
│   ├── schemas/
│   │   └── __init__.py              # Schémas Pydantic complets avec quotas
│   ├── dependencies/
│   │   └── auth.py                  # verify_api_key, require_scope, get_current_client
│   ├── middleware/
│   │   ├── rate_limit.py            # Rate limiting par plan (slowapi)
│   │   └── logging_middleware.py    # Logs HTTP structurés par requête
│   ├── routers/
│   │   ├── auth.py                  # CRUD clients, rotation de clé (/auth/*)
│   │   ├── images.py                # Endpoints images filtrés par client (/images/*)
│   │   ├── ai.py                    # Endpoints AI (/ai/summarize, /ai/draft-task)
│   │   └── files.py                 # Serveur fichiers signés (/files/signed/{token})
│   ├── services/
│   │   ├── storage_backend.py       # ABC StorageBackend + LocalStorage + S3Storage
│   │   ├── storage.py               # Orchestration upload/delete avec quota check
│   │   ├── exif_service.py          # Extraction EXIF (piexif)
│   │   ├── ocr_service.py           # OCR Tesseract
│   │   ├── ai_vision.py             # Vision AI + summarize_url + draft_task
│   │   ├── scraper.py               # Scraping web BeautifulSoup
│   │   └── pipeline.py              # Orchestration EXIF→OCR→AI + publication Redis Pub/Sub
│   └── workers/
│       ├── image_worker.py          # Worker ARQ (process_image_task, WorkerSettings)
│       └── redis_client.py          # Pool Redis async partagé
├── tests/
│   ├── conftest.py                  # Fixtures : test_db, client_a/b, auth_headers, redis_mock
│   ├── test_services.py             # Tests unitaires services (EXIF, OCR, storage)
│   ├── test_auth.py                 # Tests authentification et scopes
│   ├── test_isolation.py            # Tests isolation multi-tenants
│   ├── test_rate_limit.py           # Tests rate limiting par plan
│   ├── test_pipeline_arq.py         # Tests worker ARQ (enqueue, retry, dead-letter)
│   ├── test_storage.py              # Tests StorageBackend + quota + URLs signées
│   └── test_observability.py        # Tests /metrics et /health/detailed
├── alembic/
│   └── versions/                    # Migrations : images, api_clients, storage_used
├── worker.py                        # Point d'entrée : python worker.py
├── run.py                           # Point d'entrée serveur : python run.py
├── Makefile                         # make dev | test | ci | lint | docker-up
├── pyproject.toml                   # Config ruff, black, mypy, pytest, coverage
├── requirements.txt                 # Dépendances production
├── requirements-dev.txt             # Dépendances dev (pytest, ruff, mypy, bandit...)
├── .pre-commit-config.yaml          # Hooks pre-commit
├── .github/workflows/ci.yml         # Pipeline CI/CD (quality + tests + docker + deploy)
├── Dockerfile
└── docker-compose.yml               # backend + worker + redis + minio

Ce qui manque encore (cible de la Phase 3)

WebSockets absents : le pipeline publie déjà sur Redis Pub/Sub (Phase 2), mais aucun endpoint WebSocket n'expose ces événements aux clients. Ils doivent encore poller GET /images/{id}/status.

API non versionnée : tous les endpoints sont sous /images/, /ai/, /auth/. Si l'on change un contrat, tous les clients cassent. Aucune politique de dépréciation.

Pas de SDK : chaque client doit réimplémenter l'upload, le polling, la gestion des erreurs, le retry. Shaarli devra faire de même.

Dashboard admin absent : aucune interface pour superviser les clients, leurs quotas et la santé du hub. Les opérations admin passent par des appels curl directs.

Intégration Shaarli non finalisée : Shaarli est le premier client officiel mais l'intégration n'est pas documentée, pas testée de bout en bout, et aucun exemple de code n'existe. </project_context>

## Mission : implémenter la Phase 3 — Expérience développeur

Tu dois réaliser les 5 livrables suivants dans l'ordre indiqué. Chaque livrable doit être complet, testé et fonctionnel avant de passer au suivant.


Livrable 3.1 — WebSockets pipeline temps réel (priorité MOYENNE)

Objectif : exposer les événements Redis Pub/Sub aux clients via WebSocket, éliminant complètement le besoin de polling.

Aucune nouvelle dépendance : FastAPI supporte nativement les WebSockets via websockets (déjà inclus dans uvicorn[standard]).

Ce qui doit être créé ou modifié :

  1. app/routers/websocket.py — Nouveau router WebSocket :

    Endpoint principal :

    WS /ws/pipeline/{image_id}?token=<api_key>
    
    • Authentification via query param token (pas de header Authorization sur WebSocket)
    • Valider que l'image appartient au client authentifié avant d'accepter la connexion
    • S'abonner au channel Redis pipeline:{image_id}
    • Pusher chaque message JSON reçu vers le client WebSocket
    • Fermer proprement après réception de pipeline.done ou pipeline.error
    • Gérer la déconnexion du client : si le client se déconnecte avant la fin, se désabonner proprement de Redis

    Endpoint de liste des pipelines actifs (admin) :

    WS /ws/admin/monitor?token=<admin_api_key>
    
    • Nécessite le scope admin
    • Pusher un événement à chaque fois qu'un pipeline démarre ou se termine sur n'importe quelle image

    Buffer de reconnexion :

    • Stocker les 10 derniers événements d'un pipeline dans Redis (clé pipeline:buffer:{image_id}, TTL 60s)
    • Si un client se connecte après que le pipeline a démarré, envoyer d'abord les événements bufferisés puis continuer en live

    Format exact des messages (doit correspondre à ce que publie pipeline.py) :

    { "event": "pipeline.started",   "image_id": 42, "steps": ["exif", "ocr", "ai"], "timestamp": "..." }
    { "event": "step.completed",     "image_id": 42, "step": "exif",   "duration_ms": 45,   "data": { "camera": "Canon EOS R5", "has_gps": true } }
    { "event": "step.completed",     "image_id": 42, "step": "ocr",    "duration_ms": 820,  "data": { "has_text": true, "preview": "Café de..." } }
    { "event": "step.completed",     "image_id": 42, "step": "ai",     "duration_ms": 3200, "data": { "tags": ["café", "paris"], "confidence": 0.97 } }
    { "event": "pipeline.done",      "image_id": 42, "total_duration_ms": 4065, "status": "done" }
    { "event": "pipeline.error",     "image_id": 42, "step": "ai", "error": "API timeout", "retry_attempt": 1 }
    

    Gestion des erreurs :

    • Si l'image est déjà en statut done quand le client se connecte → envoyer immédiatement un message pipeline.done synthétique et fermer
    • Si l'image est en statut error → envoyer pipeline.error synthétique et fermer
    • Si l'image n'appartient pas au client → fermer avec code WebSocket 4003 (Forbidden)
  2. app/metrics.py — Ajouter la gauge hub_active_websockets :

    • Incrémenter à l'ouverture d'une connexion WebSocket
    • Décrémenter à la fermeture (succès ou erreur)
  3. app/main.py — Monter le router WebSocket :

    from app.routers.websocket import router as ws_router
    app.include_router(ws_router)   # pas de préfixe /api/v1 — WS garde son propre préfixe /ws
    
  4. tests/test_websocket.py — Tests complets :

    • Connexion sans token → refus immédiat (code 4001)
    • Connexion avec token valide → acceptée
    • Image appartenant à un autre client → refus (code 4003)
    • Image déjà done → reçoit pipeline.done synthétique et connexion fermée proprement
    • Réception des événements dans l'ordre : started → step×3 → done
    • Déconnexion client en cours de pipeline → désabonnement Redis propre (pas de goroutine/tâche en fuite)
    • Reconnexion → buffer de 60s rejoué

    Pour les tests, utiliser httpx avec ASGITransport ou le client WebSocket de starlette.testclient (with client.websocket_connect(...))


Livrable 3.2 — API versioning /api/v1/ + politique de dépréciation (priorité MOYENNE)

Objectif : structurer l'API sous un préfixe versionné pour garantir la stabilité des contrats pour tous les clients intégrés.

Ce qui doit être créé ou modifié :

  1. app/main.py — Restructurer le montage des routers :

    Avant (sans versioning) :

    app.include_router(images_router)
    app.include_router(ai_router)
    app.include_router(auth_router)
    

    Après (versioning propre) :

    from fastapi import APIRouter
    
    # Router versionné v1
    api_v1 = APIRouter(prefix="/api/v1", tags=["v1"])
    api_v1.include_router(images_router)
    api_v1.include_router(ai_router)
    api_v1.include_router(auth_router)
    
    # WebSocket et fichiers signés — pas de préfixe /api/v1
    app.include_router(ws_router)    # /ws/*
    app.include_router(files_router) # /files/*
    
    # Routes non versionnées (infra)
    # GET /          → info
    # GET /health
    # GET /health/detailed
    # GET /metrics
    
    app.include_router(api_v1)
    
  2. app/middleware/versioning.py — Middleware de versioning :

    • Ajouter le header X-API-Version: v1 sur toutes les réponses des routes /api/v1/*
    • Préparer (en commentaire) l'ajout futur de Deprecation: true et Sunset: <date> quand v2 existera
    • Logger un warning structlog si une route /api/v1/* est appelée après la date de sunset (configurable)
  3. app/config.py — Ajouter :

    API_V1_SUNSET_DATE: str = ""   # vide = pas de sunset, sinon "2027-06-01"
    
  4. app/main.py — Mettre à jour la documentation OpenAPI :

    app = FastAPI(
        title="Imago API",
        version="1.0.0",
        description="""
    ## Imago — API v1
    
    Hub centralisé de gestion d'images avec pipeline AI automatique.
    
    ### Authentification
    Toutes les routes `/api/v1/*` nécessitent un header :
    

    Authorization: Bearer <votre_api_key>

    
    ### Versioning
    Cette API respecte le versioning sémantique. La version actuelle est **v1**.
    Les changements breaking seront introduits dans `/api/v2/` avec un préavis minimum de 12 mois.
    
    ### Rate Limiting
    Les limites varient selon le plan :
    - `free` : 20 uploads/h, 50 requêtes AI/h
    - `standard` : 100 uploads/h, 200 requêtes AI/h
    - `premium` : 500 uploads/h, 1000 requêtes AI/h
        """,
        openapi_tags=[
            {"name": "Images",       "description": "Gestion des images et pipeline AI"},
            {"name": "Intelligence Artificielle", "description": "Résumé d'URL, rédaction de tâches"},
            {"name": "Authentification", "description": "Gestion des clients API"},
            {"name": "WebSocket",    "description": "Notifications temps réel du pipeline"},
            {"name": "Santé",        "description": "Health checks et métriques"},
        ],
    )
    
  5. tests/test_versioning.py — Tests :

    • Tous les endpoints images/ai/auth répondent sous /api/v1/
    • Les anciennes URLs sans préfixe retournent HTTP 404
    • Header X-API-Version: v1 présent sur toutes les réponses /api/v1/*
    • Header absent sur /health, /metrics, /ws/*

Livrable 3.3 — SDK Python officiel imago-client (priorité MOYENNE)

Objectif : fournir un SDK Python idiomatique que n'importe quel client (Shaarli, scripts, autres apps) peut installer avec pip install imago-client et utiliser sans connaître les détails de l'API REST.

Architecture du SDK : le SDK est un package Python séparé dans le même dépôt (monorepo), dans le dossier sdk/.

Structure du SDK :

sdk/
├── pyproject.toml                   # Package config (build-system, metadata)
├── README.md                        # Documentation d'utilisation du SDK
├── imago_client/
│   ├── __init__.py                  # Exports publics : HubClient, HubImage, HubError
│   ├── client.py                    # HubClient — point d'entrée principal
│   ├── resources/
│   │   ├── images.py                # ImagesResource — méthodes images
│   │   ├── ai.py                    # AIResource — résumé URL, tâches
│   │   └── auth.py                  # AuthResource — gestion clients (admin)
│   ├── models.py                    # Dataclasses : HubImage, HubExif, HubOcr, HubAI
│   ├── websocket.py                 # PipelineStream — suivi temps réel
│   ├── exceptions.py                # HubError, AuthError, QuotaError, NotFoundError
│   └── utils.py                     # Retry, timeout, helpers
└── tests/
    ├── test_client.py
    ├── test_images.py
    └── test_websocket_sdk.py

Ce qui doit être créé :

  1. sdk/imago_client/exceptions.py :

    class HubError(Exception):
        """Erreur de base du SDK."""
        def __init__(self, message: str, status_code: int | None = None):
            self.status_code = status_code
            super().__init__(message)
    
    class AuthError(HubError):
        """Clé API invalide ou scope manquant (401/403)."""
    
    class QuotaError(HubError):
        """Quota dépassé (413 ou 429)."""
    
    class NotFoundError(HubError):
        """Ressource introuvable (404)."""
    
    class PipelineError(HubError):
        """Erreur lors du traitement AI d'une image."""
    
  2. sdk/imago_client/models.py — Dataclasses des réponses :

    from dataclasses import dataclass, field
    from datetime import datetime
    
    @dataclass
    class HubExif:
        camera_make: str | None = None
        camera_model: str | None = None
        taken_at: datetime | None = None
        gps_latitude: float | None = None
        gps_longitude: float | None = None
        iso: int | None = None
        aperture: str | None = None
    
    @dataclass
    class HubOcr:
        has_text: bool = False
        text: str | None = None
        language: str | None = None
        confidence: float | None = None
    
    @dataclass
    class HubAI:
        description: str | None = None
        tags: list[str] = field(default_factory=list)
        confidence: float | None = None
        model_used: str | None = None
    
    @dataclass
    class HubImage:
        id: int
        uuid: str
        original_name: str
        status: str         # pending | processing | done | error
        exif: HubExif = field(default_factory=HubExif)
        ocr: HubOcr = field(default_factory=HubOcr)
        ai: HubAI = field(default_factory=HubAI)
        width: int | None = None
        height: int | None = None
        file_size: int | None = None
        uploaded_at: datetime | None = None
    
  3. sdk/imago_client/websocket.pyPipelineStream :

    class PipelineStream:
        """Suivi temps réel du pipeline via WebSocket avec fallback polling."""
    
        async def __aiter__(self) -> AsyncIterator[PipelineEvent]:
            """Itère sur les événements du pipeline jusqu'à done/error."""
    
        async def wait_until_done(self, timeout: float = 120.0) -> HubImage:
            """Attend la fin du pipeline et retourne l'image complète."""
    
        async def _try_websocket(self) -> bool:
            """Tente la connexion WebSocket. Retourne False si indisponible."""
    
        async def _fallback_polling(self, interval: float = 2.0) -> None:
            """Polling de /status si WebSocket indisponible."""
    
  4. sdk/imago_client/resources/images.pyImagesResource :

    class ImagesResource:
        async def upload(
            self,
            file: str | Path | bytes | BinaryIO,
            *,
            filename: str | None = None,
        ) -> PipelineStream:
            """Upload une image et retourne un stream de suivi du pipeline."""
    
        async def get(self, image_id: int) -> HubImage:
            """Récupère les détails complets d'une image."""
    
        async def list(
            self,
            *,
            page: int = 1,
            page_size: int = 20,
            tag: str | None = None,
            status: str | None = None,
            search: str | None = None,
        ) -> tuple[list[HubImage], int]:  # (images, total)
            """Liste les images avec pagination et filtres."""
    
        async def delete(self, image_id: int) -> None:
            """Supprime une image."""
    
        async def get_download_url(self, image_id: int, expires_in: int = 900) -> str:
            """Retourne une URL signée pour télécharger l'image."""
    
        async def reprocess(self, image_id: int) -> None:
            """Relance le pipeline AI sur une image existante."""
    
  5. sdk/imago_client/client.pyHubClient :

    class HubClient:
        def __init__(
            self,
            api_key: str,
            base_url: str = "http://localhost:8000",
            *,
            timeout: float = 30.0,
            max_retries: int = 3,
            retry_backoff: float = 1.0,
        ):
            self.images = ImagesResource(self)
            self.ai = AIResource(self)
            self.auth = AuthResource(self)    # admin uniquement
    
        async def __aenter__(self) -> "HubClient": ...
        async def __aexit__(self, *args) -> None: ...
    
        # Usage :
        # async with HubClient(api_key="sk-...") as hub:
        #     stream = await hub.images.upload("photo.jpg")
        #     image = await stream.wait_until_done()
        #     print(image.ai.description)
    
  6. sdk/imago_client/resources/ai.pyAIResource :

    class AIResource:
        async def summarize_url(self, url: str, language: str = "français") -> dict:
            """Résumé AI d'une URL web."""
    
        async def draft_task(
            self, description: str, context: str | None = None, language: str = "français"
        ) -> dict:
            """Génère une tâche structurée depuis une description libre."""
    
  7. sdk/pyproject.toml :

    [build-system]
    requires = ["hatchling"]
    build-backend = "hatchling.build"
    
    [project]
    name = "imago-client"
    version = "1.0.0"
    description = "SDK Python officiel pour le Imago"
    readme = "README.md"
    requires-python = ">=3.12"
    dependencies = [
        "httpx>=0.27.0",
        "websockets>=13.0",
        "pydantic>=2.0",
    ]
    
    [project.optional-dependencies]
    dev = ["pytest>=8.0", "pytest-asyncio", "respx"]  # respx pour mocker httpx
    
  8. sdk/README.md — Documentation complète avec exemples :

    # imago-client
    
    ## Installation
    pip install imago-client
    
    ## Usage rapide
    ```python
    import asyncio
    from imago_client import HubClient
    
    async def main():
        async with HubClient(api_key="sk-...", base_url="https://hub.example.com") as hub:
            # Upload et attente du pipeline
            stream = await hub.images.upload("photo.jpg")
            image = await stream.wait_until_done(timeout=120)
    
            print(f"Description : {image.ai.description}")
            print(f"Tags : {', '.join(image.ai.tags)}")
            print(f"Appareil : {image.exif.camera_make} {image.exif.camera_model}")
    
            # Résumé d'URL
            result = await hub.ai.summarize_url("https://example.com/article")
            print(result["summary"])
    
  9. sdk/tests/ — Tests du SDK avec respx pour mocker httpx :

    • upload() retourne un PipelineStream valide
    • wait_until_done() complète via WebSocket
    • wait_until_done() complète via polling si WebSocket indisponible
    • HubError levée sur HTTP 4xx/5xx
    • AuthError sur 401, QuotaError sur 413/429, NotFoundError sur 404
    • Retry automatique sur erreurs 5xx (max_retries fois)
    • Context manager async with HubClient(...) as hub: ferme les connexions proprement

Livrable 3.4 — Dashboard admin (priorité FAIBLE)

Objectif : fournir une interface web légère permettant de superviser le hub sans passer par curl — visualiser les clients, leurs quotas, les métriques du pipeline et la santé des composants.

Choix technique : pas de framework frontend séparé. Le dashboard est une interface HTML/JS servie directement par FastAPI via Jinja2Templates. Simple, sans build step, déployé avec le backend.

Nouvelle dépendance :

jinja2==3.1.4

Ce qui doit être créé :

  1. app/routers/admin.py — Router admin avec pages HTML et endpoints JSON :

    Pages HTML (rendu Jinja2, scope admin requis) :

    • GET /admin/ → Dashboard overview (métriques globales, santé)
    • GET /admin/clients → Liste des clients avec quotas et consommation
    • GET /admin/clients/{id} → Détail d'un client (images, tokens AI, activité)
    • GET /admin/queue → État de la file ARQ (jobs en attente, en cours, échoués)
    • GET /admin/storage → Consommation disque par client

    Endpoints JSON (appelés par le JS du dashboard) :

    • GET /admin/api/stats → Statistiques globales (total images, tokens AI, taille stockage)
    • GET /admin/api/clients → Liste clients avec métriques temps réel
    • GET /admin/api/queue/status → Jobs ARQ en cours et en attente
    • POST /admin/api/clients/{id}/toggle → Activer/désactiver un client
    • POST /admin/api/clients/{id}/reset-quota → Réinitialiser les compteurs de quota
  2. app/templates/admin/ — Templates Jinja2 :

    base.html — Layout commun :

    • Sidebar avec navigation (Dashboard, Clients, Queue, Stockage)
    • Header avec nom du hub et version
    • Zone de contenu principale
    • Style minimal avec CSS vanilla (pas de dépendance externe — tout inline ou dans static/)

    dashboard.html — Page principale :

    • Cartes métriques : total images, clients actifs, tokens AI consommés ce mois, espace utilisé
    • Graphique simple (Chart.js CDN) : uploads par jour sur 30 jours
    • Tableau santé des composants (BDD, Redis, ARQ, Tesseract, Anthropic) avec badge coloré

    clients.html — Liste des clients :

    • Tableau : nom, plan, images, stockage utilisé/quota, tokens AI ce mois, statut actif
    • Badge coloré par plan (free = gris, standard = bleu, premium = or)
    • Bouton activer/désactiver inline (appel AJAX vers /admin/api/clients/{id}/toggle)
    • Lien vers le détail de chaque client

    client_detail.html — Détail d'un client :

    • En-tête : nom, plan, API key (masquée sk-...****), date de création
    • Jauges de quota : images (X/Y), stockage (X MB / Y MB), tokens AI (X/Y)
    • Tableau des 20 dernières images avec statut pipeline et tags AI

    queue.html — État de la file ARQ :

    • Compteurs : jobs en attente, en cours, réussis, échoués (dernières 24h)
    • Tableau des jobs actifs : image_id, client, étape en cours, durée
    • Tableau des jobs échoués récents avec message d'erreur
  3. app/static/admin/ — Fichiers statiques du dashboard :

    • style.css — styles du dashboard (palette cohérente avec le projet)
    • dashboard.js — rafraîchissement automatique des métriques toutes les 30s via fetch
  4. app/main.py — Monter le dashboard :

    from fastapi.templating import Jinja2Templates
    from fastapi.staticfiles import StaticFiles
    
    templates = Jinja2Templates(directory="app/templates")
    app.mount("/admin/static", StaticFiles(directory="app/static/admin"), name="admin_static")
    app.include_router(admin_router)
    
  5. tests/test_admin.py — Tests du dashboard :

    • GET /admin/ sans scope admin → HTTP 403
    • GET /admin/ avec scope admin → HTTP 200 avec HTML valide
    • GET /admin/api/stats → JSON avec les bons champs
    • POST /admin/api/clients/{id}/toggle → change is_active
    • Client désactivé → ses requêtes API retournent 401

Livrable 3.5 — Intégration Shaarli complète + documentation (priorité HAUTE)

Objectif : finaliser l'intégration entre Shaarli (premier client officiel) et le hub, avec une documentation technique complète qui permettrait à n'importe quel développeur d'intégrer le hub en moins d'une heure.

Ce qui doit être créé :

  1. integration/shaarli/ — Module d'intégration Shaarli :

    integration/shaarli/hub_plugin.py — Plugin Python utilisable depuis Shaarli :

    """
    Plugin d'intégration Shaarli ↔ Imago.
    Installe dans Shaarli via : pip install imago-client
    """
    import asyncio
    from pathlib import Path
    from imago_client import HubClient
    
    class ShaarliHubPlugin:
        """
        Enrichit les bookmarks Shaarli avec des images analysées par le hub.
        Usage :
            plugin = ShaarliHubPlugin(api_key="sk-...", hub_url="http://hub:8000")
            # Sur ajout d'un bookmark avec image :
            result = asyncio.run(plugin.process_bookmark_image("photo.jpg", bookmark_id=42))
        """
    
        def __init__(self, api_key: str, hub_url: str):
            self.hub_url = hub_url
            self.api_key = api_key
    
        async def process_bookmark_image(
            self, image_path: str | Path, bookmark_id: int
        ) -> dict:
            """Upload une image associée à un bookmark et attend l'analyse AI."""
            async with HubClient(self.api_key, self.hub_url) as hub:
                stream = await hub.images.upload(image_path)
                image = await stream.wait_until_done(timeout=120)
                return {
                    "bookmark_id": bookmark_id,
                    "image_id": image.id,
                    "description": image.ai.description,
                    "tags": image.ai.tags,
                    "ocr_text": image.ocr.text,
                }
    
        async def enrich_bookmark_url(self, url: str) -> dict:
            """Génère un résumé AI d'un lien web pour enrichir un bookmark."""
            async with HubClient(self.api_key, self.hub_url) as hub:
                return await hub.ai.summarize_url(url)
    

    integration/shaarli/config_example.env :

    # Configuration du plugin Imago
    HUB_URL=http://imago:8000
    HUB_API_KEY=sk-votre-cle-api-ici
    HUB_TIMEOUT=120
    

    integration/shaarli/example_usage.py — Exemples commentés de tous les cas d'usage :

    • Upload d'image depuis un bookmark
    • Résumé d'URL pour un nouveau lien
    • Recherche d'images par tag
    • Gestion des erreurs (quota dépassé, timeout, réseau)
  2. docs/ — Documentation technique complète :

    docs/getting-started.md — Guide de démarrage en 5 minutes :

    # Démarrage rapide
    
    ## 1. Lancer le hub
    git clone ...
    cp .env.example .env
    # Éditer .env : ANTHROPIC_API_KEY=sk-ant-...
    docker-compose up -d
    # Hub disponible sur http://localhost:8000
    
    ## 2. Créer votre premier client
    curl -X POST http://localhost:8000/api/v1/auth/clients \
      -H "Authorization: Bearer $ADMIN_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"name": "Mon App", "plan": "standard", "scopes": ["images:read", "images:write", "ai:use"]}'
    # → { "api_key": "sk-VOTRE-CLE-ICI" }   ← la clé n'est affichée qu'une seule fois
    
    ## 3. Uploader votre première image
    curl -X POST http://localhost:8000/api/v1/images/upload \
      -H "Authorization: Bearer sk-VOTRE-CLE-ICI" \
      -F "file=@photo.jpg"
    # → { "id": 1, "status": "pending", ... }
    
    ## 4. Suivre le pipeline en temps réel (WebSocket)
    # Voir docs/websocket.md
    
    ## 5. Récupérer les résultats
    curl http://localhost:8000/api/v1/images/1 \
      -H "Authorization: Bearer sk-VOTRE-CLE-ICI"
    # → { "ai": { "description": "...", "tags": [...] }, "exif": {...}, "ocr": {...} }
    

    docs/api-reference.md — Référence complète de tous les endpoints :

    • Chaque endpoint documenté avec : méthode, URL, authentification requise, scopes, paramètres, exemple de réponse JSON
    • Codes d'erreur possibles avec leur signification

    docs/websocket.md — Guide WebSocket :

    • Connexion, authentification via query param
    • Format des événements (avec exemples JSON)
    • Gestion de la reconnexion et du buffer 60s
    • Exemples en Python, JavaScript et curl (via websocat)

    docs/sdk.md — Guide du SDK Python :

    • Installation
    • Tous les cas d'usage avec exemples complets
    • Gestion des erreurs (chaque exception du SDK documentée)
    • Configuration avancée (timeout, retry, proxy)

    docs/deployment.md — Guide de déploiement en production :

    • Docker Compose (backend + worker + Redis + MinIO)
    • Variables d'environnement obligatoires et optionnelles
    • Passage de SQLite à PostgreSQL
    • Passage de LocalStorage à S3/MinIO/R2
    • Configuration Nginx en reverse proxy
    • Recommandations de sécurité (HTTPS, headers, firewall)

    docs/shaarli-integration.md — Guide d'intégration Shaarli :

    • Installation du plugin
    • Configuration
    • Tous les cas d'usage (image bookmark, résumé URL, recherche par tag)
    • Dépannage des problèmes courants
  3. tests/test_shaarli_integration.py — Tests d'intégration end-to-end Shaarli :

    • ShaarliHubPlugin.process_bookmark_image() → image uploadée + pipeline terminé
    • ShaarliHubPlugin.enrich_bookmark_url() → résumé retourné
    • Gestion d'une QuotaError (quota dépassé)
    • Gestion d'un timeout (pipeline trop long)
    • Plugin fonctionne sans event loop existant (appel depuis code synchrone Shaarli)
  4. CHANGELOG.md — Historique des versions :

    # Changelog
    
    ## [1.0.0] — 2026-02-24
    
    ### Phase 3 — Expérience développeur
    - WebSockets : suivi temps réel du pipeline avec buffer de reconnexion
    - API versioning : tous les endpoints sous /api/v1/ avec header X-API-Version
    - SDK Python officiel : imago-client 1.0.0 sur PyPI
    - Dashboard admin : interface web de supervision des clients et quotas
    - Intégration Shaarli : plugin + documentation complète
    
    ## [0.2.0] — Phase 2 — Robustesse et scalabilité
    ...
    
    ## [0.1.0] — Phase 1 — Fondations sécurité
    ...
    
  5. README.md (racine) — Réécrire pour être le point d'entrée de toute la documentation :

    • Badge CI (passing), badge version, badge Python
    • Une phrase de description claire
    • Liens rapides vers : getting-started, api-reference, sdk, déploiement
    • Section "Architecture" avec le schéma texte du système
    • Section "Clients supportés" : Shaarli (officiel) + instructions pour créer son propre client

<execution_rules>

Règles d'exécution

Prérequis avant de commencer

  1. Vérifier que les Phases 1 et 2 sont bien en place : make ci doit passer sans erreur
  2. Redis doit être accessible en local pour les tests WebSocket
  3. Lire les fichiers existants avant de les modifier — notamment pipeline.py (qui publie déjà sur Redis) et main.py (pour comprendre la structure de montage des routers)

Ordre de réalisation

Implémenter dans l'ordre strict : 3.1 → 3.2 → 3.3 → 3.4 → 3.5. Valider chaque livrable avant de continuer.

À chaque livrable

  1. Lire les fichiers concernés avec l'outil Read ou Glob
  2. Implémenter le code complet (zéro # TODO, zéro ... comme corps de fonction)
  3. Mettre à jour requirements.txt si nécessaire
  4. Lancer pytest tests/ -v après chaque livrable — corriger avant de passer au suivant
  5. Pour le livrable 3.5 (docs), valider que tous les exemples de code dans la doc fonctionnent réellement

Qualité du code

  • Typage complet : toutes les fonctions annotées, compatible mypy --strict
  • Docstrings sur toutes les classes et méthodes publiques des modules créés
  • Zéro secret : aucune clé, URL ou credential hardcodé
  • Gestion propre des connexions WebSocket : toujours se désabonner de Redis Pub/Sub et fermer proprement même en cas d'exception
  • SDK : doit fonctionner de manière totalement indépendante du backend — aucun import depuis app/

Spécificités WebSocket (livrable 3.1)

  • Tester la déconnexion client pendant le pipeline : vérifier qu'aucune tâche orpheline ne reste dans l'event loop
  • Le buffer Redis (pipeline:buffer:{image_id}) doit avoir un TTL de 60 secondes — ne pas oublier de le configurer dans Redis
  • Si le pipeline est déjà terminé quand le client se connecte, retourner immédiatement le résultat final depuis la BDD (pas depuis Redis)

Spécificités SDK (livrable 3.3)

  • Le SDK doit gérer le retry automatique sur les erreurs 5xx (backoff exponentiel)
  • wait_until_done() doit tenter le WebSocket d'abord (timeout 5s pour la connexion), puis basculer automatiquement sur le polling si le WebSocket échoue ou est indisponible
  • Les exceptions du SDK doivent contenir le status_code HTTP pour faciliter le débogage
  • Le SDK ne doit pas avoir de dépendance vers fastapi, sqlalchemy ou tout autre élément du backend

Critère de succès final

Les commandes suivantes doivent toutes réussir :

# Tests backend complets
pytest tests/ -v --cov=app --cov-report=term-missing

# Tests SDK
cd sdk && pytest tests/ -v

# Qualité globale
make ci

# Vérification manuelle du dashboard
python run.py &
python worker.py &
curl -s http://localhost:8000/admin/api/stats | python -m json.tool

# Test WebSocket rapide (nécessite websocat)
websocat "ws://localhost:8000/ws/pipeline/1?token=sk-test-key"

</execution_rules>

<deliverable_summary>

Résumé des livrables attendus

# Fichiers créés ou modifiés Validation
3.1 app/routers/websocket.py, app/metrics.py, app/main.py, tests/test_websocket.py pytest tests/test_websocket.py
3.2 app/main.py, app/middleware/versioning.py, app/config.py, tests/test_versioning.py pytest tests/test_versioning.py
3.3 sdk/ (package complet : client, resources, models, exceptions, websocket), sdk/README.md, sdk/tests/ cd sdk && pytest tests/ -v
3.4 app/routers/admin.py, app/templates/admin/ (4 templates), app/static/admin/, app/main.py, tests/test_admin.py pytest tests/test_admin.py
3.5 integration/shaarli/, docs/ (5 guides), CHANGELOG.md, README.md, tests/test_shaarli_integration.py pytest tests/test_shaarli_integration.py

Résultat final de la Phase 3 et du projet complet :

  • Hub multi-clients sécurisé, robuste, observable et entièrement documenté
  • Pipeline AI avec suivi temps réel via WebSocket
  • API versionnée stable avec contrat de dépréciation
  • SDK Python officiel installable via pip
  • Dashboard admin pour superviser clients et quotas
  • Intégration Shaarli officielle avec documentation complète
  • make ci vert, coverage > 80%, zéro warning mypy </deliverable_summary>