diff --git a/docs/plans/v1.4.0-plan.md b/docs/plans/v1.4.0-plan.md new file mode 100644 index 0000000..e83a37f --- /dev/null +++ b/docs/plans/v1.4.0-plan.md @@ -0,0 +1,664 @@ + + +# Plan de version v1.4.0 — gitea-dashboard + +## Objectif + +Ajouter une vue milestones dediee (`--milestones`), le support d'un fichier de configuration YAML, la visibilite configurable des colonnes (`--columns`), et corriger la gestion des timeouts reseau pendant la pagination. + +## Track + +**Minor** : 6 -> 7 -> 8 -> 9 -> 10+11 -> (12) -> 13 + +--- + +## Budget de scope + +| Critere | Valeur | +|---------|--------| +| Max fichiers par phase | 5 | +| Total fichiers estimes | 10 (6 modules source + 4 fichiers de tests) | +| Fichiers crees | 1 (`config.py`) | +| Tests estimes | ~45 nouveaux (total ~167) | + +### Inclus + +- Vue milestones avec `--milestones` (#16) +- Fichier de configuration YAML (#17) +- Gestion des timeouts reseau pendant la pagination (#18) +- Visibilite configurable des colonnes avec `--columns` (#19) + +### Exclus + +- Parallelisation des appels API (ADR-003, differee) +- Export CSV/YAML +- Cache API local (fichier/SQLite) +- Dashboard interactif TUI + +### Differe (v1.5+) + +- Parallelisation des appels API +- Export CSV +- Cache API local +- Dashboard interactif (TUI) +- Sous-commandes CLI (ADR-011, a reconsiderer si modes alternatifs continuent de croitre) + +--- + +## Etapes skippees + +| Etape | Nom | Raison | +|-------|-----|--------| +| 1 | Discovery | Projet existant, discovery v1.0.0 suffisante | +| 2 | Project creation | Projet existant | +| 3 | Specs | Minor -- specs couvertes par les issues #16-#19 et ce plan | +| 4 | Research | API milestones deja utilisee (client.get_milestones). PyYAML est standard | +| 5 | Roadmap | Minor -- milestone v1.4.0 deja creee sur Gitea | +| 12 | Deploy | Outil CLI local, pas de deploiement serveur | + +--- + +## Analyse des dependances entre issues + +``` +#18 (timeout pagination) -- fondation, corrige _get_paginated dans client.py +#17 (config YAML) -- nouveau module config.py, modifie cli.py +#16 (--milestones) -- nouveau endpoint d'affichage, modifie collector.py + display.py +#19 (--columns) -- modifie display.py + cli.py, depend de #16 (nouvelles colonnes) +``` + +Dependances : +- #18 est un bugfix independant, doit etre fait en premier (stabilite du collecteur) +- #17 cree config.py et modifie cli.py ; #16 modifie aussi cli.py -> separer les phases +- #19 depend de #16 car les colonnes de la vue milestones doivent etre connues avant de les rendre configurables +- #17 et #19 modifient tous les deux cli.py mais dans des zones differentes (config resolution vs argparse columns) + +Ordre : #18 -> #17 -> #16 -> #19 + +--- + +## Evaluation de sous-versions + +4 issues dont 2 features independantes, 1 improvement, 1 bugfix. Scope < 10 fichiers. +Les issues ne justifient pas de sous-versions : elles sont suffisamment couplees (toutes touchent cli.py) et le scope total reste gerable en une version. + +--- + +## Phase 1 : Bugfix timeout pagination + configuration YAML (#18, #17) + +**Goal** : Corriger la degradation gracieuse sur timeout reseau pendant la pagination, et ajouter le support de configuration YAML. + +**Issues Gitea** : fixes #18, fixes #17 + +### Fichiers + +| Action | Fichier | Modifications | Cross-references | +|--------|---------|---------------|------------------| +| Modify | `src/gitea_dashboard/client.py` | `_get_paginated` : catch `ReadTimeout`/`ConnectTimeout` sur page intermediaire, retry avec backoff, degradation gracieuse (retourner les donnees partielles + warning) | `collector.py` (consomme _get_paginated) | +| Create | `src/gitea_dashboard/config.py` | Nouveau module : lecture YAML, resolution `${VAR}`, merge des priorites (CLI > env > config > defaults) | `cli.py` (consomme pour initialiser les args) | +| Modify | `src/gitea_dashboard/cli.py` | Ajouter `--config` dans argparse. Appeler `config.load_config()` avant le merge des args. Passer les valeurs resolues au reste du pipeline | `config.py` (load_config) | +| Modify | `tests/test_client.py` | Tests timeout pendant pagination (mock ReadTimeout sur page 2), test degradation gracieuse, test retry avec backoff | `client.py` | +| Modify | `tests/test_config.py` | Nouveau fichier tests : fixtures YAML (valide, invalide, partiel, vide), resolution `${VAR}`, priorite CLI > env > config > defaults | `config.py` | + +### Interfaces + +#### client.py (modifications) + +```python +class GiteaClient: + def _get_paginated(self, endpoint: str, params: dict | None = None) -> list[dict]: + """Requete GET avec pagination automatique. + + Comportement actuel : boucle tant que len(page) == limit (50). + Utilise _get_with_retry pour la resilience aux timeouts. + + Ajout v1.4.0 : si _get_with_retry leve une exception Timeout sur + une page intermediaire (page > 1), catch l'exception et retourner + les donnees collectees jusque-la au lieu de crasher. + Emet un warning via warnings.warn() pour signaler les donnees partielles. + + Si la premiere page echoue (page == 1), l'exception remonte + normalement (pas de donnees partielles possibles). + """ +``` + +**Pourquoi modifier _get_paginated et non _get_with_retry** : le retry existe deja dans `_get_with_retry` (ADR-007/ADR-009). Le probleme est que quand _toutes_ les tentatives de retry echouent sur une page intermediaire, _get_paginated crashe au lieu de retourner les donnees partielles. La degradation gracieuse est une responsabilite de la pagination, pas du retry. + +**Pourquoi warnings.warn et non logging** : le projet n'utilise pas le module logging. `warnings.warn` est la convention stdlib pour signaler un probleme non-fatal sans dependance supplementaire. Le CLI peut capturer les warnings pour l'affichage Rich. + +#### config.py (nouveau module) + +```python +import os +from pathlib import Path +from typing import Any + +_DEFAULT_CONFIG_PATHS = [ + Path(".gitea-dashboard.yml"), + Path.home() / ".config" / "gitea-dashboard" / "config.yml", +] + +def resolve_env_vars(value: str) -> str: + """Resout les references ${VAR} dans une valeur string. + + Remplace ${VAR} par os.environ[VAR]. + Si VAR n'est pas defini, laisse la reference telle quelle. + Ne resout pas les references imbriquees. + """ + +def load_config(config_path: str | None = None) -> dict[str, Any]: + """Charge la configuration depuis un fichier YAML. + + Ordre de recherche si config_path est None : + 1. .gitea-dashboard.yml (repertoire courant) + 2. ~/.config/gitea-dashboard/config.yml + + Retourne un dict vide si aucun fichier trouve. + Leve une erreur claire si config_path est fourni mais le fichier + n'existe pas ou est invalide. + + Les valeurs string contenant ${VAR} sont resolues via resolve_env_vars. + """ + +def merge_config( + cli_args: dict[str, Any], + env_vars: dict[str, Any], + file_config: dict[str, Any], + defaults: dict[str, Any], +) -> dict[str, Any]: + """Fusionne les sources de configuration par priorite. + + Priorite : cli_args > env_vars > file_config > defaults. + Une valeur None dans une source de priorite superieure ne masque pas + la valeur d'une source de priorite inferieure. + """ +``` + +**Pourquoi un nouveau module plutot qu'une extension de cli.py** : la gestion de configuration YAML (lecture fichier, resolution de variables, merge de priorites) est une responsabilite distincte du parsing d'arguments. ADR-006 a deja montre que la creation de modules supplementaires est acceptable quand la responsabilite est clairement distincte. Le projet passe a 7 modules source (6 + config.py). + +**Pourquoi PyYAML et non la stdlib** : Python n'a pas de parseur YAML dans la stdlib. PyYAML est la dependance la plus legere et la plus utilisee pour ce besoin. C'est une nouvelle dependance explicite dans pyproject.toml. + +#### cli.py (modifications) + +```python +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse les arguments CLI. + + Ajout v1.4.0 : + --config : chemin vers un fichier de configuration YAML alternatif + """ + +def _resolve_config(args: argparse.Namespace) -> argparse.Namespace: + """Resout la configuration en appliquant la priorite CLI > env > config > defaults. + + 1. Charge le fichier config (args.config ou chemin par defaut) + 2. Lit les variables d'environnement pertinentes (GITEA_URL, GITEA_TOKEN) + 3. Fusionne avec les valeurs CLI + 4. Retourne un Namespace enrichi + """ +``` + +### Comportement attendu + +1. Timeout sur page intermediaire pendant la pagination : + ``` + GET /api/v1/user/repos?page=1 -> 200 OK (50 repos) + GET /api/v1/user/repos?page=2 -> ReadTimeout (apres 2 retries) + # Warning : "Donnees partielles : timeout sur la page 2 de /api/v1/user/repos" + # Dashboard affiche les 50 premiers repos avec un avertissement + ``` + +2. Timeout sur la premiere page : + ``` + GET /api/v1/user/repos?page=1 -> ReadTimeout (apres 2 retries) + # Exception remonte normalement -> message d'erreur CLI + ``` + +3. Configuration YAML : + ```yaml + # ~/.config/gitea-dashboard/config.yml + url: http://192.168.0.106:3000 + token: ${GITEA_TOKEN} + sort: activity + exclude: + - archived-repo + no_desc: false + ``` + +4. Priorite de configuration : + ```bash + # config.yml a sort: activity, CLI passe --sort name + $ gitea-dashboard --sort name + # -> tri par name (CLI gagne) + ``` + +### Tests + +#### test_client.py (ajouts) + +- `test_get_paginated_timeout_page2_returns_partial` : mock page 1 OK (50 items), page 2 leve ReadTimeout -> retourne les 50 items de page 1. +- `test_get_paginated_timeout_page1_raises` : mock page 1 leve ReadTimeout -> exception remonte. +- `test_get_paginated_connect_timeout_graceful` : mock ConnectTimeout sur page 2 -> degradation gracieuse. +- `test_get_paginated_partial_data_emits_warning` : verifie que `warnings.warn` est appele avec le message de donnees partielles. + +#### test_config.py (nouveau fichier) + +- `test_load_config_valid_yaml` : fixture YAML valide -> dict avec toutes les cles. +- `test_load_config_partial_yaml` : fixture YAML avec seulement `url` et `sort` -> dict partiel. +- `test_load_config_empty_file` : fichier vide -> dict vide. +- `test_load_config_invalid_yaml` : YAML syntaxiquement invalide -> erreur claire. +- `test_load_config_custom_path` : `--config /tmp/custom.yml` -> charge le fichier specifie. +- `test_load_config_missing_custom_path` : `--config /inexistant.yml` -> erreur claire. +- `test_load_config_default_paths` : fixture dans `.gitea-dashboard.yml` -> charge automatiquement. +- `test_resolve_env_vars_simple` : `${GITEA_TOKEN}` -> valeur de la variable. +- `test_resolve_env_vars_undefined` : `${UNDEFINED}` -> laisse la reference telle quelle. +- `test_resolve_env_vars_in_list` : liste YAML avec `${VAR}` -> chaque element resolu. +- `test_merge_config_priority` : CLI > env > config > defaults, verifie la precedence. +- `test_merge_config_none_does_not_override` : CLI avec None ne masque pas config. + +### Livrable + +Le timeout pendant la pagination ne crashe plus le collecteur -- les donnees partielles sont retournees avec un warning. Le fichier `.gitea-dashboard.yml` est supporte avec resolution de variables et priorite CLI > env > config > defaults. Tous les tests passent. + +--- + +## Phase 2 : Vue milestones et colonnes configurables (#16, #19) + +**Goal** : Ajouter le flag `--milestones` pour une vue dediee des milestones par repo, et le flag `--columns` pour choisir les colonnes affichees. + +**Issues Gitea** : fixes #16, fixes #19 + +### Fichiers + +| Action | Fichier | Modifications | Cross-references | +|--------|---------|---------------|------------------| +| Modify | `src/gitea_dashboard/collector.py` | Nouvelle fonction `collect_milestones()` pour collecter les milestones de tous les repos (avec filtrage include/exclude) | `client.py` (get_milestones), `cli.py` (appelle si --milestones) | +| Modify | `src/gitea_dashboard/display.py` | Nouvelle fonction `render_milestones()` pour le tableau milestones dedie. Constante `AVAILABLE_COLUMNS` et fonction `parse_columns()` pour le parsing de `--columns`. Modifier `render_dashboard()` pour respecter la visibilite des colonnes | `collector.py` (MilestoneData ou dicts), `cli.py` (passe les colonnes) | +| Modify | `src/gitea_dashboard/exporter.py` | Supporter l'export JSON des milestones (`milestones_to_dicts()`) | `collector.py` (donnees milestones) | +| Modify | `src/gitea_dashboard/cli.py` | Ajouter `--milestones` et `--columns` dans argparse. Router vers `render_milestones()` ou `export_json()` selon le mode. Gerer `--columns help`. Alias `--no-desc` vers `--columns -description` | `display.py` (render_milestones, parse_columns), `config.py` (colonnes dans config YAML) | +| Modify | `tests/test_collector.py` | Tests `collect_milestones()` : repos avec/sans milestones, filtrage, repos vides | `collector.py` | + +### Interfaces + +#### collector.py (ajouts) + +```python +@dataclass +class MilestoneData: + """Donnees agregees d'une milestone avec son repo parent.""" + + repo_name: str + title: str + open_issues: int + closed_issues: int + progress_pct: int # Pourcentage de completion (0-100) + due_on: str | None # ISO 8601 ou None + state: str # "open" ou "closed" + +def collect_milestones( + client: GiteaClient, + include: list[str] | None = None, + exclude: list[str] | None = None, +) -> list[MilestoneData]: + """Collecte les milestones de tous les repos accessibles. + + Reutilise la logique de filtrage de collect_all (include/exclude). + Pour chaque repo filtre, appelle client.get_milestones() avec state=all + (pas seulement open, pour afficher la progression globale). + + Retourne une liste plate de MilestoneData triee par repo puis milestone. + """ +``` + +**Pourquoi un dataclass MilestoneData plutot que des dicts bruts** : coherent avec RepoData (ADR-002). Un dataclass documente les champs attendus et permet la validation. Le calcul de `progress_pct` est centralise dans le collecteur, pas dans l'affichage. + +**Pourquoi state=all et non state=open** : l'issue #16 demande une vue de progression des milestones. Les milestones fermees (100%) sont informatives pour voir l'historique. Le filtre open-only est deja dans `get_milestones()` actuel ; pour la vue dediee, on veut tout. + +#### display.py (ajouts) + +```python +AVAILABLE_COLUMNS: dict[str, str] = { + "name": "Nom du repo", + "description": "Description du repo", + "issues": "Nombre d'issues ouvertes", + "release": "Derniere release", + "commit": "Date du dernier commit", + "activity": "Indicateur d'activite", +} + +def parse_columns(columns_arg: str | None, no_desc: bool = False) -> list[str]: + """Parse l'argument --columns et retourne la liste des colonnes a afficher. + + Si columns_arg est None : retourne toutes les colonnes (sauf description si no_desc). + Si columns_arg est "help" : retourne la liste speciale ["__help__"]. + Les colonnes sont separees par des virgules. + Le prefixe "-" exclut une colonne (ex: "-description"). + Leve ValueError si une colonne inconnue est specifiee. + """ + +def render_milestones( + milestones: list[MilestoneData], + console: Console | None = None, +) -> None: + """Affiche le tableau des milestones. + + Colonnes : Repo, Milestone, Open, Closed, Progress (%). + La barre de progression utilise le pourcentage calcule. + Coloration : vert > 80%, jaune 50-80%, rouge < 50%. + """ +``` + +**Pourquoi AVAILABLE_COLUMNS est un dict et non une liste** : le dict mappe nom technique -> description lisible, utile pour `--columns help`. Une liste ne suffirait pas pour l'affichage d'aide. + +**Pourquoi parse_columns retourne ["__help__"]** : le CLI doit detecter le mode aide pour afficher les colonnes et quitter. Une valeur sentinelle est plus propre qu'un booleen supplementaire dans la signature. + +#### exporter.py (ajouts) + +```python +def milestones_to_dicts(milestones: list[MilestoneData]) -> list[dict]: + """Convertit une liste de MilestoneData en liste de dicts serialisables. + + Sanitize les champs texte (repo_name, title) pour les caracteres de controle. + """ + +def export_milestones_json(milestones: list[MilestoneData], indent: int = 2) -> str: + """Exporte les milestones en JSON formate.""" +``` + +#### cli.py (ajouts) + +```python +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse les arguments CLI. + + Ajout v1.4.0 : + --milestones : affiche la vue milestones au lieu du dashboard repos + --columns : liste des colonnes a afficher (separe par virgules) + Supporte l'exclusion par prefixe "-" + "--columns help" affiche les colonnes disponibles + """ +``` + +### Comportement attendu + +1. Vue milestones : + ``` + $ gitea-dashboard --milestones + +------------------+-------------+------+--------+----------+ + | Repo | Milestone | Open | Closed | Progress | + +------------------+-------------+------+--------+----------+ + | gitea-dashboard | v1.4.0 | 4 | 0 | 0% | + | gitea-dashboard | v1.3.0 | 0 | 5 | 100% | + | workflow | v2.6.1 | 0 | 5 | 100% | + +------------------+-------------+------+--------+----------+ + ``` + +2. Vue milestones avec filtre : + ``` + $ gitea-dashboard --milestones --repo gitea + # Affiche uniquement les milestones des repos contenant "gitea" + ``` + +3. Vue milestones en JSON : + ``` + $ gitea-dashboard --milestones --format json + [{"repo_name": "gitea-dashboard", "title": "v1.4.0", ...}] + ``` + +4. Colonnes configurables : + ``` + $ gitea-dashboard --columns name,issues,release + # Affiche seulement les colonnes name, issues, release + + $ gitea-dashboard --columns -description,-commit + # Affiche tout sauf description et commit + + $ gitea-dashboard --columns help + # Colonnes disponibles : name, description, issues, release, commit, activity + ``` + +5. Retrocompatibilite `--no-desc` : + ``` + $ gitea-dashboard --no-desc + # Equivalent a --columns -description + # Les deux flags coexistent + ``` + +### Tests + +#### test_collector.py (ajouts) + +- `test_collect_milestones_basic` : 2 repos avec milestones -> liste plate de MilestoneData. +- `test_collect_milestones_empty_repo` : repo sans milestone -> pas dans la liste. +- `test_collect_milestones_progress_calculation` : 3 open, 7 closed -> progress_pct == 70. +- `test_collect_milestones_with_include_filter` : filtre include respecte. +- `test_collect_milestones_with_exclude_filter` : filtre exclude respecte. + +#### test_display.py (ajouts) + +- `test_render_milestones_basic` : capture console, verifie le tableau avec colonnes attendues. +- `test_render_milestones_empty` : liste vide -> message "Aucune milestone trouvee." +- `test_render_milestones_progress_colors` : verifie la coloration selon le pourcentage. +- `test_parse_columns_all_default` : None -> toutes les colonnes. +- `test_parse_columns_inclusion` : "name,issues" -> ["name", "issues"]. +- `test_parse_columns_exclusion` : "-description,-commit" -> toutes sauf description et commit. +- `test_parse_columns_unknown_raises` : "unknown" -> ValueError. +- `test_parse_columns_help` : "help" -> ["__help__"]. +- `test_parse_columns_no_desc_compat` : no_desc=True -> description exclue. +- `test_render_dashboard_with_columns` : colonnes specifiques -> seules ces colonnes affichees. + +#### test_exporter.py (ajouts) + +- `test_export_milestones_json_basic` : MilestoneData -> JSON valide. +- `test_export_milestones_json_empty` : liste vide -> "[]". + +#### test_cli.py (ajouts) + +- `test_parse_args_milestones` : `--milestones` -> `Namespace(milestones=True)`. +- `test_main_milestones_mode` : mock collect_milestones + render_milestones, verifie le routage. +- `test_parse_args_columns` : `--columns name,issues` -> `Namespace(columns="name,issues")`. +- `test_main_columns_help` : `--columns help` -> affiche la liste et quitte. +- `test_main_no_desc_and_columns_compat` : `--no-desc --columns -commit` -> les deux s'appliquent. + +### Livrable + +Le flag `--milestones` affiche un tableau dedie avec la progression des milestones par repo. Le flag `--columns` permet de choisir les colonnes affichees avec support d'inclusion et d'exclusion. `--no-desc` reste fonctionnel comme alias. L'export JSON fonctionne pour les deux modes. Tous les tests passent. + +--- + +## Phase 3 : Audit + +**Goal** : Audit de qualite (reviewer) et de securite (guardian). Score cible : 100. Plancher : 50. + +## Phase 4 : Smoke test + +**Goal** : Tests E2E sur l'instance Gitea reelle. Verification manuelle des nouvelles fonctionnalites (--milestones, --columns, config YAML, degradation gracieuse timeout). + +## Phase 5 : Documentation + Release + +**Goal** : Mise a jour README.md, CHANGELOG.md (format Keep a Changelog). Bump de version a 1.4.0. Creation du tag et de la release Gitea. + +## Phase 6 : Retrospective + +**Goal** : Metriques, MEMORY.md, revue des issues Gitea, analyse du workflow. + +--- + +## Architecture des modules (impact v1.4.0) + +``` + gitea-dashboard v1.4.0 + ===================== + + Terminal Application Gitea API + -------- ----------- --------- + + +------------------+ + $ gitea-dashboard | cli.py | + --milestones | - parse args | + --columns | - resolve config | + --config | - route modes | + | - gere erreurs | + +--------+---------+ + | + +--------+---------+ + | --milestones? | + +--+----------+----+ + | | + oui | | non + v v + collect_milestones() collect_all() + | | + v +-------+-------+ + render_milestones | | + ou export_json v v + +------------+ +-------------+ + | display.py | | exporter.py | + | + colonnes | | + milestones| + <--------------------| + --columns| | + sanitize |---------> stdout (JSON) + Output Rich | + milest. | +-------------+ + (tableaux) +------------+ + + +------------------+ + | config.py | <-- NEW + | + load YAML | + | + resolve ${VAR} | + | + merge priority | + +------------------+ + + +------------------+ + | client.py | + | + get_version() |-----> GET /api/v1/version + | + retry HTTP 429 |-----> GET /api/v1/user/repos + | + timeout gracf. |-----> GET .../releases/latest + +------------------+-----> GET .../milestones (state=all) + -----> GET .../commits?limit=1 +``` + +| Module | Impact v1.4.0 | Detail | +|--------|--------------|--------| +| `client.py` | Modifie | Degradation gracieuse dans `_get_paginated` sur timeout page intermediaire | +| `collector.py` | Modifie | Nouveau dataclass `MilestoneData`, nouvelle fonction `collect_milestones()` | +| `display.py` | Modifie | `render_milestones()`, `parse_columns()`, `AVAILABLE_COLUMNS`, colonnes configurables dans `render_dashboard()` | +| `exporter.py` | Modifie | `milestones_to_dicts()`, `export_milestones_json()` | +| `cli.py` | Modifie | Options `--milestones`, `--columns`, `--config`. Resolution config YAML. Routage du mode milestones | +| `config.py` | Cree | Lecture YAML, resolution `${VAR}`, merge de priorites | + +--- + +## Decisions architecturales + +### ADR-012 : Degradation gracieuse sur timeout dans _get_paginated (v1.4.0) + +**Date** : 2026-03-13 +**Statut** : accepte + +**Contexte** : Un timeout reseau sur une page intermediaire de la pagination fait crasher tout le collecteur. Le retry existant (ADR-007/ADR-009) retente les requetes individuelles, mais apres epuisement des retries, l'exception remonte et les donnees des pages precedentes sont perdues. + +**Decision** : Dans `_get_paginated`, catch les exceptions Timeout apres epuisement des retries uniquement pour les pages > 1. Retourner les donnees collectees jusque-la et emettre un `warnings.warn()`. Si la premiere page echoue, l'exception remonte normalement (pas de donnees partielles possibles). + +**Consequences** : +- Le dashboard affiche un resultat partiel plutot qu'un crash +- L'utilisateur est informe via un warning (visible dans la console) +- La premiere page echouee reste un crash clair (pas de faux resultat vide) +- Coherent avec le principe "Gestion gracieuse" de CLAUDE.md +- Risque : l'utilisateur pourrait ne pas remarquer le warning -> l'affichage CLI devra etre explicite + +### ADR-013 : Nouveau module config.py pour la configuration YAML (v1.4.0) + +**Date** : 2026-03-13 +**Statut** : accepte + +**Contexte** : L'issue #17 demande un fichier de configuration YAML. La logique (lecture fichier, resolution variables, merge de priorites) est substantielle et distincte du parsing CLI. + +**Decision** : Creer `config.py` comme 7eme module source. ADR-002 (4 modules max) est relaxe pour la 3eme fois (apres ADR-006 pour exporter.py). Le principe "un module = une responsabilite" reste respecte. + +**Consequences** : +- Separation claire : cli.py parse les args, config.py resout la configuration +- Le module est testable independamment avec des fixtures YAML +- Nouvelle dependance PyYAML dans pyproject.toml (premiere dependance ajoutee depuis la creation du projet) +- Le merge de priorites (CLI > env > config > defaults) est centralise et testable +- Si d'autres formats de config apparaissent (TOML, INI), le module absorbe la complexite + +### ADR-014 : Dataclass MilestoneData pour la vue milestones (v1.4.0) + +**Date** : 2026-03-13 +**Statut** : accepte + +**Contexte** : La vue `--milestones` collecte des milestones de tous les repos. Les milestones de l'API sont des dicts bruts sans reference au repo parent. Le calcul du pourcentage de progression est necessaire. + +**Decision** : Creer un dataclass `MilestoneData` dans collector.py, incluant `repo_name` et `progress_pct` pre-calcule. La collecte utilise `state=all` (pas seulement open) pour afficher l'historique complet. + +**Consequences** : +- Coherent avec RepoData : donnees normalisees et documentees +- Le calcul du pourcentage est centralise dans le collecteur (pas dans display.py) +- `state=all` augmente le nombre d'appels API mais donne une vue complete +- Le client.get_milestones() existant utilise `state=open` -> la nouvelle collecte appellera directement avec `state=all` ou une nouvelle methode + +### ADR-015 : Colonnes configurables par inclusion/exclusion (v1.4.0) + +**Date** : 2026-03-13 +**Statut** : accepte + +**Contexte** : L'issue #19 demande de pouvoir choisir les colonnes affichees. L'approche actuelle (`--no-desc`) est ad hoc pour une seule colonne. Un systeme generique est maintenant justifie. + +**Decision** : Ajouter `--columns` avec une syntaxe a virgules. Support de l'inclusion directe (`name,issues`) et de l'exclusion par prefixe `-` (`-description,-commit`). `--no-desc` reste fonctionnel comme alias de `--columns -description`. + +**Consequences** : +- Remplace l'approche YAGNI de v1.3.0 (ADR-011 notait "un systeme generique serait over-engineere") -- maintenant justifie par l'issue #19 +- Retrocompatible : `--no-desc` continue de fonctionner +- `--columns help` fournit une aide contextuelle sans documentation externe +- Les deux syntaxes (inclusion et exclusion) couvrent les cas d'usage courants +- Risque : `--no-desc` + `--columns` en meme temps doit etre gere (les deux s'appliquent cumulativement) + +--- + +## Risques d'audit + +| Zone | Risque | Severite estimee | +|------|--------|-----------------| +| `client.py` -- degradation gracieuse | Le warning pourrait etre silencieux si capture par un framework de test. Doit etre visible dans la sortie CLI | major | +| `config.py` -- resolution ${VAR} | Un `${VAR}` non resolu dans `token` pourrait envoyer une reference liteerale comme token API. Doit etre detecte et signale | critical | +| `config.py` -- YAML injection | PyYAML `safe_load` requis pour eviter l'execution de code. `yaml.load` sans Loader est une faille connue | critical | +| `config.py` -- token en clair dans le fichier | Si l'utilisateur ecrit `token: abc123` au lieu de `token: ${GITEA_TOKEN}`, le token est en clair sur le disque. Documenter le risque, recommander `${VAR}` | major | +| `display.py` -- `--columns` + `--no-desc` | Les deux flags doivent etre cumulatifs, pas contradictoires. Tester la combinaison | minor | +| `display.py` -- colonnes inconnues | `--columns unknown` doit lever une erreur claire, pas un KeyError silencieux | minor | +| `collector.py` -- state=all milestones | Plus d'appels API que `state=open`. Risque de rate limiting sur les instances avec beaucoup de repos/milestones | minor | +| `exporter.py` -- milestones JSON | Le format JSON des milestones doit etre coherent avec celui des repos (meme structure de sanitisation) | minor | +| `pyproject.toml` -- PyYAML | Nouvelle dependance a auditer (pas de CVE connue sur les versions recentes) | minor | + +--- + +## Issues Gitea rattachees + +| Issue | Titre | Phase | +|-------|-------|-------| +| [#18](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/18) | fix: handle API timeout during paginated requests | Phase 1 | +| [#17](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/17) | feat: YAML configuration file support | Phase 1 | +| [#16](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/16) | feat: milestone progress view (--milestones) | Phase 2 | +| [#19](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/19) | improvement: configurable column visibility (--columns) | Phase 2 | + +--- + +## Dependances + +| Dependance | Type | Version | Changement v1.4.0 | +|------------|------|---------|--------------------| +| Python | Runtime | >= 3.10 | Inchange | +| requests | Librairie | >= 2.31 | Inchange | +| rich | Librairie | >= 13.0 | Inchange | +| PyYAML | Librairie | >= 6.0 | **Nouveau** (#17) | +| pytest | Dev | >= 7.0 | Inchange | +| ruff | Dev | >= 0.4 | Inchange | +| Instance Gitea | Service externe | 192.168.0.106:3000 | Inchange | + +--- + +## Criteres de validation par issue + +| Issue | Criteres de validation | +|-------|----------------------| +| #16 | `--milestones` affiche un tableau avec colonnes Repo/Milestone/Open/Closed/Progress. Compatible `--repo` et `--exclude`. Compatible `--format json`. Tests : collecte, affichage, filtrage, export JSON. | +| #17 | `.gitea-dashboard.yml` ou `~/.config/gitea-dashboard/config.yml` charge. `--config ` fonctionne. Priorite CLI > env > config > defaults. Resolution `${VAR}`. Tests : YAML valide/invalide/partiel/vide, resolution vars, priorite. PyYAML dans pyproject.toml. | +| #18 | Timeout sur page > 1 retourne donnees partielles + warning. Timeout sur page 1 crashe normalement. Retry (max 2) avec backoff lineaire (1s, 2s) sur ReadTimeout et ConnectTimeout. Tests : mock timeout page 2, degradation gracieuse, warning emis. | +| #19 | `--columns name,issues` affiche seulement ces colonnes. `--columns -description` exclut la colonne. `--columns help` affiche la liste. `--no-desc` reste fonctionnel. Erreur claire si colonne inconnue. Tests : parsing, inclusion, exclusion, combinaison avec --no-desc, validation. | diff --git a/docs/technical/ARCHITECTURE.md b/docs/technical/ARCHITECTURE.md index d5d460b..38bc4ee 100644 --- a/docs/technical/ARCHITECTURE.md +++ b/docs/technical/ARCHITECTURE.md @@ -160,3 +160,9 @@ Decisions cles pour v1.3.0 : - **ADR-009** : Retry HTTP 429 avec Retry-After dans _get_with_retry - **ADR-010** : Sanitisation des caracteres de controle dans exporter.py - **ADR-011** : --health comme commande alternative, pas sous-commande + +Decisions cles pour v1.4.0 : +- **ADR-012** : Degradation gracieuse sur timeout dans _get_paginated +- **ADR-013** : Nouveau module config.py pour la configuration YAML +- **ADR-014** : Dataclass MilestoneData pour la vue milestones +- **ADR-015** : Colonnes configurables par inclusion/exclusion diff --git a/docs/technical/decisions.md b/docs/technical/decisions.md index aa05fcc..899c814 100644 --- a/docs/technical/decisions.md +++ b/docs/technical/decisions.md @@ -167,3 +167,62 @@ - Un seul niveau d'arguments - `--health` est mutuellement exclusif avec le mode dashboard - Si d'autres modes alternatifs apparaissent, reconsiderer les sous-commandes + +## ADR-012 : Degradation gracieuse sur timeout dans _get_paginated (v1.4.0) + +**Date** : 2026-03-13 +**Statut** : accepte + +**Contexte** : Un timeout reseau sur une page intermediaire de la pagination fait crasher tout le collecteur. Le retry existant (ADR-007/ADR-009) retente les requetes individuelles, mais apres epuisement des retries, l'exception remonte et les donnees des pages precedentes sont perdues. + +**Decision** : Dans `_get_paginated`, catch les exceptions Timeout apres epuisement des retries uniquement pour les pages > 1. Retourner les donnees collectees jusque-la et emettre un `warnings.warn()`. Si la premiere page echoue, l'exception remonte normalement. + +**Consequences** : +- Le dashboard affiche un resultat partiel plutot qu'un crash +- L'utilisateur est informe via un warning +- La premiere page echouee reste un crash clair +- Coherent avec le principe "Gestion gracieuse" de CLAUDE.md + +## ADR-013 : Nouveau module config.py pour la configuration YAML (v1.4.0) + +**Date** : 2026-03-13 +**Statut** : accepte + +**Contexte** : L'issue #17 demande un fichier de configuration YAML. La logique (lecture fichier, resolution variables, merge de priorites) est distincte du parsing CLI. + +**Decision** : Creer `config.py` comme 7eme module source. Nouvelle dependance PyYAML. Le principe "un module = une responsabilite" de ADR-002 reste respecte. + +**Consequences** : +- Separation claire : cli.py parse les args, config.py resout la configuration +- Le module est testable independamment avec des fixtures YAML +- Premiere nouvelle dependance ajoutee au projet (PyYAML) +- Le merge de priorites (CLI > env > config > defaults) est centralise et testable + +## ADR-014 : Dataclass MilestoneData pour la vue milestones (v1.4.0) + +**Date** : 2026-03-13 +**Statut** : accepte + +**Contexte** : La vue `--milestones` collecte des milestones de tous les repos. Les milestones de l'API sont des dicts bruts sans reference au repo parent. + +**Decision** : Creer un dataclass `MilestoneData` dans collector.py. Collecte avec `state=all` pour afficher l'historique complet. + +**Consequences** : +- Coherent avec RepoData : donnees normalisees et documentees +- Le calcul du pourcentage de progression est centralise dans le collecteur +- `state=all` augmente les appels API mais donne une vue complete + +## ADR-015 : Colonnes configurables par inclusion/exclusion (v1.4.0) + +**Date** : 2026-03-13 +**Statut** : accepte + +**Contexte** : L'issue #19 demande de pouvoir choisir les colonnes affichees. L'approche actuelle (`--no-desc`) est ad hoc. Un systeme generique est maintenant justifie par le besoin. + +**Decision** : Ajouter `--columns` avec syntaxe a virgules. Support inclusion directe et exclusion par prefixe `-`. `--no-desc` reste fonctionnel comme alias. + +**Consequences** : +- Remplace l'approche YAGNI de v1.3.0 (maintenant justifie) +- Retrocompatible : `--no-desc` continue de fonctionner +- `--columns help` fournit une aide contextuelle +- Les deux flags combines s'appliquent cumulativement