Skip to content

Latest commit

 

History

History
481 lines (390 loc) · 32.5 KB

File metadata and controls

481 lines (390 loc) · 32.5 KB

Audit d'Architecture Complet — Gw2Analytics

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)


Pass 1 — Architecture Globale

Structure du dépôt

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èmes architecturaux

# 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

Note Architecture Globale : 5.5/10

Top 10 Problèmes Architecturaux

  1. Pas de Repository Pattern → violation DIP
  2. Service Layer procédural, pas orienté domaine
  3. Routes monolithiques (882 lignes pour players)
  4. Pas de couche Use Case
  5. Pas d'event bus pour les webhooks
  6. Pas de contrat formel frontend/backend
  7. Cache ad-hoc sans stratégie
  8. Dépendances circulaires entre services
  9. Pas de bounded contexts
  10. Ordre d'include des routes fragile

Pass 2 — Backend (FastAPI + SQLAlchemy)

Problèmes Backend

# 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

Note Backend : 5/10

Top 10 Quick Wins Backend

  1. Ajouter timeout à asyncio.to_thread dans uploads
  2. Supprimer les orphelins S3 après rollback
  3. Utiliser INSERT ON CONFLICT au lieu de delete+insert
  4. Ajouter retry sur les appels MinIO
  5. Streamer le JSONL au lieu de tout charger en mémoire
  6. Ajouter healthcheck S3 + DB
  7. Configurer httpx timeout pour webhook dispatch
  8. Supprimer les flush() prématurés
  9. Ajouter cache busting pour Fernet KEK rotation
  10. Logger le drift au lieu de crash

Pass 3 — Frontend

Analyse limitée car le frontend Next.js est volumineux et les fichiers complets n'ont pas été inspectés en détail.

Problèmes identifiés

# 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

Note Frontend Estimée : 4/10


Pass 4 — Base de Données

Problèmes du Schéma

# 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êtes Problématiques

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

Nouveaux Index Recommandés

-- 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);

Note Base de Données : 5.5/10


Pass 5 — Performance

Top 30 Optimisations Classées par ROI

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

Note Performance : 4.5/10


Pass 6 — Sécurité

Problèmes de Sécurité

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

Plan de Correction Sécurité

Critical (0) : Aucun problème critique immédiat identifié (les uploads ont une validation de taille, les webhooks ont une protection SSRF).

High (2) :

  1. Ajouter rate limiting sur tous les endpoints (slowapi)
  2. Ajouter une couche d'authentification (même basique) pour les endpoints non-publics

Medium (6) :

  1. Ajouter validation MIME type sur upload
  2. Ajouter CSP header
  3. Utiliser un secret manager pour KEK en prod
  4. Configurer CORS strict en prod via CI/CD
  5. Configurer X-Forwarded-For trust proprement
  6. Ajouter validation de taille dans Caddy

Note Sécurité : 5/10


Pass 7 — Qualité du Code

Problèmes de Qualité

# 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

Note Qualité du Code : 4.5/10


Pass 8 — Plan de Refactoring

Roadmap — 5 Sprints


Sprint 1 — Fondations et Sécurité (Semaines 1-2)

Objectifs :

  • Sécuriser les endpoints critiques
  • Ajouter les index DB essentiels
  • Quick wins performance

Fichiers :

  • routes/uploads.py — Rate limiting + streaming + retry S3
  • routes/players.py — Rate limiting
  • routes/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


Sprint 2 — Extraction du Service Layer (Semaines 3-4)

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.py
  • routes/webhooks.py → services/webhook_service.py
  • routes/uploads.py → services/upload_service.py
  • Nouveaux : repositories/ package

Difficulté : Moyen Bénéfices : Testabilité, SRP, maintenabilité Risques : Régression — nécessite tests complets


Sprint 3 — Refactoring Base de Données (Semaines 5-6)

Objectifs :

  • Normaliser les schémas
  • Migrer les types INTEGER → BIGINT où nécessaire
  • Ajouter FK manquantes
  • Créer table player_boons normalisée

Fichiers :

  • models/fight.py — Refactoring boon columns
  • models/upload.py — BIGINT pour size
  • models/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.


Sprint 4 — Performance et Architecture (Semaines 7-8)

Objectifs :

  • Streaming upload et parse
  • Cache Redis pour requêtes fréquentes
  • Keyset pagination
  • OpenTelemetry tracing

Fichiers :

  • routes/uploads.py — Streaming
  • services/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)


Sprint 5 — Qualité et Documentation (Semaines 9-10)

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écoupage
  • services/guild_service.py — Implémenter ou supprimer
  • workers/parser_settings.py — Supprimer guard inutile
  • Nouveau script : openapi-typescript génération

Difficulté : Faible Bénéfices : Maintenabilité, onboarding Risques : Faibles


Scores

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

Temps Estimé

  • 10 semaines (5 sprints de 2 semaines)
  • 2-3 développeurs senior
  • ~400-600 heures de travail effectif

Dette Technique Restante Après Refactoring

  • 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

Résumé Problèmes CRITIQUES

  1. Aucune authentification sur la majorité des endpoints API (HIGH)
  2. Pas de rate limiting généralisé (HIGH)
  3. Validation de taille upload redondante mais pas de streaming (MEDIUM)
  4. ORM anémique sans Repository pattern (CRITICAL archi)
  5. Routes monolithiques (882 lignes players.py) (HIGH)
  6. Delete+insert dans player_summaries au lieu d'upsert (MEDIUM perf)
  7. Types INTEGER risquent l'overflow pour les métriques (MEDIUM DB)
  8. Pas de pagination keyset — OFFSET sur grandes tables (MEDIUM perf)
  9. Schéma boon non normalisé (28 colonnes) (LOW-MEDIUM)
  10. 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.