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:
664
docs/plans/v1.4.0-plan.md
Normal file
664
docs/plans/v1.4.0-plan.md
Normal 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. |
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user