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