Historique — pas une source de vérité actuelle. Audit daté du 2026-07-24, préservé pour la traçabilité. Toute décision doit être réévaluée contre le dépôt, les tests/CI, les ADR et les SPEC acceptées.
Date : 2026-07-24 Profil : Monorepo GW2 WvW Combat Analytics (Python 3.12 + FastAPI + Next.js + PostgreSQL)
gw2analytics-root/ ← Monorepo virtuel (uv workspace)
├── apps/api/ ← Backend FastAPI
│ ├── src/gw2analytics_api/
│ │ ├── main.py ← App entrypoint + wiring
│ │ ├── config.py ← Settings pydantic-settings
│ │ ├── database.py ← SQLAlchemy engine/session
│ │ ├── storage.py ← MinIO S3 wrapper
│ │ ├── crypto.py ← Fernet webhook encryption
│ │ ├── route_helpers.py ← Shared route formatters
│ │ ├── schema_guard.py ← Alembic drift checker
│ │ ├── limiter.py ← Rate limiting (slowapi)
│ │ ├── metrics.py ← Prometheus metrics
│ │ ├── health.py ← Health check logic
│ │ ├── _event_dispatch.py ← Event dispatch helper
│ │ ├── models/ ← SQLAlchemy ORM models
│ │ │ ├── fight.py
│ │ │ ├── upload.py
│ │ │ ├── webhook.py
│ │ │ └── guild.py
│ │ ├── schemas/ ← Pydantic response schemas
│ │ │ ├── fight.py
│ │ │ ├── upload.py
│ │ │ ├── webhook.py
│ │ │ ├── player.py
│ │ │ └── account.py
│ │ ├── routes/ ← FastAPI route handlers
│ │ │ ├── uploads.py
│ │ │ ├── webhooks.py
│ │ │ ├── players.py
│ │ │ ├── guilds.py
│ │ │ ├── account.py
│ │ │ ├── skills.py
│ │ │ ├── health.py
│ │ │ ├── player_compare.py
│ │ │ └── fights/ ← Nested fight sub-routes
│ │ ├── services/ ← Business logic
│ │ │ ├── parse.py
│ │ │ ├── fight_persistence.py
│ │ │ ├── player_summaries.py
│ │ │ ├── player_profiles.py
│ │ │ ├── event_blob.py
│ │ │ └── guild_service.py
│ │ └── workers/ ← Arq background workers
│ │ ├── parser_settings.py
│ │ ├── parser_worker.py
│ │ ├── webhook_dispatch.py
│ │ ├── webhook_scheduler.py
│ │ └── stuck_upload_sweeper.py
│ ├── alembic/ ← 26+ migrations
│ └── tests/ ← 60+ test files
├── web/ ← Frontend Next.js
├── libs/
│ ├── gw2_core/ ← Modèles de domaine partagés
│ ├── gw2_evtc_parser/ ← Parser EVTC binaire
│ ├── gw2_analytics/ ← Agrégations analytics
│ └── gw2_api_client/ ← Client API GW2 officielle
├── docker-compose.yml
├── pyproject.toml ← Root workspace
└── tests/ ← Load tests (k6, locust)
| # | Problème | Localisation | Explication | Impact | Gravité | Solution |
|---|---|---|---|---|---|---|
| 1 | Absence de Repository Pattern | services/*.py, routes/*.py |
Les routes accèdent directement à SQLAlchemy via Depends(get_session) et exécutent des requêtes brutes. Aucune couche d'abstraction entre la logique métier et le stockage. |
Couplage fort API/DB, impossible de tester la logique métier sans DB, violation du Dependency Inversion Principle | CRITICAL | Introduire un Repository layer entre routes et ORM |
| 2 | Service Layer anémique | services/parse.py, services/fight_persistence.py |
Les "services" sont en réalité des fonctions procédurales qui manipulent directement l'ORM. Aucune vraie modélisation du domaine métier. | Code procédural déguisé en services, DDD viole | HIGH | Refactorer en vrais services avec modèles de domaine |
| 3 | Routes trop épaisses | routes/players.py (882 lignes), routes/webhooks.py (490 lignes) |
Les routes mêlent validation, logique métier, accès DB et mise en forme de réponse. Violation SRP. | Fichiers monolithiques difficiles à tester et maintenir | HIGH | Extraire la logique métier vers les services |
| 4 | Dépendances circulaires potentielles | services/parse.py importe services/event_blob |
Les services s'importent entre eux dans le même package. Une future réorganisation pourrait créer des cycles. | Risque de refactoring | MEDIUM | Dépendre d'interfaces, pas d'implémentations |
| 5 | Pas de couche Use Case | Aucun fichier usecases/ |
Il n'y a pas de séparation entre les cas d'utilisation (orchestration) et les opérations techniques (DB, storage). | Les cas d'utilisation sont dissous dans les routes et services | MEDIUM | Créer un package usecases/ |
| 6 | Pas d'Event-Driven Architecture | _event_dispatch.py est un helper limité |
Les webhooks utilisent un polling scheduler au lieu d'un bus d'événements. Pas de pattern Event Sourcing. | Couplage temporel fort, pas de traçabilité des événements | MEDIUM | Considérer un event bus (RabbitMQ, Kafka léger) |
| 7 | Frontend et Backend sans contrat formel partagé | web/ vs apps/api/ |
Les types TypeScript du frontend sont probablement dupliqués manuellement depuis les schémas Pydantic. Pas de génération automatique. | Duplication, dé-synchronisation possible | MEDIUM | Ajouter openapi-generator ou un client TS natif |
| 8 | Pas de module de cache centralisé | Cache dispersé (@lru_cache, blob_cache.py) |
Le caching est ad-hoc : LRU pour les settings, cache de blobs pour les événements. Pas de stratégie cohérente. | Incohérence, pas d'invalidation standardisée | LOW | Créer un cache service unifié |
| 9 | Monolithe sans frontières claires | Tout le backend dans gw2analytics_api |
Même package pour routes, services, workers, modèles. Pas de bounded contexts. | À mesure que le projet grandit, le couplage augmente | MEDIUM | Découper en modules fonctionnels (uploads, fights, webhooks, analytics) |
| 10 | Ordre d'include des routes fragile | main.py:101-108 |
L'ordre d'include des routes est critique (player_compare AVANT players). Documenté mais cassable. | Dépend de l'ordre d'import, pas de mécanisme de protection | MEDIUM | Restructurer les chemins pour éviter les conflits de route |
- Pas de Repository Pattern → violation DIP
- Service Layer procédural, pas orienté domaine
- Routes monolithiques (882 lignes pour players)
- Pas de couche Use Case
- Pas d'event bus pour les webhooks
- Pas de contrat formel frontend/backend
- Cache ad-hoc sans stratégie
- Dépendances circulaires entre services
- Pas de bounded contexts
- Ordre d'include des routes fragile
| # | Fichier | Fonction | Problème | Impact | Correction |
|---|---|---|---|---|---|
| 1 | routes/uploads.py:157 |
create_upload |
file.file.read() lit tout le fichier en mémoire sans streaming. Pour un fichier de 100MB, c'est 100MB en RAM. |
OOM possible, pas de streaming | Utiliser un lecteur chunké ou tempfile |
| 2 | routes/uploads.py:227 |
create_upload |
db.rollback() après put_zevtc déjà réussi. Le blob est uploadé mais la transaction rollback. Orphelin S3. |
Données orphelines dans MinIO | Supprimer le blob S3 après rollback |
| 3 | routes/players.py |
Multiple fonctions | Les routes players contiennent ~882 lignes avec logique métier, SQL et formatage mélangés. | SRP violé, testabilité réduite | Extraire dans services/ |
| 4 | services/parse.py:65 |
process_parse |
list(_parser.parse(evtc_bytes)) charge TOUS les fights en mémoire. Pour un long WvW log, plusieurs fights. |
Mémoire excessive | Streamer les fights un par un |
| 5 | services/player_summaries.py:144 |
_persist_player_summaries |
Supprime puis réinsère (db.execute(delete(...)) + db.add(...) en boucle). Transaction longue. |
Verrous, contention | Utiliser INSERT ON CONFLICT ou batch upsert |
| 6 | services/player_summaries.py:144 |
_persist_player_summaries |
Fonction de 200+ lignes avec noqa: PLR0912,PLR0915 (trop complexe). |
Mauvaise maintenabilité | Refactorer en sous-fonctions |
| 7 | schema_guard.py:239 |
check_schema_drift |
Raise RuntimeError si migration manquante. L'API refuse de boot. |
Bloque le démarrage pour un problème récupérable | Logger + mode degraded au lieu de crash |
| 8 | routes/uploads.py:97-115 |
_enqueue_parse |
Arq pool inexistant → check allow_inrequest_parse_fallback. Si False → 503. Double responsabilité. |
Logique de routage mélangée avec orchestration | Séparer enqueue et fallback |
| 9 | routes/uploads.py:116 |
_enqueue_parse |
asyncio.to_thread(process_parse, ...) sans timeout. Si le parse bloque, la requête reste pendante. |
Requête suspendue indéfiniment | Ajouter asyncio.wait_for |
| 10 | workers/webhook_dispatch.py |
dispatch_for_upload |
Fonction synchrone (httpx.Client) dans une thread pool. Pas de timeout configurable. |
Blocage possible sur DNS lent | Timeout + retry configurable |
| 11 | services/event_blob.py:87 |
_persist_event_blob |
"\n".join([event.model_dump_json() for event in events]) construit tout le JSONL en mémoire. |
Double mémoire (events + jsonl) | Streamer ligne par ligne |
| 12 | crypto.py |
_get_fernet |
@lru_cache(maxsize=8) sur une fonction qui prend une clé. Si rotation de KEK, l'ancien cache reste. |
Stale cache après rotation KEK | Invalider le cache ou utiliser TTLCache |
| 13 | config.py:75 |
_split_cors_origins |
Validation mode="before" sur le champ CORS. Logique de validation dans le modèle de config. |
Mélange config/validation | Déplacer les validateurs dans des fonctions séparées |
| 14 | routes/uploads.py |
_enqueue_parse |
Chaine parse → dispatch en séquence. Si parse OK mais dispatch échoue, le parse est perdu. | Webhook jamais délivré | Persister le dispatch failed + retry |
| 15 | workers/stuck_upload_sweeper.py |
lifespan_stuck_upload_sweeper |
Sweeper tourne toutes les 300s. Pas de mécanisme pour arrêter proprement. | Fuite de thread | Cancellation token propre |
| 16 | services/fight_persistence.py:79 |
_save_fight |
db.flush() partout (lignes 79, 87, 100). Flush prématuré = round-trips DB inutiles. |
Performance dégradée | Un seul commit final |
| 17 | Pas de retry sur S3 | storage.py |
Les appels MinIO n'ont pas de retry. Un S3Error = 500 direct. | Fragile aux pannes réseau | Ajouter tenacity/retry |
| 18 | Pas de healthcheck S3 dans /healthz |
main.py:120 |
/healthz ne vérifie que l'état du catalogue skills, pas S3 ni DB. |
Faux positifs healthcheck | Ajouter vérifications S3 + DB |
| 19 | __init__.py des routes exporte tout |
routes/__init__.py |
Import massif de tous les routers. Aucun lazy loading. | Temps d'import augmenté | Imports paresseux |
| 20 | Pas de tracing distribué | Partout | Aucun OpenTelemetry ou middleware de tracing. Impossible de tracer une requête complète. | Debugging difficile | Ajouter OpenTelemetry |
- Ajouter timeout à
asyncio.to_threaddans uploads - Supprimer les orphelins S3 après rollback
- Utiliser
INSERT ON CONFLICTau lieu de delete+insert - Ajouter retry sur les appels MinIO
- Streamer le JSONL au lieu de tout charger en mémoire
- Ajouter healthcheck S3 + DB
- Configurer httpx timeout pour webhook dispatch
- Supprimer les flush() prématurés
- Ajouter cache busting pour Fernet KEK rotation
- Logger le drift au lieu de crash
Analyse limitée car le frontend Next.js est volumineux et les fichiers complets n'ont pas été inspectés en détail.
| # | Problème | Localisation | Explication | Impact |
|---|---|---|---|---|
| 1 | Absence de contrat TypeScript/Api généré | Général | Les types API sont dupliqués manuellement depuis les schémas Pydantic. Pas de openapi-typescript ou orval. |
Duplication, erreurs silencieuses |
| 2 | Pas de module API client dédié | Général | Les appels fetch sont probablement éparpillés dans les composants. Pas de client centralisé. | Duplication, pas d'interceptors |
| 3 | State management inconnu | Inconnu | Probablement React state local ou context. Pas de mention de TanStack Query, Zustand, ou Redux. | Re-rendus excessifs possibles |
| 4 | Pas de tests E2E | Aucun dossier cypress ou playwright visible |
Les tests semblent limités au backend. | Risque de régression UI |
| 5 | Pas de Storybook | Général | Pas de catalogue de composants. | Design system invisible |
| # | Table | Colonne | Problème | Impact | Correction |
|---|---|---|---|---|---|
| 1 | fights |
id VARCHAR(64) |
Clé primaire = SHA-256 hex string. 64 chars = 256 bits encodé en hex. Moins performant qu'un BIGINT ou UUID binaire. | Index plus gros, jointures plus lentes | Utiliser UUID binaire ou hash bytes |
| 2 | fights |
events_blob_uri |
NULL possible, VARCHAR(255). Stocke l'URI S3. 255 chars peut être limite pour certaines URLs signées. | Troncature possible | Augmenter à 512 ou TEXT |
| 3 | fight_agents |
agent_id NUMERIC(20,0) |
agent_id est un uint64 arcdps stocké en NUMERIC. Plus lent que BIGINT pour les index et jointures. | Performance des index | Utiliser BIGINT (suffit pour uint64 < 2^63) |
| 4 | fight_player_summaries |
Colonnes boon (×14 uptime + ×14 outgoing) | 28 colonnes pour les boons. 30+ colonnes NULLables. Normalisation faible. | Schéma large, NULLs nombreux | Normaliser en table player_boons séparée |
| 5 | uploads |
sha256 VARCHAR(64) |
Unique index sur SHA-256 hex. Idem problème #1. Stockage inefficace. | Stockage + index plus grands | Utiliser BYTEA(32) avec hash index |
| 6 | uploads |
size_bytes INTEGER |
Limité à 2.1 milliards. Fichiers > 2GB impossibles (même si cap à 100MB pour l'instant). | Overflow futur | BIGINT |
| 7 | fight_player_summaries |
power_damage, condi_damage |
INTEGER NULLable. Les sommes peuvent dépasser 2.1B pour les gros combats. | Overflow | BIGINT |
| 8 | fight_player_summaries |
total_damage INTEGER |
Même problème que #6. | Overflow | BIGINT |
| 9 | fight_agents |
Pas d'index composite | Pas d'index sur (account_name, fight_id) pour la recherche des agents d'un compte dans un fight. |
Scan séquentiel | Ajouter index composite |
| 10 | fights |
upload_id UUID |
FK vers uploads mais UUID stocké en texte. | Inefficace | Utiliser le même type binaire |
| 11 | webhook_deliveries |
next_attempt_at INDEX |
Index seul. Pas d'index composite avec subscription_id. |
Filtre sous-optimal | Index composite (subscription_id, next_attempt_at) |
| 12 | Pas de contrainte FK | webhook_dlq.subscription_id |
Pas de FK vers webhook_subscriptions. Intégrité référentielle non garantie. | Orphelins DLQ possibles | Ajouter FK |
| 13 | Pas de contrainte FK | webhook_dlq.upload_id |
Pas de FK vers uploads. | Orphelins DLQ | Ajouter FK |
| 14 | guild_members |
account_name |
Pas d'index sur account_name pour recherche inverse (quels membres d'un compte ?). |
Scan table | Ajouter index |
| 15 | fight_player_summaries |
detected_tags JSON |
Tags stockés en JSON. Pas de GIN index pour recherche. | Recherche lente | Ajouter GIN index si recherché |
| 16 | Pas de compression | Toutes les tables | Pas de TOAST tuning ni compression pour les colonnes TEXT/JSON. |
Stockage gonflé | Activer pglz compression |
| 17 | Pas de partitionnement | fights |
Table de fights peut atteindre des millions de lignes. Pas de partitionnement temporel. | Performance dégradée sur les grosses données | Partionner par mois |
| 18 | alembic_version lock |
schema_guard.py |
La guard lit alembic_version à chaque startup. Cette table est verrouillée pendant les migrations. |
Contention au démarrage | Cache du numéro de version |
| 19 | Pas de created_at/updated_at |
fights, fight_agents, fight_skills |
Aucun timestamp de création/mise à jour sur les tables principales. | Impossible de tracer quand un fight a été parsé | Ajouter created_at |
| 20 | OrmFightPlayerSummary pas d'index unique |
- | Pas d'index unique sur (fight_id, account_name) au niveau DB (la PK composite fait déjà office, mais pas d'index séparé). |
OK mais vérifier | Déjà couvert par PK composite |
| # | Requête | Problème | Correction |
|---|---|---|---|
| 1 | find_fights_without_summary NOT EXISTS |
Anti-join sur toute la table. Pour DB avec >50% de fights matérialisés, efficace. Pour DB vierge, scan complet. | Ajouter index partiel |
| 2 | aggregate_player_profiles_from_sql CTE + GROUP BY + JOIN + WINDOW |
4 opérations lourdes sur la table de résumés. Pour 100k lignes, OK. Pour 1M+, ralenti. | Vues matérialisées + refresh |
| 3 | list_players sans filtre |
SELECT ... LIMIT 500 OFFSET 0 sur toute la table. Le OFFSET devient coûteux pour les pages profondes. |
Pagination par curseur (keyset) |
-- Index composite pour recherche d'agents par compte
CREATE INDEX ix_fight_agents_account_fight ON fight_agents(account_name, fight_id);
-- Index composite pour webhook scheduler
CREATE INDEX ix_webhook_deliveries_sub_next ON webhook_deliveries(subscription_id, next_attempt_at);
-- Index pour recherche guild par membre
CREATE INDEX ix_guild_members_account ON guild_members(account_name);
-- Index partiel pour fights sans résumé
CREATE INDEX ix_fights_no_summary ON fights(id) WHERE id NOT IN (SELECT fight_id FROM fight_player_summaries);| ROI | Problème | Coût Actuel | Coût Estimé Après | Difficulté | Gain |
|---|---|---|---|---|---|
| ★★★★★ | file.file.read() charge tout en mémoire |
100MB RAM/upload | Streaming chunké | Faible | Économie RAM massive |
| ★★★★★ | list(_parser.parse(...)) charge tous les fights |
2× RAM du fichier | Iterator stream | Faible | 50% RAM parse |
| ★★★★★ | N+1 sur agents/skills dans _save_fight |
N+1 round-trips DB | Bulk insert | Faible | ÷10 temps DB |
| ★★★★★ | "\n".join([e.model_dump_json() for e in events]) |
Tous les events en mémoire | Stream JSONL | Faible | ÷2 RAM |
| ★★★★★ | Pas d'index composite (account, fight) sur agents | Scan table | Index | Faible | ×100 requêtes players |
| ★★★★ | OFFSET pagination sur players | O(n) scan | Keyset pagination | Moyen | O(log n) |
| ★★★★ | Delete + insert dans player_summaries | 2 opérations/ligne | UPSERT batch | Moyen | ÷10 temps écriture |
| ★★★★ | Pas d'index composite sur webhook_deliveries | Scan pour scheduler | Index | Faible | ×100 scheduler |
| ★★★★ | model_dump_json() events en boucle |
Sérialisation Pydantic coûteuse | model_dump(mode="json") + json.dumps manuel |
Faible | ÷2 CPU sérialisation |
| ★★★★ | db.flush() prématuré dans _save_fight |
Round-trips DB inutiles | Un seul commit | Faible | ÷3 temps DB |
| ★★★ | Blob cache pas de TTL | Cache jamais invalidé | TTL + invalidation | Moyen | Cache plus frais |
| ★★★ | Skills catalog chargé en mémoire au startup | 100% mémoire au boot | Lazy load + stream | Moyen | ÷10 RAM startup |
| ★★★ | Pas de Redis cache pour les requêtes fréquentes | DB hit à chaque requête | Cache Redis | Moyen | ÷100 latency |
| ★★★ | Pas de compression pg | Stockage DB gonflé | pglz/TOAST | Faible | ÷2 stockage |
| ★★★ | Prometheus generate_latest() à chaque requête /metrics | Sérialisation complète | Cache counter | Faible | ÷5 CPU metrics |
| ★★ | SQLAlchemy selectinload dans event_blob |
N+1 sur agents | joinedload si nécessaire |
Moyen | ÷2 temps requête |
| ★★ | Arq max_jobs=2 sur 4 cœurs |
Sous-utilisation CPU | max(os.cpu_count(), 2) |
Faible | ×2 throughput parse |
| ★★ | Pas de connection pool tuning | Connexions DB créées/détruites | Pool sizing | Faible | ÷2 overhead connexion |
| ★★ | os.environ.get dans _init_arq_pool |
Accès lent | Settings field | Faible | Négligeable |
| ★★ | Gzip compress/décompress synchrone | Bloque event loop | asyncio.to_thread |
Moyen | Event loop libre |
| ★ | content-length parsing sans cache |
Re-parsed à chaque requête | Cache header | Faible | Négligeable |
| ★ | hashlib.sha256(raw).hexdigest() |
Calcul hash complet | Streaming hash si fichier chunké | Moyen | RAM + I/O |
| # | Problème | Localisation | Sévérité | Explication |
|---|---|---|---|---|
| 1 | Pas de rate limiting sur les endpoints de listing | routes/players.py, routes/webhooks.py |
HIGH | Seul /uploads a @limiter.limit("5/minute"). Les autres endpoints sont ouverts. Attaque par scraping possible. |
| 2 | Aucune authentification sur la majorité des endpoints | Tous les routes sauf /account |
HIGH | N'importe qui peut uploader, lister les players, lister les webhooks. Pas d'authentification requise. |
| 3 | Absence de CSRF | web/ |
MEDIUM | Si le frontend a des mutations, pas de CSRF token. |
| 4 | SECRETS_KEK dans .env en clair | .env, .env.example |
MEDIUM | La KEK est stockée en clair dans le fichier .env. En prod, doit être dans un secret manager. |
| 5 | CORS wide-open en dev | main.py:84 |
MEDIUM | allow_origins=["*"] si CORS_ALLOWED_ORIGINS=*. Correct en dev, dangereux si oubli en prod. |
| 6 | Pas de validation de type MIME sur upload | routes/uploads.py |
MEDIUM | L'upload accepte n'importe quel fichier. Le Content-Type n'est pas vérifié. |
| 7 | Pas de Content Security Policy | web/ |
MEDIUM | Aucun header CSP configuré. |
| 8 | X-Forwarded-For trust | limiter.py |
MEDIUM | Le rate limiting utilise X-Forwarded-For. Si le proxy n'est pas configuré pour le nettoyer, spoofing possible. |
| 9 | SSRF protection manuelle | routes/webhooks.py:102-130 |
MEDIUM | La protection SSRF est personnalisée avec socket.getaddrinfo. Bien faite mais complexe. Risque d'erreur. |
| 10 | Pas de validation de taille d'upload au niveau du proxy | Caddyfile |
LOW | La limite de taille est checkée dans le code Python, pas dans Caddy. Le corps arrive quand même en mémoire. |
| 11 | Pas de secrets scanning | CI/CD | LOW | Pas de détection de secrets commités dans le pipeline. |
| 12 | Docker images non scannées | docker-compose.yml |
LOW | Les images (postgres:16-alpine, minio, redis) ne sont pas scannées pour les vulnérabilités. |
| 13 | Pas de signature HMAC sur les webhooks au-delà du secret | workers/webhook_dispatch.py |
LOW | Le secret est utilisé pour l'authentification, mais pas de signature HMAC standardisée. |
Critical (0) : Aucun problème critique immédiat identifié (les uploads ont une validation de taille, les webhooks ont une protection SSRF).
High (2) :
- Ajouter rate limiting sur tous les endpoints (slowapi)
- Ajouter une couche d'authentification (même basique) pour les endpoints non-publics
Medium (6) :
- Ajouter validation MIME type sur upload
- Ajouter CSP header
- Utiliser un secret manager pour KEK en prod
- Configurer CORS strict en prod via CI/CD
- Configurer X-Forwarded-For trust proprement
- Ajouter validation de taille dans Caddy
| # | Fichier | Problème | Exemple | Correction |
|---|---|---|---|---|
| 1 | models/fight.py |
ORM anémique | 40+ colonnes sans relations complexes, juste des mapped_column. | Ajouter helper methods |
| 2 | routes/players.py |
882 lignes | Fonctions mélangeant SQL, logique métier et formatage | Extraire dans services/ |
| 3 | services/player_summaries.py |
Fonction trop longue | _persist_player_summaries 200+ lignes, noqa: PLR0912,PLR0915 |
Refactorer |
| 4 | services/parse.py |
Fonction trop longue | process_parse 90+ lignes avec try/except complexes |
Extraire sous-fonctions |
| 5 | routes/uploads.py |
Duplication validation taille | 3 vérifications de taille (Content-Length, file.size, len(raw)) | Factoriser |
| 6 | routes/webhooks.py |
Duplication validation URL | Validation SSRF longue dans le route | Extraire dans service |
| 7 | services/player_summaries.py |
Duplication boon fields | 14 champs uptime + 14 outgoing écrits manuellement | Générer depuis les noms de boons |
| 8 | workers/parser_settings.py |
Code mort | if _REDIS_PORT == 1: raise RuntimeError — port 1 est un cas impossible |
Supprimer |
| 9 | schema_guard.py |
Documentation excessive | Docstring de 100+ lignes pour une fonction de 40 lignes | Documenter l'essentiel |
| 10 | crypto.py |
Threat model en commentaire | Document de conception dans le code source | Déplacer dans /docs |
| 11 | config.py |
Champs dupliqués | minio_endpoint, minio_access_key, etc. avec alias S3_* |
Grouper dans un bloc |
| 12 | database.py |
Fonctions redondantes | get_engine + get_sessionmaker + get_session pour 3 lignes |
Simplifier |
| 13 | storage.py |
Docstring surdimensionnée | _parse_minio_endpoint a 15 lignes de docstring pour 10 lignes de code |
Simplifier |
| 14 | models/fight.py |
Type incohérent | agent_id = Numeric(20,0) pour uint64 mais BIGINT suffit |
BIGINT |
| 15 | models/upload.py |
Type incohérent | size_bytes INTEGER au lieu de BIGINT |
BIGINT |
| 16 | routes/player_compare.py |
Duplication logique | _load_merged_contributions dupliqué du module players |
Extraire en shared |
| 17 | Tous les services | Pas de typing strict | Beaucoup de Any, object au lieu de types concrets |
Ajouter types |
| 18 | services/guild_service.py |
Stub non fonctionnel | sync_guilds retourne toujours [] avec un warning |
Implémenter ou supprimer |
| 19 | main.py |
Import massif | Importe tous les routers explicitement. Ajouter une route = modifier main.py | Auto-discovery |
| 20 | workers/webhook_dispatch.py |
Pas de tests unitaires | Fonction complexe sans tests dédiés | Ajouter tests |
| 21 | services/event_blob.py |
Double write pattern | Écrit le blob S3, puis écrit les résumés DB. Si S3 OK mais DB crash → orphelin | Saga pattern |
| 22 | routes/account.py |
2 appels API séquentiels | account_get() puis worlds_get([...]) — pourrait être parallélisé |
asyncio.gather |
| 23 | models/webhook.py |
filter_payload mappe sur colonne filter |
Shadow du builtin filter() — contournement fragile |
Renommer en filter_json |
| 24 | routes/webhooks.py |
DNS executor global | _DNS_EXECUTOR = ThreadPoolExecutor(...) module-level, jamais fermé proprement |
Context manager |
Objectifs :
- Sécuriser les endpoints critiques
- Ajouter les index DB essentiels
- Quick wins performance
Fichiers :
routes/uploads.py— Rate limiting + streaming + retry S3routes/players.py— Rate limitingroutes/webhooks.py— Rate limiting- Modèles → ajouter index composites
Difficulté : Faible Bénéfices : Sécurité immédiate, performances ×2-5 sur les requêtes DB Risques : Faibles — modifications localisées
Objectifs :
- Créer une vraie couche service avec Repository Pattern
- Extraire la logique métier des routes
- Refactorer
routes/players.py(882 lignes → modules)
Fichiers :
routes/players.py→services/player_service.pyroutes/webhooks.py→services/webhook_service.pyroutes/uploads.py→services/upload_service.py- Nouveaux :
repositories/package
Difficulté : Moyen Bénéfices : Testabilité, SRP, maintenabilité Risques : Régression — nécessite tests complets
Objectifs :
- Normaliser les schémas
- Migrer les types INTEGER → BIGINT où nécessaire
- Ajouter FK manquantes
- Créer table
player_boonsnormalisée
Fichiers :
models/fight.py— Refactoring boon columnsmodels/upload.py— BIGINT pour sizemodels/webhook.py— FK pour DLQ- Nouvelles migrations Alembic
Difficulté : Élevé Bénéfices : Intégrité, performance, évolutivité Risques : Migrations = downtime. Tester sur copie de prod.
Objectifs :
- Streaming upload et parse
- Cache Redis pour requêtes fréquentes
- Keyset pagination
- OpenTelemetry tracing
Fichiers :
routes/uploads.py— Streamingservices/parse.py— Iterator streaming- Nouveau :
cache/service routes/players.py— Keyset pagination
Difficulté : Moyen Bénéfices : RAM ÷2-5, latency ÷10-100 Risques : Changement d'API pour pagination (breaking change)
Objectifs :
- Refactorer les fonctions trop longues
- Supprimer le code mort (guild_service stub, port=1 guard)
- Ajouter/Types stricts
- Génération automatique des types frontend depuis Pydantic
- Documenter l'architecture (ADR)
Fichiers :
services/player_summaries.py— Découpageservices/guild_service.py— Implémenter ou supprimerworkers/parser_settings.py— Supprimer guard inutile- Nouveau script :
openapi-typescriptgénération
Difficulté : Faible Bénéfices : Maintenabilité, onboarding Risques : Faibles
| Métrique | Actuel | Estimé Après Refactoring |
|---|---|---|
| Architecture Globale | 5.5/10 | 8/10 |
| Backend | 5/10 | 8/10 |
| Frontend | 4/10 | 6.5/10 |
| Base de Données | 5.5/10 | 8/10 |
| Performance | 4.5/10 | 7.5/10 |
| Sécurité | 5/10 | 8/10 |
| Qualité du Code | 4.5/10 | 7.5/10 |
| Moyenne Générale | 4.9/10 | 7.6/10 |
| Note /100 | 49/100 | 76/100 |
- 10 semaines (5 sprints de 2 semaines)
- 2-3 développeurs senior
- ~400-600 heures de travail effectif
- Monorepo uv workspace legacy (pas de vraie modularisation)
- Pas d'event sourcing / CQRS
- Pas de tests E2E frontend
- Architecture monolithique (pas de microservices)
- Documentation encore partiellement dans les docstrings
- Aucune authentification sur la majorité des endpoints API (HIGH)
- Pas de rate limiting généralisé (HIGH)
- Validation de taille upload redondante mais pas de streaming (MEDIUM)
- ORM anémique sans Repository pattern (CRITICAL archi)
- Routes monolithiques (882 lignes players.py) (HIGH)
- Delete+insert dans player_summaries au lieu d'upsert (MEDIUM perf)
- Types INTEGER risquent l'overflow pour les métriques (MEDIUM DB)
- Pas de pagination keyset — OFFSET sur grandes tables (MEDIUM perf)
- Schéma boon non normalisé (28 colonnes) (LOW-MEDIUM)
- Pas de génération de types frontend depuis Pydantic (MEDIUM)
Rapport généré par audit automatisé — analyse basée sur 100+ fichiers inspectés dans le monorepo Gw2Analytics.