docs(v1.4.0): version plan and ADR

Plan 2 phases : bugfix timeout + config YAML, puis vue milestones + colonnes.
ADR-012 a ADR-015 couvrant degradation gracieuse, config.py, MilestoneData,
et colonnes configurables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sylvain
2026-03-13 03:34:55 +01:00
parent 98223e4995
commit a1f613f3d8
3 changed files with 729 additions and 0 deletions

664
docs/plans/v1.4.0-plan.md Normal file
View File

@@ -0,0 +1,664 @@
<!-- Type: reference (Diataxis). Style: factuel, structure par phases, actionnable par le builder. -->
# 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 <path>` 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. |