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

856 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
---
<role>
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.
</role>
<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>
## 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`) :
```json
{ "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 :
```python
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) :
```python
app.include_router(images_router)
app.include_router(ai_router)
app.include_router(auth_router)
```
**Après** (versioning propre) :
```python
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 :
```python
API_V1_SUNSET_DATE: str = "" # vide = pas de sunset, sinon "2027-06-01"
```
4. `app/main.py` — Mettre à jour la documentation OpenAPI :
```python
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` :
```python
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 :
```python
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.py` — `PipelineStream` :
```python
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.py` — `ImagesResource` :
```python
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.py` — `HubClient` :
```python
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.py` — `AIResource` :
```python
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` :
```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 :
```markdown
# 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 :
```python
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 :
```python
"""
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`** :
```bash
# 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 :
```markdown
# 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 :
```markdown
# 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
</mission>
<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 :
```bash
# 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>